Checkpoint repo restructure
@@ -1,12 +1,21 @@
|
||||
{
|
||||
"name": "basecoat",
|
||||
"name": "basecoat-css",
|
||||
"version": "1.0.0",
|
||||
"description": "shadcn/ui for the rest of us",
|
||||
"author": "hunvreus",
|
||||
"description": "Tailwind CSS for Basecoat components",
|
||||
"author": {
|
||||
"name": "hunvreus",
|
||||
"url": "https://x.com/hunvreus"
|
||||
},
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"main": "dist/basecoat.css",
|
||||
"style": "dist/basecoat.css",
|
||||
"files": [
|
||||
"dist/",
|
||||
"templates/"
|
||||
],
|
||||
"workspaces": [
|
||||
"packages/css"
|
||||
"site"
|
||||
],
|
||||
"keywords": [
|
||||
"components",
|
||||
@@ -23,7 +32,6 @@
|
||||
"jinja",
|
||||
"nunjucks"
|
||||
],
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/hunvreus/basecoat.git"
|
||||
@@ -31,27 +39,102 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/hunvreus/basecoat/issues"
|
||||
},
|
||||
"homepage": "https://github.com/hunvreus/basecoat#readme",
|
||||
"homepage": "https://basecoatui.com/installation#npm",
|
||||
"exports": {
|
||||
".": "./dist/basecoat.css",
|
||||
"./css": "./dist/basecoat.css",
|
||||
"./basecoat": "./dist/js/basecoat.js",
|
||||
"./basecoat.min": "./dist/js/basecoat.min.js",
|
||||
"./all": "./dist/js/all.js",
|
||||
"./all.min": "./dist/js/all.min.js",
|
||||
"./accordion": "./dist/js/accordion.js",
|
||||
"./accordion.min": "./dist/js/accordion.min.js",
|
||||
"./chart": "./dist/js/chart.js",
|
||||
"./chart.min": "./dist/js/chart.min.js",
|
||||
"./combobox": "./dist/js/combobox.js",
|
||||
"./combobox.min": "./dist/js/combobox.min.js",
|
||||
"./command": "./dist/js/command.js",
|
||||
"./command.min": "./dist/js/command.min.js",
|
||||
"./dropdown-menu": "./dist/js/dropdown-menu.js",
|
||||
"./dropdown-menu.min": "./dist/js/dropdown-menu.min.js",
|
||||
"./drawer": "./dist/js/drawer.js",
|
||||
"./drawer.min": "./dist/js/drawer.min.js",
|
||||
"./popover": "./dist/js/popover.js",
|
||||
"./popover.min": "./dist/js/popover.min.js",
|
||||
"./range": "./dist/js/range.js",
|
||||
"./range.min": "./dist/js/range.min.js",
|
||||
"./select": "./dist/js/select.js",
|
||||
"./select.min": "./dist/js/select.min.js",
|
||||
"./sidebar": "./dist/js/sidebar.js",
|
||||
"./sidebar.min": "./dist/js/sidebar.min.js",
|
||||
"./tabs": "./dist/js/tabs.js",
|
||||
"./tabs.min": "./dist/js/tabs.min.js",
|
||||
"./toast": "./dist/js/toast.js",
|
||||
"./toast.min": "./dist/js/toast.min.js",
|
||||
"./all.css": "./dist/basecoat.all.css",
|
||||
"./base": "./dist/basecoat-base.css",
|
||||
"./base.css": "./dist/basecoat-base.css",
|
||||
"./base.cdn.css": "./dist/basecoat-base.cdn.css",
|
||||
"./compat": "./dist/basecoat-compat.css",
|
||||
"./compat.css": "./dist/basecoat-compat.css",
|
||||
"./compat.cdn.css": "./dist/basecoat-compat.cdn.css",
|
||||
"./compat/legacy.css": "./dist/compat/legacy.css",
|
||||
"./components": "./dist/basecoat-components.css",
|
||||
"./components.css": "./dist/basecoat-components.css",
|
||||
"./components/*": "./dist/components/*.css",
|
||||
"./components/*.css": "./dist/components/*.css",
|
||||
"./base/base.css": "./dist/base/base.css",
|
||||
"./vega": "./dist/basecoat-vega.css",
|
||||
"./vega.css": "./dist/basecoat-vega.css",
|
||||
"./nova": "./dist/basecoat-nova.css",
|
||||
"./nova.css": "./dist/basecoat-nova.css",
|
||||
"./maia": "./dist/basecoat-maia.css",
|
||||
"./maia.css": "./dist/basecoat-maia.css",
|
||||
"./lyra": "./dist/basecoat-lyra.css",
|
||||
"./lyra.css": "./dist/basecoat-lyra.css",
|
||||
"./mira": "./dist/basecoat-mira.css",
|
||||
"./mira.css": "./dist/basecoat-mira.css",
|
||||
"./luma": "./dist/basecoat-luma.css",
|
||||
"./luma.css": "./dist/basecoat-luma.css",
|
||||
"./sera": "./dist/basecoat-sera.css",
|
||||
"./sera.css": "./dist/basecoat-sera.css",
|
||||
"./rhea": "./dist/basecoat-rhea.css",
|
||||
"./rhea.css": "./dist/basecoat-rhea.css",
|
||||
"./styles/vega": "./dist/styles/vega.css",
|
||||
"./styles/nova": "./dist/styles/nova.css",
|
||||
"./styles/maia": "./dist/styles/maia.css",
|
||||
"./styles/lyra": "./dist/styles/lyra.css",
|
||||
"./styles/mira": "./dist/styles/mira.css",
|
||||
"./styles/luma": "./dist/styles/luma.css",
|
||||
"./styles/sera": "./dist/styles/sera.css",
|
||||
"./styles/rhea": "./dist/styles/rhea.css",
|
||||
"./styles/*": "./dist/styles/*.css",
|
||||
"./styles/*.css": "./dist/styles/*.css",
|
||||
"./package.json": "./package.json",
|
||||
"./styles/style-vega.css": "./dist/styles/vega.css",
|
||||
"./styles/style-nova.css": "./dist/styles/nova.css",
|
||||
"./styles/style-maia.css": "./dist/styles/maia.css",
|
||||
"./styles/style-lyra.css": "./dist/styles/lyra.css",
|
||||
"./styles/style-mira.css": "./dist/styles/mira.css",
|
||||
"./styles/style-luma.css": "./dist/styles/luma.css",
|
||||
"./styles/style-sera.css": "./dist/styles/sera.css",
|
||||
"./styles/style-rhea.css": "./dist/styles/rhea.css",
|
||||
"./styles/style-*.css": "./dist/styles/*.css",
|
||||
"./templates/nunjucks/*": "./templates/nunjucks/*",
|
||||
"./templates/jinja/*": "./templates/jinja/*"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node ./scripts/build.js",
|
||||
"docs:build": "npm run build && astro build",
|
||||
"docs:dev": "astro dev",
|
||||
"workers:dev": "npm run docs:build && wrangler dev",
|
||||
"workers:deploy": "npm run docs:build && wrangler deploy",
|
||||
"workers:preview": "npm run docs:build && wrangler versions upload"
|
||||
"docs:build": "npm run build && npm run build --workspace site",
|
||||
"docs:dev": "npm run dev --workspace site",
|
||||
"workers:dev": "npm run build && npm run workers:dev --workspace site",
|
||||
"workers:deploy": "npm run build && npm run workers:deploy --workspace site",
|
||||
"workers:preview": "npm run build && npm run workers:preview --workspace site"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fontsource/geist-mono": "^5.2.8",
|
||||
"@fontsource/geist-sans": "^5.2.5",
|
||||
"@tailwindcss/cli": "^4.1.17",
|
||||
"astro": "^6.4.6",
|
||||
"htmx.org": "^2.0.10",
|
||||
"lucide-static": "^1.17.0",
|
||||
"nunjucks": "^3.2.4",
|
||||
"prettier": "^3.5.3",
|
||||
"reallysimpledocs": "^1.0.5",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"terser": "^5.42.0",
|
||||
"wrangler": "^4.50.0"
|
||||
"terser": "^5.42.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
# basecoat-css
|
||||
|
||||
This package provides CSS, JavaScript helpers, and optional Nunjucks/Jinja templates for [Basecoat](https://basecoatui.com).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Your project must have [Tailwind CSS](https://tailwindcss.com/docs/installation) installed and configured, as Basecoat relies on Tailwind utility classes and theming.
|
||||
|
||||
## Installation
|
||||
|
||||
Install with any package manager:
|
||||
|
||||
```bash
|
||||
npm install basecoat-css # or pnpm add / yarn add / bun add
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Add it just after Tailwind in your stylesheet:
|
||||
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
@import "basecoat-css";
|
||||
```
|
||||
|
||||
That's it, you can use any Basecoat class (`btn`, `card`, `input`, etc) in your markup.
|
||||
|
||||
To use a specific style, import its standalone bundle instead:
|
||||
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
@import "basecoat-css/sera";
|
||||
```
|
||||
|
||||
### (Optional) JavaScript
|
||||
|
||||
Some interactive components need JavaScript. Import all component scripts, or cherry-pick only the entrypoints you use.
|
||||
|
||||
With a build tool (ESM):
|
||||
|
||||
```js
|
||||
import 'basecoat-css/all';
|
||||
```
|
||||
|
||||
Or cherry-pick the components you need:
|
||||
|
||||
```js
|
||||
import 'basecoat-css/tabs';
|
||||
import 'basecoat-css/popover';
|
||||
```
|
||||
|
||||
Without a build tool, copy the files from `node_modules`:
|
||||
|
||||
```bash
|
||||
npx copyfiles -u 1 "node_modules/basecoat-css/dist/js/**/*" public/js/basecoat
|
||||
```
|
||||
|
||||
Then reference what you need, e.g.
|
||||
|
||||
```html
|
||||
<script src="/js/basecoat/tabs.min.js" defer></script>
|
||||
```
|
||||
|
||||
### (Optional) Templates
|
||||
|
||||
Nunjucks and Jinja templates ship with the package. Copy the template folder for your engine into your app:
|
||||
|
||||
```bash
|
||||
cp -R node_modules/basecoat-css/templates/nunjucks ./templates/basecoat
|
||||
cp -R node_modules/basecoat-css/templates/jinja ./templates/basecoat
|
||||
```
|
||||
|
||||
Templates are meant to be copied into your app and customized.
|
||||
|
||||
## Documentation
|
||||
|
||||
For more detailed information on components, their usage, and customization options, see the [Basecoat documentation](https://basecoatui.com/installation).
|
||||
|
||||
## License
|
||||
|
||||
[MIT](https://github.com/hunvreus/basecoat/blob/main/LICENSE.md)
|
||||
@@ -1,124 +0,0 @@
|
||||
{
|
||||
"name": "basecoat-css",
|
||||
"version": "1.0.0",
|
||||
"description": "Tailwind CSS for Basecoat components",
|
||||
"author": {
|
||||
"name": "hunvreus",
|
||||
"url": "https://x.com/hunvreus"
|
||||
},
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"main": "dist/basecoat.css",
|
||||
"style": "dist/basecoat.css",
|
||||
"files": [
|
||||
"dist/",
|
||||
"templates/"
|
||||
],
|
||||
"keywords": [
|
||||
"components",
|
||||
"component library",
|
||||
"component system",
|
||||
"ui",
|
||||
"ui kit",
|
||||
"shadcn",
|
||||
"shadcn/ui",
|
||||
"tailwind",
|
||||
"tailwindcss",
|
||||
"css",
|
||||
"html",
|
||||
"jinja",
|
||||
"nunjucks"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/hunvreus/basecoat.git",
|
||||
"directory": "packages/css"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/hunvreus/basecoat/issues"
|
||||
},
|
||||
"homepage": "https://basecoatui.com/installation#npm",
|
||||
"exports": {
|
||||
".": "./dist/basecoat.css",
|
||||
"./css": "./dist/basecoat.css",
|
||||
"./basecoat": "./dist/js/basecoat.js",
|
||||
"./basecoat.min": "./dist/js/basecoat.min.js",
|
||||
"./all": "./dist/js/all.js",
|
||||
"./all.min": "./dist/js/all.min.js",
|
||||
"./accordion": "./dist/js/accordion.js",
|
||||
"./accordion.min": "./dist/js/accordion.min.js",
|
||||
"./chart": "./dist/js/chart.js",
|
||||
"./chart.min": "./dist/js/chart.min.js",
|
||||
"./combobox": "./dist/js/combobox.js",
|
||||
"./combobox.min": "./dist/js/combobox.min.js",
|
||||
"./command": "./dist/js/command.js",
|
||||
"./command.min": "./dist/js/command.min.js",
|
||||
"./dropdown-menu": "./dist/js/dropdown-menu.js",
|
||||
"./dropdown-menu.min": "./dist/js/dropdown-menu.min.js",
|
||||
"./drawer": "./dist/js/drawer.js",
|
||||
"./drawer.min": "./dist/js/drawer.min.js",
|
||||
"./popover": "./dist/js/popover.js",
|
||||
"./popover.min": "./dist/js/popover.min.js",
|
||||
"./range": "./dist/js/range.js",
|
||||
"./range.min": "./dist/js/range.min.js",
|
||||
"./select": "./dist/js/select.js",
|
||||
"./select.min": "./dist/js/select.min.js",
|
||||
"./sidebar": "./dist/js/sidebar.js",
|
||||
"./sidebar.min": "./dist/js/sidebar.min.js",
|
||||
"./tabs": "./dist/js/tabs.js",
|
||||
"./tabs.min": "./dist/js/tabs.min.js",
|
||||
"./toast": "./dist/js/toast.js",
|
||||
"./toast.min": "./dist/js/toast.min.js",
|
||||
"./all.css": "./dist/basecoat.all.css",
|
||||
"./base": "./dist/basecoat-base.css",
|
||||
"./base.css": "./dist/basecoat-base.css",
|
||||
"./base.cdn.css": "./dist/basecoat-base.cdn.css",
|
||||
"./compat": "./dist/basecoat-compat.css",
|
||||
"./compat.css": "./dist/basecoat-compat.css",
|
||||
"./compat.cdn.css": "./dist/basecoat-compat.cdn.css",
|
||||
"./compat/legacy.css": "./dist/compat/legacy.css",
|
||||
"./components": "./dist/basecoat-components.css",
|
||||
"./components.css": "./dist/basecoat-components.css",
|
||||
"./components/*": "./dist/components/*.css",
|
||||
"./components/*.css": "./dist/components/*.css",
|
||||
"./base/base.css": "./dist/base/base.css",
|
||||
"./vega": "./dist/basecoat-vega.css",
|
||||
"./vega.css": "./dist/basecoat-vega.css",
|
||||
"./nova": "./dist/basecoat-nova.css",
|
||||
"./nova.css": "./dist/basecoat-nova.css",
|
||||
"./maia": "./dist/basecoat-maia.css",
|
||||
"./maia.css": "./dist/basecoat-maia.css",
|
||||
"./lyra": "./dist/basecoat-lyra.css",
|
||||
"./lyra.css": "./dist/basecoat-lyra.css",
|
||||
"./mira": "./dist/basecoat-mira.css",
|
||||
"./mira.css": "./dist/basecoat-mira.css",
|
||||
"./luma": "./dist/basecoat-luma.css",
|
||||
"./luma.css": "./dist/basecoat-luma.css",
|
||||
"./sera": "./dist/basecoat-sera.css",
|
||||
"./sera.css": "./dist/basecoat-sera.css",
|
||||
"./rhea": "./dist/basecoat-rhea.css",
|
||||
"./rhea.css": "./dist/basecoat-rhea.css",
|
||||
"./styles/vega": "./dist/styles/vega.css",
|
||||
"./styles/nova": "./dist/styles/nova.css",
|
||||
"./styles/maia": "./dist/styles/maia.css",
|
||||
"./styles/lyra": "./dist/styles/lyra.css",
|
||||
"./styles/mira": "./dist/styles/mira.css",
|
||||
"./styles/luma": "./dist/styles/luma.css",
|
||||
"./styles/sera": "./dist/styles/sera.css",
|
||||
"./styles/rhea": "./dist/styles/rhea.css",
|
||||
"./styles/*": "./dist/styles/*.css",
|
||||
"./styles/*.css": "./dist/styles/*.css",
|
||||
"./package.json": "./package.json",
|
||||
"./styles/style-vega.css": "./dist/styles/vega.css",
|
||||
"./styles/style-nova.css": "./dist/styles/nova.css",
|
||||
"./styles/style-maia.css": "./dist/styles/maia.css",
|
||||
"./styles/style-lyra.css": "./dist/styles/lyra.css",
|
||||
"./styles/style-mira.css": "./dist/styles/mira.css",
|
||||
"./styles/style-luma.css": "./dist/styles/luma.css",
|
||||
"./styles/style-sera.css": "./dist/styles/sera.css",
|
||||
"./styles/style-rhea.css": "./dist/styles/rhea.css",
|
||||
"./styles/style-*.css": "./dist/styles/*.css",
|
||||
"./templates/nunjucks/*": "./templates/nunjucks/*",
|
||||
"./templates/jinja/*": "./templates/jinja/*"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 591 B After Width: | Height: | Size: 591 B |
|
Before Width: | Height: | Size: 510 B After Width: | Height: | Size: 510 B |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -1,142 +0,0 @@
|
||||
{#
|
||||
Renders an input-first combobox component.
|
||||
#}
|
||||
{% macro combobox(
|
||||
id=None,
|
||||
selected=None,
|
||||
name=None,
|
||||
multiple=false,
|
||||
placeholder=None,
|
||||
close_on_select=false,
|
||||
clear=false,
|
||||
auto_highlight=false,
|
||||
format="value",
|
||||
main_attrs={},
|
||||
input_attrs={},
|
||||
popover_attrs={},
|
||||
listbox_attrs={},
|
||||
hidden_input_attrs={}
|
||||
) %}
|
||||
{% set id = id or ("combobox-" + (range(100000, 999999) | random | string)) %}
|
||||
{% set selected_entries = namespace(items=[]) %}
|
||||
{% if selected is defined and selected is iterable and selected is not string %}
|
||||
{% set selected_values = selected %}
|
||||
{% for item in selected %}
|
||||
{% if item.value is defined %}
|
||||
{% set selected_entries.items = selected_entries.items + [{'value': item.value, 'label': item.label if item.label is defined else item.value}] %}
|
||||
{% else %}
|
||||
{% set selected_entries.items = selected_entries.items + [{'value': item, 'label': item}] %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% elif selected is defined and selected %}
|
||||
{% if selected.value is defined %}
|
||||
{% set selected_values = [selected.value] %}
|
||||
{% set selected_entries.items = [{'value': selected.value, 'label': selected.label if selected.label is defined else selected.value}] %}
|
||||
{% else %}
|
||||
{% set selected_values = [selected] %}
|
||||
{% set selected_entries.items = [{'value': selected, 'label': selected}] %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% set selected_values = [] %}
|
||||
{% endif %}
|
||||
{% set hidden_value = ((selected_entries.items | tojson if multiple else ((selected_entries.items[0] if selected_entries.items else none) | tojson)) if format == "object" else (selected_values | tojson if multiple else (selected_values[0] if selected_values else ''))) %}
|
||||
|
||||
<div
|
||||
id="{{ id }}"
|
||||
class="combobox {{ main_attrs.class }}"
|
||||
{% if format == "object" %}data-format="object"{% endif %}
|
||||
{% if multiple and close_on_select %}data-close-on-select="true"{% endif %}
|
||||
{% if auto_highlight %}data-auto-highlight="true"{% endif %}
|
||||
{% for key, value in main_attrs.items() %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
{% if multiple %}
|
||||
<input
|
||||
type="text"
|
||||
role="combobox"
|
||||
class="{{ input_attrs.class }}"
|
||||
placeholder="{{ placeholder or '' }}"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
aria-autocomplete="list"
|
||||
aria-expanded="false"
|
||||
aria-controls="{{ id }}-listbox"
|
||||
{% for key, value in input_attrs.items() %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
{% elif clear %}
|
||||
<div class="input-group {{ input_attrs.class }}">
|
||||
<input
|
||||
type="text"
|
||||
role="combobox"
|
||||
placeholder="{{ placeholder or '' }}"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
aria-autocomplete="list"
|
||||
aria-expanded="false"
|
||||
aria-controls="{{ id }}-listbox"
|
||||
{% for key, value in input_attrs.items() %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
<span role="group" data-align="inline-end">
|
||||
<button type="button" data-clear aria-label="Clear selection">
|
||||
<svg class="size-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
<button type="button" class="btn" data-variant="ghost" data-size="icon-xs" aria-label="Open suggestions" aria-haspopup="listbox" aria-expanded="false" aria-controls="{{ id }}-listbox">
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<input
|
||||
type="text"
|
||||
role="combobox"
|
||||
placeholder="{{ placeholder or '' }}"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
aria-autocomplete="list"
|
||||
aria-expanded="false"
|
||||
aria-controls="{{ id }}-listbox"
|
||||
class="{{ input_attrs.class }}"
|
||||
{% for key, value in input_attrs.items() %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="combobox-trigger-icon"><path d="m6 9 6 6 6-6"/></svg>
|
||||
{% endif %}
|
||||
<div
|
||||
id="{{ id }}-popover"
|
||||
data-popover
|
||||
aria-hidden="true"
|
||||
{% for key, value in popover_attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
<div
|
||||
role="listbox"
|
||||
id="{{ id }}-listbox"
|
||||
aria-orientation="vertical"
|
||||
{% if multiple %}aria-multiselectable="true"{% endif %}
|
||||
{% for key, value in listbox_attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{{ caller() if caller else '' }}
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="hidden"
|
||||
{% if name %}name="{{ name }}"{% endif %}
|
||||
value='{{ hidden_value }}'
|
||||
{% for key, value in hidden_input_attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -1,203 +0,0 @@
|
||||
{#
|
||||
Renders a standalone command menu component with search and keyboard navigation.
|
||||
|
||||
@param id {string} [optional] - Unique identifier for the command component.
|
||||
@param items {array} [optional] - Array of items to render (alternative to using caller).
|
||||
@param placeholder {string} [optional] [default="Type a command or search..."] - Placeholder text for the search input.
|
||||
@param empty_text {string} [optional] [default="No results found."] - Text displayed when no results match the search.
|
||||
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
|
||||
@param input_attrs {object} [optional] - Additional HTML attributes for the search input.
|
||||
@param menu_attrs {object} [optional] - Additional HTML attributes for the menu div.
|
||||
#}
|
||||
{% macro command(
|
||||
id=None,
|
||||
items=None,
|
||||
placeholder="Type a command or search...",
|
||||
empty_text="No results found.",
|
||||
main_attrs={},
|
||||
input_attrs={},
|
||||
menu_attrs={}
|
||||
) %}
|
||||
{% set id = id or ("command-" ~ range(100000, 999999) | random | string) %}
|
||||
<div
|
||||
id="{{ id }}"
|
||||
class="command {{ main_attrs.class }}"
|
||||
aria-label="Command menu"
|
||||
{% for key, value in main_attrs.items() %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
<header>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-icon lucide-search"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
<input
|
||||
type="text"
|
||||
id="{{ id }}-input"
|
||||
placeholder="{{ placeholder }}"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
aria-autocomplete="list"
|
||||
role="combobox"
|
||||
aria-expanded="true"
|
||||
aria-controls="{{ id }}-menu"
|
||||
{% for key, value in input_attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
</header>
|
||||
<div
|
||||
role="menu"
|
||||
id="{{ id }}-menu"
|
||||
aria-orientation="vertical"
|
||||
data-empty="{{ empty_text }}"
|
||||
class="{{ menu_attrs.class }}"
|
||||
{% for key, value in menu_attrs.items() %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
{% if items and items|length > 0 %}
|
||||
{{ render_command_items(items, id ~ "-items" if id else "items") }}
|
||||
{% else %}
|
||||
{{ caller() if caller }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{#
|
||||
Renders a command dialog (modal command palette).
|
||||
|
||||
@param id {string} [optional] - Unique identifier for the command dialog.
|
||||
@param items {array} [optional] - Array of items to render (alternative to using caller).
|
||||
@param placeholder {string} [optional] [default="Type a command or search..."] - Placeholder text for the search input.
|
||||
@param empty_text {string} [optional] [default="No results found."] - Text displayed when no results match the search.
|
||||
@param dialog_attrs {object} [optional] - Additional HTML attributes for the dialog element.
|
||||
@param input_attrs {object} [optional] - Additional HTML attributes for the search input.
|
||||
@param menu_attrs {object} [optional] - Additional HTML attributes for the menu div.
|
||||
@param open {boolean} [optional] [default=False] - Whether the command dialog should be open initially.
|
||||
#}
|
||||
{% macro command_dialog(
|
||||
id=None,
|
||||
items=None,
|
||||
placeholder="Type a command or search...",
|
||||
empty_text="No results found.",
|
||||
dialog_attrs={},
|
||||
input_attrs={},
|
||||
menu_attrs={},
|
||||
open=False
|
||||
) %}
|
||||
{% set id = id or ("command-dialog-" ~ range(100000, 999999) | random | string) %}
|
||||
<dialog
|
||||
id="{{ id }}"
|
||||
class="command-dialog {{ dialog_attrs.class }}"
|
||||
aria-label="Command menu"
|
||||
{% if open %}open{% endif %}
|
||||
onclick="if (event.target === this) this.close()"
|
||||
{% for key, value in dialog_attrs.items() %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
<div class="command">
|
||||
<header>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-icon lucide-search"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
<input
|
||||
type="text"
|
||||
id="{{ id }}-input"
|
||||
placeholder="{{ placeholder }}"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
aria-autocomplete="list"
|
||||
role="combobox"
|
||||
aria-expanded="true"
|
||||
aria-controls="{{ id }}-menu"
|
||||
{% for key, value in input_attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
</header>
|
||||
<div
|
||||
role="menu"
|
||||
id="{{ id }}-menu"
|
||||
aria-orientation="vertical"
|
||||
data-empty="{{ empty_text }}"
|
||||
class="{{ menu_attrs.class }}"
|
||||
{% for key, value in menu_attrs.items() %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
{% if items and items|length > 0 %}
|
||||
{{ render_command_items(items, id ~ "-items" if id else "items") }}
|
||||
{% else %}
|
||||
{{ caller() if caller }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
{% endmacro %}
|
||||
|
||||
{#
|
||||
Renders a list of items for the command component.
|
||||
|
||||
@param items {array} - Array of items to render. Each item can be:
|
||||
- { type: "item", label: "Text or HTML", icon: "HTML", url: "href", keywords: "search terms", disabled: true, attrs: {} }
|
||||
- { type: "group", label: "Group Title", items: [...] }
|
||||
- { type: "separator" }
|
||||
Note: Use attrs to add data-label for alternative search text if needed. If url is provided, renders as <a role="menuitem"> instead of <div role="menuitem">. Label supports HTML (marked as safe).
|
||||
@param parent_id_prefix {string} [optional] - The prefix for the item id.
|
||||
#}
|
||||
{% macro render_command_items(items, parent_id_prefix="items") %}
|
||||
{% for item in items %}
|
||||
{% set item_id = parent_id_prefix ~ "-" ~ loop.index %}
|
||||
{% if item.type == "group" %}
|
||||
{% set group_label_id = item.id if item.id else "group-label-" + item_id %}
|
||||
<div
|
||||
role="group"
|
||||
aria-labelledby="{{ group_label_id }}"
|
||||
{% if item.attrs %}
|
||||
{% for key, value in item.attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
<span role="heading" id="{{ group_label_id }}">{{ item.label | safe }}</span>
|
||||
{{ render_command_items(item['items'], item_id) if item['items'] }}
|
||||
</div>
|
||||
{% elif item.type == "separator" %}
|
||||
<hr role="separator" />
|
||||
{% elif item.type == "item" or not item.type %}
|
||||
{% if item.url %}
|
||||
<a
|
||||
id="{{ item_id }}"
|
||||
href="{{ item.url }}"
|
||||
role="menuitem"
|
||||
{% if item.keywords %}data-keywords="{{ item.keywords }}"{% endif %}
|
||||
{% if item.disabled %}aria-disabled="true"{% endif %}
|
||||
{% if item.attrs %}
|
||||
{% for key, value in item.attrs.items() %}
|
||||
{% if key != "href" %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
{% if item.icon %}{{ item.icon | safe }}{% endif %}
|
||||
{{ item.label | safe }}
|
||||
</a>
|
||||
{% else %}
|
||||
<div
|
||||
id="{{ item_id }}"
|
||||
role="menuitem"
|
||||
{% if item.keywords %}data-keywords="{{ item.keywords }}"{% endif %}
|
||||
{% if item.disabled %}aria-disabled="true"{% endif %}
|
||||
{% if item.attrs %}
|
||||
{% for key, value in item.attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
{% if item.icon %}{{ item.icon | safe }}{% endif %}
|
||||
{{ item.label | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
@@ -1,92 +0,0 @@
|
||||
{#
|
||||
Renders a dialog component with optional trigger, header, body, and footer.
|
||||
|
||||
@param id {string} - Unique identifier for the dialog component.
|
||||
@param trigger {string} [optional] - Text or HTML for the button that triggers the dialog.
|
||||
@param title {string} [optional] - Title text displayed in the dialog header.
|
||||
@param description {string} [optional] - Description text displayed below the title.
|
||||
@param footer {string} [optional] - HTML content for the dialog footer.
|
||||
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
|
||||
@param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger button.
|
||||
@param dialog_attrs {object} [optional] - Additional HTML attributes for the dialog content container.
|
||||
@param header_attrs {object} [optional] - Additional HTML attributes for the dialog header.
|
||||
@param body_attrs {object} [optional] - Additional HTML attributes for the dialog body section.
|
||||
@param footer_attrs {object} [optional] - Additional HTML attributes for the dialog footer.
|
||||
@param open {boolean} [optional] [default=false] - Whether the dialog should be open initially.
|
||||
@param close_button {boolean} [optional] [default=true] - Whether to include a close button.
|
||||
@param close_on_overlay_click {boolean} [optional] [default=true] - Whether clicking the overlay closes the dialog.
|
||||
#}
|
||||
{% macro dialog(
|
||||
id=None,
|
||||
trigger=None,
|
||||
title=None,
|
||||
description=None,
|
||||
footer=None,
|
||||
dialog_attrs={},
|
||||
trigger_attrs={},
|
||||
header_attrs={},
|
||||
body_attrs={},
|
||||
footer_attrs={},
|
||||
open=false,
|
||||
close_button=true,
|
||||
close_on_overlay_click=true
|
||||
) %}
|
||||
{% set id = id or ("dialog-" + (range(100000, 999999) | random | string)) %}
|
||||
{% if trigger %}
|
||||
<button
|
||||
type="button"
|
||||
onclick="document.getElementById('{{ id }}').showModal()"
|
||||
{% for key, value in trigger_attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{{ trigger | safe }}
|
||||
</button>
|
||||
{% endif %}
|
||||
<dialog
|
||||
id="{{ id }}"
|
||||
class="dialog {{ dialog_attrs.class }}"
|
||||
aria-labelledby="{{ id }}-title"
|
||||
{% if description %}aria-describedby="{{ id }}-description"{% endif %}
|
||||
{% if close_on_overlay_click %}onclick="if (event.target === this) this.close()"{% endif %}
|
||||
{% for key, value in dialog_attrs.items() %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
<div>
|
||||
{% if title or description %}
|
||||
<header
|
||||
{% for key, value in header_attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
<h2 id="{{ id }}-title">{{ title | safe }}</h2>
|
||||
{% if description %}<p id="{{ id }}-description">{{ description | safe }}</p>{% endif %}
|
||||
</header>
|
||||
{% endif %}
|
||||
{% if caller %}
|
||||
<section
|
||||
{% for key, value in body_attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{{ caller() }}
|
||||
</section>
|
||||
{% endif %}
|
||||
{% if footer %}
|
||||
<footer
|
||||
{% for key, value in footer_attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{{ footer | safe }}
|
||||
</footer>
|
||||
{% endif %}
|
||||
{% if close_button %}
|
||||
<button type="button" class="btn" data-variant="ghost" data-size="icon-sm" aria-label="Close dialog" onclick="this.closest('dialog').close()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</dialog>
|
||||
{% endmacro %}
|
||||
@@ -1,124 +0,0 @@
|
||||
{#
|
||||
Renders a dropdown menu component.
|
||||
|
||||
@param id {string} [optional] - Unique identifier for the dropdown component.
|
||||
@param trigger {string} [optional] - HTML content for the button that triggers the dropdown.
|
||||
@param items {array} [optional] - Array of menu items for the dropdown.
|
||||
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
|
||||
@param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger button.
|
||||
@param popover_attrs {object} [optional] - Additional HTML attributes for the dropdown content div.
|
||||
#}
|
||||
{% macro dropdown_menu(
|
||||
trigger,
|
||||
id=None,
|
||||
items=None,
|
||||
main_attrs={},
|
||||
trigger_attrs={},
|
||||
popover_attrs={},
|
||||
menu_attrs={}
|
||||
) %}
|
||||
{% set id = id or ("dropdown-menu-" + (range(100000, 999999) | random | string)) %}
|
||||
|
||||
<div
|
||||
id="{{ id }}"
|
||||
class="dropdown-menu {{ main_attrs.class }}"
|
||||
{% for key, value in main_attrs.items() %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
id="{{ id }}-trigger"
|
||||
aria-haspopup="menu"
|
||||
aria-controls="{{ id }}-menu"
|
||||
aria-expanded="false"
|
||||
{% for key, value in trigger_attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{{ trigger | safe }}
|
||||
</button>
|
||||
<div
|
||||
id="{{ id }}-popover"
|
||||
data-popover
|
||||
aria-hidden="true"
|
||||
{% for key, value in popover_attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
<div
|
||||
role="menu"
|
||||
id="{{ id }}-menu"
|
||||
aria-labelledby="{{ id }}-trigger"
|
||||
{% for key, value in menu_attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{% if items %}
|
||||
{{ render_dropdown_items(items, id ~ "-items" if id else "items") }}
|
||||
{% else %}
|
||||
{{ caller() if caller }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{#
|
||||
Renders dropdown menu items recursively.
|
||||
|
||||
@param items {array} - The array of items to render.
|
||||
@param parent_id_prefix {string} [optional] - Prefix for generating element IDs.
|
||||
#}
|
||||
{% macro render_dropdown_items(items, parent_id_prefix="items") %}
|
||||
{% for item in items %}
|
||||
{% set item_id = parent_id_prefix ~ "-" ~ loop.index %}
|
||||
|
||||
{% if item.type == "group" %}
|
||||
{% set group_label_id = item.id if item.id else "group-label-" + item_id %}
|
||||
<div
|
||||
role="group"
|
||||
aria-labelledby="{{ group_label_id }}"
|
||||
{% if item.attrs %}
|
||||
{% for key, value in item.attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
<div role="heading" id="{{ group_label_id }}">{{ item.label }}</div>
|
||||
{{ render_dropdown_items(item['items'], item_id) if item['items'] }}
|
||||
</div>
|
||||
{% elif item.type == "separator" %}
|
||||
<hr role="separator" />
|
||||
{% elif item.type == "item" or not item.type %}
|
||||
{% if item.url %}
|
||||
<a
|
||||
id="{{ item_id }}"
|
||||
role="menuitem"
|
||||
href="{{ item.url }}"
|
||||
{% if item.attrs %}
|
||||
{% for key, value in item.attrs.items() %}
|
||||
{% if key != "url" %} {{ key }}="{{ value }}" {% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
{% if item.icon %}{{ item.icon | safe }}{% endif %}
|
||||
{{ item.label | safe }}
|
||||
</a>
|
||||
{% else %}
|
||||
<div
|
||||
id="{{ item_id }}"
|
||||
role="menuitem"
|
||||
{% if item.attrs %}
|
||||
{% for key, value in item.attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
{% if item.icon %}{{ item.icon | safe }}{% endif %}
|
||||
{{ item.label | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
@@ -1,48 +0,0 @@
|
||||
{#
|
||||
Renders a generic popover component, often used internally by other components like dropdowns or selects.
|
||||
|
||||
@param id {string} [optional] - Unique identifier for the popover component.
|
||||
@param trigger {string} [optional] - HTML content for the element that triggers the popover.
|
||||
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
|
||||
@param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger element.
|
||||
@param popover_attrs {object} [optional] - Additional HTML attributes for the popover content div.
|
||||
#}
|
||||
{% macro popover(
|
||||
trigger,
|
||||
id=None,
|
||||
main_attrs={},
|
||||
trigger_attrs={},
|
||||
popover_attrs={}
|
||||
) %}
|
||||
{% set id = id or ("popover-" + (range(100000, 999999) | random | string)) %}
|
||||
|
||||
<div
|
||||
id="{{ id }}"
|
||||
class="popover {{ main_attrs.class }}"
|
||||
{% for key, value in main_attrs.items() %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
<button
|
||||
id="{{ id }}-trigger"
|
||||
type="button"
|
||||
aria-expanded="false"
|
||||
aria-controls="{{ id }}-popover"
|
||||
{% for key, value in trigger_attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{{ trigger | safe }}
|
||||
</button>
|
||||
<div
|
||||
id="{{ id }}-popover"
|
||||
data-popover
|
||||
aria-hidden="true"
|
||||
{% for key, value in popover_attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{{ caller() if caller }}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -1,183 +0,0 @@
|
||||
{#
|
||||
Renders a select component.
|
||||
|
||||
@param id {string} [optional] - Unique identifier for the select component.
|
||||
@param selected {string|array} [optional] - The initially selected value(s).
|
||||
@param name {string} [optional] - The name attribute for the hidden input storing the selected value.
|
||||
@param multiple {boolean} [optional] [default=false] - Enables multiple selection mode.
|
||||
@param placeholder {string} [optional] - Placeholder text shown when no option is selected.
|
||||
@param close_on_select {boolean} [optional] [default=false] - Closes the popover when selecting an option in multiple mode.
|
||||
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
|
||||
@param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger button.
|
||||
@param popover_attrs {object} [optional] - Additional HTML attributes for the popover content div.
|
||||
@param listbox_attrs {object} [optional] - Additional HTML attributes for the listbox div.
|
||||
@param input_attrs {object} [optional] - Additional HTML attributes for the hidden input.
|
||||
#}
|
||||
{% macro select(
|
||||
id=None,
|
||||
selected=None,
|
||||
name=None,
|
||||
items=[],
|
||||
multiple=false,
|
||||
placeholder=None,
|
||||
close_on_select=false,
|
||||
format="value",
|
||||
main_attrs={},
|
||||
trigger_attrs={},
|
||||
popover_attrs={},
|
||||
listbox_attrs={},
|
||||
input_attrs={}
|
||||
) %}
|
||||
{% set id = id or ("select-" + (range(100000, 999999) | random | string)) %}
|
||||
|
||||
{% if selected is defined and selected is iterable and selected is not string %}
|
||||
{% set selectedArray = selected %}
|
||||
{% elif selected is defined %}
|
||||
{% set selectedArray = [selected] %}
|
||||
{% else %}
|
||||
{% set selectedArray = [] %}
|
||||
{% endif %}
|
||||
{% set first_option = namespace(item=None) %}
|
||||
{% set selected_options = namespace(items=[]) %}
|
||||
|
||||
{% if items %}
|
||||
{% for item in items %}
|
||||
{% if item.type == "group" %}
|
||||
{% for sub_item in item['items'] %}
|
||||
{% if not first_option.item %}
|
||||
{% set first_option.item = sub_item %}
|
||||
{% endif %}
|
||||
{% if sub_item.value in selectedArray %}
|
||||
{% set selected_options.items = selected_options.items + [sub_item] %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% if not first_option.item %}
|
||||
{% set first_option.item = item %}
|
||||
{% endif %}
|
||||
{% if item.value in selectedArray %}
|
||||
{% set selected_options.items = selected_options.items + [item] %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% set default_option = (selected_options.items[0] if selected_options.items else (none if placeholder else first_option.item)) %}
|
||||
{% set labels = [] %}
|
||||
{% if multiple and selected_options.items %}
|
||||
{% for opt in selected_options.items %}
|
||||
{% set labels = labels + [opt.label] %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% set default_label = (', '.join(labels) if (multiple and selected_options.items) else ((placeholder or '') if (placeholder and not selected_options.items) else (default_option.label if default_option else ''))) %}
|
||||
{% set selected_payload = namespace(items=[]) %}
|
||||
{% if multiple and selected_options.items %}
|
||||
{% for opt in selected_options.items %}
|
||||
{% set selected_payload.items = selected_payload.items + [{'value': opt.value, 'label': opt.label}] %}
|
||||
{% endfor %}
|
||||
{% elif default_option %}
|
||||
{% set selected_payload.items = [{'value': default_option.value, 'label': default_option.label}] %}
|
||||
{% endif %}
|
||||
|
||||
<div
|
||||
id="{{ id }}"
|
||||
class="select {{ main_attrs.class }}"
|
||||
{% if format == "object" %}data-format="object"{% endif %}
|
||||
{% if placeholder %}data-placeholder="{{ placeholder }}"{% endif %}
|
||||
{% if multiple and close_on_select %}data-close-on-select="true"{% endif %}
|
||||
{% for key, value in main_attrs.items() %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="{{ trigger_attrs.class }}"
|
||||
id="{{ id }}-trigger"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded="false"
|
||||
aria-controls="{{ id }}-listbox"
|
||||
{% for key, value in trigger_attrs.items() %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
<span class="truncate">{{ default_label }}</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down-icon lucide-chevron-down text-muted-foreground opacity-50 shrink-0"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
<div
|
||||
id="{{ id }}-popover"
|
||||
data-popover
|
||||
aria-hidden="true"
|
||||
{% for key, value in popover_attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
<div
|
||||
role="listbox"
|
||||
id="{{ id }}-listbox"
|
||||
aria-orientation="vertical"
|
||||
aria-labelledby="{{ id }}-trigger"
|
||||
{% if multiple %}aria-multiselectable="true"{% endif %}
|
||||
{% for key, value in listbox_attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{% if items %}
|
||||
{{ render_select_items(items, selectedArray, id ~ "-items" if id else "items") }}
|
||||
{% else %}
|
||||
{{ caller() if caller }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="hidden"
|
||||
name="{{ name or id ~ '-value' }}"
|
||||
value="{% if format == 'object' %}{{ (selected_payload.items if multiple else (selected_payload.items[0] if selected_payload.items else none)) | tojson }}{% elif multiple %}{{ selectedArray | tojson }}{% else %}{{ (default_option.value if default_option) or '' }}{% endif %}"
|
||||
{% for key, value in input_attrs.items() %}
|
||||
{% if key != 'name' and key != 'value' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{#
|
||||
Renders a list of items for the select component.
|
||||
|
||||
@param items {array} - The array of items to render.
|
||||
@param parent_id_prefix {string} [optional] - The prefix for the item id.
|
||||
#}
|
||||
{% macro render_select_items(items, selectedArray, parent_id_prefix="items") %}
|
||||
{% for item in items %}
|
||||
{% set item_id = parent_id_prefix ~ "-" ~ loop.index %}
|
||||
{% if item.type == "group" %}
|
||||
{% set group_label_id = item.id if item.id else "group-label-" + item_id %}
|
||||
<div
|
||||
role="group"
|
||||
aria-labelledby="{{ group_label_id }}"
|
||||
{% if item.attrs %}
|
||||
{% for key, value in item.attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
<div role="heading" id="{{ group_label_id }}">{{ item.label }}</div>
|
||||
{{ render_select_items(item['items'], selectedArray, item_id) if item['items'] }}
|
||||
</div>
|
||||
{% elif item.type == "separator" %}
|
||||
<hr role="separator" />
|
||||
{% elif item.type == "item" or not item.type %}
|
||||
<div
|
||||
id="{{ item_id }}"
|
||||
role="option"
|
||||
{% if item.value is defined and item.value is not none %}data-value="{{ item.value }}"{% endif %}
|
||||
{% if item.value in selectedArray %}aria-selected="true"{% endif %}
|
||||
{% if item.attrs %}
|
||||
{% for key, value in item.attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
{{ item.label | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
@@ -1,144 +0,0 @@
|
||||
{#
|
||||
Renders a sidebar component.
|
||||
|
||||
@param id {string} [optional] - Unique identifier for the sidebar component.
|
||||
@param label {string} [optional] - Label for the sidebar navigation.
|
||||
@param open {boolean} [optional] - Whether the sidebar is open.
|
||||
@param side {string} [optional] - Side of the sidebar to display.
|
||||
@param header {string} [optional] - Header content for the sidebar.
|
||||
@param footer {string} [optional] - Footer content for the sidebar.
|
||||
@param menu {array} [optional] - Array of menu items for the sidebar.
|
||||
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
|
||||
@param header_attrs {object} [optional] - Additional HTML attributes for the header div.
|
||||
@param content_attrs {object} [optional] - Additional HTML attributes for the content div.
|
||||
@param footer_attrs {object} [optional] - Additional HTML attributes for the footer div.
|
||||
#}
|
||||
{% macro sidebar(
|
||||
id=None,
|
||||
label="Sidebar navigation",
|
||||
open=true,
|
||||
side=None,
|
||||
header=None,
|
||||
footer=None,
|
||||
menu=None,
|
||||
main_attrs={},
|
||||
header_attrs={},
|
||||
content_attrs={},
|
||||
footer_attrs={}
|
||||
) %}
|
||||
<aside
|
||||
{% if id %}id="{{ id }}"{% endif %}
|
||||
class="sidebar {{ main_attrs.class }}"
|
||||
data-side="{{ side if side else "left" }}"
|
||||
aria-hidden="{{ "true" if not open else "false" }}"
|
||||
{{ "inert" if not open }}
|
||||
{% for key, value in main_attrs.items() %}
|
||||
{% if key != "class" %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
<nav
|
||||
aria-label="{{ label }}"
|
||||
>
|
||||
{% if header %}
|
||||
<header
|
||||
{% for key, value in header_attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{{ header | safe }}
|
||||
</header>
|
||||
{% endif %}
|
||||
|
||||
<section
|
||||
{% for key, value in content_attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{% if menu %}
|
||||
{{ render_sidebar_content(menu, id ~ "-content" if id else "content") }}
|
||||
{% else %}
|
||||
{{ caller() if caller }}
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{% if footer %}
|
||||
<footer
|
||||
{% for key, value in footer_attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{{ footer | safe }}
|
||||
</footer>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</aside>
|
||||
{% endmacro %}
|
||||
|
||||
{#
|
||||
Renders sidebar content recursively (groups, items, submenus, separators).
|
||||
|
||||
@param items {array} - The array of items to render.
|
||||
@param parent_id_prefix {string} [optional] - Prefix for generating element IDs.
|
||||
#}
|
||||
{% macro render_sidebar_content(items, parent_id_prefix="content") %}
|
||||
{% for item in items %}
|
||||
{% set item_id = parent_id_prefix ~ "-" ~ loop.index %}
|
||||
|
||||
{% if item.type == "group" %}
|
||||
{% set group_label_id = item.id if item.id else "group-label-" + item_id %}
|
||||
<div
|
||||
role="group"
|
||||
{% if item.label %}aria-labelledby="{{ group_label_id }}"{% endif %}
|
||||
{% if item.attrs %}
|
||||
{% for key, value in item.attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
{% if item.label %}
|
||||
<h3 id="{{ group_label_id }}">{{ item.label }}</h3>
|
||||
{% endif %}
|
||||
<ul>
|
||||
{{ render_sidebar_content(item["items"], item_id) if item["items"] }}
|
||||
</ul>
|
||||
</div>
|
||||
{% elif item.type == "separator" %}
|
||||
<hr role="separator"/>
|
||||
{% elif item.type == "submenu" %}
|
||||
<li>
|
||||
<details
|
||||
id="submenu-{{ item_id }}"
|
||||
{{ "open" if item.open }}
|
||||
{% if item.attrs %}
|
||||
{% for key, value in item.attrs.items() %}
|
||||
{% if key != "open" %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
<summary aria-controls="submenu-{{ item_id }}-content">
|
||||
{% if item.icon %}{{ item.icon | safe }}{% endif %}
|
||||
<span>{{ item.label | safe }}</span>
|
||||
</summary>
|
||||
<ul id="submenu-{{ item_id }}-content">
|
||||
{{ render_sidebar_content(item["items"], item_id) if item["items"] }}
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
{% elif item.type == "item" or not item.type %}
|
||||
<li>
|
||||
<a
|
||||
href="{{ item.url }}"
|
||||
{% if item.current %}aria-current="page"{% endif %}
|
||||
{% if item.attrs %}
|
||||
{% for key, value in item.attrs.items() %}
|
||||
{% if key != "href" and key != "aria-current" %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
{% if item.icon %}{{ item.icon | safe }}{% endif %}
|
||||
<span>{{ item.label | safe }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
@@ -1,80 +0,0 @@
|
||||
{#
|
||||
Renders a tabs component.
|
||||
|
||||
@param id {string} - Unique identifier for the tabs component.
|
||||
@param tabsets {array} - An array of objects, each representing a tab and its panel.
|
||||
Each object should have:
|
||||
- id {string}: Unique identifier prefix for the tab and panel.
|
||||
- tab {string}: HTML content for the tab button.
|
||||
- panel {string} [optional]: HTML content for the tab panel.
|
||||
- tab_attrs {object} [optional]: Additional HTML attributes for the tab button.
|
||||
- panel_attrs {object} [optional]: Additional HTML attributes for the tab panel div.
|
||||
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
|
||||
@param tablist_attrs {object} [optional] - Additional HTML attributes for the tablist nav element.
|
||||
Set aria-orientation="vertical" for vertical tabs or data-variant="line" for line tabs.
|
||||
@param default_tab_index {number} [optional] [default=1] - The 1-based index of the tab to be active initially.
|
||||
#}
|
||||
{% macro tabs(
|
||||
id=None,
|
||||
tabsets=[],
|
||||
main_attrs={},
|
||||
tablist_attrs={},
|
||||
default_tab_index=1
|
||||
)
|
||||
%}
|
||||
{% set id = id or ("tabs-" + (range(100000, 999999) | random | string)) %}
|
||||
{% set orientation = tablist_attrs['aria-orientation'] or 'horizontal' %}
|
||||
<div
|
||||
class="tabs {{ main_attrs.class }}"
|
||||
id="{{ id }}"
|
||||
{% for key, value in main_attrs.items() %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
<nav
|
||||
role="tablist"
|
||||
aria-orientation="{{ orientation }}"
|
||||
{% for key, value in tablist_attrs.items() %}
|
||||
{% if key != 'aria-orientation' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
{% for tabset in tabsets %}
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
id="{{ id }}-tab-{{ loop.index }}"
|
||||
aria-controls="{{ id }}-panel-{{ loop.index }}"
|
||||
aria-selected="{{ 'true' if loop.index == default_tab_index else 'false' }}"
|
||||
tabindex="{{ '0' if loop.index == default_tab_index and not tabset.tab_attrs.disabled and tabset.tab_attrs['aria-disabled'] != 'true' else '-1' }}"
|
||||
{% if tabset.tab_attrs %}
|
||||
{% for key, value in tabset.tab_attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
{{ tabset.tab | safe }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
|
||||
{% for tabset in tabsets %}
|
||||
{% if tabset.panel %}
|
||||
<div
|
||||
role="tabpanel"
|
||||
id="{{ id }}-panel-{{ loop.index }}"
|
||||
aria-labelledby="{{ id }}-tab-{{ loop.index }}"
|
||||
tabindex="-1"
|
||||
aria-selected="{{ 'true' if loop.index == default_tab_index else 'false' }}"
|
||||
{% if loop.index != default_tab_index %}hidden{% endif %}
|
||||
{% if tabset.panel_attrs %}
|
||||
{% for key, value in tabset.panel_attrs.items() %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
{{ tabset.panel | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -1,117 +0,0 @@
|
||||
{#
|
||||
Renders a toaster container and individual toast messages.
|
||||
Can render the initial container or be used with htmx OOB swaps to add toasts dynamically.
|
||||
|
||||
@param id {string} [optional] [default="toaster"] - Unique identifier for the toaster container.
|
||||
@param toasts {array} [optional] - An array of toast objects to render initially. See the <code>toast()</code> macro for more details.
|
||||
@param attrs {object} [optional] - Additional HTML attributes for the main toaster container div.
|
||||
|
||||
Supports caller() for custom toast markup.
|
||||
#}
|
||||
{% macro toaster(
|
||||
id="toaster",
|
||||
toasts=[],
|
||||
attrs={}
|
||||
) %}
|
||||
<div
|
||||
id="{{ id }}"
|
||||
class="toaster {{ attrs.class }}"
|
||||
{% for key, value in attrs.items() %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
{% for item in toasts %}
|
||||
{{ toast(
|
||||
category=item.category,
|
||||
title=item.title,
|
||||
description=item.description,
|
||||
action=item.action,
|
||||
cancel=item.cancel,
|
||||
attrs=item.attrs
|
||||
) }}
|
||||
{% endfor %}
|
||||
{{ caller() if caller }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{#
|
||||
Renders a single toast message.
|
||||
|
||||
@param category {string} [optional] [default="success"] - Type of toast ('success', 'error', 'info', 'warning'). Determines icon and ARIA role.
|
||||
@param title {string} [optional] - The main title text of the toast.
|
||||
@param description {string} [optional] - The secondary description text.
|
||||
@param action {object} [optional] - Defines an action button.
|
||||
- label {string}: Button text.
|
||||
- onclick {string}: JavaScript code to execute on click.
|
||||
- url {string}: URL for an anchor link button.
|
||||
@param cancel {object} [optional] - Defines a cancel/dismiss button (similar structure to action).
|
||||
- label {string}: Button text.
|
||||
- onclick {string}: JavaScript code to execute on click.
|
||||
- url {string}: URL for an anchor link button.
|
||||
@param attrs {object} [optional] - Additional HTML attributes for the toast div.
|
||||
#}
|
||||
{% macro toast(
|
||||
category="success",
|
||||
title="",
|
||||
description="",
|
||||
action=None,
|
||||
cancel=None,
|
||||
attrs={}
|
||||
) %}
|
||||
<div
|
||||
class="toast {{ attrs.class }}"
|
||||
role="{{ 'alert' if category == 'error' else 'status' }}"
|
||||
aria-atomic="true"
|
||||
aria-hidden="false"
|
||||
{% if category %}data-category="{{ category }}"{% endif %}
|
||||
{% for key, value in attrs.items() %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
<div class="toast-content">
|
||||
{% if category in ["error", "success", "info", "warning"] %}
|
||||
{% if category == "success" %}
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>
|
||||
{% elif category == "error" %}
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>
|
||||
{% elif category == "info" %}
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
|
||||
{% elif category == "warning" %}
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<section>
|
||||
{% if title %}<h2>{{ title }}</h2>{% endif %}
|
||||
{% if description %}<p>{{ description }}</p>{% endif %}
|
||||
</section>
|
||||
{% if action or cancel %}
|
||||
<footer>
|
||||
{% if action %}
|
||||
{% if action.href %}
|
||||
<a
|
||||
href="{{ action.href }}"
|
||||
class="btn" data-size="sm"
|
||||
data-toast-action
|
||||
>{{ action.label }}</a>
|
||||
{% else %}
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
data-toast-action
|
||||
{% if action.onclick %}onclick="{{ action.onclick }}"{% endif %}
|
||||
>{{ action.label }}</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if cancel %}
|
||||
<button
|
||||
type="button"
|
||||
class="btn" data-variant="outline" data-size="sm"
|
||||
data-toast-cancel
|
||||
{% if cancel.onclick %}onclick="{{ cancel.onclick }}"{% endif %}
|
||||
>{{ cancel.label }}</button>
|
||||
{% endif %}
|
||||
</footer>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -1,157 +0,0 @@
|
||||
{#
|
||||
Renders an input-first combobox component.
|
||||
|
||||
@param id {string} [optional] - Unique identifier for the combobox component.
|
||||
@param selected {string|array} [optional] - The initially selected value(s).
|
||||
@param name {string} [optional] - The name attribute for the hidden input storing the selected value.
|
||||
@param multiple {boolean} [optional] [default=false] - Enables multiple selection mode with chips.
|
||||
@param placeholder {string} [optional] - Placeholder text for the visible input.
|
||||
@param close_on_select {boolean} [optional] [default=false] - Closes the popover when selecting an option in multiple mode.
|
||||
@param clear {boolean} [optional] [default=false] - Shows a clear button in single-select mode.
|
||||
@param auto_highlight {boolean} [optional] [default=false] - Highlights the first matching option while filtering.
|
||||
@param format {string} [optional] [default="value"] - Hidden input serialization format: "value" or "object".
|
||||
@param main_attrs {object} [optional] - Additional HTML attributes for the root div.
|
||||
@param input_attrs {object} [optional] - Additional HTML attributes for the visible input.
|
||||
@param popover_attrs {object} [optional] - Additional HTML attributes for the popover div.
|
||||
@param listbox_attrs {object} [optional] - Additional HTML attributes for the listbox div.
|
||||
@param hidden_input_attrs {object} [optional] - Additional HTML attributes for the hidden input.
|
||||
#}
|
||||
{% macro combobox(
|
||||
id=None,
|
||||
selected=None,
|
||||
name=None,
|
||||
multiple=false,
|
||||
placeholder=None,
|
||||
close_on_select=false,
|
||||
clear=false,
|
||||
auto_highlight=false,
|
||||
format="value",
|
||||
main_attrs={},
|
||||
input_attrs={},
|
||||
popover_attrs={},
|
||||
listbox_attrs={},
|
||||
hidden_input_attrs={}
|
||||
) %}
|
||||
{% set id = id or ("combobox-" + (range(100000, 999999) | random | string)) %}
|
||||
{% set selected_values = [] %}
|
||||
{% set selected_entries = [] %}
|
||||
{% if selected is defined and selected is iterable and selected is not string %}
|
||||
{% for item in selected %}
|
||||
{% if item.value is defined %}
|
||||
{% set selected_values = (selected_values.push(item.value), selected_values) %}
|
||||
{% set selected_entries = (selected_entries.push({ value: item.value, label: (item.label if item.label is defined else item.value) }), selected_entries) %}
|
||||
{% else %}
|
||||
{% set selected_values = (selected_values.push(item), selected_values) %}
|
||||
{% set selected_entries = (selected_entries.push({ value: item, label: item }), selected_entries) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% elif selected is defined and selected %}
|
||||
{% if selected.value is defined %}
|
||||
{% set selected_values = [selected.value] %}
|
||||
{% set selected_entries = [{ value: selected.value, label: (selected.label if selected.label is defined else selected.value) }] %}
|
||||
{% else %}
|
||||
{% set selected_values = [selected] %}
|
||||
{% set selected_entries = [{ value: selected, label: selected }] %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% set hidden_value = ((selected_entries | dump if multiple else ((selected_entries[0] if selected_entries[0] else None) | dump)) if format == "object" else (selected_values | dump if multiple else (selected_values[0] if selected_values[0] else ''))) %}
|
||||
|
||||
<div
|
||||
id="{{ id }}"
|
||||
class="combobox {{ main_attrs.class }}"
|
||||
{% if format == "object" %}data-format="object"{% endif %}
|
||||
{% if multiple and close_on_select %}data-close-on-select="true"{% endif %}
|
||||
{% if auto_highlight %}data-auto-highlight="true"{% endif %}
|
||||
{% for key, value in main_attrs %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
{% if multiple %}
|
||||
<input
|
||||
type="text"
|
||||
role="combobox"
|
||||
class="{{ input_attrs.class }}"
|
||||
placeholder="{{ placeholder or '' }}"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
aria-autocomplete="list"
|
||||
aria-expanded="false"
|
||||
aria-controls="{{ id }}-listbox"
|
||||
{% for key, value in input_attrs %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
{% elif clear %}
|
||||
<div class="input-group {{ input_attrs.class }}">
|
||||
<input
|
||||
type="text"
|
||||
role="combobox"
|
||||
placeholder="{{ placeholder or '' }}"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
aria-autocomplete="list"
|
||||
aria-expanded="false"
|
||||
aria-controls="{{ id }}-listbox"
|
||||
{% for key, value in input_attrs %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
<span role="group" data-align="inline-end">
|
||||
<button type="button" data-clear aria-label="Clear selection">
|
||||
<svg class="size-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
<button type="button" class="btn" data-variant="ghost" data-size="icon-xs" aria-label="Open suggestions" aria-haspopup="listbox" aria-expanded="false" aria-controls="{{ id }}-listbox">
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<input
|
||||
type="text"
|
||||
role="combobox"
|
||||
placeholder="{{ placeholder or '' }}"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
aria-autocomplete="list"
|
||||
aria-expanded="false"
|
||||
aria-controls="{{ id }}-listbox"
|
||||
class="{{ input_attrs.class }}"
|
||||
{% for key, value in input_attrs %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="combobox-trigger-icon"><path d="m6 9 6 6 6-6"/></svg>
|
||||
{% endif %}
|
||||
<div
|
||||
id="{{ id }}-popover"
|
||||
data-popover
|
||||
aria-hidden="true"
|
||||
{% for key, value in popover_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
<div
|
||||
role="listbox"
|
||||
id="{{ id }}-listbox"
|
||||
aria-orientation="vertical"
|
||||
{% if multiple %}aria-multiselectable="true"{% endif %}
|
||||
{% for key, value in listbox_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{{ caller() if caller else '' }}
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="hidden"
|
||||
{% if name %}name="{{ name }}"{% endif %}
|
||||
value='{{ hidden_value }}'
|
||||
{% for key, value in hidden_input_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@@ -1,203 +0,0 @@
|
||||
{#
|
||||
Renders a standalone command menu component with search and keyboard navigation.
|
||||
|
||||
@param id {string} [optional] - Unique identifier for the command component.
|
||||
@param items {array} [optional] - Array of items to render (alternative to using caller).
|
||||
@param placeholder {string} [optional] [default="Type a command or search..."] - Placeholder text for the search input.
|
||||
@param empty_text {string} [optional] [default="No results found."] - Text displayed when no results match the search.
|
||||
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
|
||||
@param input_attrs {object} [optional] - Additional HTML attributes for the search input.
|
||||
@param menu_attrs {object} [optional] - Additional HTML attributes for the menu div.
|
||||
#}
|
||||
{% macro command(
|
||||
id=None,
|
||||
items=None,
|
||||
placeholder="Type a command or search...",
|
||||
empty_text="No results found.",
|
||||
main_attrs={},
|
||||
input_attrs={},
|
||||
menu_attrs={}
|
||||
) %}
|
||||
{% set id = id or ("command-" + (range(100000, 999999) | random | string)) %}
|
||||
<div
|
||||
id="{{ id }}"
|
||||
class="command {{ main_attrs.class }}"
|
||||
aria-label="Command menu"
|
||||
{% for key, value in main_attrs %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
<header>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-icon lucide-search"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
<input
|
||||
type="text"
|
||||
id="{{ id }}-input"
|
||||
placeholder="{{ placeholder }}"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
aria-autocomplete="list"
|
||||
role="combobox"
|
||||
aria-expanded="true"
|
||||
aria-controls="{{ id }}-menu"
|
||||
{% for key, value in input_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
</header>
|
||||
<div
|
||||
role="menu"
|
||||
id="{{ id }}-menu"
|
||||
aria-orientation="vertical"
|
||||
data-empty="{{ empty_text }}"
|
||||
class="{{ menu_attrs.class }}"
|
||||
{% for key, value in menu_attrs %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
{% if items and items.length > 0 %}
|
||||
{{ render_command_items(items, id ~ "-items" if id else "items") }}
|
||||
{% else %}
|
||||
{{ caller() if caller }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{#
|
||||
Renders a command dialog (modal command palette).
|
||||
|
||||
@param id {string} [optional] - Unique identifier for the command dialog.
|
||||
@param items {array} [optional] - Array of items to render (alternative to using caller).
|
||||
@param placeholder {string} [optional] [default="Type a command or search..."] - Placeholder text for the search input.
|
||||
@param empty_text {string} [optional] [default="No results found."] - Text displayed when no results match the search.
|
||||
@param dialog_attrs {object} [optional] - Additional HTML attributes for the dialog element.
|
||||
@param input_attrs {object} [optional] - Additional HTML attributes for the search input.
|
||||
@param menu_attrs {object} [optional] - Additional HTML attributes for the menu div.
|
||||
@param open {boolean} [optional] [default=false] - Whether the command dialog should be open initially.
|
||||
#}
|
||||
{% macro command_dialog(
|
||||
id=None,
|
||||
items=None,
|
||||
placeholder="Type a command or search...",
|
||||
empty_text="No results found.",
|
||||
dialog_attrs={},
|
||||
input_attrs={},
|
||||
menu_attrs={},
|
||||
open=false
|
||||
) %}
|
||||
{% set id = id or ("command-dialog-" + (range(100000, 999999) | random | string)) %}
|
||||
<dialog
|
||||
id="{{ id }}"
|
||||
class="command-dialog {{ dialog_attrs.class }}"
|
||||
aria-label="Command menu"
|
||||
{% if open %}open{% endif %}
|
||||
onclick="if (event.target === this) this.close()"
|
||||
{% for key, value in dialog_attrs %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
<div class="command">
|
||||
<header>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-icon lucide-search"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
<input
|
||||
type="text"
|
||||
id="{{ id }}-input"
|
||||
placeholder="{{ placeholder }}"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
aria-autocomplete="list"
|
||||
role="combobox"
|
||||
aria-expanded="true"
|
||||
aria-controls="{{ id }}-menu"
|
||||
{% for key, value in input_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
</header>
|
||||
<div
|
||||
role="menu"
|
||||
id="{{ id }}-menu"
|
||||
aria-orientation="vertical"
|
||||
data-empty="{{ empty_text }}"
|
||||
class="{{ menu_attrs.class }}"
|
||||
{% for key, value in menu_attrs %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
{% if items and items.length > 0 %}
|
||||
{{ render_command_items(items, id ~ "-items" if id else "items") }}
|
||||
{% else %}
|
||||
{{ caller() if caller }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
{% endmacro %}
|
||||
|
||||
{#
|
||||
Renders a list of items for the command component.
|
||||
|
||||
@param items {array} - Array of items to render. Each item can be:
|
||||
- { type: "item", label: "Text or HTML", icon: "HTML", url: "href", keywords: "search terms", disabled: true, attrs: {} }
|
||||
- { type: "group", label: "Group Title", items: [...] }
|
||||
- { type: "separator" }
|
||||
Note: Use attrs to add data-label for alternative search text if needed. If url is provided, renders as <a role="menuitem"> instead of <div role="menuitem">. Label supports HTML (marked as safe).
|
||||
@param parent_id_prefix {string} [optional] - The prefix for the item id.
|
||||
#}
|
||||
{% macro render_command_items(items, parent_id_prefix="items") %}
|
||||
{% for item in items %}
|
||||
{% set item_id = parent_id_prefix ~ "-" ~ loop.index %}
|
||||
{% if item.type == "group" %}
|
||||
{% set group_label_id = item.id if item.id else "group-label-" + item_id %}
|
||||
<div
|
||||
role="group"
|
||||
aria-labelledby="{{ group_label_id }}"
|
||||
{% if item.attrs %}
|
||||
{% for key, value in item.attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
<span role="heading" id="{{ group_label_id }}">{{ item.label | safe }}</span>
|
||||
{{ render_command_items(item.items, item_id) if item.items }}
|
||||
</div>
|
||||
{% elif item.type == "separator" %}
|
||||
<hr role="separator" />
|
||||
{% elif item.type == "item" or not item.type %}
|
||||
{% if item.url %}
|
||||
<a
|
||||
id="{{ item_id }}"
|
||||
href="{{ item.url }}"
|
||||
role="menuitem"
|
||||
{% if item.keywords %}data-keywords="{{ item.keywords }}"{% endif %}
|
||||
{% if item.disabled %}aria-disabled="true"{% endif %}
|
||||
{% if item.attrs %}
|
||||
{% for key, value in item.attrs %}
|
||||
{% if key != "href" %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
{% if item.icon %}{{ item.icon | safe }}{% endif %}
|
||||
{{ item.label | safe }}
|
||||
</a>
|
||||
{% else %}
|
||||
<div
|
||||
id="{{ item_id }}"
|
||||
role="menuitem"
|
||||
{% if item.keywords %}data-keywords="{{ item.keywords }}"{% endif %}
|
||||
{% if item.disabled %}aria-disabled="true"{% endif %}
|
||||
{% if item.attrs %}
|
||||
{% for key, value in item.attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
>
|
||||
{% if item.icon %}{{ item.icon | safe }}{% endif %}
|
||||
{{ item.label | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
@@ -1,92 +0,0 @@
|
||||
{#
|
||||
Renders a dialog component with optional trigger, header, body, and footer.
|
||||
|
||||
@param id {string} - Unique identifier for the dialog component.
|
||||
@param trigger {string} [optional] - Text or HTML for the button that triggers the dialog.
|
||||
@param title {string} [optional] - Title text displayed in the dialog header.
|
||||
@param description {string} [optional] - Description text displayed below the title.
|
||||
@param footer {string} [optional] - HTML content for the dialog footer.
|
||||
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
|
||||
@param trigger_attrs {object} [optional] - Additional HTML attributes for the trigger button.
|
||||
@param dialog_attrs {object} [optional] - Additional HTML attributes for the dialog content container.
|
||||
@param header_attrs {object} [optional] - Additional HTML attributes for the dialog header.
|
||||
@param body_attrs {object} [optional] - Additional HTML attributes for the dialog body section.
|
||||
@param footer_attrs {object} [optional] - Additional HTML attributes for the dialog footer.
|
||||
@param open {boolean} [optional] [default=false] - Whether the dialog should be open initially.
|
||||
@param close_button {boolean} [optional] [default=true] - Whether to include a close button.
|
||||
@param close_on_overlay_click {boolean} [optional] [default=true] - Whether clicking the overlay closes the dialog.
|
||||
#}
|
||||
{% macro dialog(
|
||||
id=None,
|
||||
trigger=None,
|
||||
title=None,
|
||||
description=None,
|
||||
footer=None,
|
||||
dialog_attrs={},
|
||||
trigger_attrs={},
|
||||
header_attrs={},
|
||||
body_attrs={},
|
||||
footer_attrs={},
|
||||
open=false,
|
||||
close_button=true,
|
||||
close_on_overlay_click=true
|
||||
) %}
|
||||
{% set id = id or ("dialog-" + (range(100000, 999999) | random | string)) %}
|
||||
{% if trigger %}
|
||||
<button
|
||||
type="button"
|
||||
onclick="document.getElementById('{{ id }}').showModal()"
|
||||
{% for key, value in trigger_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{{ trigger | safe }}
|
||||
</button>
|
||||
{% endif %}
|
||||
<dialog
|
||||
id="{{ id }}"
|
||||
class="dialog {{ dialog_attrs.class }}"
|
||||
aria-labelledby="{{ id }}-title"
|
||||
{% if description %}aria-describedby="{{ id }}-description"{% endif %}
|
||||
{% if close_on_overlay_click %}onclick="if (event.target === this) this.close()"{% endif %}
|
||||
{% for key, value in dialog_attrs %}
|
||||
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
<div>
|
||||
{% if title or description %}
|
||||
<header
|
||||
{% for key, value in header_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
<h2 id="{{ id }}-title">{{ title | safe }}</h2>
|
||||
{% if description %}<p id="{{ id }}-description">{{ description | safe }}</p>{% endif %}
|
||||
</header>
|
||||
{% endif %}
|
||||
{% if caller %}
|
||||
<section
|
||||
{% for key, value in body_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{{ caller() }}
|
||||
</section>
|
||||
{% endif %}
|
||||
{% if footer %}
|
||||
<footer
|
||||
{% for key, value in footer_attrs %}
|
||||
{{ key }}="{{ value }}"
|
||||
{% endfor %}
|
||||
>
|
||||
{{ footer | safe }}
|
||||
</footer>
|
||||
{% endif %}
|
||||
{% if close_button %}
|
||||
<button type="button" class="btn" data-variant="ghost" data-size="icon-sm" aria-label="Close dialog" onclick="this.closest('dialog').close()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</dialog>
|
||||
{% endmacro %}
|
||||