Replace Tailwind with custom CSS rules in documentation panel (#6978)
Closes #6705 This PR removes Tailwind from dependencies of the documentation panel and also brings some essential design improvements. List of fixed things: - Massive refactoring, removing unnecessary HTML tags, and simplifying the overall layout. - Spacing to the right of the bullet in the bullet lists is much smaller. Bullets also have the same color as the following text. - Paddings before headers increased. - Colon used instead of the comma between the method link and its summary. - Module or type names in the top header are now delimited by `.`, which matches the Enso language. - We now use Twemoji icons in marked sections. Now `Info` and `Important` icons are much nicer. - Constructors are now in a separate section as methods. - Headers correctly handle long entry names. List of not fixed things: (I will create additional issues for them) - Argument names in the list are not bold. We receive a `<ul>` HTML code from the engine. We don't know whether it contains argument names or something else. It requires support from the LS protocol. - No highlight for things delimited by `<pre>` tags in the text (like `Nothing` on the screenshot below). It also requires support from the LS protocol because we don't inspect received text and can't modify it. ![image](https://github.com/enso-org/enso/assets/6566674/868f1e03-e53a-4eb6-a306-8b439264fec5) In the video from IDE, you can see some issues with spacing in the documentation, namely redundant blank lines at the beginning of each example and multiline (and multi-paragraph!) summaries for methods. This can't be fixed on the IDE side. I will create separate issues for that. https://github.com/enso-org/enso/assets/6566674/e4cef3d1-7a13-4d3b-b11d-f5ee2c05ca82 https://github.com/enso-org/enso/assets/6566674/43970982-ba43-4530-8ffd-6bce790d5d77
10
app/gui/view/documentation/README.md
Normal file
@ -0,0 +1,10 @@
|
||||
## Icons license
|
||||
|
||||
We use two Twemoji SVG icons for our documentation panel, you can find them at:
|
||||
|
||||
- `assets/icon-important.svg`
|
||||
- `assets/icon-info.svg`
|
||||
|
||||
Twemoji SVG icons are licensed under CC-BY 4.0:
|
||||
https://creativecommons.org/licenses/by/4.0/. Copyright 2020 Twitter, Inc and
|
||||
other contributors.
|
@ -1,3 +1,4 @@
|
||||
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.1981 14.1473L27.3799 22.6473C29.3027 25.2912 27.4141 29 24.1449 29H7.85509C4.58595 29 2.69733 25.2912 4.62015 22.6473L10.802 14.1473C11.1273 13.7 11.5367 13.3316 12 13.0575V6H11.5C10.6716 6 10 5.32843 10 4.5C10 3.67157 10.6716 3 11.5 3H20.5C21.3284 3 22 3.67157 22 4.5C22 5.32843 21.3284 6 20.5 6H20V13.0575C20.4633 13.3316 20.8727 13.7 21.1981 14.1473Z" fill="#6DA85E" fill-opacity="0.7"/>
|
||||
<g clip-path="url(#clip0_7717_245736)">
|
||||
<path d="M21.1981 14.1473L27.3799 22.6473C29.3027 25.2912 27.4141 29 24.1449 29H7.85509C4.58595 29 2.69733 25.2912 4.62015 22.6473L10.802 14.1473C11.1273 13.7 11.5367 13.3316 12 13.0575V6H11.5C10.6716 6 10 5.32843 10 4.5C10 3.67157 10.6716 3 11.5 3H20.5C21.3284 3 22 3.67157 22 4.5C22 5.32843 21.3284 6 20.5 6H20V13.0575C20.4633 13.3316 20.8727 13.7 21.1981 14.1473Z" fill="#6DA85E"/>
|
||||
@ -7,3 +8,4 @@
|
||||
<rect width="32" height="12" fill="white" transform="translate(0 20)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 967 B After Width: | Height: | Size: 1.0 KiB |
1
app/gui/view/documentation/assets/icon-important.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg"><circle fill="#BE1931" cx="18" cy="32" r="3"/><path fill="#BE1931" d="M21 24c0 1.657-1.344 3-3 3-1.657 0-3-1.343-3-3V5c0-1.657 1.343-3 3-3 1.656 0 3 1.343 3 3v19z"/></svg>
|
After Width: | Height: | Size: 232 B |
1
app/gui/view/documentation/assets/icon-info.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg"><path fill="#3B88C3" d="M0 4c0-2.209 1.791-4 4-4h28c2.209 0 4 1.791 4 4v28c0 2.209-1.791 4-4 4H4c-2.209 0-4-1.791-4-4V4z"/><path fill="#FFF" d="M20.512 8.071c0 1.395-1.115 2.573-2.511 2.573-1.333 0-2.511-1.209-2.511-2.573 0-1.271 1.178-2.45 2.511-2.45 1.333.001 2.511 1.148 2.511 2.45zm-4.744 6.728c0-1.488.931-2.481 2.232-2.481 1.302 0 2.232.992 2.232 2.481v11.906c0 1.488-.93 2.48-2.232 2.48s-2.232-.992-2.232-2.48V14.799z"/></svg>
|
After Width: | Height: | Size: 494 B |
@ -1,3 +1,4 @@
|
||||
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="32" height="32" fill="white"/>
|
||||
<mask id="path-1-inside-1_7717_245722" fill="white">
|
||||
<rect x="2" y="2" width="13" height="13" rx="2"/>
|
||||
@ -15,3 +16,4 @@
|
||||
<rect x="17" y="17" width="13" height="13" rx="2"/>
|
||||
</mask>
|
||||
<rect x="17" y="17" width="13" height="13" rx="2" stroke="#1F71D3" stroke-opacity="0.6" stroke-width="13" mask="url(#path-4-inside-4_7717_245722)"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.1 KiB |
@ -1,5 +1,7 @@
|
||||
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 16C3.58172 16 1.93129e-07 12.4183 0 8C-1.93129e-07 3.58172 3.58172 1.93129e-07 8 0H24C28.4183 -1.93129e-07 32 3.58172 32 8V24C32 28.4183 28.4183 32 24 32C19.5817 32 16 28.4183 16 24C16 19.5817 12.4183 16 8 16Z" fill="#9640DA" fill-opacity="0.3"/>
|
||||
<ellipse cx="24" cy="24" rx="4" ry="4" transform="rotate(-90 24 24)" fill="#9640DA"/>
|
||||
<ellipse cx="24" cy="8" rx="4" ry="4" transform="rotate(-90 24 8)" fill="#9640DA"/>
|
||||
<ellipse cx="8" cy="8" rx="4" ry="4" transform="rotate(-90 8 8)" fill="#9640DA"/>
|
||||
<circle cx="8" cy="24" r="5" transform="rotate(-90 8 24)" fill="#9640DA" fill-opacity="0.5"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 605 B After Width: | Height: | Size: 673 B |
@ -1,23 +0,0 @@
|
||||
/*
|
||||
In this file, one can define custom CSS rules for use in the documentation panel.
|
||||
The Tailwind CLI utility uses this file as an input file, and the content is copied to
|
||||
the final CSS stylesheet of the documentation panel.
|
||||
See the crate documentation to learn more.
|
||||
*/
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.enso-docs {
|
||||
font-family: "M PLUS 1", DejaVuSansMonoBook, sans-serif;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
svg {
|
||||
pointer-events: none;
|
||||
}
|
237
app/gui/view/documentation/assets/styles.css
Normal file
@ -0,0 +1,237 @@
|
||||
/* Custom CSS rules used by the documentation panel. */
|
||||
|
||||
/* Common parts. */
|
||||
|
||||
:root {
|
||||
--enso-docs-type-name-color: #9640da;
|
||||
--enso-docs-module-name-color: #a239e2;
|
||||
--enso-docs-methods-header-color: #1f71d3;
|
||||
--enso-docs-method-name-color: #1f71d3;
|
||||
--enso-docs-types-header-color: #1f71d3;
|
||||
--enso-docs-examples-header-color: #6da85e;
|
||||
--enso-docs-important-background-color: #edefe7;
|
||||
--enso-docs-info-background-color: #e6f1f8;
|
||||
--enso-docs-example-background-color: #e6f1f8;
|
||||
--enso-docs-background-color: #fcfeff;
|
||||
--enso-docs-text-color: #434343;
|
||||
--enso-docs-tag-background-color: #f5f5f5;
|
||||
--enso-docs-caption-background-color: #0077f6;
|
||||
}
|
||||
|
||||
.enso-docs {
|
||||
font-family: "M PLUS 1", DejaVuSansMonoBook, sans-serif;
|
||||
font-size: 11.5px;
|
||||
color: var(--enso-docs-text-color);
|
||||
background-color: var(--enso-docs-background-color);
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* It's better to use `.unordered-list` class here instead of `ul`, but it will break the keyed lists that a provided by the engine.
|
||||
* We need a separate way to represent keyed lists without embedded HTML tags. */
|
||||
.enso-docs ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
.enso-docs ul li:before {
|
||||
content: "•";
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.enso-docs ul li.type-item:before {
|
||||
color: var(--enso-docs-type-name-color);
|
||||
}
|
||||
|
||||
.enso-docs ul li.method-item:before {
|
||||
color: var(--enso-docs-method-name-color);
|
||||
}
|
||||
|
||||
.enso-docs .paragraph {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.enso-docs .link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.enso-docs .link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.enso-docs .method {
|
||||
color: var(--enso-docs-method-name-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.enso-docs .constructor {
|
||||
color: var(--enso-docs-type-name-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.enso-docs .type {
|
||||
color: var(--enso-docs-type-name-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.enso-docs .entry-name {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.enso-docs .arguments {
|
||||
opacity: 0.34;
|
||||
}
|
||||
|
||||
.enso-docs .section-content {
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
/* Headers. */
|
||||
|
||||
.enso-docs .header-top {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: var(--enso-docs-type-name-color);
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
div.enso-docs .header-icon {
|
||||
align-self: start;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.enso-docs .header-icon svg {
|
||||
pointer-events: none;
|
||||
width: 0.85em;
|
||||
height: 0.85em;
|
||||
margin: 0.1em 0.05em 0 0.05em;
|
||||
fill: none;
|
||||
align-self: flex-start;
|
||||
padding-top: 0.25em;
|
||||
}
|
||||
|
||||
.enso-docs .header-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.enso-docs .header-text {
|
||||
padding-left: 0.25em;
|
||||
}
|
||||
|
||||
.enso-docs .section-header {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin: 1rem 0 0.25rem 0;
|
||||
}
|
||||
|
||||
.enso-docs .methods-header {
|
||||
color: var(--enso-docs-methods-header-color);
|
||||
}
|
||||
|
||||
.enso-docs .types-header {
|
||||
color: var(--enso-docs-types-header-color);
|
||||
}
|
||||
|
||||
.enso-docs .examples-header {
|
||||
color: var(--enso-docs-examples-header-color);
|
||||
}
|
||||
|
||||
/* Marked sections, such as `Info` and `Important` sections. */
|
||||
|
||||
.enso-docs .background-info {
|
||||
background-color: var(--enso-docs-info-background-color);
|
||||
}
|
||||
|
||||
.enso-docs .background-important {
|
||||
background-color: var(--enso-docs-important-background-color);
|
||||
}
|
||||
|
||||
.enso-docs .marked-container {
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.enso-docs .marked-header {
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
div.enso-docs .marked-icon {
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
div.enso-docs .marked-icon-important {
|
||||
margin: 0 0 0 0;
|
||||
}
|
||||
|
||||
div.enso-docs .marked-icon-info {
|
||||
margin: 0 0.25em 0 0;
|
||||
}
|
||||
|
||||
.enso-docs .marked-icon svg {
|
||||
pointer-events: none;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
margin: 0 0.05em 0 0.05em;
|
||||
vertical-align: -0.1em;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
/* Examples. */
|
||||
|
||||
.enso-docs .example-container {
|
||||
background-color: var(--enso-docs-example-background-color);
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.enso-docs .example {
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
margin: 0.05rem 0.1rem;
|
||||
}
|
||||
|
||||
/* Tags */
|
||||
|
||||
.enso-docs .tags-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.enso-docs .tag {
|
||||
background-color: var(--enso-docs-tag-background-color);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.15rem 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* "Hover item preview" caption. */
|
||||
|
||||
.enso-docs-caption-container {
|
||||
background-color: var(--enso-docs-caption-background-color);
|
||||
border-top-left-radius: 14px;
|
||||
border-top-right-radius: 14px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.enso-docs-caption {
|
||||
font-family: "M PLUS 1", DejaVuSansMonoBook, sans-serif;
|
||||
color: white;
|
||||
font-size: 11.5px;
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
//! This script would run Tailwind CLI utility to generate a CSS stylesheet by scanning the
|
||||
//! source code for class names and including needed CSS rules in the output file.
|
||||
//!
|
||||
//! See crate documentation to learn more.
|
||||
|
||||
use ide_ci::prelude::*;
|
||||
|
||||
use ide_ci::programs::Npm;
|
||||
|
||||
|
||||
|
||||
/// The path to the input file. One can define arbitrary CSS rules there and they will be copied
|
||||
/// in the output file.
|
||||
const CSS_INPUT_PATH: &str = "assets/input.css";
|
||||
/// The filename of the resulting CSS stylesheet. It will be generated inside `OUT_DIR`.
|
||||
const CSS_OUTPUT_FILENAME: &str = "stylesheet.css";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result {
|
||||
// We should rerun the tailwind on changes in our sources.
|
||||
// Tailwind scans this directory to determine the used classes.
|
||||
println!("cargo:rerun-if-changed=src");
|
||||
// We should rerun the tailwind on changes in the input CSS file.
|
||||
// It may contain custom CSS rules.
|
||||
println!("cargo:rerun-if-changed={CSS_INPUT_PATH}");
|
||||
println!("cargo:rerun-if-changed=tailwind.config.js");
|
||||
|
||||
install_and_run_tailwind().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn install_and_run_tailwind() -> Result {
|
||||
Npm.cmd()?.install().run_ok().await?;
|
||||
let out_path = PathBuf::from(std::env::var("OUT_DIR").unwrap()).join(CSS_OUTPUT_FILENAME);
|
||||
let args: &[&str] = &["--", "-i", CSS_INPUT_PATH, "-o", out_path.as_str()];
|
||||
Npm.cmd()?.run("generate", args).run_ok().await?;
|
||||
Ok(())
|
||||
}
|
1334
app/gui/view/documentation/package-lock.json
generated
@ -1,11 +0,0 @@
|
||||
{
|
||||
"name": "enso-tailwind-wrapper",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"tailwindcss": "^3.2.4"
|
||||
},
|
||||
"scripts": {
|
||||
"generate": "tailwindcss"
|
||||
}
|
||||
}
|
@ -9,7 +9,6 @@ use enso_doc_parser::Mark;
|
||||
use enso_profiler as profiler;
|
||||
use enso_profiler::profile;
|
||||
use enso_suggestion_database::documentation_ir::BuiltinDocumentation;
|
||||
use enso_suggestion_database::documentation_ir::Constructors;
|
||||
use enso_suggestion_database::documentation_ir::Documentation;
|
||||
use enso_suggestion_database::documentation_ir::EntryDocumentation;
|
||||
use enso_suggestion_database::documentation_ir::Examples;
|
||||
@ -37,17 +36,13 @@ type Icon = &'static str;
|
||||
const ICON_TYPE: &str = include_str!("../assets/icon-type.svg");
|
||||
const ICON_METHODS: &str = include_str!("../assets/icon-methods.svg");
|
||||
const ICON_EXAMPLES: &str = include_str!("../assets/icon-examples.svg");
|
||||
/// A value for `viewBox` attribute of the SVG icon. Depends on the size of the icon exported from
|
||||
/// Figma.
|
||||
const ICON_VIEWBOX: &str = "0 0 32 32";
|
||||
const ICON_SVG_XMLNS: &str = "http://www.w3.org/2000/svg";
|
||||
const ICON_INFO: &str = include_str!("../assets/icon-info.svg");
|
||||
const ICON_IMPORTANT: &str = include_str!("../assets/icon-important.svg");
|
||||
|
||||
/// A single icon used in headers. `content` is an SVG code of the icon's content _without_ the
|
||||
/// surrounding `<svg>` tags.
|
||||
fn svg_icon(content: &'static str) -> impl Render {
|
||||
let class = "w-[12px] h-[12px] fill-none flex-shrink-0 mt-0.5";
|
||||
/// A single icon used in headers. `content` is an SVG code of the icon.
|
||||
fn svg_icon(content: &'static str, class: &'static str) -> impl Render {
|
||||
owned_html! {
|
||||
svg(class=class, viewBox=ICON_VIEWBOX, xmlns=ICON_SVG_XMLNS) {
|
||||
div(class=class) {
|
||||
:Raw(content)
|
||||
}
|
||||
}
|
||||
@ -120,7 +115,7 @@ fn render_documentation(docs: Documentation) -> String {
|
||||
/// Render the documentation of the virtual component group. Includes the name of the group.
|
||||
fn render_virtual_component_group_docs(name: ImString) -> String {
|
||||
let content = owned_html! {
|
||||
h1(class="text-2xl font-bold") {
|
||||
h1(class="header-top") {
|
||||
: &*name
|
||||
}
|
||||
};
|
||||
@ -149,33 +144,57 @@ struct BackLink {
|
||||
/// - Methods.
|
||||
/// - Examples.
|
||||
fn render_type_documentation(docs: &TypeDocumentation, back_link: Option<BackLink>) -> String {
|
||||
let constructors_exist = !docs.constructors.is_empty();
|
||||
let methods_exist = !docs.methods.is_empty();
|
||||
let examples_exist = !docs.examples.is_empty();
|
||||
let name = &docs.name;
|
||||
let arguments = &docs.arguments;
|
||||
let synopsis = &docs.synopsis;
|
||||
let constructors = &docs.constructors;
|
||||
let synopsis = section_content(type_synopsis(synopsis, constructors));
|
||||
let synopsis = section_content(type_synopsis(synopsis));
|
||||
let constructors = section_content(list_of_functions(constructors));
|
||||
let methods = section_content(list_of_functions(&docs.methods));
|
||||
let examples = section_content(list_of_examples(&docs.examples));
|
||||
let tags = section_content(list_of_tags(&docs.tags));
|
||||
let header_text = type_header(name.name(), arguments_list(arguments), back_link.as_ref());
|
||||
let header = header(ICON_TYPE, header_text, "header-top");
|
||||
|
||||
let content = owned_html! {
|
||||
: header(ICON_TYPE, type_header(name.name(), arguments_list(arguments), back_link.as_ref()));
|
||||
: &header;
|
||||
: &tags;
|
||||
: &synopsis;
|
||||
@ if constructors_exist {
|
||||
: constructors_header();
|
||||
: &constructors;
|
||||
}
|
||||
@ if methods_exist {
|
||||
: header(ICON_METHODS, methods_header());
|
||||
: methods_header();
|
||||
: &methods;
|
||||
}
|
||||
@ if examples_exist {
|
||||
: header(ICON_EXAMPLES, examples_header());
|
||||
: examples_header();
|
||||
: &examples;
|
||||
}
|
||||
};
|
||||
docs_content(content).into_string().unwrap()
|
||||
}
|
||||
|
||||
fn constructors_header() -> impl Render {
|
||||
header(ICON_METHODS, "Constructors", "section-header methods-header")
|
||||
}
|
||||
|
||||
fn methods_header() -> impl Render {
|
||||
header(ICON_METHODS, "Methods", "section-header methods-header")
|
||||
}
|
||||
|
||||
fn examples_header() -> impl Render {
|
||||
header(ICON_EXAMPLES, "Examples", "section-header examples-header")
|
||||
}
|
||||
|
||||
fn types_header() -> impl Render {
|
||||
header(ICON_METHODS, "Types", "section-header types-header")
|
||||
}
|
||||
|
||||
/// A header for the type documentation.
|
||||
fn type_header<'a>(
|
||||
name: &'a str,
|
||||
@ -184,76 +203,29 @@ fn type_header<'a>(
|
||||
) -> Box<dyn Render + 'a> {
|
||||
box_html! {
|
||||
@ if let Some(BackLink { id, displayed }) = &back_link {
|
||||
a(id=id, class="text-2xl font-bold text-typeName hover:underline cursor-pointer") {
|
||||
a(id=id, class="link") {
|
||||
: displayed;
|
||||
}
|
||||
: " :: ";
|
||||
}
|
||||
span(class="text-2xl font-bold text-typeName") {
|
||||
span { : name }
|
||||
span(class="opacity-34") { : &arguments }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A header for the "Methods" section.
|
||||
fn methods_header() -> impl Render {
|
||||
owned_html! {
|
||||
h1(class="text-xl font-semibold text-methodsHeader") {
|
||||
: "Methods"
|
||||
: ".";
|
||||
}
|
||||
: &name;
|
||||
span(class="arguments") { : &arguments }
|
||||
}
|
||||
}
|
||||
|
||||
/// A synopsis of the type. Contains a list of constructors, if it is not empty.
|
||||
fn type_synopsis<'a>(
|
||||
synopsis: &'a Synopsis,
|
||||
constructors: &'a Constructors,
|
||||
) -> Box<dyn Render + 'a> {
|
||||
fn type_synopsis<'a>(synopsis: &'a Synopsis) -> Box<dyn Render + 'a> {
|
||||
box_html! {
|
||||
@ for p in synopsis.iter() {
|
||||
: paragraph(p);
|
||||
}
|
||||
@ if !constructors.is_empty() {
|
||||
p {
|
||||
: "Constructors:"
|
||||
}
|
||||
}
|
||||
ul(class="list-disc list-outside marker:text-typeName") {
|
||||
@ for method in constructors.iter() {
|
||||
: single_constructor(method);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A documentation for a single constructor in the list.
|
||||
/// If the first [`DocSection`] is of type [`DocSection::Paragraph`], it is rendered on the first
|
||||
/// line, after the list of arguments.
|
||||
fn single_constructor<'a>(constructor: &'a Function) -> Box<dyn Render + 'a> {
|
||||
let first = match &constructor.synopsis.as_ref()[..] {
|
||||
[DocSection::Paragraph { body }, ..] => Some(body),
|
||||
_ => None,
|
||||
};
|
||||
box_html! {
|
||||
li(id=anchor_name(&constructor.name), class="hover:underline cursor-pointer") {
|
||||
span(class=labels!("text-typeName", "font-bold")) {
|
||||
span(class="opacity-85") {
|
||||
: constructor.name.name();
|
||||
}
|
||||
span(class="opacity-34") { : arguments_list(&constructor.arguments); }
|
||||
}
|
||||
@ if let Some(first) = first {
|
||||
span { : ", "; : Raw(first); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of methods defined for the type.
|
||||
fn list_of_functions<'a>(functions: &'a [Function]) -> Box<dyn Render + 'a> {
|
||||
box_html! {
|
||||
ul(class="list-disc list-inside") {
|
||||
ul(class="unordered-list") {
|
||||
@ for f in functions.iter() {
|
||||
: single_function(f);
|
||||
}
|
||||
@ -270,15 +242,13 @@ fn single_function<'a>(function: &'a Function) -> Box<dyn Render + 'a> {
|
||||
_ => None,
|
||||
};
|
||||
box_html! {
|
||||
li(id=anchor_name(&function.name), class="hover:underline cursor-pointer") {
|
||||
span(class=labels!("text-methodName", "font-semibold")) {
|
||||
span(class="opacity-85") {
|
||||
: function.name.name();
|
||||
}
|
||||
span(class="opacity-34") { : arguments_list(&function.arguments); }
|
||||
li(class="method-item") {
|
||||
a(id=anchor_name(&function.name), class="link method") {
|
||||
span(class="entry-name") { : function.name.name(); }
|
||||
span(class="arguments") { : arguments_list(&function.arguments); }
|
||||
}
|
||||
@ if let Some(first) = first {
|
||||
span { : ", "; : Raw(first); }
|
||||
: ": "; : Raw(first);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -306,19 +276,19 @@ fn render_module_documentation(docs: &ModuleDocumentation) -> String {
|
||||
let examples = section_content(list_of_examples(&docs.examples));
|
||||
let tags = section_content(list_of_tags(&docs.tags));
|
||||
let content = owned_html! {
|
||||
: header(ICON_TYPE, module_header(name.name()));
|
||||
: header(ICON_TYPE, name.name(), "header-top");
|
||||
: &tags;
|
||||
: &synopsis;
|
||||
@ if types_exist {
|
||||
: header(ICON_METHODS, types_header());
|
||||
: types_header();
|
||||
: &types;
|
||||
}
|
||||
@ if methods_exist {
|
||||
: header(ICON_METHODS, functions_header());
|
||||
: methods_header();
|
||||
: &methods;
|
||||
}
|
||||
@ if examples_exist {
|
||||
: header(ICON_EXAMPLES, examples_header());
|
||||
: examples_header();
|
||||
: &examples;
|
||||
}
|
||||
};
|
||||
@ -328,7 +298,7 @@ fn render_module_documentation(docs: &ModuleDocumentation) -> String {
|
||||
/// A list of types defined in the module.
|
||||
fn list_of_types<'a>(types: &'a Types) -> Box<dyn Render + 'a> {
|
||||
box_html! {
|
||||
ul(class="list-disc list-inside") {
|
||||
ul(class="unordered-list") {
|
||||
@ for type_ in types.iter() {
|
||||
: single_type(type_);
|
||||
}
|
||||
@ -339,11 +309,11 @@ fn list_of_types<'a>(types: &'a Types) -> Box<dyn Render + 'a> {
|
||||
/// A single type in the list.
|
||||
fn single_type<'a>(type_: &'a TypeDocumentation) -> Box<dyn Render + 'a> {
|
||||
box_html! {
|
||||
li(id=anchor_name(&type_.name), class="text-typeName font-semibold hover:underline cursor-pointer") {
|
||||
span(class="opacity-85") {
|
||||
: type_.name.name();
|
||||
li(class="type-item") {
|
||||
a(id=anchor_name(&type_.name), class="link type") {
|
||||
span(class="entry-name") { : type_.name.name(); }
|
||||
span(class="arguments") { : arguments_list(&type_.arguments); }
|
||||
}
|
||||
span(class="opacity-34") { : arguments_list(&type_.arguments); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -352,7 +322,7 @@ fn single_type<'a>(type_: &'a TypeDocumentation) -> Box<dyn Render + 'a> {
|
||||
fn list_of_examples<'a>(examples: &'a Examples) -> Box<dyn Render + 'a> {
|
||||
box_html! {
|
||||
@ for example in examples.iter() {
|
||||
div(class="bg-exampleBackground rounded p-3 mb-1") {
|
||||
div(class="example-container") {
|
||||
: Raw(example_from_doc_section(example));
|
||||
}
|
||||
}
|
||||
@ -363,40 +333,12 @@ fn list_of_examples<'a>(examples: &'a Examples) -> Box<dyn Render + 'a> {
|
||||
/// with a preformatted HTML code, but we need to modify some tags in order to properly style it.
|
||||
fn example_from_doc_section(doc_section: &DocSection) -> String {
|
||||
match doc_section {
|
||||
DocSection::Marked { mark: Mark::Example, body, .. } => body
|
||||
.replace("<pre>", "<div class=\"whitespace-pre overflow-x-auto py-2\">")
|
||||
.replace("</pre>", "</div>"),
|
||||
DocSection::Marked { mark: Mark::Example, body, .. } =>
|
||||
body.replace("<pre>", "<div class=\"example\">").replace("</pre>", "</div>"),
|
||||
_ => String::from("Invalid example"),
|
||||
}
|
||||
}
|
||||
|
||||
/// A header for the module documentation.
|
||||
fn module_header<'a>(name: &'a str) -> Box<dyn Render + 'a> {
|
||||
box_html! {
|
||||
h1(class="text-2xl font-bold text-moduleName") {
|
||||
: name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A header for the "Functions" section.
|
||||
fn functions_header() -> impl Render {
|
||||
owned_html! {
|
||||
h1(class="text-xl font-semibold text-methodsHeader") {
|
||||
: "Functions"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A header for the "Types" section.
|
||||
fn types_header() -> impl Render {
|
||||
owned_html! {
|
||||
h1(class="text-xl font-semibold text-typesHeader") {
|
||||
: "Types"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A synopsis of the module.
|
||||
fn module_synopsis<'a>(synopsis: &'a Synopsis) -> Box<dyn Render + 'a> {
|
||||
box_html! {
|
||||
@ -417,12 +359,14 @@ fn render_function_documentation(docs: &Function, back_link: Option<BackLink>) -
|
||||
let synopsis = section_content(function_synopsis(synopsis));
|
||||
let tags = section_content(list_of_tags(tags));
|
||||
let examples = section_content(list_of_examples(&docs.examples));
|
||||
let header_text = function_header(name.name(), arguments_list(arguments), back_link.as_ref());
|
||||
let header = header(ICON_TYPE, header_text, "header-top");
|
||||
let content = owned_html! {
|
||||
: header(ICON_TYPE, function_header(name.name(), arguments_list(arguments), back_link.as_ref()));
|
||||
: &header;
|
||||
: &tags;
|
||||
: &synopsis;
|
||||
@ if examples_exist {
|
||||
: header(ICON_EXAMPLES, examples_header());
|
||||
: examples_header();
|
||||
: &examples;
|
||||
}
|
||||
};
|
||||
@ -437,15 +381,13 @@ fn function_header<'a>(
|
||||
) -> Box<dyn Render + 'a> {
|
||||
box_html! {
|
||||
@ if let Some(BackLink { id, displayed }) = &back_link {
|
||||
a(id=id, class="text-2xl font-bold text-typeName hover:underline cursor-pointer") {
|
||||
a(id=id, class="link") {
|
||||
: displayed;
|
||||
}
|
||||
: " :: ";
|
||||
}
|
||||
span(class="text-2xl font-bold text-typeName") {
|
||||
span { : name }
|
||||
span(class="opacity-34") { : &arguments }
|
||||
: ".";
|
||||
}
|
||||
: name;
|
||||
span(class="arguments") { : &arguments }
|
||||
}
|
||||
}
|
||||
|
||||
@ -469,13 +411,15 @@ fn render_local_documentation(docs: &LocalDocumentation) -> String {
|
||||
let synopsis = section_content(local_synopsis(synopsis));
|
||||
let tags = section_content(list_of_tags(tags));
|
||||
let examples = section_content(list_of_examples(&docs.examples));
|
||||
let header_text = local_header(name.name(), return_type.name());
|
||||
let header = header(ICON_TYPE, header_text, "header-top");
|
||||
|
||||
let content = owned_html! {
|
||||
: header(ICON_TYPE, local_header(name.name(), return_type.name()));
|
||||
: &header;
|
||||
: &tags;
|
||||
: &synopsis;
|
||||
@ if examples_exist {
|
||||
: header(ICON_EXAMPLES, examples_header());
|
||||
: examples_header();
|
||||
: &examples;
|
||||
}
|
||||
};
|
||||
@ -485,11 +429,8 @@ fn render_local_documentation(docs: &LocalDocumentation) -> String {
|
||||
/// A header for the local documentation.
|
||||
fn local_header<'a>(name: &'a str, return_type: &'a str) -> Box<dyn Render + 'a> {
|
||||
box_html! {
|
||||
span(class="text-2xl font-bold") {
|
||||
span(class="text-type") { : name }
|
||||
: " ";
|
||||
span(class="text-arguments") { : return_type }
|
||||
}
|
||||
: name; : " ";
|
||||
span(class="arguments") { : return_type }
|
||||
}
|
||||
}
|
||||
|
||||
@ -526,7 +467,7 @@ fn render_builtin_documentation(docs: &BuiltinDocumentation) -> String {
|
||||
/// class.
|
||||
fn docs_content(content: impl Render) -> impl Render {
|
||||
owned_html! {
|
||||
div(class="enso-docs text-docsText text-base bg-docsBackground pl-4 pr-2") {
|
||||
div(class="enso-docs") {
|
||||
: &content;
|
||||
}
|
||||
}
|
||||
@ -534,18 +475,18 @@ fn docs_content(content: impl Render) -> impl Render {
|
||||
|
||||
fn section_content(content: impl Render) -> impl Render {
|
||||
owned_html! {
|
||||
div(class="pl-5") {
|
||||
div(class="section-content") {
|
||||
: &content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic header. Contains an icon on the left followed by an arbitrary content.
|
||||
fn header(icon: Icon, content: impl Render) -> impl Render {
|
||||
fn header(icon: Icon, content: impl Render, class: &'static str) -> impl Render {
|
||||
owned_html! {
|
||||
div(class="flex flex-row items-center my-2") {
|
||||
: svg_icon(icon);
|
||||
div(class="ml-2") {
|
||||
div(class=labels!("header-container", class)) {
|
||||
: svg_icon(icon, "header-icon");
|
||||
div(class="header-text") {
|
||||
: &content;
|
||||
}
|
||||
}
|
||||
@ -555,33 +496,20 @@ fn header(icon: Icon, content: impl Render) -> impl Render {
|
||||
/// List of arguments of the type or function.
|
||||
fn arguments_list<'a>(arguments: &'a [Argument]) -> Box<dyn Render + 'a> {
|
||||
box_html! {
|
||||
span {
|
||||
@ for arg in arguments {
|
||||
: single_argument(arg);
|
||||
}
|
||||
@ for arg in arguments {
|
||||
: " ";
|
||||
: single_argument(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A single argument of the type or function. May contain default value.
|
||||
fn single_argument(argument: &Argument) -> impl Render {
|
||||
fn single_argument(argument: &Argument) -> String {
|
||||
let Argument { name, default_value, .. } = argument;
|
||||
let text = if let Some(default_value) = default_value {
|
||||
if let Some(default_value) = default_value {
|
||||
format!("{name} = {default_value},")
|
||||
} else {
|
||||
name.to_string()
|
||||
};
|
||||
owned_html! {
|
||||
span { : " "; : &text; }
|
||||
}
|
||||
}
|
||||
|
||||
/// A header for the "Examples" section.
|
||||
fn examples_header() -> impl Render {
|
||||
owned_html! {
|
||||
h1(class="text-xl font-semibold text-examplesHeader") {
|
||||
: "Examples"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -591,38 +519,39 @@ fn paragraph<'a>(doc_section: &'a DocSection) -> Box<dyn Render + 'a> {
|
||||
match doc_section {
|
||||
DocSection::Keyed { key, body } => {
|
||||
box_html! {
|
||||
p { : Raw(key); : ": "; }
|
||||
p(class="paragraph") { : Raw(key); : ": "; }
|
||||
: Raw(body);
|
||||
}
|
||||
}
|
||||
DocSection::Paragraph { body } => {
|
||||
box_html! {
|
||||
p { : Raw(body); }
|
||||
p(class="paragraph") { : Raw(body); }
|
||||
}
|
||||
}
|
||||
DocSection::Marked { mark, header, body } => {
|
||||
let background_color = match mark {
|
||||
Mark::Important => "bg-importantBackground",
|
||||
Mark::Info => "bg-infoBackground",
|
||||
Mark::Important => "background-important",
|
||||
Mark::Info => "background-info",
|
||||
_ => "",
|
||||
};
|
||||
let mark = match mark {
|
||||
Mark::Important => String::from("!"),
|
||||
Mark::Info => String::from("ℹ"),
|
||||
_ => String::from("Unexpected mark."),
|
||||
let mark: Box<dyn Render> = match mark {
|
||||
Mark::Important =>
|
||||
Box::new(svg_icon(ICON_IMPORTANT, "marked-icon marked-icon-important")),
|
||||
Mark::Info => Box::new(svg_icon(ICON_INFO, "marked-icon marked-icon-info")),
|
||||
_ => Box::new(String::from("Unexpected mark.")),
|
||||
};
|
||||
box_html! {
|
||||
div(class=labels!(background_color, "rounded-lg", "p-2", "my-2")) {
|
||||
p(class="text-lg") {
|
||||
span(class="font-bold") { : &mark; }
|
||||
div(class=labels!(background_color, "marked-container")) {
|
||||
div(class="marked-header") {
|
||||
: &mark;
|
||||
: " "; : header;
|
||||
}
|
||||
p { : Raw(body); }
|
||||
p(class="paragraph") { : Raw(body); }
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => box_html! {
|
||||
p { : "Unexpected doc section type." }
|
||||
p(class="paragraph") { : "Unexpected doc section type." }
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -630,7 +559,7 @@ fn paragraph<'a>(doc_section: &'a DocSection) -> Box<dyn Render + 'a> {
|
||||
/// A list of tags.
|
||||
fn list_of_tags<'a>(tags: &'a [Tag]) -> Box<dyn Render + 'a> {
|
||||
box_html! {
|
||||
div(class="flex flex-row flex-wrap") {
|
||||
div(class="tags-container") {
|
||||
@ for tag in tags {
|
||||
: single_tag(tag);
|
||||
}
|
||||
@ -641,7 +570,7 @@ fn list_of_tags<'a>(tags: &'a [Tag]) -> Box<dyn Render + 'a> {
|
||||
/// A single tag in the list.
|
||||
fn single_tag<'a>(tag: &'a Tag) -> Box<dyn Render + 'a> {
|
||||
box_html! {
|
||||
div(class="bg-tagBackground rounded-lg px-2 py-1 mr-2 mb-1") {
|
||||
div(class="tag") {
|
||||
: &*tag.name;
|
||||
@if !tag.body.is_empty() {
|
||||
: "=";
|
||||
@ -666,9 +595,8 @@ pub fn anchor_name(name: &QualifiedName) -> String {
|
||||
/// "Hovered item preview" caption on top of the documentation panel.
|
||||
pub fn caption_html() -> String {
|
||||
owned_html! {
|
||||
div(class="bg-captionBackground rounded-t-[14px] w-full h-full flex \
|
||||
items-center justify-center") {
|
||||
div(class="text-base text-white") {
|
||||
div(class="enso-docs-caption-container") {
|
||||
div(class="enso-docs-caption") {
|
||||
: "Hovered item preview. Press the right mouse button to lock it.";
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,5 @@
|
||||
//! Documentation view visualization generating and presenting Enso Documentation under
|
||||
//! the documented node.
|
||||
//!
|
||||
//! # Tailwind CSS
|
||||
//!
|
||||
//! This crate uses the [`Tailwind CSS`] framework to style the HTML code displayed inside the
|
||||
//! documentation panel. [`Tailwind CSS`] is a utility-first CSS framework packed with classes like
|
||||
//! `flex`, `w-1/2`, `h-32`, or `bg-gray-200`. It allows for defining any visual style by combining
|
||||
//! these classes. The `build.rs` script runs the [`Tailwind CSS`] utility to generate a
|
||||
//! CSS stylesheet by scanning the source code for class names and including needed CSS rules in the
|
||||
//! output file. It means one can set `Tailwind` classes for any DOM element, and the stylesheet
|
||||
//! will automatically update with needed CSS rules.
|
||||
//!
|
||||
//! The build script runs `npx tailwindcss`, so one should have [Node.js] installed. Installing the
|
||||
//! `Tailwind` utility is not strictly required because the `npx` would download it
|
||||
//! automatically if needed.
|
||||
//!
|
||||
//! [`Tailwind CSS`]: https://tailwindcss.com/
|
||||
|
||||
#![recursion_limit = "1024"]
|
||||
// === Features ===
|
||||
@ -168,7 +152,7 @@ impl Model {
|
||||
|
||||
/// Add `<style>` tag with the stylesheet to the `outer_dom`.
|
||||
fn load_css_stylesheet(&self) {
|
||||
let stylesheet = include_str!(concat!(env!("OUT_DIR"), "/stylesheet.css"));
|
||||
let stylesheet = include_str!("../assets/styles.css");
|
||||
let element = web::document.create_element_or_panic("style");
|
||||
element.set_inner_html(stylesheet);
|
||||
self.outer_dom.append_or_warn(&element);
|
||||
|
@ -1,34 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['src/**/*.rs'],
|
||||
theme: {
|
||||
fontSize: {
|
||||
base: '11.5px',
|
||||
lg: '13px',
|
||||
xl: '15px',
|
||||
'2xl': '17px',
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
typeName: '#9640da',
|
||||
moduleName: '#a239e2',
|
||||
methodsHeader: '#1f71d3',
|
||||
methodName: '#1f71d3',
|
||||
typesHeader: '#1f71d3',
|
||||
examplesHeader: '#6da85e',
|
||||
importantBackground: '#edefe7',
|
||||
infoBackground: '#e6f1f8',
|
||||
exampleBackground: '#e6f1f8',
|
||||
docsBackground: '#fcfeff',
|
||||
docsText: '#434343',
|
||||
tagBackground: '#f5f5f5',
|
||||
captionBackground: '#0077f6',
|
||||
},
|
||||
opacity: {
|
||||
85: '.85',
|
||||
34: '.34',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
@ -95,7 +95,7 @@ fn database() -> SuggestionDatabase {
|
||||
Standard.Base {
|
||||
#[with_doc_section(doc_section!("Maybe type."))]
|
||||
#[with_doc_section(doc_section!(@ "Annotated", ""))]
|
||||
type Maybe (a) {
|
||||
type Delimited_Format (a) {
|
||||
#[with_doc_section(doc_section!("Some constructor."))]
|
||||
#[with_doc_section(doc_section!(> "Example", "Some 1"))]
|
||||
#[with_doc_section(doc_section!("Documentation for the Some(a) constructor."))]
|
||||
@ -109,7 +109,7 @@ fn database() -> SuggestionDatabase {
|
||||
fn is_some(self) -> Standard.Base.Boolean;
|
||||
|
||||
#[with_doc_section(doc_section!("Documentation for the Maybe.map() method."))]
|
||||
fn map (f) -> Standard.Base.Maybe;
|
||||
fn comment_all_characters (self) -> Standard.Base.Maybe;
|
||||
}
|
||||
|
||||
#[with_doc_section(doc_section!("Documentation for the foo method."))]
|
||||
|