-
Encapsulated
+
@@ -85,8 +85,8 @@
-
Simple
+
@@ -98,8 +98,8 @@
-
Modern
+
@@ -113,8 +113,8 @@
+
+
-
Composable
+
@@ -144,7 +144,7 @@
- Ready to get going? Engage!
Get started → diff --git a/docs/components/NavExtra.jinja b/docs/components/NavExtra.jinja index dcd113f..e6bb984 100644 --- a/docs/components/NavExtra.jinja +++ b/docs/components/NavExtra.jinja @@ -1,3 +1 @@ - - {% include "svg/github.svg" %} - \ No newline at end of file + \ No newline at end of file diff --git a/docs/components/ui/Accordion/DemoResult.jinja b/docs/components/ui/Accordion/DemoResult.jinja index 9ab8b9e..8666e2d 100644 --- a/docs/components/ui/Accordion/DemoResult.jinja +++ b/docs/components/ui/Accordion/DemoResult.jinja @@ -2,9 +2,10 @@ -Is it accessible?
diff --git a/docs/components/ui/Dialog/DemoPreview.jinja b/docs/components/ui/Dialog/DemoPreview.jinja
index b994685..e69de29 100644
--- a/docs/components/ui/Dialog/DemoPreview.jinja
+++ b/docs/components/ui/Dialog/DemoPreview.jinja
@@ -1,4 +0,0 @@
-
-
diff --git a/docs/components/ui/LinkedList/DemoResult.jinja b/docs/components/ui/LinkedList/DemoResult.jinja
index c689671..e3d9631 100644
--- a/docs/components/ui/LinkedList/DemoResult.jinja
+++ b/docs/components/ui/LinkedList/DemoResult.jinja
@@ -2,9 +2,10 @@
-
-
-
+
diff --git a/docs/components/ui/Menu/DemoResult.jinja b/docs/components/ui/Menu/DemoResult.jinja
index bf2165b..b62226c 100644
--- a/docs/components/ui/Menu/DemoResult.jinja
+++ b/docs/components/ui/Menu/DemoResult.jinja
@@ -2,9 +2,10 @@
-
+
Menu keyboard_arrow_down
diff --git a/docs/components/ui/Popover/DemoResult.jinja b/docs/components/ui/Popover/DemoResult.jinja
index d878f2b..0bf47cb 100644
--- a/docs/components/ui/Popover/DemoResult.jinja
+++ b/docs/components/ui/Popover/DemoResult.jinja
@@ -2,9 +2,10 @@
-
+
Open popover
diff --git a/docs/components/ui/Tabs/DemoResult.jinja b/docs/components/ui/Tabs/DemoResult.jinja
index 0b0f54f..3306eac 100644
--- a/docs/components/ui/Tabs/DemoResult.jinja
+++ b/docs/components/ui/Tabs/DemoResult.jinja
@@ -2,9 +2,10 @@
-
+
Recent
diff --git a/docs/components/ui/Tabs/ManualResult.jinja b/docs/components/ui/Tabs/ManualResult.jinja
index c916b5c..2a067bd 100644
--- a/docs/components/ui/Tabs/ManualResult.jinja
+++ b/docs/components/ui/Tabs/ManualResult.jinja
@@ -2,9 +2,10 @@
-
+
Recent
diff --git a/docs/components/ui/Tabs/SelectResult.jinja b/docs/components/ui/Tabs/SelectResult.jinja
index 81dd33b..ffdf0ea 100644
--- a/docs/components/ui/Tabs/SelectResult.jinja
+++ b/docs/components/ui/Tabs/SelectResult.jinja
@@ -2,9 +2,10 @@
-
+
Recent
diff --git a/docs/components/ui/Tabs/VerticalResult.jinja b/docs/components/ui/Tabs/VerticalResult.jinja
index af2b703..b83204a 100644
--- a/docs/components/ui/Tabs/VerticalResult.jinja
+++ b/docs/components/ui/Tabs/VerticalResult.jinja
@@ -2,9 +2,10 @@
-
+
Recent
diff --git a/docs/content/api.md b/docs/content/api.md
new file mode 100644
index 0000000..b2eaf50
--- /dev/null
+++ b/docs/content/api.md
@@ -0,0 +1,27 @@
+---
+title: "API reference"
+---
+
+
+
+
+
+
+----
+
+
+
+
+
+----
+
+## Exceptions
+
+
+
+
+
+
+
+
+
diff --git a/docs/content/guide/components.md b/docs/content/guide/components.md
index f67d1ce..c4a1b29 100644
--- a/docs/content/guide/components.md
+++ b/docs/content/guide/components.md
@@ -1,24 +1,86 @@
---
title: Components
-description: The components are `.jinja` files with snippets of template code. They look like a fragment of a regular Jinja template – and they could be – except for the optional special comments at the beginning of the file.
+description: All about declaring and using components.
---
-The components are `.jinja` files with snippets of template code.
-They look like a fragment of a regular Jinja template – and they could be – except for the optional special comments at the beginning of the file.
+## Declaring and Using Components
-
-
-
+The components are simple text files that look like regular Jinja templates, with three requirements:
+
+**First**, components must be placed inside a folder registered in the catalog or a subfolder of it.
+
+```python
+catalog.add_folder("myapp/components")
+```
+
+You can call that folder whatever you want, not just "components". You can also add more than one folder:
+
+```python
+catalog.add_folder("myapp/layouts")
+catalog.add_folder("myapp/components")
+```
+
+If you end up having more than one component with the same name, the one in the first folder will take priority.
+
+**Second**, they must have a ".jinja" extension. This also helps code editors automatically select the correct language syntax to highlight. However, you can configure it in the catalog.
+
+**Third**, the component name must start with an uppercase letter. Why? This is how JinjaX differentiates a component from a regular HTML tag when using it. I recommend using PascalCase names, like Python classes.
+
+The name of the file (minus the extension) is also how you call the component. For example, if the file is "components/PersonForm.jinja":
+
+```
+└ myapp/
+ ├── app.py
+ ├── components/
+ └─ PersonForm.jinja
+```
+
+The name of the component is "PersonForm" and can be called like this:
+
+From Python code or a non-component template:
+
+- `catalog.render("PersonForm")`
+
+From another component:
+
+- ` some content `, or
+- ` `
+
+If the component is in a subfolder, the name of that folder becomes part of its name too:
+
+```
+└ myapp/
+ ├── app.py
+ ├── components/
+ └─ person
+ └─ PersonForm.jinja
+```
+
+A "components/person/PersonForm.jinja" component is named "person.PersonForm", meaning the name of the subfolder and the name of the file separated by a dot. This is the full name you use to call it:
+
+From Python code or a non-component template:
+
+- `catalog.render("person.PersonForm")`
+
+From another component:
+
+- ` some content `, or
+- ` `
+
+Notice how the folder name doesn't need to start with an uppercase if you don't want it to.
+
+
+
-## Component Arguments
+## Taking Arguments
-More often than not, a component takes one or more arguments to render. Every argument must be declared at the beginning of the component with `{#def arguments #}`. The syntax is very similar to how you declare the arguments of a python function:
+More often than not, a component takes one or more arguments to render. Every argument must be declared at the beginning of the component with `{#def arg1, arg2, ... #}`.
-```html+jinja title="components/Form.jinja"
-{#def action, method='post', multipart=False #}
+```html+jinja
+{#def action, method="post", multipart=False #}
-
+
+
+
+ ...
```
-The values of the declared arguments can be used in the template as values with the same name.
+### Expressions
+
+There are two different but equivalent ways to pass non-string arguments:
+"Jinja-like", where you use double curly braces instead of quotes:
-### Components with content
+```html+jinja title="Jinja-like"
+
+```
-There is actually always an extra implicit argument: the content inside the component. This could be anything: text, HTML, and/or other components; but the component recieves it already rendered to a string.
+... and "Vue-like", where you keep using quotes, but prefix the name of the attribute with a colon:
+
+```html+jinja title="Vue-like"
+
+```
+
+
+ For `True` values, you can just use the name, like in HTML:
+
+ ```html+jinja +
+ ```
+
+
+
+ You can also use dashes when passing an argument, but they will be translated to underscores:
+
+ ```html+jinja +
+ ```
+
+ ```html+jinja title="Example.jinja" + {#def aria_label = "" #} + ... + ``` +
+
+## With Content
+
+There is always an extra implicit argument: **the content** inside the component. This could be anything: text, HTML, and/or other components; but when the component receives it, it is already rendered to a string.
```html+jinja
{# Component with content #}
@@ -62,7 +187,7 @@ A great use case of the `content` is to make layout components:
}"
/>
-Everything between the open and close tags of the components will be rendered and passed to the `Layout` component as a implicit, `content` variable.
+Everything between the open and close tags of the components will be rendered and passed to the `Layout` component as an implicit `content` variable.
To test a component in isolation, you can also manually send a content argument using the special `__content` argument:
@@ -70,6 +195,179 @@ To test a component in isolation, you can also manually send a content argument
catalog.render("PageLayout", title="Hello world", __content="TEST")
```
-### Extra arguments
+## Extra Arguments
+
+If you pass arguments not declared in a component, those are not discarded but rather collected in an `attrs` object.
+
+You then call `attrs.render()` to render the received arguments as HTML attributes.
+
+For example, this component:
+
+```html+jinja title="Card.jinja"
+{#def title #}
+bla
+```
+
+Will be rendered as:
+
+```html
+
+The string values passed into components as attrs are not cast to `str` until the string representation is **actually** needed, for example when `attrs.render()` is invoked.
+
+
+### `attrs` Methods
+
+#### `.render(name=value, ...)`
+
+Renders the attributes and properties as a string.
+
+Any arguments you use with this function are merged with the existing
+attibutes/properties by the same rules as the `HTMLAttrs.set()` function:
+
+- Pass a name and a value to set an attribute (e.g. `type="text"`)
+- Use `True` as a value to set a property (e.g. `disabled`)
+- Use `False` to remove an attribute or property
+- The existing attribute/property is overwritten **except** if it is `class`.
+ The new classes are appended to the old ones instead of replacing them.
+- The underscores in the names will be translated automatically to dashes,
+ so `aria_selected` becomes the attribute `aria-selected`.
+
+To provide consistent output, the attributes and properties
+are sorted by name and rendered like this:
+` + `.
+
+```html+jinja
+
+```
+```html+jinja
+
+ ```html+jinja +
+ ```html+jinja +
+ ```html+jinja title="Example.jinja" + {#def aria_label = "" #} + ... + ``` +
+
+```
+
+Called as:
+
+```html
+{{ title }}
+ {{ content }} +
+
+```
+
+You can add or remove arguments before rendering them using the other methods of the `attrs` object. For example:
+
+```html+jinja
+{#def title #}
+{% do attrs.set(id="mycard") -%}
+
+Products
+ bla +
+
+```
+
+Or directly in the `attrs.render()` call:
+
+```html+jinja
+{#def title #}
+
+{{ title }}
+ {{ content }} +
+
+```
+
+{{ title }}
+ {{ content }} +
+
+
+
+
+```
+
+
+Using `` to pass the extra arguments to other components **WILL NOT WORK**. That is because the components are translated to macros before the page render.
+
+You must pass them as the special argument `__attrs`.
+
+```html+jinja
+{#--- WRONG 😵 ---#}
+
+
+{#--- GOOD 👍 ---#}
+
+
+```
+
+
+#### `.set(name=value, ...)`
+
+Sets an attribute or property
+
+- Pass a name and a value to set an attribute (e.g. `type="text"`)
+- Use `True` as a value to set a property (e.g. `disabled`)
+- Use `False` to remove an attribute or property
+- If the attribute is "class", the new classes are appended to
+ the old ones (if not repeated) instead of replacing them.
+- The underscores in the names will be translated automatically to dashes,
+ so `aria_selected` becomes the attribute `aria-selected`.
+
+```html+jinja title="Adding attributes/properties"
+{% do attrs.set(
+ id="loremipsum",
+ disabled=True,
+ data_test="foobar",
+ class="m-2 p-4",
+) %}
+```
+
+```html+jinja title="Removing attributes/properties"
+{% do attrs.set(
+ title=False,
+ disabled=False,
+ data_test=False,
+ class=False,
+) %}
+```
+
+#### `.setdefault(name=value, ...)`
+
+Adds an attribute, but only if it's not already present.
+
+The underscores in the names will be translated automatically to dashes, so `aria_selected`
+becomes the attribute `aria-selected`.
+
+```html+jinja
+{% do attrs.setdefault(
+ aria_label="Products"
+) %}
+```
+
+#### `.add_class(name1, name2, ...)`
+
+Adds one or more classes to the list of classes, if not already present.
+
+```html+jinja
+{% do attrs.add_class("hidden") %}
+{% do attrs.add_class("active", "animated") %}
+```
+
+#### `.remove_class(name1, name2, ...)`
+
+Removes one or more classes from the list of classes.
+
+```html+jinja
+{% do attrs.remove_class("hidden") %}
+{% do attrs.remove_class("active", "animated") %}
+```
+
+#### `.get(name, default=None)`
+
+Returns the value of the attribute or property,
+or the default value if it doesn't exist.
+
+```html+jinja
+{%- set role = attrs.get("role", "tab") %}
+```
-If you pass arguments not declared in a component, those are not discarded, but rather collected in a `attrs` object. Read more about it in the next section.
+...
\ No newline at end of file
diff --git a/docs/content/guide/css_and_js.md b/docs/content/guide/css_and_js.md
index fd83ef3..8e296e2 100644
--- a/docs/content/guide/css_and_js.md
+++ b/docs/content/guide/css_and_js.md
@@ -6,23 +6,19 @@ description: Your components might need custom styles or custom JavaScript for m
Your components might need custom styles or custom JavaScript for many reasons.
-
-Instead of using global stylesheet or script files, writing assets for each individual
-component has several advantages:
+Instead of using global stylesheet or script files, writing assets for each individual component has several advantages:
- **Portability**: You can copy a component from one project to another, knowing it will keep working as expected.
-- **Performance**: Only load the CSS and JS that you need on each page. Also, the browser will already have cached the assets of the components for other pages that use them.
+- **Performance**: Only load the CSS and JS that you need on each page. Additionally, the browser will have already cached the assets of the components for other pages that use them.
- **Simple testing**: You can test the JS of a component independently from others.
-
## Auto-loading assets
-JinjaX searches for `.css` and `.js` files with the same name as your component in the same folder and automatically adds them to the list of assets that will be included in the page. For example, if your component is `components/common/Form.jinja`, both `components/common/Form.css` and `components/common/Form.js` will be added to the list, but only if those files exist.
-
+JinjaX searches for `.css` and `.js` files with the same name as your component in the same folder and automatically adds them to the list of assets included on the page. For example, if your component is `components/common/Form.jinja`, both `components/common/Form.css` and `components/common/Form.js` will be added to the list, but only if those files exist.
## Manually declaring assets
-In addition to auto-loading assets, the CSS and/or the JS of a component can be declared in the metadata header with `{#css ... #}` and `{#js ... #}`.
+In addition to auto-loading assets, the CSS and/or JS of a component can be declared in the metadata header with `{#css ... #}` and `{#js ... #}`.
```html
{#css lorem.css, ipsum.css #}
@@ -31,23 +27,15 @@ In addition to auto-loading assets, the CSS and/or the JS of a component can be
- The file paths must be relative to the root of your components catalog (e.g., `components/form.js`) or absolute (e.g., `http://example.com/styles.css`).
- Multiple assets must be separated by commas.
-- Only **one** `{#css ... #}` and **one** `{#js ... #}` tag is allowed per component at most,
- but both are optional.
-
+- Only **one** `{#css ... #}` and **one** `{#js ... #}` tag is allowed per component at most, but both are optional.
### Global assets
-The best practice is to store both CSS and JS files of the component within the same folder.
-Doing this has several advantages, including easier component reuse in other
-projects, improved code readability, and simplified debugging.
+The best practice is to store both CSS and JS files of the component within the same folder. Doing this has several advantages, including easier component reuse in other projects, improved code readability, and simplified debugging.
-However, there are instances when you may need to rely on global CSS or JS files,
-such as third-party libraries. In such cases, you can specify these dependencies
-in the component's metadata using URLs that start with either
-"/", "http://," or "https://."
+However, there are instances when you may need to rely on global CSS or JS files, such as third-party libraries. In such cases, you can specify these dependencies in the component's metadata using URLs that start with either "/", "http://," or "https://."
-When you do this, JinjaX will render them as is; instead of prepending them
-with the component's prefix like it normally does.
+When you do this, JinjaX will render them as is, instead of prepending them with the component's prefix like it normally does.
For example, this code:
@@ -68,7 +56,6 @@ will be rendered as this HTML output:
```
-
## Including assets in your pages
The catalog will collect all CSS and JS file paths from the components used on a "page" render on the `catalog.collected_css` and `catalog.collected_js` lists.
@@ -92,7 +79,7 @@ For example, after rendering this component:
```
-Assuming the `Card`, and `Button` components declare CSS assets, this will be the state of the `collected_css` list:
+Assuming the `Card` and `Button` components declare CSS assets, this will be the state of the `collected_css` list:
```py
catalog.collected_css
@@ -130,9 +117,9 @@ The variable will be rendered as:
## Middleware
-The tags above will not work at all if your application can't return the content of those files, and right now it can't.
+The tags above will not work if your application can't return the content of those files. Currently, it can't.
-For that reason, JinjaX includes a WSGI middleware that will process those URLs if you add it to your application.
+For that reason, JinjaX includes WSGI middleware that will process those URLs if you add it to your application.
```py
from flask import Flask
@@ -154,17 +141,16 @@ app.wsgi_app = catalog.get_middleware(
The middleware uses the battle-tested [Whitenoise library](http://whitenoise.evans.io/) and will only respond to the *.css* and *.js* files inside the component(s) folder(s). You can configure it to also return files with other extensions. For example:
```python
-catalog.get_middleware(app, allowed_ext=[".css", ".js", ".svg", ".png"])
+catalog.get_middleware(app, allowed_ext=[".css", .js", .svg", ".png"])
```
-Be aware that, if you use this option, `get_middleware()` must be called **after** all folders are added.
-
+Be aware that if you use this option, `get_middleware()` must be called **after** all folders are added.
## Good practices
### CSS Scoping
-The styles of your components will not be auto-scoped. This means the styles of a component can affect other components, and, likewise, it will be affected by global styles or the styles of other components.
+The styles of your components will not be auto-scoped. This means the styles of a component can affect other components and likewise, it will be affected by global styles or the styles of other components.
To protect yourself against that, *always* add a custom class to the root element(s) of your component and use it to scope the rest of the component styles.
@@ -218,7 +204,6 @@ a { color: blue; }
Always use a class **instead of** an `id`, or the component will not be usable more than once per page.
-
### JS events
Your components might be inserted in the page on-the-fly, after the JavaScript files have been loaded and executed. So, attaching events to the elements on the page on load will not be enough:
@@ -226,7 +211,7 @@ Your components might be inserted in the page on-the-fly, after the JavaScript f
```js title="components/card.js"
// This will fail for any Card component inserted after page load
document.querySelectorAll('.Card button.share')
- .forEach( (node) => {
+ .forEach((node) => {
node.addEventListener("click", handleClick)
})
diff --git a/docs/content/guide/extra.md b/docs/content/guide/extra.md
deleted file mode 100644
index 49c5541..0000000
--- a/docs/content/guide/extra.md
+++ /dev/null
@@ -1,170 +0,0 @@
----
-title: Extra Arguments
-description: If you pass arguments not declared in a component, those are not discarded, but rather collected in a `attrs` object that can render these extra arguments calling `attrs.render()`
----
-
-
-If you pass arguments not declared in a component, those are not discarded, but rather collected in a `attrs` object that can render these extra arguments calling `attrs.render()`
-
-
-For example, this component:
-
-```html+jinja title="components/Card.jinja"
-{#def title #}
-bla
-```
-
-Will be rendered as:
-
-```html
- + `.
-
-```html+jinja
-
-```
-
-
-Using `` to pass the extra arguments to other components **WILL NOT WORK**. That is because the components are translated to macros before the page render.
-
-You must pass them as the special argument `__attrs`.
-
-```html+jinja
-{#--- WRONG 😵 ---#}
-
-
-{#--- GOOD 👍 ---#}
-
-```
-
-
-
-### `.set(name=value, ...)`
-
-Sets an attribute or property:
-
-- Pass a name and a value to set an attribute (e.g. `type="text"`)
-- Use `True` as value to set a property (e.g. `disabled`)
-- Use `False` to remove an attribute or property
-
-The underscores in the names will be translated automatically to dashes, so `aria_selected`
-becomes the attribute `aria-selected`.
-
-The current attribute/property are overwritten **except** if is "class" or "classes".
-In those cases, the new classes are appended to the old ones instead of replacing them.
-
-
-#### Adding attributes/properties
-
-```html+jinja
-{% do attrs.set(
- id="loremipsum",
- disabled=True,
- data_test="foobar",
- class="m-2 p-4",
-) %}
-```
-
-#### Removing attributes/properties
-
-```html+jinja
-{% do attrs.set(
- title=False,
- disabled=False,
- data_test=False,
- class=False,
-) %}
-```
-
-
-### `.setdefault(name=value, ...)`
-
-Adds an attribute or sets a property, *but only if it's not already present*.
-Doesn't work eith properties.
-
-The underscores in the names will be translated automatically to dashes, so `aria_selected`
-becomes the attribute `aria-selected`.
-
-```html+jinja
-{% do attrs.setdefault(
- aria_label="Products"
-) %}
-```
-
-
-### `.remove_class(name1, name2, ...)`
-
-Removes one or more classes from the list of classes.
-
-```html+jinja
-{% do attrs.remove_class("hidden") %}
-{% do attrs.remove_class("active", "animated") %}
-```
-
-
-### `.get(name, default=None)`
-
-Returns the value of the attribute or property, or the default value if it doesn't exists.
-
-```html+jinja
-{%- set role = attrs.get("role", "tab") %}
-```
-
-...
diff --git a/docs/content/guide/index.md b/docs/content/guide/index.md
index bcbc63c..0c092ea 100644
--- a/docs/content/guide/index.md
+++ b/docs/content/guide/index.md
@@ -1,22 +1,74 @@
---
-title: Quickstart
+title: Introduction
---
-
+
+JinjaX is a Python library for creating reusable "components": encapsulated template snippets that can take arguments and render to HTML. They are similar to React or Vue components, but they render on the server side, not in the browser.
-## Installation
+Unlike Jinja's `{% include "..." %}` or macros, JinjaX components integrate naturally with the rest of your template code.
-Install the package using `pip`.
+```html+jinja
+ ... ` or just ` `.
+```html+jinja
+{% block content %}
+ ... `
+## How It Works
+JinjaX uses Jinja to render the component templates. In fact, it currently works as a pre-processor, replacing all:
-### Components arguments
+```html
+content
+```
-A component can only use data you pass it explicitly and global variables.
-To declare what arguments it takes, begin the file with a `{#def ... #}` Jinja comment.
-Some of these arguments might have a default value (making them optional):
+with function calls like:
```html+jinja
-{#def title, message='Hi' #}
-
-
+An overview of what Jinja is about, and a glimpse into my disjointed decision-making process that got me here.
+
+
+## Components are cool
+
+Despite the complexity of a single-page application, some programmers claim React or Vue offer a better development experience than traditional server-side rendered templates. I believe this is mostly because of the greatest improvement React introduced to web development: components.
+
+
+Components, *as a way to organize template code*. Reactivity is cool too, but unrelated to the main issue.
+
+
+When writing Python, we aim for the code to be easy to understand and test. However, we often forget all of that when writing templates that don't even meet basic standards: long methods, deep conditional nesting, and mysterious variables everywhere.
+
+Components are way cooler than the HTML soup tag of server-side rendered templates. They make it very clear what arguments they take and how they can render. More than anything, components are modular: markup, logic, and relevant styles all in one package. You can copy and paste them between projects, and you can share them with other people.
+
+This means a community has formed around sharing these components. Now you can easily find hundreds of ready-to-use components—some of them very polished—for every common UI widget, even the "complex" ones, like color-pickers. The big problem is that you can only use them with React (and Vue components with Vue, etc.) and in a single-page application.
+
+Jinja is about bringing that innovation back to server-side-rendered applications.
+
+## Not quite there: Jinja macros
+
+An underestimated feature of Jinja is *macros*. Jinja [macros](https://jinja.palletsprojects.com/en/3.0.x/templates/#macros) are template snippets that work like functions: They can have positional or keyword arguments, and when called return the rendered text inside.
+
+```html+jinja
+{% macro input(name, value="", type="text", size=20) -%}
+
+{%- endmacro %}
+
+{% macro button(type="button") -%}
+
+{%- endmacro %}
+```
+
+You can then import the macro to your template to use it:
+
+```html+jinja
+{% from 'forms.html' import input, button %}
+
+
+
+ Click Me
+
+
+```
+
+But macros are *almost* there. They would be a great foundation if we could adjust the syntax just a little.
+
+## Strong alternative: Mako
+
+At some point, I considered dropping this idea and switching to [Mako](https://www.makotemplates.org/), a template library by Michael Bayer (of SQLAlchemy fame).
+
+It's a hidden gem that doesn't get much attention because of network effects. See how close you can get with it:
+
+```html+mako
+<%def name="layout()"> # <--- A "macro"
+
-
-
-```
-
-Called as:
-
-```html+jinja
-{{ title }}
- {{ content }} -
-
-```
-
-You can add or remove arguments before rendering them using the other methods of the `attrs` object. For example:
-
-```html+jinja
-{#def title #}
-{% do attrs.set(id="mycard") -%}
-
-Products
- bla -
-
-```
-
-Or directly in the `attrs.render()` call:
-
-```html+jinja
-{#def title #}
-
-{{ title }}
- {{ content }} -
-
-```
-
-## `attrs` methods
-
-
-### `.render(name=value, ...)`
-
-Renders the current attributes and properties as a string.
-Any attributes/properties you pass to this method, will be used to call `attrs.set(**kwargs)` before rendering.
-
-- Pass a name and a value to set an attribute (e.g. `type="text"`)
-- Use `True` as value to set a property (e.g. `disabled`)
-- Use `False` to remove an attribute or property
-
-The underscores in the names will be translated automatically to dashes, so `aria_selected`
-becomes the attribute `aria-selected`.
-
-The current attribute/property are overwritten **except** if is "class" or "classes".
-In those cases, the new classes are appended to the old ones instead of replacing them.
-
-To provide consistent output, the attributes and properties are sorted by name and rendered like this: `{{ title }}
- {{ content }} -
+
+
+ {% endfor %}
+
+
+```
+
+## Features
+
+### Simple
+
+JinjaX components are simple Jinja templates. You use them as if they were HTML tags without having to import them: easy to use and easy to read.
+
+### Encapsulated
+
+They are independent of each other and can link to their own CSS and JS, so you can freely copy and paste components between applications.
+
+### Testable
+
+All components can be unit tested independently of the pages where they are used.
+
+### Composable
+
+A JinjaX component can wrap HTML code or other components with a natural syntax, as if they were another tag.
+
+### Modern
+
+They are a great complement to technologies like [TailwindCSS](https://tailwindcss.com/), [htmx](https://htmx.org/), or [Hotwire](https://hotwired.dev/).
+
+## Usage
+
+#### Install
+
+Install the library using `pip`.
```bash
pip install jinjax
```
-## Usage
+#### Components folder
-The first thing you must do in your app is to create a "catalog" of components.
-This is the object that manage the components and its global settings. Then, you add to the catalog the folder(s) with your components.
+Then, create a folder that will contain your components, for example:
+
+```
+└ myapp/
+ ├── app.py
+ ├── components/ 🆕
+ │ └── Card.jinja 🆕
+ ├── static/
+ ├── templates/
+ └── views/
+└─ requirements.txt
+```
+
+#### Catalog
+
+Finally, you must create a "catalog" of components in your app. This is the object that manages the components and their global settings. You then add the path of the folder with your components to the catalog:
```python
from jinjax import Catalog
@@ -25,49 +77,55 @@ catalog = Catalog()
catalog.add_folder("myapp/components")
```
-You use the catalog to render a parent component from your views:
+#### Render
+
+You will use the catalog to render components from your views.
```python
def myview():
...
return catalog.render(
- "ComponentName",
+ "Page",
title="Lorem ipsum",
message="Hello",
)
-
```
-## Components
-
-The components are `.jinja` files with snippets of template code (HTML or otherwise). They can also call other components.
-
-
-### Components names
+In this example, it is a component for the whole page, but you can also render smaller components, even from inside a regular Jinja template if you add the catalog as a global:
-The components **must** start with an uppercase. I recommend that you use PascalCase names, like Python classes.
+```python
+app.jinja_env.globals["catalog"] = catalog
+```
-For example, if the filename es `PersonForm.jinja`, the name of the component is `PersonForm` and can be used like `Products
+ {% for product in products %} +
+ {{ catalog.irender("LikeButton", title="Like and subscribe!", post=post) }}
+
+Lorem ipsum
+{{ catalog.irender("CommentForm", post=post) }} +{% endblock %} +``` -You can organize your components in subfolders, using a dot (`.`) to indicate a subfolder. For example, you would call a `components/Person/Form.jinja` components as `{{ title }}
-{{ message }}. This is my component
+{% call catalog.irender("Component", attr="value") %}content{% endcall %}
```
-## Jinja
+These calls are evaluated at render time. Each call loads the source of the component file, parses it to extract the names of CSS/JS files, required and/or optional attributes, pre-processes the template (replacing components with function calls, as before), and finally renders the new template.
+
+### Reusing Jinja's Globals, Filters, and Tests
-JinjaX use Jinja internally to render the templates. You can add your own global variables and functions, filters, tests, and Jinja extensions when creating the catalog:
+You can add your own global variables and functions, filters, tests, and Jinja extensions when creating the catalog:
```python
from jinjax import Catalog
@@ -80,7 +138,7 @@ catalog = Catalog(
)
```
-or afterwards.
+or afterward.
```python
catalog.jinja_env.globals.update({ ... })
@@ -95,7 +153,7 @@ The ["do" extension](https://jinja.palletsprojects.com/en/3.0.x/extensions/#expr
{% do attrs.set(class="btn", disabled=True) %}
```
-### Reusing an existing Jinja Environment
+### Reusing an Existing Jinja Environment
You can also reuse an existing Jinja Environment, for example:
@@ -104,16 +162,15 @@ You can also reuse an existing Jinja Environment, for example:
```python
app = Flask(__name__)
-# Here we add the flask Jinja globals, filters, etc. like `url_for()`
+# Here we add the Flask Jinja globals, filters, etc., like `url_for()`
catalog = jinjax.Catalog(jinja_env=app.jinja_env)
-
```
#### Django:
-First, configure Jinja in setting.py and [jinja_env.py](https://docs.djangoproject.com/en/5.0/topics/templates/#django.template.backends.jinja2.Jinja2))
+First, configure Jinja in `settings.py` and [jinja_env.py](https://docs.djangoproject.com/en/5.0/topics/templates/#django.template.backends.jinja2.Jinja2).
-To have a separate "components" folder for shared components and also have "components" subfolder at each django app level
+To have a separate "components" folder for shared components and also have "components" subfolders at each Django app level:
```python
import jinjax
@@ -134,3 +191,6 @@ def environment(loader: FileSystemLoader, **options):
return env
```
+#### FastAPI:
+
+TBD
\ No newline at end of file
diff --git a/docs/content/guide/integrations.md b/docs/content/guide/integrations.md
new file mode 100644
index 0000000..0884d22
--- /dev/null
+++ b/docs/content/guide/integrations.md
@@ -0,0 +1,3 @@
+---
+title: Integrations
+---
\ No newline at end of file
diff --git a/docs/content/guide/motivation.md b/docs/content/guide/motivation.md
new file mode 100644
index 0000000..90602b5
--- /dev/null
+++ b/docs/content/guide/motivation.md
@@ -0,0 +1,115 @@
+---
+title: Motivation
+---
+{{ input("username") }}
+{{ input("password", type="password") }}
+{% call button("submit") %}Submit{% endcall %} +``` +You must use the `{% call x %}` to pass the child content to the macro—by using the weird incantation `{{ caller() }}`—otherwise you can just call it like it were a function. + +So, can we use macros as components and call it a day? Well... no. This looks terrible: + +```html+jinja +{% call Card(label="Hello") %} + {% call MyButton(color="blue", shadowSize=2) %} + {{ Icon(name="ok") }} Click Me + {% endcall %} +{% endcall %} +``` + +compared to how you would write it with JSX: + +```html +
+
+%def>
+
+## calls the layout def <--- Look! Python-style comments
+
+<%self:layout>
+ <%def name="header()"> # <--- This is like a "slot"!
+ I am the header
+ %def>
+ <%def name="sidebar()">
+
+ ${caller.header()}
+
+
+
+ ${caller.sidebar()}
+
+
+
+ ${caller.body()}
+
+ -
+
- sidebar 1 +
- sidebar 2 +
+
Enter or Space keys on a `PopButton` will trigger
+- Pressing the Enter or Space keys on a `PopButton` will trigger
the button action (open, close, or toggle state), and close *all* the `Popover` with `mode="auto"`.
-- Pressing the Escape key will close *all* the `Popover` with `mode="auto"`.
+- Pressing the Escape key will close *all* the `Popover` with `mode="auto"`.
diff --git a/docs/content/ui/popover.md b/docs/content/ui/popover.md
index 73d4ec7..673f96d 100644
--- a/docs/content/ui/popover.md
+++ b/docs/content/ui/popover.md
@@ -198,7 +198,7 @@ by [Mozilla Contributors](https://developer.mozilla.org/en-US/docs/MDN/Community
### Keyboard interaction
-- Pressing the Enter or Space keys on a `PopButton` will trigger
+- Pressing the Enter or Space keys on a `PopButton` will trigger
the button action (open, close, or toggle state), and close *all* the `Popover` with `mode="auto"`.
-- Pressing the Escape key will close *all* the `Popover` with `mode="auto"`.
+- Pressing the Escape key will close *all* the `Popover` with `mode="auto"`.
diff --git a/docs/content/ui/reldate.md b/docs/content/ui/reldate.md
index ddf13a9..e3624c8 100644
--- a/docs/content/ui/reldate.md
+++ b/docs/content/ui/reldate.md
@@ -11,14 +11,14 @@ etc. using JavaScript's ` |
-| ` ` |
-| ` ` |
-| ` ` |
-| ` ` |
-| ` ` |
+| Source | Relative date
+| -----------------------------------------| --------------
+| ` ` | 6 months ago
+| ` ` | yesterday
+| ` ` | 5 hours ago
+| ` ` | in 19 hours
+| ` ` | next week
+| ` ` | 32 years ago
## How it works
@@ -39,12 +39,12 @@ Both can be a comma-separated lists of locales (e.g.: `"en-US,en-UK,en`). If non
*Some examples (as if the datetime was `June 20th, 2024 6:30pm`)*:
-| Source | Relative date
-| ------------| --------------
-| ` ` |
-| ` ` |
-| ` ` |
-| ` ` |
+| Source | Relative date
+| ---------------------------------------------------------------| --------------
+| ` ` | 6 mesi fa
+| ` ` | hier
+| ` ` | dentro de 19 horas
+| ` ` | dentro de 19 horas
## Component arguments
diff --git a/docs/content/ui/tabs.md b/docs/content/ui/tabs.md
index 70ae7c5..314a467 100644
--- a/docs/content/ui/tabs.md
+++ b/docs/content/ui/tabs.md
@@ -39,9 +39,9 @@ Disabling tabs might be confusing for users. Instead, I reccomend you either rem
## Manually activating tabs
-By default, tabs are automatically selected as the user navigates through them using the arrow keys.
+By default, tabs are automatically selected as the user navigates through them using the arrow kbds.
-If you'd rather not change the current tab until the user presses Enter or Space , use the `manual` attribute on the `TabGroup` component.
+If you'd rather not change the current tab until the user presses Enter or Space, use the `manual` attribute on the `TabGroup` component.
Remember to add styles to the `:focus` state of the tab so is clear to the user that the tab is focused.
@@ -58,7 +58,7 @@ The manual prop has no impact on mouse interactions — tabs will still be selec
## Vertical tabs
-If you've styled your `TabList` to appear vertically, use the `vertical` attribute to enable navigating with the ↑ and ↓ arrow keys instead of ← and → , and to update the `aria-orientation` attribute for assistive technologies.
+If you've styled your `TabList` to appear vertically, use the `vertical` attribute to enable navigating with the ↑ and ↓ arrow kbds instead of ← and →, and to update the `aria-orientation` attribute for assistive technologies.
↑ and ↓ arrow keys to move between tabs instead of the defaults ← and → arrow keys.
-| manual | `bool` | `false` | If `true`, selecting a tab with the keyboard won't activate it, you must press Enter os Space keys to do it.
+| vertical | `bool` | `false` | Use the ↑ and ↓ arrow kbds to move between tabs instead of the defaults ← and → arrow kbds.
+| manual | `bool` | `false` | If `true`, selecting a tab with the keyboard won't activate it, you must press Enter os Space kbds to do it.
| tag | `str` | `"nav"` | HTML tag used for rendering the wrapper.
@@ -155,8 +155,8 @@ All interactions apply when a `Tab` component is focused.
| Command | Description
| ------------------------------------------------------------------------------------- | -----------
-| ← / → arrow keys | Selects the previous/next non-disabled tab, cycling from last to first and vice versa.
-| ↑ / ↓ arrow keys when `vertical` is set | Selects the previous/next non-disabled tab, cycling from last to first and vice versa.
-| Enter or Space when `manual` is set | Activates the selected tab
-| Home or PageUp | Activates the **first** tab
-| End or PageDown | Activates the **last** tab
+| ← / → arrow kbds | Selects the previous/next non-disabled tab, cycling from last to first and vice versa.
+| ↑ / ↓ arrow kbds when `vertical` is set | Selects the previous/next non-disabled tab, cycling from last to first and vice versa.
+| Enter or Space when `manual` is set | Activates the selected tab
+| Home or PageUp | Activates the **first** tab
+| End or PageDown | Activates the **last** tab
diff --git a/docs/deploy.sh b/docs/deploy.sh
index f64013e..9fe8897 100755
--- a/docs/deploy.sh
+++ b/docs/deploy.sh
@@ -1,3 +1,4 @@
#!/bin/bash
python docs.py build
+ssh code 'rm -rf /var/www/jinjax/build'
rsync --recursive --delete --progress build code:/var/www/jinjax/
diff --git a/docs/docs.py b/docs/docs.py
index b9f91e2..4c0af71 100755
--- a/docs/docs.py
+++ b/docs/docs.py
@@ -6,7 +6,7 @@
from claydocs import Docs
-logging.getLogger("jinjax").setLevel(logging.ERROR)
+logging.getLogger("jinjax").setLevel(logging.INFO)
logging.getLogger("jinjax").addHandler(logging.StreamHandler())
here = Path(__file__).parent
@@ -18,10 +18,13 @@
[
"guide/index.md",
"guide/components.md",
- "guide/extra.md",
"guide/css_and_js.md",
+ # "guide/integrations.md",
+ # "guide/performance.md",
+ "guide/motivation.md",
],
],
+ "api.md",
[
"UI components", [
"ui/index.md",
@@ -42,10 +45,17 @@ def get_docs() -> Docs:
content_folder=root_path,
add_ons=[jinjax_ui],
search=False,
- cache=True,
+ cache=False,
domain="https://jinjax.scaletti.dev",
default_component="Page",
default_social="SocialCard",
+ metadata={
+ "name": "JinjaX",
+ "language": "en",
+ "license": "MIT",
+ "version": "0.43",
+ "web": "https://jinjax.scaletti.dev",
+ }
)
docs.add_folder(here / "components")
docs.add_folder(here / "theme")
diff --git a/docs/static/docs.css b/docs/static/docs.css
index ab33f0c..30c9898 100644
--- a/docs/static/docs.css
+++ b/docs/static/docs.css
@@ -1,3 +1,9 @@
+.bg-cover {
+ position: absolute;
+ z-index: -1;
+ inset: 0;
+}
+
.Logo {
display: flex;
height: 2.5rem;
@@ -229,6 +235,7 @@
margin-bottom: 0.5rem;
display: flex;
align-items: center;
+ flex-direction: row-reverse;
}
& .card > .header img {
float: left;
diff --git a/docs/static/prose.css b/docs/static/prose.css
index e6158c7..328b214 100644
--- a/docs/static/prose.css
+++ b/docs/static/prose.css
@@ -14,8 +14,9 @@
--cd-prose-pre-code: rgb(238 238 238);
--cd-prose-pre-border: rgb(51, 51, 51);
--cd-prose-pre-bg: rgb(24 24 24);
- --cd-prose-th-borders: #d4d4d8;
- --cd-prose-td-borders: #e4e4e7;
+ --cd-prose-th-borders: #ddd;
+ --cd-prose-td-borders: #eee;
+ --cd-prose-bg-hover: rgba(0,0,0,0.035);
--cd-prose-invert-body: #d4d4d8;
--cd-prose-invert-headings: #fff;
@@ -34,6 +35,7 @@
--cd-prose-invert-pre-bg: rgb(24 24 24);
--cd-prose-invert-th-borders: #52525b;
--cd-prose-invert-td-borders: #3f3f46;
+ --cd-prose-invert-bg-hover: rgba(0,0,0,0.035);
}
.dark .prose {
@@ -54,6 +56,7 @@
--cd-prose-pre-bg: var(--cd-prose-invert-pre-bg);
--cd-prose-th-borders: var(--cd-prose-invert-th-borders);
--cd-prose-td-borders: var(--cd-prose-invert-td-borders);
+ --cd-prose-bg-hover: var(--cd-prose-invert-bg-hover);
}
.prose {
@@ -225,7 +228,7 @@
color: var(--cd-prose-headings);
font-weight: 600;
font-size: 1.4em;
- margin-top: 1em;
+ margin-top: 1.6em;
margin-bottom: 0.4em;
line-height: 1.6;
}
@@ -249,6 +252,26 @@
color: inherit;
}
+.prose :where(h5):not(:where([class~="not-prose"] *)) {
+ color: var(--cd-prose-headings);
+ font-weight: 600;
+ font-size: 1em;
+ margin-top: 1em;
+ margin-bottom: 0.5em;
+ line-height: 1.5;
+}
+
+
+.prose :where(h6):not(:where([class~="not-prose"] *)) {
+ color: var(--cd-prose-headings);
+ font-weight: 600;
+ font-size: 1em;
+ margin-top: 1em;
+ margin-bottom: 0.5em;
+ line-height: 1.4;
+}
+
+
.prose :where(img):not(:where([class~="not-prose"] *)) {
margin-top: 2em;
margin-bottom: 2em;
@@ -314,6 +337,8 @@
margin-bottom: 2em;
font-size: 0.875em;
line-height: 1.7142857;
+ border-width: 1px;
+ border-color: var(--cd-prose-td-borders);
}
.prose :where(thead):not(:where([class~="not-prose"] *)) {
@@ -325,14 +350,21 @@
color: var(--cd-prose-headings);
font-weight: 600;
vertical-align: bottom;
- padding-right: 0.5714286em;
- padding-bottom: 0.5714286em;
- padding-left: 0.5714286em;
+ border-left-width: 1px;
+ border-left-color: var(--cd-prose-th-borders);
+ /* text-transform: uppercase; */
+}
+.prose :where(thead th:first-child):not(:where([class~="not-prose"] *)) {
+ border-left-width: 0;
}
.prose :where(tbody tr):not(:where([class~="not-prose"] *)) {
border-bottom-width: 1px;
border-bottom-color: var(--cd-prose-td-borders);
+ transition: background-color 125ms;
+}
+.prose :where(tbody tr:hover):not(:where([class~="not-prose"] *)) {
+ background-color: var(--cd-prose-bg-hover);
}
.prose :where(tbody tr:last-child):not(:where([class~="not-prose"] *)) {
@@ -341,6 +373,14 @@
.prose :where(tbody td):not(:where([class~="not-prose"] *)) {
vertical-align: baseline;
+ border-left-width: 1px;
+ border-left-color: var(--cd-prose-th-borders);
+}
+.prose :where(tbody td:first-child):not(:where([class~="not-prose"] *)) {
+ border-left-width: 0;
+}
+.prose :where(tbody td p:first-child):not(:where([class~="not-prose"] *)) {
+ margin-top: 0;
}
.prose :where(tfoot):not(:where([class~="not-prose"] *)) {
@@ -352,6 +392,10 @@
vertical-align: top;
}
+.prose :where(th, td):not(:where([class~="not-prose"] *)) {
+ padding: 0.5rem 1rem;
+}
+
.prose :where(video):not(:where([class~="not-prose"] *)) {
margin-top: 2em;
margin-bottom: 2em;
@@ -417,29 +461,6 @@
margin-top: 0;
}
-.prose :where(thead th:first-child):not(:where([class~="not-prose"] *)) {
- padding-left: 0;
-}
-
-.prose :where(thead th:last-child):not(:where([class~="not-prose"] *)) {
- padding-right: 0;
-}
-
-.prose :where(tbody td, tfoot td):not(:where([class~="not-prose"] *)) {
- padding-top: 0.5714286em;
- padding-right: 0.5714286em;
- padding-bottom: 0.5714286em;
- padding-left: 0.5714286em;
-}
-
-.prose :where(tbody td:first-child, tfoot td:first-child):not(:where([class~="not-prose"] *)) {
- padding-left: 0;
-}
-
-.prose :where(tbody td:last-child, tfoot td:last-child):not(:where([class~="not-prose"] *)) {
- padding-right: 0;
-}
-
.prose :where(.prose > :first-child):not(:where([class~="not-prose"] *)) {
margin-top: 0;
}
@@ -493,23 +514,47 @@ pre code {
line-height: inherit;
}
-pre .filename {
- display: block;
- padding: 0.4em 1em;
- font-weight: 500;
- font-size: 0.9em;
- border-bottom: 1px solid rgb(var(--border-color));
-}
pre a {
text-decoration: none;
}
+.highlight {
+ margin-top: 0.5rem;
+ margin-bottom: 1rem;
+ border-radius: 6px;
+}
+.highlight:has(> .filename) {
+ background-color: rgb(249 250 251);
+ border: 1px solid rgb(153, 153, 153);
+}
+.highlight:is(.dark *):has(> .filename) {
+ background-color: rgb(55 65 81);
+ border-color: rgb(75 85 99);
+}
+.highlight > .filename {
+ border-radius: 6px 0 0 0;
+ display: inline-block;
+ border-right: 1px solid rgb(153, 153, 153);
+ background-color: #e7e9ed;
+ padding: 0.5rem;
+ color: #333;
+ font-weight: 500;
+ font-size: 0.9em;
+}
+.highlight:is(.dark *) > .filename {
+ border-color: rgb(75 85 99);
+ background-color: #111;
+ color: rgb(255 255 255);
+}
.highlight pre {
background-color: rgba(0, 0, 0, 0.9);
border-radius: 6px;
font-size: 0.98rem;
line-height: 1.4;
}
+.highlight .filename + pre {
+ border-radius: 0 0 6px 6px;
+}
.highlight pre code { color: white; }
.highlight pre code [data-linenos]:before {
diff --git a/docs/static/theme.css b/docs/static/theme.css
index e9297e8..c28925e 100644
--- a/docs/static/theme.css
+++ b/docs/static/theme.css
@@ -57,7 +57,25 @@ html {
--cd-text-color: rgb(23 23 23);
--cd-text-color-mild: rgb(63 63 70);
--cd-border-color: #e3e3e4;
+
+ --cd-nav-bg-color: rgba(255, 255, 255, 0.8);
+ --cd-nav-bg-color-hover: rgb(244, 244, 244);
+
+ --doc-symbol-parameter-fg-color: #df50af;
+ --doc-symbol-attribute-fg-color: #953800;
+ --doc-symbol-function-fg-color: #8250df;
+ --doc-symbol-method-fg-color: #8250df;
+ --doc-symbol-class-fg-color: #0550ae;
+ --doc-symbol-module-fg-color: #5cad0f;
+
+ --doc-symbol-parameter-bg-color: #df50af1a;
+ --doc-symbol-attribute-bg-color: #9538001a;
+ --doc-symbol-function-bg-color: #8250df1a;
+ --doc-symbol-method-bg-color: #8250df1a;
+ --doc-symbol-class-bg-color: #0550ae1a;
+ --doc-symbol-module-bg-color: #5cad0f1a;
}
+
html.dark {
--cd-brand-color: #3451b2;
@@ -66,6 +84,23 @@ html.dark {
--cd-text-color: rgb(250 250 250);
--cd-text-color-mild: rgb(161 161 170);
--cd-border-color: rgb(60 60 60);
+
+ --cd-nav-bg-color: rgba(60, 60, 60, 0.8);
+ --cd-nav-bg-color-hover: rgb(70, 70, 70);
+
+ --doc-symbol-parameter-fg-color: #ffa8cc;
+ --doc-symbol-attribute-fg-color: #ffa657;
+ --doc-symbol-function-fg-color: #d2a8ff;
+ --doc-symbol-method-fg-color: #d2a8ff;
+ --doc-symbol-class-fg-color: #79c0ff;
+ --doc-symbol-module-fg-color: #baff79;
+
+ --doc-symbol-parameter-bg-color: #ffa8cc1a;
+ --doc-symbol-attribute-bg-color: #ffa6571a;
+ --doc-symbol-function-bg-color: #d2a8ff1a;
+ --doc-symbol-method-bg-color: #d2a8ff1a;
+ --doc-symbol-class-bg-color: #79c0ff1a;
+ --doc-symbol-module-bg-color: #baff791a;
}
/* ---------------------------------------------------------------------- */
@@ -393,7 +428,7 @@ img, video {
/* ---------------------------------------------------------------------- */
-html:has(.tNavMobile:popover-open) {
+html:has(.cd-nav-mobile:popover-open) {
overflow: hidden !important;
overflow-x: hidden !important;
overflow-y: hidden !important;
@@ -411,28 +446,27 @@ html.dark body {
background-color: var(--cd-bg-color-dark);
}
-.icon {
- font-family: var(--cd-font-icons);
- font-weight: normal;
- font-style: normal;
- letter-spacing: normal;
- text-transform: none;
+.keys,
+kbd:not(.keys > kbd) {
+ font-family: var(--cd-font-mono);
display: inline-block;
- white-space: nowrap;
- word-wrap: normal;
- direction: ltr;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- text-rendering: optimizeLegibility;
- font-feature-settings: "liga";
- cursor: default;
- pointer-events: none;
-}
+ padding: 0.2rem 0.25rem;
+ margin-left: 0.1rem;
+ margin-right: 0.1rem;
+ font-size: 0.875rem;
+ line-height: 1;
+ font-weight: 500;
+ letter-spacing: -0.025em;
+ line-height: 1;
+ border-radius: 0.25rem;
+ border-width: 1px;
+ border-color: #ffffff;
+ box-shadow: 0 0 2px 0 #000;
-.bg-cover {
- position: absolute;
- z-index: -1;
- inset: 0;
+ &:is(.dark *) {
+ border-color: rgb(0 0 0);
+ background-color: rgb(24 24 27);
+ }
}
.scrollbar-thin {
@@ -469,7 +503,190 @@ h6:hover a.headerlink {
opacity: 0.5;
}
-.cards {
+/* ---------------------------------------------------------------------- */
+
+.doc-symbol {
+ border-radius: 0.1rem;
+ padding: 0 0.3em;
+ font-weight: bold;
+}
+.doc-symbol-attr {
+ color: var(--doc-symbol-attribute-fg-color) !important;
+ background-color: var(--doc-symbol-attribute-bg-color) !important;
+}
+.doc-symbol-function {
+ color: var(--doc-symbol-function-fg-color) !important;
+ background-color: var(--doc-symbol-function-bg-color) !important;
+}
+.doc-symbol-method {
+ color: var(--doc-symbol-method-fg-color) !important;
+ background-color: var(--doc-symbol-method-bg-color) !important;
+}
+.doc-symbol-class {
+ color: var(--doc-symbol-class-fg-color) !important;
+ background-color: var(--doc-symbol-class-bg-color) !important;
+}
+.doc-symbol-module {
+ color: var(--doc-symbol-module-fg-color) !important;
+ background-color: var(--doc-symbol-module-bg-color) !important;
+}
+
+.doc-oname {
+ font-weight: normal;
+}
+.doc-olabel {
+ font-size: 0.6em !important;
+ color: #36464e !important;
+ font-weight: 400;
+ padding: 0.1rem 0.4rem !important;
+}
+
+.doc-attrs ~ .doc-methods,
+.doc-properties ~ .doc-methods {
+ margin-top; 1rem;
+}
+
+/* ---------------------------------------------------------------------- */
+
+.icon {
+ font-family: var(--cd-font-icons);
+ font-weight: normal;
+ font-style: normal;
+ letter-spacing: normal;
+ text-transform: none;
+ display: inline-block;
+ white-space: nowrap;
+ word-wrap: normal;
+ direction: ltr;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ text-rendering: optimizeLegibility;
+ font-feature-settings: "liga";
+ cursor: default;
+ pointer-events: none;
+}
+
+/* ---------------------------------------------------------------------- */
+
+.cd-source {
+ display: flex;
+ align-items: center;
+ font-size: 0.85rem;
+ line-height: 1.2;
+ white-space: nowrap;
+ cursor: pointer;
+ text-decoration: none;
+ padding: 0.5rem 0.75rem;
+ min-width: 150px;
+ backdrop-filter: blur(4px);
+ background-color: var(--cd-nav-bg-color);
+ border-radius: 1rem;
+ transition: background 300ms ease-in-out;
+
+ &:hover {
+ background-color: var(--cd-nav-bg-color-hover);
+ }
+ & > div {
+ opacity: 0.8;
+ transition: opacity 300ms ease-in-out;
+ }
+ &:hover > div {
+ opacity: 1;
+ }
+ & .cd-source__icon {
+ padding-right: 0.5rem;
+ }
+ & .cd-source__icon svg {
+ height: 1.5rem;
+ width: 1.5rem;
+ fill: currentcolor;
+ display: block;
+ }
+ & .cd-source__label {
+ font-size: 0.9rem;
+ font-weight: bold;
+ }
+ & .cd-source__repo {
+ display: inline-block;
+ max-width: calc(100% - 1.2rem);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: middle;
+ }
+ @media (max-width: 480px) {
+ & {
+ min-width: 0;
+ }
+ & .cd-source__icon {
+ padding-right: 0;
+ }
+ & .cd-source__repo {
+ display: none;
+ }
+ }
+ & .cd-source__facts {
+ display: hidden;
+ gap: 0.4rem;
+ list-style-type: none;
+ margin: 0.1rem 0 0;
+ overflow: hidden;
+ padding: 0;
+ width: 100%;
+ opacity: 0;
+ transform: translateY(100%);
+ transition: all 0.5s ease-out;
+ }
+ & .cd-source__facts.cd-source__facts--visible {
+ display: flex;
+ opacity: 1;
+ transform: translateY(0);
+ }
+ & .cd-source__facts [data-fact] {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: flex;
+ align-items: center;
+ line-height: 1;
+ }
+ & .cd-source__facts [data-fact]:nth-child(1n+2) {
+ flex-shrink: 0;
+ }
+ & .cd-source__facts [data-fact]:not([hidden]):before {
+ width: 0.6rem;
+ padding-right: 0.8rem;
+ font-family: var(--cd-font-icons);
+ font-weight: normal;
+ font-style: normal;
+ letter-spacing: normal;
+ text-transform: none;
+ display: inline-block;
+ white-space: nowrap;
+ word-wrap: normal;
+ direction: ltr;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ text-rendering: optimizeLegibility;
+ font-feature-settings: "liga";
+ cursor: default;
+ pointer-events: none;
+ }
+ & .cd-source__facts [data-fact="version"]:not([hidden]):before {
+ content: "tag";
+ }
+ & .cd-source__facts [data-fact="stars"]:not([hidden]):before {
+ content: "star";
+ }
+ & .cd-source__facts [data-fact="forks"]:not([hidden]):before {
+ content: "fork_right";
+ }
+ & .cd-source__facts [data-fact="numrepos"]:not([hidden]):before {
+ content: "numbers";
+ }
+}
+
+/* ---------------------------------------------------------------------- */
+
+.cd-cards {
& {
display: grid;
grid-gap: 1rem;
@@ -478,7 +695,7 @@ h6:hover a.headerlink {
& { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 900px) {
- & { grid-template-columns: repeat(3, 1fr); }
+ & { grid-template-columns: repeat(4, 1fr); }
}
& a.card {
display: block;
@@ -499,7 +716,9 @@ h6:hover a.headerlink {
}
}
-.text-button {
+/* ---------------------------------------------------------------------- */
+
+.cd-text-button {
display: inline-flex;
cursor: pointer;
touch-action: manipulation;
@@ -555,7 +774,7 @@ h6:hover a.headerlink {
/* ---------------------------------------------------------------------- */
-.tCallout {
+.cd-callout {
--bg-color: rgb(244 244 245);
--border-color: rgb(212 212 216);
--text-color: rgb(39 39 42);
@@ -678,12 +897,13 @@ h6:hover a.headerlink {
}
}
/* Cannot be nested */
-.tCallout::selection {
+.cd-callout::selection {
background-color: oklch(from var(--bg-color) calc(l * 0.9) calc(c * 3) h);
}
+/* ---------------------------------------------------------------------- */
-.tExampleTabs {
+.cd-example-tabs {
position: relative;
margin-top: 2rem;
margin-bottom: 3rem;
@@ -720,7 +940,7 @@ h6:hover a.headerlink {
& .example-tablist:is(.dark *) {
border-color: rgb(75 85 99);
background-color: rgb(55 65 81);
- color: rgb(156 163 175);
+ color: rgb(156 173 175);
}
& .example-tab {
@@ -753,7 +973,7 @@ h6:hover a.headerlink {
}
& .example-tab.ui-selected:is(.dark *) {
color: white;
- background-color: black;
+ background-color: #111;
}
& .example-tabpanel {
@@ -778,8 +998,9 @@ h6:hover a.headerlink {
}
}
+/* ---------------------------------------------------------------------- */
-.tFooter {
+.cd-footer {
padding-top: 1.25rem;
padding-bottom: 1.25rem;
padding-left: var(--cd-padding-left);
@@ -813,6 +1034,16 @@ h6:hover a.headerlink {
& .themeswitch {
margin-left: 1.5rem;
margin-right: 0;
+ opacity: 0.8;
+ border-radius: 1rem;
+ background-color: var(--cd-nav-bg-color);
+ transition: opacity 300ms ease-in-out, background 300ms ease-in-out;
+ padding-left: 0.5rem;
+ padding-right: 0.5rem;
+ }
+ & .themeswitch:hover {
+ opacity: 1;
+ background-color: var(--cd-nav-bg-color-hover);
}
@media (max-width: 640px) {
& .built-with,
@@ -822,8 +1053,9 @@ h6:hover a.headerlink {
}
}
+/* ---------------------------------------------------------------------- */
-.tHeader {
+.cd-header {
margin-bottom: 2rem;
& div p {
@@ -856,46 +1088,23 @@ h6:hover a.headerlink {
}
}
+/* ---------------------------------------------------------------------- */
-.tKey {
- font-family: var(--cd-font-mono);
- display: inline-block;
- padding: 0.2rem 0.25rem;
- margin-left: 0.1rem;
- margin-right: 0.1rem;
- font-size: 0.875rem;
- line-height: 1;
- font-weight: 500;
- letter-spacing: -0.025em;
- line-height: 1;
- border-radius: 0.25rem;
- border-width: 1px;
- border-color: #ffffff;
- box-shadow: 0 0 2px 0 #000;
-
- &:is(.dark *) {
- border-color: rgb(0 0 0);
- background-color: rgb(24 24 27);
- }
-}
-
-
-.tNavBar {
+.cd-navbar {
display: flex;
align-items: center;
- border-radius: 20px;
+ border-radius: 1rem;
+ padding: 0 0.75rem;
font-size: 0.875rem;
font-weight: bold;
- padding: 0 0.75rem;
backdrop-filter: blur(4px);
- background-color: rgba(255, 255, 255, 0.8);
+ background-color: var(--cd-nav-bg-color);
box-shadow: rgb(15, 15, 15) 0px 0px 0px 0px inset,
rgba(163, 163, 170, 0.3) 0px 0px 0px 1px inset,
rgba(255, 255, 255, 0.2) 0px 20px 25px -5px,
rgba(255, 255, 255, 0.2) 0px 8px 10px -6px;
&:is(.dark *) {
- background-color: rgba(41, 37, 36, 0.8);
box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px inset,
rgba(63, 63, 70, 0.3) 0px 0px 0px 1px inset,
rgba(0, 0, 0, 0.2) 0px 20px 25px -5px,
@@ -905,25 +1114,26 @@ h6:hover a.headerlink {
& a {
white-space: nowrap;
padding: 0.75rem;
+ }
+ & a,
+ & button {
opacity: 0.8;
- border-radius: 4px;
transition: opacity 300ms ease-in-out, background 300ms ease-in-out;
+ border-radius: 4px;
}
- & a:hover {
- opacity: 1;
- background-color: rgb(244, 244, 244);
- }
- &:is(.dark *) a:hover {
+ & a:hover,
+ & button:hover {
opacity: 1;
- background-color: rgba(244, 244, 244, 0.1);
+ background-color: var(--cd-nav-bg-color-hover);
}
& a svg {
height: 20px;
}
}
+/* ---------------------------------------------------------------------- */
-.tNavTop {
+.cd-nav-top {
z-index: 1000;
width: 100%;
margin-top: 1rem;
@@ -948,15 +1158,14 @@ h6:hover a.headerlink {
padding: 0.75rem;
margin-left: -0.75rem;
margin-right: auto;
+ background-color: transparent;
+ transition: background 300ms ease-in-out;
}
@media (min-width: 640px) {
& .logo {
border-radius: 4px;
backdrop-filter: blur(4px);
- background-color: rgba(255, 255, 255, 0.8);
- }
- & .logo:is(.dark *) {
- background-color: rgba(41, 37, 36, 0.8);
+ background-color: var(--cd-nav-bg-color);
}
}
@@ -969,25 +1178,25 @@ h6:hover a.headerlink {
}
}
& .nav-extra {
- margin-left: 1rem;
+ margin-left: 0.75rem;
}
& .nav-links > .themeswitch {
border: none;
- background: none;
outline: none;
- margin: 0 0.25rem 0 0.5rem;
+ margin-left: 0.25rem;
}
- & .toggleSidebar {
+ & .cd-toggle-sidebar {
font-size: 1rem;
- margin-left: 1rem;
+ margin-left: 0.75rem;
width: 4.5rem;
line-height: 1;
padding: 0.75rem 0.5rem;
}
}
+/* ---------------------------------------------------------------------- */
-.tNavGlobal {
+.cd-nav-global {
/* position: sticky; */
/* top: 0; */
z-index: 0;
@@ -1028,44 +1237,46 @@ h6:hover a.headerlink {
}
}
+/* ---------------------------------------------------------------------- */
-.tNavLocal {
+.cd-nav-local {
position: -webkit-sticky;
position: sticky;
top: 0;
z-index: 0;
- height: 100vh;
- width: 14rem;
- padding-bottom: 2rem;
- overflow-y: auto;
- overflow-x: hidden;
- -ms-scroll-chaining: none;
- overscroll-behavior: contain;
- scroll-behavior: smooth;
+ width: 20rem;
+ height: calc(100vh - 5.5rem);
+ margin-right: -1rem;
overflow: hidden;
+ border-left-width: 1px;
+ border-color: rgb(228 228 231);
@media ((min-width: 1024px) and (min-height: 640px)) {
& {
- top: 3rem;
- height: calc(100vh - 3rem);
+ top: 5rem;
}
}
-
+ &:is(.dark *) {
+ border-color: rgb(82 82 91);
+ }
& .wrapper {
- margin-top: 1.5rem;
+ position: absolute;
+ inset: 0;
+ overflow-y: auto;
+ overflow-x: hidden;
+ -ms-scroll-chaining: none;
+ overscroll-behavior: contain;
+ scroll-behavior: smooth;
border-left-width: 1px;
- border-color: rgb(228 228 231);
- padding: 0 0 3.5rem;
+ padding: 1rem 0.5rem 1.5rem 0.5rem;
font-size: 0.8rem;
line-height: 1.2;
}
- & .wrapper:is(.dark *) {
- border-color: rgb(82 82 91);
- }
}
+/* ---------------------------------------------------------------------- */
-.tNavMobile {
+.cd-nav-mobile {
position: fixed;
left: 0;
top: 0;
@@ -1118,9 +1329,9 @@ h6:hover a.headerlink {
background: none;
outline: none;
}
- & .toggleSidebar {
+ & .cd-toggle-sidebar {
font-size: 1rem;
- margin-left: 1rem;
+ margin-left: 0.75rem;
width: 4.5rem;
line-height: 1;
padding: 0.75rem 0.5rem;
@@ -1140,32 +1351,32 @@ h6:hover a.headerlink {
}
/* Transition for the popover's backdrop.
::backdrop cannot be nested */
-.tNavMobile::backdrop {
+.cd-nav-mobile::backdrop {
transition: all 0.5s allow-discrete;
/* Final state of the exit animation */
backdrop-filter: blur(0);
background-color: rgb(0 0 0 / 0%);
}
-.tNavMobile:popover-open::backdrop {
+.cd-nav-mobile:popover-open::backdrop {
backdrop-filter: blur(2px);
background-color: rgb(0 0 0 / 15%);
}
@starting-style {
- .tNavMobile:popover-open::backdrop {
+ .cd-nav-mobile:popover-open::backdrop {
backdrop-filter: blur(0);
background-color: rgb(0 0 0 / 0%);
}
}
+/* ---------------------------------------------------------------------- */
-.tPageSingle > main {
+.cd-page-single > main {
margin: 0;
max-width: 100%;
padding: 0;
}
-
-.tPage {
+.cd-page {
& .page-wrapper {
z-index: 10;
margin-left: auto;
@@ -1184,17 +1395,18 @@ h6:hover a.headerlink {
}
@media (min-width: 640px) {
& {
- padding-bottom: 3rem;
+ padding-bottom: 1rem;
}
}
}
+/* ---------------------------------------------------------------------- */
-.tPrevNext {
+.cd-prevnext {
display: flex;
align-items: stretch;
width: 100%;
- margin-top: 4rem;
+ margin-top: 2rem;
padding-top: 2rem;
padding-bottom: 2rem;
padding-left: var(--cd-padding-left);
@@ -1226,20 +1438,23 @@ h6:hover a.headerlink {
}
& a.prev:hover,
& a.next:hover {
- border-color: black;
+ border-color: rgb(113, 113, 122);
}
- & a.prev:is(.dark *):hover,
- & a.next:is(.dark *):hover {
- border-color: white;
+ &:is(.dark *) a.prev:hover,
+ &:is(.dark *) a.next:hover {
+ border-color: rgb(150, 150, 150);
}
& .section {
- font-size: 0.8rem;
+ font-size: 0.875rem;
line-height: 1;
- color: rgb(113 113 122);
+ color: rgb(113, 113, 122);
margin-bottom: 0.1rem;
}
+ &:is(.dark *) .section {
+ color: rgb(150, 150, 150);
+ }
& .title {
- font-size: 0.9rem;
+ font-size: 1rem;
line-height: 1.1;
}
& i {
@@ -1250,8 +1465,9 @@ h6:hover a.headerlink {
}
}
+/* ---------------------------------------------------------------------- */
-.tThemeSwitch {
+.cd-theme-switch {
display: flex;
align-items: center;
justify-content: center;
@@ -1381,8 +1597,11 @@ h6:hover a.headerlink {
& .light-text,
& .dark-text {
padding: 0.5rem;
+ padding-left: 0;
display: none;
white-space: nowrap;
+ font-weight: bold;
+ text-align: left;
}
@media (min-width: 1024px) {
& .light-text {
@@ -1397,8 +1616,9 @@ h6:hover a.headerlink {
}
}
+/* ---------------------------------------------------------------------- */
-.tToc {
+.cd-toc {
& details,
& section {
margin-top: 1.5rem;
@@ -1472,8 +1692,9 @@ h6:hover a.headerlink {
}
}
+/* ---------------------------------------------------------------------- */
-.tTocPage {
+.cd-toc-page {
& a {
display: flex;
align-items: center;
@@ -1499,46 +1720,44 @@ h6:hover a.headerlink {
border-color: var(--cd-brand-color);
}
- & a.indent-1 { margin-left: 0.5rem; }
- & a.indent-2 { margin-left: 1rem; }
- & a.indent-3 { margin-left: 1.5rem; }
- & a.indent-4 { margin-left: 2rem; }
+ & a.indent-1 { margin-left: 0; }
+ & a.indent-2 { margin-left: 0.5rem; }
+ & a.indent-3 { margin-left: 1rem; }
+ & a.indent-4 { margin-left: 1.5rem; }
+ & a.indent-5 { margin-left: 2rem; }
}
/* ---------------------------------------------------------------------- */
-.tNavGlobal,
-.tNavLocal,
-.tNavMobile {
+.cd-nav-global,
+.cd-nav-local,
+.cd-nav-mobile {
display: none;
}
-.tNavMobile:popover-open,
-.tNavTop .toggleSidebar {
+.cd-nav-mobile:popover-open,
+.cd-nav-top .cd-toggle-sidebar {
display: block;
}
@media (min-width: 924px) {
- .tNavMobile,
- .tNavTop .toggleSidebar {
+ .cd-nav-mobile,
+ .cd-nav-top .cd-toggle-sidebar {
display: none;
}
}
@media (min-width: 924px) {
- .tPage .page-wrapper > main {
- padding-left: 2rem;
- }
- .tNavGlobal {
+ .cd-nav-global {
display: block;
}
}
@media (min-width: 1024px) {
- .tPage .page-wrapper > main {
+ .cd-page .page-wrapper > main {
margin-left: 2.5rem;
margin-right: 2.5rem;
}
- .tNavLocal {
+ .cd-nav-local {
display: block;
}
}
diff --git a/docs/theme/Autodoc.jinja b/docs/theme/Autodoc.jinja
new file mode 100644
index 0000000..e1a967c
--- /dev/null
+++ b/docs/theme/Autodoc.jinja
@@ -0,0 +1,127 @@
+{#def
+ obj: dict | None = None,
+ name: str = "",
+ level: int = 2,
+ members: bool = True,
+#}
+
+{% set obj = obj or autodoc(name) %}
+
+
+
+
+{%- if obj.short_description -%}
+
+
+{%- endif %}
+
+{%- if obj.description -%}
+
Tabs
diff --git a/docs/content/ui/menu.md b/docs/content/ui/menu.md index 9af9983..12539d5 100644 --- a/docs/content/ui/menu.md +++ b/docs/content/ui/menu.md @@ -71,7 +71,7 @@ To animate a menu, follow the [Animating popovers section](/headless/popover#ani | --------------- | --------- | ---------- | -------------- | `target` | `str` | | Required. The ID of the linked `Popover` component. | `action` | `str` | `"toggle"` | `"open"`, `"close"`, or `"toggle"`. -| `tag` | `str` | `"button"` | HTML tag of the component. +| `tag` | `str` | `"button"` | HTML tag of the component. ### Menu @@ -106,7 +106,7 @@ To animate a menu, follow the [Animating popovers section](/headless/popover#ani ### Keyboard interaction -- Pressing the{{ obj.symbol }}
+ {{ name or obj.name }}
+ {% if obj.label -%}
+
+ {{ obj.label }}
+
+ {%- endif %}
+
+ {{ obj.short_description | markdown | utils.widont }}
+
+{% endif -%}
+
+{%- if obj.signature -%}
+
+{% filter markdown -%}
+```python
+{{ obj.signature }}
+```
+{%- endfilter %}
+
+{%- endif %}
+
+{% if obj.bases -%}
+
+
+{%- endif %}
+
+{% if obj.params -%}
+Bases:
+ {%- for base in obj.bases %} {{ base }}
{% if not loop.last %}, {% endif %}
+ {%- endfor %}
+
Argument | Description |
---|---|
{{ param.name }} |
+ {{ param.description | markdown | utils.widont }} | +
+ {{ obj.description | markdown | utils.widont }}
+
+{% endif -%}
+
+{% if obj.examples -%}
+
+
Example:
+ +{% for ex in obj.examples -%} +
+{% if ex.description %}{{ ex.description | markdown | utils.widont }}{% endif %}
+{% if ex.snippet %}{{ ex.snippet }}{% endif %}
+
+
+{%- endif %}
+
+{% if obj.raises -%}
+
+
+{% endfor -%}
+
+{%- endif %}
+
+{% if obj.returns -%}
+Returns:
+ + {% if ex.returns -%} +{{ obj.returns }}
+ {%- endif %} + {% if ex.many_returns -%} +-
+ {% for return in ex.many_returns %}
+
- {{ return }} + {%- endfor %} +
Raises:
+ +-
+ {% for raises in obj.raises -%}
+
- {{ raises.description | markdown | utils.widont }}
-
+ {% endfor -%}
+
+{%- endif %}
+
+{% if members -%}
+ {% if obj.attrs or obj.properties-%}
+
+ {% for attr in obj.attrs -%}
+
+ {% endfor %}
+ {% for attr in obj.properties %}
+
+ {%- endfor %}
+
+ {%- endif %}
+
+ {% if obj.methods -%}
+
+ {% for method in obj.methods %}
+
+ {%- endfor %}
+
+ {%- endif %}
+{%- endif %}
\ No newline at end of file
diff --git a/docs/theme/Callout.jinja b/docs/theme/Callout.jinja
index 8330cf5..4279b3a 100644
--- a/docs/theme/Callout.jinja
+++ b/docs/theme/Callout.jinja
@@ -20,7 +20,7 @@
{% if title -%}
-
+