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
This commit is contained in:
Ilya Bogdanov 2023-06-22 10:32:13 +03:00 committed by GitHub
parent e91b162027
commit e18ffb7ca0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 358 additions and 1631 deletions

View 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.

View File

@ -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

View 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

View 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

View File

@ -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

View File

@ -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

View File

@ -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;
}

View 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;
}

View File

@ -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(())
}

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +0,0 @@
{
"name": "enso-tailwind-wrapper",
"version": "1.0.0",
"dependencies": {},
"devDependencies": {
"tailwindcss": "^3.2.4"
},
"scripts": {
"generate": "tailwindcss"
}
}

View File

@ -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.";
}
}

View File

@ -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);

View File

@ -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: [],
}

View File

@ -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."))]