Reusable, server-rendered UI components for Python web apps.
+
Jinjalume component gallery
+
Reusable, server-rendered UI components for Python web apps, with native browser behavior and an opt-in token-based dark theme.
- {{ alert("The first Jinjalume components are ready for feedback and contribution.", title="Welcome to the MVP", variant="info") }}
+ {{ alert("The component library is ready for feedback and contribution.", title="Welcome to Jinjalume", variant="info") }}
{% endcall %}
- {% call card("Form input", "A baseline input with help and error states.") %}
- {{ input_field("email", label="Email address", placeholder="you@example.com", help_text="We will never share your email.", required=True) }}
+ {% call card("Native dialog", "The modal uses the browser's dialog element and needs no library JavaScript.") %}
+
+ {% call modal("demo-modal", "Example dialog", description="Close it with the button or the Escape key.") %}
+
This content is rendered on the server inside a native HTML dialog.
+ {% endcall %}
{% endcall %}
{% endblock %}
diff --git a/demo/templates/partials/htmx_result.html b/demo/templates/partials/htmx_result.html
new file mode 100644
index 0000000..a12c037
--- /dev/null
+++ b/demo/templates/partials/htmx_result.html
@@ -0,0 +1,5 @@
+{% from "jinjalume/components/alert.html" import alert %}
+
+{% if result_message %}
+ {{ alert(result_message, variant=result_variant, title=result_title) }}
+{% endif %}
diff --git a/docs/components.md b/docs/components.md
new file mode 100644
index 0000000..a3813bf
--- /dev/null
+++ b/docs/components.md
@@ -0,0 +1,253 @@
+# Jinjalume component reference
+
+The demo gallery at `/` renders every component in this reference. Start it with:
+
+```bash
+make css
+flask --app demo.app run --debug
+```
+
+Every example assumes `Jinjalume(app)` has been initialized and imports the macro from its package template.
+
+## Button
+
+```jinja
+{% from "jinjalume/components/button.html" import button %}
+
+{{ button("Save changes", variant="primary", type="submit") }}
+{{ button("Cancel", variant="secondary", href="/cancel") }}
+{{ button("Delete", variant="danger", size="sm") }}
+```
+
+Signature: `button(label, variant="primary", size="md", type="button", href=None, class_name="", disabled=False)`
+
+`variant` supports `primary`, `secondary`, and `danger`. `size` supports `sm`, `md`, and `lg`. With `href`, the macro emits an anchor; otherwise it emits a button. `disabled` is native for buttons and expressed with `aria-disabled` plus `tabindex=-1` for links.
+
+Rendered shape:
+
+```html
+
+```
+
+## Badge
+
+```jinja
+{% from "jinjalume/components/badge.html" import badge %}
+
+{{ badge("Draft") }}
+{{ badge("Published", variant="success") }}
+{{ badge("Needs review", variant="warning") }}
+{{ badge("Failed", variant="danger") }}
+```
+
+Signature: `badge(label, variant="neutral", class_name="")`
+
+`variant` supports `neutral`, `success`, `warning`, and `danger`.
+
+Rendered shape:
+
+```html
+Published
+```
+
+## Alert
+
+```jinja
+{% from "jinjalume/components/alert.html" import alert %}
+
+{{ alert("Your profile was saved.", variant="success", title="Success") }}
+{{ alert("Check the highlighted fields.", variant="warning") }}
+```
+
+Signature: `alert(message, variant="info", title=None, class_name="")`
+
+`variant` supports `info`, `success`, `warning`, and `danger`. Alerts use `role="alert"` and accept plain text or already-rendered Jinja content according to the consuming application's autoescape policy.
+
+Rendered shape:
+
+```html
+
+
Success
+
Your profile was saved.
+
+```
+
+## Card
+
+Cards use a caller block for body content:
+
+```jinja
+{% from "jinjalume/components/card.html" import card %}
+
+{% call card("Account", "Update your contact details.") %}
+
Your account is active.
+{% endcall %}
+```
+
+Signature: `card(title=None, description=None, class_name="")`
+
+`title` and `description` are optional. The caller block is required and is rendered inside the card body.
+
+Rendered shape:
+
+```html
+
+
Account
+
Update your contact details.
+
...
+
+```
+
+## Input field
+
+```jinja
+{% from "jinjalume/components/input.html" import input_field %}
+
+{{ input_field(
+ "email",
+ label="Email address",
+ type="email",
+ value="person@example.com",
+ placeholder="you@example.com",
+ help_text="We will never share your email.",
+ required=True
+) }}
+```
+
+Signature: `input_field(name, label=None, value="", type="text", placeholder="", help_text=None, error=None, required=False, class_name="")`
+
+`type` is passed to the native input. When `error` is present, the control gets `aria-invalid="true"` and a linked error message. Help and error messages get deterministic IDs and are combined in `aria-describedby`.
+
+Rendered shape:
+
+```html
+
+
+
We will never share your email.
+```
+
+## Select field
+
+```jinja
+{% from "jinjalume/components/select.html" import select_field %}
+
+{{ select_field(
+ "status",
+ label="Status",
+ options=[
+ {"value": "draft", "label": "Draft"},
+ {"value": "published", "label": "Published"},
+ {"value": "archived", "label": "Archived", "disabled": True}
+ ],
+ value="published",
+ help_text="Choose the publication state.",
+ required=True
+) }}
+```
+
+Signature: `select_field(name, label=None, options=[], value="", help_text=None, error=None, required=False, class_name="")`
+
+Each option may be a mapping with `value`, `label`, and optional `disabled` keys, or a two-item `(value, label)` pair. The option whose value matches `value` is selected. The macro emits a native `