Prepare 1.0.0 release

This commit is contained in:
Ronan Berder
2026-06-27 18:07:58 +08:00
parent f254b92bd3
commit a9e1fe7b91
56 changed files with 5595 additions and 9121 deletions
-1
View File
@@ -1 +0,0 @@
docs/src/assets/styles.css merge=theirs
+4 -5
View File
@@ -4,12 +4,11 @@
_site/
node_modules/
packages/css/dist/
# Logs
npm-debug.log*
dist/
docs/generated/
public/assets/js/
public/fragments/
templates/
site/dist/
site/public/assets/js/
site/public/fragments/
+6 -5
View File
@@ -14,10 +14,11 @@
## Source Boundaries
- Do not hand-edit build outputs:
- `packages/*/dist/**`
- `dist/**`
- `docs/generated/**`
- `public/assets/**`
- `templates/**`
- `site/dist/**`
- `site/public/assets/js/**`
- `site/public/fragments/**`
- Generated source entrypoints such as `src/css/basecoat-vega.css` are committed, but must be regenerated through `scripts/generate-css-entrypoints.js` or build scripts, not manually maintained.
## Public API and Markup
@@ -64,8 +65,8 @@
- component CSS: `src/css/components/<component>.css`
- style packs: `src/css/styles/{vega,nova,maia,lyra,mira,luma,sera,rhea}.css`
- JS: `src/js/<component>.js` when behavior changes
- templates: `src/nunjucks/*.njk` and `src/jinja/*.html.jinja` when markup changes
- docs: `docs/src/components/<component>.md`
- templates: `src/templates/nunjucks/*.njk` and `src/templates/jinja/*.html.jinja` when markup changes
- docs: `site/src/docs/components/<component>.mdx`
- Map upstream concepts, not implementation details. Do not port `cn-*` selectors directly.
- Classify differences as `drift fixed`, `intentional`, or `deferred`.
+2
View File
@@ -11,10 +11,12 @@
### Added
- Added Nunjucks and Jinja template files to the `basecoat-css` package.
- Added a custom docs 404 page for Cloudflare static asset fallback.
### Changed
- Narrowed dark-mode generated selectors to `html.dark` to reduce broad style recalculation work.
- Switched the docs site to Astro's sitemap integration and updated `robots.txt` to point at the generated sitemap index.
### Fixed
+1 -1
View File
@@ -14,7 +14,7 @@
## Cloudflare Workers deploys
Docs are built with 11ty and deployed as Workers Static Assets from `_site`.
Docs are built with ReallySimpleDocs/Astro and deployed as Workers Static Assets from `site/dist`.
- Local preview: `npm run workers:dev`
- Production deploy: `npm run workers:deploy`
+20 -30
View File
@@ -2,17 +2,18 @@
Basecoat is a Tailwind CSS, vanilla HTML/CSS/JavaScript implementation of the shadcn/ui design system. It provides shadcn-style components for any web stack without React, Radix, or framework runtime dependencies.
![screenshot](public/assets/images/screenshot.png)
![screenshot](site/public/assets/images/screenshot.png)
## Features
- Semantic HTML-first components.
- Tailwind CSS v4 source files and generated CSS bundles.
- Small vanilla JavaScript for components that need behavior.
- Nunjucks and Jinja templates.
- Standalone style packs: Vega, Nova, Maia, Lyra, Mira, Luma, Sera, and Rhea.
- Dark mode and CSS variable theming.
- Compatible with shadcn/ui themes.
- Dark mode.
- CDN, npm, and template usage paths.
- Nunjucks and Jinja templates.
## Documentation
@@ -20,16 +21,6 @@ Basecoat is a Tailwind CSS, vanilla HTML/CSS/JavaScript implementation of the sh
- Installation: [basecoatui.com/installation](https://basecoatui.com/installation)
- Customization: [basecoatui.com/customization](https://basecoatui.com/customization)
## Packages
This repository publishes one workspace package:
- `basecoat-css`: CSS, JavaScript, Nunjucks templates, and Jinja templates.
Package details live in:
- `packages/css/README.md`
## Install
```bash
@@ -62,23 +53,21 @@ Use the styleless base plus a custom style file:
```text
.
├── docs/
│ ├── generated/ Generated RSD/Astro docs input
│ └── src/ Documentation source pages, examples, and fragments
├── packages/
│ └── css/ Published CSS package
├── public/ Generated docs assets for Astro
├── dist/ Generated package output
├── scripts/ Build and generation scripts
└── src/
├── components/ Astro overrides for ReallySimpleDocs
├── css/
│ ├── base/ Shared tokens, base layer, and semantic utilities
│ ├── components/ Component structure and behavior hooks
│ └── styles/ Style-pack visual rules
├── docs.css Documentation-site CSS extension
├── jinja/ Jinja component templates
├── js/ Vanilla JS components and registry
└── nunjucks/ Nunjucks component templates
├── site/ ReallySimpleDocs/Astro documentation app
│ ├── public/ Static site assets
│ └── src/
│ ├── components/ RSD component overrides
│ ├── docs/ Documentation pages and examples
│ ├── fragments/ Site-only rendered fragments
│ ├── pages/ Astro routes outside the docs tree
│ └── scripts/ Site scripts
├── src/ Package source
│ ├── css/
│ ├── js/
│ └── templates/
└── templates/ Generated package template output
```
## CSS Architecture
@@ -111,8 +100,9 @@ These entrypoints are generated by `scripts/generate-css-entrypoints.js` and by
```bash
# Install dependencies.
npm i
npm --prefix site i
# Generate docs input and run the ReallySimpleDocs/Astro site.
# Run the ReallySimpleDocs/Astro site.
npm run docs:dev
# Build package assets.
+6 -8801
View File
File diff suppressed because it is too large Load Diff
+7 -8
View File
@@ -14,9 +14,6 @@
"dist/",
"templates/"
],
"workspaces": [
"site"
],
"keywords": [
"components",
"component library",
@@ -125,11 +122,13 @@
},
"scripts": {
"build": "node ./scripts/build.js",
"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"
"prepack": "node ./scripts/build.js",
"docs:build": "npm run build && npm --prefix site run build",
"docs:dev": "npm --prefix site run dev",
"docs:preview": "npm run docs:build && npm --prefix site run preview",
"workers:dev": "npm run build && npm --prefix site run workers:dev",
"workers:deploy": "npm run build && npm --prefix site run workers:deploy",
"workers:preview": "npm run build && npm --prefix site run workers:preview"
},
"devDependencies": {
"@tailwindcss/cli": "^4.1.17",
+6 -7
View File
@@ -64,23 +64,22 @@ async function copyDirRecursive(src, dest) {
}
}
// Main build function to prepare packages for publishing.
// Main build function to prepare the package for publishing.
async function build() {
console.log('Starting build process...');
await generateCssEntrypoints();
// Define all necessary paths
const cssPackageDir = path.join(projectRoot, 'packages', 'css');
const cssDistDir = path.join(cssPackageDir, 'dist');
const cssTemplatesDir = path.join(cssPackageDir, 'templates');
const cssDistDir = path.join(projectRoot, 'dist');
const cssTemplatesDir = path.join(projectRoot, 'templates');
const srcDir = path.join(projectRoot, 'src');
const srcCssDir = path.join(srcDir, 'css');
const srcCssStylesDir = path.join(srcCssDir, 'styles');
const srcCssCompatDir = path.join(srcCssDir, 'compat');
const srcJsDir = path.join(srcDir, 'js');
const srcNunjucksDir = path.join(srcDir, 'nunjucks');
const srcJinjaDir = path.join(srcDir, 'jinja');
const srcNunjucksDir = path.join(srcDir, 'templates', 'nunjucks');
const srcJinjaDir = path.join(srcDir, 'templates', 'jinja');
// Clean previous build artifacts
console.log('Cleaning distribution directories...');
@@ -128,7 +127,7 @@ async function build() {
// Build CSS package
console.log('Building CSS package...');
await ensureDir(cssDistDir); // Ensure dist dir exists for css package
await ensureDir(cssDistDir);
const styles = ['vega', 'nova', 'maia', 'lyra', 'mira', 'luma', 'sera', 'rhea'];
await fs.copyFile(path.join(srcCssDir, 'basecoat.css'), path.join(cssDistDir, 'basecoat.css'));
await fs.copyFile(path.join(srcCssDir, 'basecoat.all.css'), path.join(cssDistDir, 'basecoat.all.css'));
+21 -10
View File
@@ -1,8 +1,18 @@
import { defineConfig } from 'astro/config';
import { readFileSync } from 'node:fs';
import sitemap from '@astrojs/sitemap';
import reallySimpleDocs from 'reallysimpledocs/astro';
const packageJson = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
const includeInSitemap = (page) => {
const { pathname } = new URL(page);
return pathname === '/' || (
pathname.endsWith('/') &&
!pathname.startsWith('/fragments/')
);
};
export default defineConfig({
site: process.env.SITE_URL || 'https://basecoatui.com',
@@ -16,7 +26,7 @@ export default defineConfig({
},
integrations: [
reallySimpleDocs({
docsDir: './docs/src',
docsDir: './src/docs',
routeBase: '/',
style: 'vega',
css: false,
@@ -28,9 +38,9 @@ export default defineConfig({
'hx-push-url': 'true',
},
components: {
Head: './docs/src/site/StyleHead.astro',
SidebarHeader: './docs/src/site/SidebarHeader.astro',
ContentHeader: './docs/src/site/ContentHeader.astro',
Head: './src/components/StyleHead.astro',
SidebarHeader: './src/components/SidebarHeader.astro',
ContentHeader: './src/components/ContentHeader.astro',
},
site: {
title: 'Basecoat',
@@ -42,15 +52,16 @@ export default defineConfig({
name: 'Ronan Berder',
x: '@hunvreus',
},
assets: {
favicon: 'favicon.svg',
appleTouchIcon: 'apple-touch-icon.png',
socialImage: 'social.png',
},
favicon: 'favicon.svg',
appleTouchIcon: 'apple-touch-icon.png',
socialImage: 'social.png',
logo: {
svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" class="h-4 w-4"><rect width="256" height="256" fill="none"></rect><line x1="208" y1="128" x2="128" y2="208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"></line><line x1="192" y1="40" x2="40" y2="192" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"></line></svg>',
},
},
}),
sitemap({
filter: includeInSitemap,
}),
],
});
+5119
View File
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
{
"private": true,
"type": "module",
"scripts": {
"build": "astro build",
"dev": "astro dev",
"preview": "astro preview",
"workers:dev": "npm run build && wrangler dev",
"workers:deploy": "npm run build && wrangler deploy",
"workers:preview": "npm run build && wrangler versions upload"
},
"dependencies": {
"@astrojs/sitemap": "^3.7.3",
"basecoat-css": "file:..",
"htmx.org": "^2.0.10",
"lucide-static": "^1.17.0",
"nunjucks": "^3.2.4",
"reallysimpledocs": "^1.0.6"
},
"devDependencies": {
"astro": "^6.4.6",
"wrangler": "^4.50.0"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 101 KiB

@@ -1,12 +1,12 @@
---
import vegaUrl from "../assets/styles/style-vega.css?url";
import novaUrl from "../assets/styles/style-nova.css?url";
import maiaUrl from "../assets/styles/style-maia.css?url";
import lyraUrl from "../assets/styles/style-lyra.css?url";
import miraUrl from "../assets/styles/style-mira.css?url";
import lumaUrl from "../assets/styles/style-luma.css?url";
import seraUrl from "../assets/styles/style-sera.css?url";
import rheaUrl from "../assets/styles/style-rhea.css?url";
import vegaUrl from "../docs/assets/styles/style-vega.css?url";
import novaUrl from "../docs/assets/styles/style-nova.css?url";
import maiaUrl from "../docs/assets/styles/style-maia.css?url";
import lyraUrl from "../docs/assets/styles/style-lyra.css?url";
import miraUrl from "../docs/assets/styles/style-mira.css?url";
import lumaUrl from "../docs/assets/styles/style-luma.css?url";
import seraUrl from "../docs/assets/styles/style-sera.css?url";
import rheaUrl from "../docs/assets/styles/style-rhea.css?url";
const styleUrls = {
vega: vegaUrl,
@@ -47,7 +47,7 @@ const styleUrls = {
};
const getStyleHref = (style) => {
const href = styleUrls[style];
return href?.startsWith("/docs/src/assets/styles/") ? `${href}?direct` : href;
return href?.startsWith("/src/docs/assets/styles/") ? `${href}?direct` : href;
};
const applyStyle = (style) => {
@@ -1,13 +1,13 @@
---
import vegaUrl from "../assets/styles/style-vega.css?url";
import novaUrl from "../assets/styles/style-nova.css?url";
import maiaUrl from "../assets/styles/style-maia.css?url";
import lyraUrl from "../assets/styles/style-lyra.css?url";
import miraUrl from "../assets/styles/style-mira.css?url";
import lumaUrl from "../assets/styles/style-luma.css?url";
import seraUrl from "../assets/styles/style-sera.css?url";
import rheaUrl from "../assets/styles/style-rhea.css?url";
import demoInteractionsUrl from "./demo-interactions.js?url";
import vegaUrl from "../docs/assets/styles/style-vega.css?url";
import novaUrl from "../docs/assets/styles/style-nova.css?url";
import maiaUrl from "../docs/assets/styles/style-maia.css?url";
import lyraUrl from "../docs/assets/styles/style-lyra.css?url";
import miraUrl from "../docs/assets/styles/style-mira.css?url";
import lumaUrl from "../docs/assets/styles/style-luma.css?url";
import seraUrl from "../docs/assets/styles/style-sera.css?url";
import rheaUrl from "../docs/assets/styles/style-rhea.css?url";
import demoInteractionsUrl from "../scripts/demo-interactions.js?url";
import htmxUrl from "htmx.org/dist/htmx.min.js?url";
const styleUrls = {
@@ -104,7 +104,7 @@ const styleUrls = {
};
const getStyleHref = (style) => {
const href = styleUrls[style];
return href?.startsWith("/docs/src/assets/styles/") ? `${href}?direct` : href;
return href?.startsWith("/src/docs/assets/styles/") ? `${href}?direct` : href;
};
const style = getStoredStyle();
+4 -4
View File
@@ -1,8 +1,8 @@
@import "tailwindcss" source(none);
@source "../../../../docs/src/**/*.{astro,html,js,md,mdx}";
@source "../../../../src/pages/**/*.{astro,html,js}";
@source "../../../**/*.{astro,html,js,md,mdx}";
@source "../../../pages/**/*.{astro,html,js}";
@reference "tailwindcss";
@reference "../../../../src/css/base/base.css";
@import "../../../../src/css/basecoat-luma.css";
@reference "../../../../../src/css/base/base.css";
@import "../../../../../src/css/basecoat-luma.css";
@import "reallysimpledocs/css";
@import "reallysimpledocs/css/styles/luma";
+4 -4
View File
@@ -1,8 +1,8 @@
@import "tailwindcss" source(none);
@source "../../../../docs/src/**/*.{astro,html,js,md,mdx}";
@source "../../../../src/pages/**/*.{astro,html,js}";
@source "../../../**/*.{astro,html,js,md,mdx}";
@source "../../../pages/**/*.{astro,html,js}";
@reference "tailwindcss";
@reference "../../../../src/css/base/base.css";
@import "../../../../src/css/basecoat-lyra.css";
@reference "../../../../../src/css/base/base.css";
@import "../../../../../src/css/basecoat-lyra.css";
@import "reallysimpledocs/css";
@import "reallysimpledocs/css/styles/lyra";
+4 -4
View File
@@ -1,8 +1,8 @@
@import "tailwindcss" source(none);
@source "../../../../docs/src/**/*.{astro,html,js,md,mdx}";
@source "../../../../src/pages/**/*.{astro,html,js}";
@source "../../../**/*.{astro,html,js,md,mdx}";
@source "../../../pages/**/*.{astro,html,js}";
@reference "tailwindcss";
@reference "../../../../src/css/base/base.css";
@import "../../../../src/css/basecoat-maia.css";
@reference "../../../../../src/css/base/base.css";
@import "../../../../../src/css/basecoat-maia.css";
@import "reallysimpledocs/css";
@import "reallysimpledocs/css/styles/maia";
+4 -4
View File
@@ -1,8 +1,8 @@
@import "tailwindcss" source(none);
@source "../../../../docs/src/**/*.{astro,html,js,md,mdx}";
@source "../../../../src/pages/**/*.{astro,html,js}";
@source "../../../**/*.{astro,html,js,md,mdx}";
@source "../../../pages/**/*.{astro,html,js}";
@reference "tailwindcss";
@reference "../../../../src/css/base/base.css";
@import "../../../../src/css/basecoat-mira.css";
@reference "../../../../../src/css/base/base.css";
@import "../../../../../src/css/basecoat-mira.css";
@import "reallysimpledocs/css";
@import "reallysimpledocs/css/styles/mira";
+4 -4
View File
@@ -1,8 +1,8 @@
@import "tailwindcss" source(none);
@source "../../../../docs/src/**/*.{astro,html,js,md,mdx}";
@source "../../../../src/pages/**/*.{astro,html,js}";
@source "../../../**/*.{astro,html,js,md,mdx}";
@source "../../../pages/**/*.{astro,html,js}";
@reference "tailwindcss";
@reference "../../../../src/css/base/base.css";
@import "../../../../src/css/basecoat-nova.css";
@reference "../../../../../src/css/base/base.css";
@import "../../../../../src/css/basecoat-nova.css";
@import "reallysimpledocs/css";
@import "reallysimpledocs/css/styles/nova";
+4 -4
View File
@@ -1,8 +1,8 @@
@import "tailwindcss" source(none);
@source "../../../../docs/src/**/*.{astro,html,js,md,mdx}";
@source "../../../../src/pages/**/*.{astro,html,js}";
@source "../../../**/*.{astro,html,js,md,mdx}";
@source "../../../pages/**/*.{astro,html,js}";
@reference "tailwindcss";
@reference "../../../../src/css/base/base.css";
@import "../../../../src/css/basecoat-rhea.css";
@reference "../../../../../src/css/base/base.css";
@import "../../../../../src/css/basecoat-rhea.css";
@import "reallysimpledocs/css";
@import "reallysimpledocs/css/styles/rhea";
+4 -4
View File
@@ -1,8 +1,8 @@
@import "tailwindcss" source(none);
@source "../../../../docs/src/**/*.{astro,html,js,md,mdx}";
@source "../../../../src/pages/**/*.{astro,html,js}";
@source "../../../**/*.{astro,html,js,md,mdx}";
@source "../../../pages/**/*.{astro,html,js}";
@reference "tailwindcss";
@reference "../../../../src/css/base/base.css";
@import "../../../../src/css/basecoat-sera.css";
@reference "../../../../../src/css/base/base.css";
@import "../../../../../src/css/basecoat-sera.css";
@import "reallysimpledocs/css";
@import "reallysimpledocs/css/styles/sera";
+4 -4
View File
@@ -1,8 +1,8 @@
@import "tailwindcss" source(none);
@source "../../../../docs/src/**/*.{astro,html,js,md,mdx}";
@source "../../../../src/pages/**/*.{astro,html,js}";
@source "../../../**/*.{astro,html,js,md,mdx}";
@source "../../../pages/**/*.{astro,html,js}";
@reference "tailwindcss";
@reference "../../../../src/css/base/base.css";
@import "../../../../src/css/basecoat-vega.css";
@reference "../../../../../src/css/base/base.css";
@import "../../../../../src/css/basecoat-vega.css";
@import "reallysimpledocs/css";
@import "reallysimpledocs/css/styles/vega";
+3 -3
View File
@@ -1,6 +1,6 @@
import basecoatRuntimeSource from "../../../src/js/basecoat.js?raw"
import chartRuntimeSource from "../../../src/js/chart.js?raw"
import chartExamplesSource from "../site/chart-examples.js?raw"
import basecoatRuntimeSource from "../../../../src/js/basecoat.js?raw"
import chartRuntimeSource from "../../../../src/js/chart.js?raw"
import chartExamplesSource from "../../scripts/chart-examples.js?raw"
# Chart
+1 -1
View File
@@ -1,4 +1,4 @@
import packageJson from "../../packages/css/package.json"
import packageJson from "../../../package.json"
export const basecoatVersion = packageJson.version
+15 -12
View File
@@ -12,17 +12,17 @@ The templates are versioned with `basecoat-css`, so the generated markup matches
| Component | Macro | Nunjucks | Jinja |
| --- | --- | --- | --- |
| [Combobox](/components/combobox) | `combobox()` | [combobox.njk](https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/combobox.njk#L18) | [combobox.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/jinja/combobox.html.jinja#L4) |
| [Command](/components/command) | `command()` | [command.njk](https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/command.njk#L12) | [command.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/jinja/command.html.jinja#L12) |
| [Command](/components/command) | `command_dialog()` | [command.njk](https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/command.njk#L79) | [command.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/jinja/command.html.jinja#L79) |
| [Dialog](/components/dialog) | `dialog()` | [dialog.njk](https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/dialog.njk#L19) | [dialog.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/jinja/dialog.html.jinja#L19) |
| [Dropdown Menu](/components/dropdown-menu) | `dropdown_menu()` | [dropdown-menu.njk](https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/dropdown-menu.njk#L11) | [dropdown-menu.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/jinja/dropdown-menu.html.jinja#L11) |
| [Popover](/components/popover) | `popover()` | [popover.njk](https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/popover.njk#L10) | [popover.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/jinja/popover.html.jinja#L10) |
| [Select](/components/select) | `select()` | [select.njk](https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/select.njk#L17) | [select.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/jinja/select.html.jinja#L16) |
| [Sidebar](/components/sidebar) | `sidebar()` | [sidebar.njk](https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/sidebar.njk#L16) | [sidebar.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/jinja/sidebar.html.jinja#L16) |
| [Tabs](/components/tabs) | `tabs()` | [tabs.njk](https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/tabs.njk#L16) | [tabs.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/jinja/tabs.html.jinja#L17) |
| [Toast](/components/toast) | `toaster()` | [toast.njk](https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/toast.njk#L11) | [toast.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/jinja/toast.html.jinja#L11) |
| [Toast](/components/toast) | `toast()` | [toast.njk](https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/toast.njk#L53) | [toast.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/jinja/toast.html.jinja#L53) |
| [Combobox](/components/combobox) | `combobox()` | [combobox.njk](https://github.com/hunvreus/basecoat/blob/main/src/templates/nunjucks/combobox.njk) | [combobox.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/templates/jinja/combobox.html.jinja) |
| [Command](/components/command) | `command()` | [command.njk](https://github.com/hunvreus/basecoat/blob/main/src/templates/nunjucks/command.njk) | [command.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/templates/jinja/command.html.jinja) |
| [Command](/components/command) | `command_dialog()` | [command.njk](https://github.com/hunvreus/basecoat/blob/main/src/templates/nunjucks/command.njk) | [command.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/templates/jinja/command.html.jinja) |
| [Dialog](/components/dialog) | `dialog()` | [dialog.njk](https://github.com/hunvreus/basecoat/blob/main/src/templates/nunjucks/dialog.njk) | [dialog.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/templates/jinja/dialog.html.jinja) |
| [Dropdown Menu](/components/dropdown-menu) | `dropdown_menu()` | [dropdown-menu.njk](https://github.com/hunvreus/basecoat/blob/main/src/templates/nunjucks/dropdown-menu.njk) | [dropdown-menu.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/templates/jinja/dropdown-menu.html.jinja) |
| [Popover](/components/popover) | `popover()` | [popover.njk](https://github.com/hunvreus/basecoat/blob/main/src/templates/nunjucks/popover.njk) | [popover.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/templates/jinja/popover.html.jinja) |
| [Select](/components/select) | `select()` | [select.njk](https://github.com/hunvreus/basecoat/blob/main/src/templates/nunjucks/select.njk) | [select.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/templates/jinja/select.html.jinja) |
| [Sidebar](/components/sidebar) | `sidebar()` | [sidebar.njk](https://github.com/hunvreus/basecoat/blob/main/src/templates/nunjucks/sidebar.njk) | [sidebar.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/templates/jinja/sidebar.html.jinja) |
| [Tabs](/components/tabs) | `tabs()` | [tabs.njk](https://github.com/hunvreus/basecoat/blob/main/src/templates/nunjucks/tabs.njk) | [tabs.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/templates/jinja/tabs.html.jinja) |
| [Toast](/components/toast) | `toaster()` | [toast.njk](https://github.com/hunvreus/basecoat/blob/main/src/templates/nunjucks/toast.njk) | [toast.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/templates/jinja/toast.html.jinja) |
| [Toast](/components/toast) | `toast()` | [toast.njk](https://github.com/hunvreus/basecoat/blob/main/src/templates/nunjucks/toast.njk) | [toast.html.jinja](https://github.com/hunvreus/basecoat/blob/main/src/templates/jinja/toast.html.jinja) |
Some macro files include internal recursive helpers such as `render_select_items()`. Those helpers are implementation details and are not part of the public API.
@@ -46,7 +46,7 @@ Some macro files include internal recursive helpers such as `render_select_items
Copy only the files you use if you do not want the full template folder.
You can also copy the [Nunjucks templates](https://github.com/hunvreus/basecoat/tree/main/src/nunjucks) or [Jinja templates](https://github.com/hunvreus/basecoat/tree/main/src/jinja) from GitHub.
You can also copy the [Nunjucks templates](https://github.com/hunvreus/basecoat/tree/main/src/templates/nunjucks) or [Jinja templates](https://github.com/hunvreus/basecoat/tree/main/src/templates/jinja) from GitHub.
</Step>
<Step>
### Use a Macro
@@ -93,6 +93,7 @@ Some macro files include internal recursive helpers such as `render_select_items
| `multiple` | `boolean` | `false` | Enables chip-based multiple selection. |
| `placeholder` | `string` | `None` / `none` | Visible input placeholder. |
| `close_on_select` | `boolean` | `false` | Closes the popover after each option selection in multiple mode. |
| `clear` | `boolean` | `false` | Shows a clear button in single-select mode. |
| `auto_highlight` | `boolean` | `false` | Highlights the first matching option while filtering. |
| `format` | `"value" \| "object"` | `"value"` | Hidden input serialization format. |
| `main_attrs` | `object` | `{}` | Extra attributes for the `.combobox` root. |
@@ -225,6 +226,8 @@ Some macro files include internal recursive helpers such as `render_select_items
| `category` | `"success" \| "error" \| "info" \| "warning"` | `"success"` | Toast category. Controls icon and ARIA role. |
| `title` | `string` | `""` | Toast title. |
| `description` | `string` | `""` | Toast description. |
| `duration` | `number` | default timeout | Timeout in milliseconds. Use `-1` to keep the toast open. |
| `icon` | `string` | category icon | Custom icon HTML. |
| `action` | `object` | `None` / `none` | Action button/link config. Supports `label`, `onclick`, and `href`. |
| `cancel` | `object` | `None` / `none` | Cancel button/link config. Supports `label`, `onclick`, and `href`. |
| `attrs` | `object` | `{}` | Extra attributes for the `.toast` element. |
+53
View File
@@ -0,0 +1,53 @@
---
import StyleHead from "../components/StyleHead.astro";
const title = "Page not found";
const description = "This page does not exist, or it may have moved.";
---
<!doctype html>
<html lang="en">
<head>
<script is:inline>
(() => {
const media = matchMedia("(prefers-color-scheme: dark)");
const getStoredTheme = () => {
try {
return localStorage.getItem("themeMode");
} catch (_) {
return null;
}
};
const syncTheme = () => {
const stored = getStoredTheme();
document.documentElement.classList.toggle("dark", stored ? stored === "dark" : media.matches);
};
try {
syncTheme();
} catch (_) {}
})();
</script>
<StyleHead />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title} | Basecoat</title>
<meta name="description" content={description} />
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg" />
</head>
<body class="min-h-dvh bg-background text-foreground antialiased">
<main class="grid min-h-dvh place-items-center px-6 py-16">
<section class="empty max-w-sm">
<header>
<h1>Page not found</h1>
<p>This page does not exist, or it may have moved. Try the docs or return home.</p>
</header>
<footer>
<div class="flex gap-2">
<a class="btn" data-size="sm" href="/">Home</a>
<a class="btn" data-variant="outline" data-size="sm" href="/introduction/">Docs</a>
</div>
</footer>
</section>
</main>
</body>
</html>
+3 -3
View File
@@ -5,8 +5,8 @@ import Nunjucks from 'nunjucks';
const types = ['success', 'error', 'info', 'warning'];
const env = new Nunjucks.Environment(
new Nunjucks.FileSystemLoader([
path.resolve('src/nunjucks'),
path.resolve('docs/src/fragments/toast'),
path.resolve('../src/templates/nunjucks'),
path.resolve('src/fragments/toast'),
]),
{ autoescape: true },
);
@@ -18,7 +18,7 @@ export function getStaticPaths() {
export function GET({ params }) {
const type = String(params.type || '');
if (!types.includes(type)) return new Response('Not found', { status: 404 });
const source = fs.readFileSync(path.resolve('docs/src/fragments/toast', `${type}.njk`), 'utf8');
const source = fs.readFileSync(path.resolve('src/fragments/toast', `${type}.njk`), 'utf8');
const html = env.renderString(source).trim();
return new Response(html, { headers: { 'content-type': 'text/html; charset=utf-8' } });
}
+10 -10
View File
@@ -1,15 +1,15 @@
---
import { CommandDialog } from "reallysimpledocs/components";
import basecoatJsUrl from "basecoat-css/all.min?url";
import StyleHead from "../../docs/src/site/StyleHead.astro";
import vegaUrl from "../../docs/src/assets/styles/style-vega.css?url";
import novaUrl from "../../docs/src/assets/styles/style-nova.css?url";
import maiaUrl from "../../docs/src/assets/styles/style-maia.css?url";
import lyraUrl from "../../docs/src/assets/styles/style-lyra.css?url";
import miraUrl from "../../docs/src/assets/styles/style-mira.css?url";
import lumaUrl from "../../docs/src/assets/styles/style-luma.css?url";
import seraUrl from "../../docs/src/assets/styles/style-sera.css?url";
import rheaUrl from "../../docs/src/assets/styles/style-rhea.css?url";
import StyleHead from "../components/StyleHead.astro";
import vegaUrl from "../docs/assets/styles/style-vega.css?url";
import novaUrl from "../docs/assets/styles/style-nova.css?url";
import maiaUrl from "../docs/assets/styles/style-maia.css?url";
import lyraUrl from "../docs/assets/styles/style-lyra.css?url";
import miraUrl from "../docs/assets/styles/style-mira.css?url";
import lumaUrl from "../docs/assets/styles/style-luma.css?url";
import seraUrl from "../docs/assets/styles/style-sera.css?url";
import rheaUrl from "../docs/assets/styles/style-rhea.css?url";
import ArrowRight from "lucide-static/icons/arrow-right.svg?raw";
import Activity from "lucide-static/icons/activity.svg?raw";
import BanknoteArrowUp from "lucide-static/icons/banknote-arrow-up.svg?raw";
@@ -664,7 +664,7 @@ const sidebarSections = [
};
const applyStyle = (style) => {
const nextStyle = Object.prototype.hasOwnProperty.call(styleUrls, style) ? style : "vega";
const href = styleUrls[nextStyle]?.startsWith("/docs/src/assets/styles/")
const href = styleUrls[nextStyle]?.startsWith("/src/docs/assets/styles/")
? `${styleUrls[nextStyle]}?direct`
: styleUrls[nextStyle];
const link = getStyleLink();
+1 -1
View File
@@ -1,6 +1,6 @@
export function GET({ site }) {
const origin = site?.toString().replace(/\/$/, '') || 'https://basecoatui.com';
return new Response(`User-agent: *\nAllow: /\n\nSitemap: ${origin}/sitemap.xml\n`, {
return new Response(`User-agent: *\nAllow: /\n\nSitemap: ${origin}/sitemap-index.xml\n`, {
headers: { 'content-type': 'text/plain; charset=utf-8' },
});
}
-26
View File
@@ -1,26 +0,0 @@
import fs from 'node:fs';
import path from 'node:path';
const routePath = (slug) => slug === 'index' ? '/' : `/${slug.replace(/^\/+|\/+$/g, '')}/`;
function flatten(menu) {
const out = [];
const walk = (items = []) => {
for (const item of items) {
if (typeof item === 'string') out.push(item);
else if (item?.slug) out.push(item.slug);
else if (item?.type === 'submenu') walk(item.items || []);
}
};
for (const group of menu || []) if (group?.type === 'group') walk(group.items || []);
return out;
}
export function GET({ site }) {
const origin = site?.toString().replace(/\/$/, '') || 'https://basecoatui.com';
const manifest = JSON.parse(fs.readFileSync(path.resolve('docs/src/docs.json'), 'utf8'));
const urls = flatten(manifest.menu).map((slug) => ` <url><loc>${origin}${routePath(slug)}</loc></url>`).join('\n');
return new Response(`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls}\n</urlset>\n`, {
headers: { 'content-type': 'application/xml; charset=utf-8' },
});
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"$schema": "./node_modules/wrangler/config-schema.json",
"$schema": "../node_modules/wrangler/config-schema.json",
"name": "basecoat",
"compatibility_date": "2026-06-02",
"assets": {
+10 -5
View File
@@ -18,6 +18,11 @@
hidden_input_attrs={}
) %}
{% set id = id or ("combobox-" + (range(100000, 999999) | random | string)) %}
{% set main_attrs = main_attrs or {} %}
{% set input_attrs = input_attrs or {} %}
{% set popover_attrs = popover_attrs or {} %}
{% set listbox_attrs = listbox_attrs or {} %}
{% set hidden_input_attrs = hidden_input_attrs or {} %}
{% set selected_entries = namespace(items=[]) %}
{% if selected is defined and selected is iterable and selected is not string %}
{% set selected_values = selected %}
@@ -43,7 +48,7 @@
<div
id="{{ id }}"
class="combobox {{ main_attrs.class }}"
class="combobox {{ main_attrs.class | default('') }}"
{% 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 %}
@@ -55,7 +60,7 @@
<input
type="text"
role="combobox"
class="{{ input_attrs.class }}"
class="{{ input_attrs.class | default('') }}"
placeholder="{{ placeholder or '' }}"
autocomplete="off"
autocorrect="off"
@@ -68,7 +73,7 @@
{% endfor %}
>
{% elif clear %}
<div class="input-group {{ input_attrs.class }}">
<div class="input-group {{ input_attrs.class | default('') }}">
<input
type="text"
role="combobox"
@@ -103,7 +108,7 @@
aria-autocomplete="list"
aria-expanded="false"
aria-controls="{{ id }}-listbox"
class="{{ input_attrs.class }}"
class="{{ input_attrs.class | default('') }}"
{% for key, value in input_attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
@@ -127,7 +132,7 @@
{{ key }}="{{ value }}"
{% endfor %}
>
{{ caller() if caller else '' }}
{{ caller() if caller is defined else '' }}
</div>
</div>
<input
+31 -24
View File
@@ -19,9 +19,12 @@
menu_attrs={}
) %}
{% set id = id or ("command-" ~ range(100000, 999999) | random | string) %}
{% set main_attrs = main_attrs or {} %}
{% set input_attrs = input_attrs or {} %}
{% set menu_attrs = menu_attrs or {} %}
<div
id="{{ id }}"
class="command {{ main_attrs.class }}"
class="command {{ main_attrs.class | default('') }}"
aria-label="Command menu"
{% for key, value in main_attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
@@ -50,7 +53,7 @@
id="{{ id }}-menu"
aria-orientation="vertical"
data-empty="{{ empty_text }}"
class="{{ menu_attrs.class }}"
class="{{ menu_attrs.class | default('') }}"
{% for key, value in menu_attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
@@ -58,7 +61,7 @@
{% if items and items|length > 0 %}
{{ render_command_items(items, id ~ "-items" if id else "items") }}
{% else %}
{{ caller() if caller }}
{{ caller() if caller is defined }}
{% endif %}
</div>
</div>
@@ -87,9 +90,12 @@
open=False
) %}
{% set id = id or ("command-dialog-" ~ range(100000, 999999) | random | string) %}
{% set dialog_attrs = dialog_attrs or {} %}
{% set input_attrs = input_attrs or {} %}
{% set menu_attrs = menu_attrs or {} %}
<dialog
id="{{ id }}"
class="command-dialog {{ dialog_attrs.class }}"
class="command-dialog {{ dialog_attrs.class | default('') }}"
aria-label="Command menu"
{% if open %}open{% endif %}
onclick="if (event.target === this) this.close()"
@@ -121,7 +127,7 @@
id="{{ id }}-menu"
aria-orientation="vertical"
data-empty="{{ empty_text }}"
class="{{ menu_attrs.class }}"
class="{{ menu_attrs.class | default('') }}"
{% for key, value in menu_attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
@@ -129,7 +135,7 @@
{% if items and items|length > 0 %}
{{ render_command_items(items, id ~ "-items" if id else "items") }}
{% else %}
{{ caller() if caller }}
{{ caller() if caller is defined }}
{% endif %}
</div>
</div>
@@ -149,53 +155,54 @@
{% 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 %}
{% set item_type = item.type | default("item") %}
{% if item_type == "group" %}
{% set group_label_id = item.id | default("group-label-" + item_id) %}
<div
role="group"
aria-labelledby="{{ group_label_id }}"
{% if item.attrs %}
{% if item.attrs is defined and 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'] }}
<span role="heading" id="{{ group_label_id }}">{{ item.label | default('') | safe }}</span>
{{ render_command_items(item.get('items', []), item_id) if item.get('items') }}
</div>
{% elif item.type == "separator" %}
{% elif item_type == "separator" %}
<hr role="separator" />
{% elif item.type == "item" or not item.type %}
{% if item.url %}
{% elif item_type == "item" %}
{% if item.url is defined and 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 %}
{% if item.keywords is defined and item.keywords %}data-keywords="{{ item.keywords }}"{% endif %}
{% if item.disabled is defined and item.disabled %}aria-disabled="true"{% endif %}
{% if item.attrs is defined and 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 }}
{% if item.icon is defined and item.icon %}{{ item.icon | safe }}{% endif %}
{{ item.label | default('') | 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 %}
{% if item.keywords is defined and item.keywords %}data-keywords="{{ item.keywords }}"{% endif %}
{% if item.disabled is defined and item.disabled %}aria-disabled="true"{% endif %}
{% if item.attrs is defined and item.attrs %}
{% for key, value in item.attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
{% endif %}
>
{% if item.icon %}{{ item.icon | safe }}{% endif %}
{{ item.label | safe }}
{% if item.icon is defined and item.icon %}{{ item.icon | safe }}{% endif %}
{{ item.label | default('') | safe }}
</div>
{% endif %}
{% endif %}
+10 -5
View File
@@ -6,7 +6,6 @@
@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.
@@ -32,6 +31,11 @@
close_on_overlay_click=true
) %}
{% set id = id or ("dialog-" + (range(100000, 999999) | random | string)) %}
{% set dialog_attrs = dialog_attrs or {} %}
{% set trigger_attrs = trigger_attrs or {} %}
{% set header_attrs = header_attrs or {} %}
{% set body_attrs = body_attrs or {} %}
{% set footer_attrs = footer_attrs or {} %}
{% if trigger %}
<button
type="button"
@@ -45,9 +49,10 @@
{% endif %}
<dialog
id="{{ id }}"
class="dialog {{ dialog_attrs.class }}"
aria-labelledby="{{ id }}-title"
class="dialog {{ dialog_attrs.class | default('') }}"
{% if title %}aria-labelledby="{{ id }}-title"{% endif %}
{% if description %}aria-describedby="{{ id }}-description"{% endif %}
{% if open %}open{% 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 %}
@@ -60,11 +65,11 @@
{{ key }}="{{ value }}"
{% endfor %}
>
<h2 id="{{ id }}-title">{{ title | safe }}</h2>
{% if title %}<h2 id="{{ id }}-title">{{ title | safe }}</h2>{% endif %}
{% if description %}<p id="{{ id }}-description">{{ description | safe }}</p>{% endif %}
</header>
{% endif %}
{% if caller %}
{% if caller is defined %}
<section
{% for key, value in body_attrs.items() %}
{{ key }}="{{ value }}"
+23 -17
View File
@@ -7,6 +7,7 @@
@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.
@param menu_attrs {object} [optional] - Additional HTML attributes for the menu div.
#}
{% macro dropdown_menu(
trigger,
@@ -18,10 +19,14 @@
menu_attrs={}
) %}
{% set id = id or ("dropdown-menu-" + (range(100000, 999999) | random | string)) %}
{% set main_attrs = main_attrs or {} %}
{% set trigger_attrs = trigger_attrs or {} %}
{% set popover_attrs = popover_attrs or {} %}
{% set menu_attrs = menu_attrs or {} %}
<div
id="{{ id }}"
class="dropdown-menu {{ main_attrs.class }}"
class="dropdown-menu {{ main_attrs.class | default('') }}"
{% for key, value in main_attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
@@ -57,7 +62,7 @@
{% if items %}
{{ render_dropdown_items(items, id ~ "-items" if id else "items") }}
{% else %}
{{ caller() if caller }}
{{ caller() if caller is defined }}
{% endif %}
</div>
</div>
@@ -74,49 +79,50 @@
{% 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 %}
{% set item_type = item.type | default("item") %}
{% if item_type == "group" %}
{% set group_label_id = item.id | default("group-label-" + item_id) %}
<div
role="group"
aria-labelledby="{{ group_label_id }}"
{% if item.attrs %}
{% if item.attrs is defined and 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 role="heading" id="{{ group_label_id }}">{{ item.label | default('') }}</div>
{{ render_dropdown_items(item.get('items', []), item_id) if item.get('items') }}
</div>
{% elif item.type == "separator" %}
{% elif item_type == "separator" %}
<hr role="separator" />
{% elif item.type == "item" or not item.type %}
{% if item.url %}
{% elif item_type == "item" %}
{% if item.url is defined and item.url %}
<a
id="{{ item_id }}"
role="menuitem"
href="{{ item.url }}"
{% if item.attrs %}
{% if item.attrs is defined and item.attrs %}
{% for key, value in item.attrs.items() %}
{% if key != "url" %} {{ key }}="{{ value }}" {% endif %}
{% if key != "href" and key != "url" %} {{ key }}="{{ value }}" {% endif %}
{% endfor %}
{% endif %}
>
{% if item.icon %}{{ item.icon | safe }}{% endif %}
{{ item.label | safe }}
{% if item.icon is defined and item.icon %}{{ item.icon | safe }}{% endif %}
{{ item.label | default('') | safe }}
</a>
{% else %}
<div
id="{{ item_id }}"
role="menuitem"
{% if item.attrs %}
{% if item.attrs is defined and item.attrs %}
{% for key, value in item.attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
{% endif %}
>
{% if item.icon %}{{ item.icon | safe }}{% endif %}
{{ item.label | safe }}
{% if item.icon is defined and item.icon %}{{ item.icon | safe }}{% endif %}
{{ item.label | default('') | safe }}
</div>
{% endif %}
{% endif %}
+6 -3
View File
@@ -15,10 +15,13 @@
popover_attrs={}
) %}
{% set id = id or ("popover-" + (range(100000, 999999) | random | string)) %}
{% set main_attrs = main_attrs or {} %}
{% set trigger_attrs = trigger_attrs or {} %}
{% set popover_attrs = popover_attrs or {} %}
<div
id="{{ id }}"
class="popover {{ main_attrs.class }}"
class="popover {{ main_attrs.class | default('') }}"
{% for key, value in main_attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
@@ -42,7 +45,7 @@
{{ key }}="{{ value }}"
{% endfor %}
>
{{ caller() if caller }}
{{ caller() if caller is defined }}
</div>
</div>
{% endmacro %}
{% endmacro %}
+25 -17
View File
@@ -7,6 +7,7 @@
@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 format {string} [optional] [default="value"] - Hidden input serialization format: "value" or "object".
@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.
@@ -29,10 +30,15 @@
input_attrs={}
) %}
{% set id = id or ("select-" + (range(100000, 999999) | random | string)) %}
{% set main_attrs = main_attrs or {} %}
{% set trigger_attrs = trigger_attrs or {} %}
{% set popover_attrs = popover_attrs or {} %}
{% set listbox_attrs = listbox_attrs or {} %}
{% set input_attrs = input_attrs or {} %}
{% if selected is defined and selected is iterable and selected is not string %}
{% if selected is defined and selected is not none and selected is iterable and selected is not string %}
{% set selectedArray = selected %}
{% elif selected is defined %}
{% elif selected is defined and selected is not none %}
{% set selectedArray = [selected] %}
{% else %}
{% set selectedArray = [] %}
@@ -42,8 +48,9 @@
{% if items %}
{% for item in items %}
{% if item.type == "group" %}
{% for sub_item in item['items'] %}
{% set item_type = item.type | default("item") %}
{% if item_type == "group" %}
{% for sub_item in item.get('items', []) %}
{% if not first_option.item %}
{% set first_option.item = sub_item %}
{% endif %}
@@ -81,7 +88,7 @@
<div
id="{{ id }}"
class="select {{ main_attrs.class }}"
class="select {{ main_attrs.class | default('') }}"
{% 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 %}
@@ -91,7 +98,7 @@
>
<button
type="button"
class="{{ trigger_attrs.class }}"
class="{{ trigger_attrs.class | default('') }}"
id="{{ id }}-trigger"
aria-haspopup="listbox"
aria-expanded="false"
@@ -124,7 +131,7 @@
{% if items %}
{{ render_select_items(items, selectedArray, id ~ "-items" if id else "items") }}
{% else %}
{{ caller() if caller }}
{{ caller() if caller is defined }}
{% endif %}
</div>
</div>
@@ -148,35 +155,36 @@
{% 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 %}
{% set item_type = item.type | default("item") %}
{% if item_type == "group" %}
{% set group_label_id = item.id | default("group-label-" + item_id) %}
<div
role="group"
aria-labelledby="{{ group_label_id }}"
{% if item.attrs %}
{% if item.attrs is defined and 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 role="heading" id="{{ group_label_id }}">{{ item.label | default('') }}</div>
{{ render_select_items(item.get('items', []), selectedArray, item_id) if item.get('items') }}
</div>
{% elif item.type == "separator" %}
{% elif item_type == "separator" %}
<hr role="separator" />
{% elif item.type == "item" or not item.type %}
{% elif item_type == "item" %}
<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 %}
{% if item.value is defined and item.value in selectedArray %}aria-selected="true"{% endif %}
{% if item.attrs is defined and item.attrs %}
{% for key, value in item.attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
{% endif %}
>
{{ item.label | safe }}
{{ item.label | default(item.value | default('')) | safe }}
</div>
{% endif %}
{% endfor %}
+26 -21
View File
@@ -26,9 +26,13 @@
content_attrs={},
footer_attrs={}
) %}
{% set main_attrs = main_attrs or {} %}
{% set header_attrs = header_attrs or {} %}
{% set content_attrs = content_attrs or {} %}
{% set footer_attrs = footer_attrs or {} %}
<aside
{% if id %}id="{{ id }}"{% endif %}
class="sidebar {{ main_attrs.class }}"
class="sidebar {{ main_attrs.class | default('') }}"
data-side="{{ side if side else "left" }}"
aria-hidden="{{ "true" if not open else "false" }}"
{{ "inert" if not open }}
@@ -57,7 +61,7 @@
{% if menu %}
{{ render_sidebar_content(menu, id ~ "-content" if id else "content") }}
{% else %}
{{ caller() if caller }}
{{ caller() if caller is defined }}
{% endif %}
</section>
@@ -84,59 +88,60 @@
{% 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 %}
{% set item_type = item.type | default("item") %}
{% if item_type == "group" %}
{% set group_label_id = item.id | default("group-label-" + item_id) %}
<div
role="group"
{% if item.label %}aria-labelledby="{{ group_label_id }}"{% endif %}
{% if item.attrs %}
{% if item.label is defined and item.label %}aria-labelledby="{{ group_label_id }}"{% endif %}
{% if item.attrs is defined and item.attrs %}
{% for key, value in item.attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
{% endif %}
>
{% if item.label %}
{% if item.label is defined and item.label %}
<h3 id="{{ group_label_id }}">{{ item.label }}</h3>
{% endif %}
<ul>
{{ render_sidebar_content(item["items"], item_id) if item["items"] }}
{{ render_sidebar_content(item.get("items", []), item_id) if item.get("items") }}
</ul>
</div>
{% elif item.type == "separator" %}
{% elif item_type == "separator" %}
<hr role="separator"/>
{% elif item.type == "submenu" %}
{% elif item_type == "submenu" %}
<li>
<details
id="submenu-{{ item_id }}"
{{ "open" if item.open }}
{% if item.attrs %}
{{ "open" if item.open is defined and item.open }}
{% if item.attrs is defined and 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>
{% if item.icon is defined and item.icon %}{{ item.icon | safe }}{% endif %}
<span>{{ item.label | default('') | safe }}</span>
</summary>
<ul id="submenu-{{ item_id }}-content">
{{ render_sidebar_content(item["items"], item_id) if item["items"] }}
{{ render_sidebar_content(item.get("items", []), item_id) if item.get("items") }}
</ul>
</details>
</li>
{% elif item.type == "item" or not item.type %}
{% elif item_type == "item" %}
<li>
<a
href="{{ item.url }}"
{% if item.current %}aria-current="page"{% endif %}
{% if item.attrs %}
href="{{ item.url | default('#') }}"
{% if item.current is defined and item.current %}aria-current="page"{% endif %}
{% if item.attrs is defined and 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>
{% if item.icon is defined and item.icon %}{{ item.icon | safe }}{% endif %}
<span>{{ item.label | default('') | safe }}</span>
</a>
</li>
{% endif %}
+10 -8
View File
@@ -4,7 +4,6 @@
@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.
@@ -23,9 +22,11 @@
)
%}
{% set id = id or ("tabs-" + (range(100000, 999999) | random | string)) %}
{% set orientation = tablist_attrs['aria-orientation'] or 'horizontal' %}
{% set main_attrs = main_attrs or {} %}
{% set tablist_attrs = tablist_attrs or {} %}
{% set orientation = tablist_attrs.get('aria-orientation', 'horizontal') %}
<div
class="tabs {{ main_attrs.class }}"
class="tabs {{ main_attrs.class | default('') }}"
id="{{ id }}"
{% for key, value in main_attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
@@ -39,15 +40,16 @@
{% endfor %}
>
{% for tabset in tabsets %}
{% set tab_attrs = tabset.tab_attrs | default({}) %}
<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() %}
tabindex="{{ '0' if loop.index == default_tab_index and not (tab_attrs.disabled | default(false)) and (tab_attrs['aria-disabled'] | default('false')) != 'true' else '-1' }}"
{% if tab_attrs %}
{% for key, value in tab_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
{% endif %}
@@ -58,7 +60,7 @@
</nav>
{% for tabset in tabsets %}
{% if tabset.panel %}
{% if tabset.panel is defined and tabset.panel %}
<div
role="tabpanel"
id="{{ id }}-panel-{{ loop.index }}"
@@ -66,7 +68,7 @@
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 %}
{% if tabset.panel_attrs is defined and tabset.panel_attrs %}
{% for key, value in tabset.panel_attrs.items() %}
{{ key }}="{{ value }}"
{% endfor %}
+46 -24
View File
@@ -13,24 +13,27 @@
toasts=[],
attrs={}
) %}
{% set attrs = attrs or {} %}
<div
id="{{ id }}"
class="toaster {{ attrs.class }}"
class="toaster {{ attrs.class | default('') }}"
{% 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
category=item.category | default("success"),
title=item.title | default(""),
description=item.description | default(""),
action=item.action | default(None),
cancel=item.cancel | default(None),
duration=item.duration | default(None),
icon=item.icon | default(None),
attrs=item.attrs | default({})
) }}
{% endfor %}
{{ caller() if caller }}
{{ caller() if caller is defined }}
</div>
{% endmacro %}
@@ -40,14 +43,16 @@
@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 duration {number} [optional] - Timeout in milliseconds. Use -1 to keep the toast open.
@param icon {string} [optional] - Custom icon HTML. Defaults to the category icon.
@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.
- href {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.
- href {string}: URL for an anchor link button.
@param attrs {object} [optional] - Additional HTML attributes for the toast div.
#}
{% macro toast(
@@ -56,20 +61,28 @@
description="",
action=None,
cancel=None,
duration=None,
icon=None,
attrs={}
) %}
{% set attrs = attrs or {} %}
{% set action_href = action.href if action and action.href is defined and action.href else (action.url if action and action.url is defined and action.url else None) %}
{% set cancel_href = cancel.href if cancel and cancel.href is defined and cancel.href else (cancel.url if cancel and cancel.url is defined and cancel.url else None) %}
<div
class="toast {{ attrs.class }}"
class="toast {{ attrs.class | default('') }}"
role="{{ 'alert' if category == 'error' else 'status' }}"
aria-atomic="true"
aria-hidden="false"
{% if category %}data-category="{{ category }}"{% endif %}
{% if duration is not none %}data-duration="{{ duration }}"{% 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 icon %}
{{ icon | safe }}
{% elif 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" %}
@@ -87,31 +100,40 @@
{% if action or cancel %}
<footer>
{% if action %}
{% if action.href %}
{% if action_href %}
<a
href="{{ action.href }}"
href="{{ action_href }}"
class="btn" data-size="sm"
data-toast-action
>{{ action.label }}</a>
>{{ action.label | default('') }}</a>
{% else %}
<button
type="button"
class="btn"
data-size="sm"
data-toast-action
{% if action.onclick %}onclick="{{ action.onclick }}"{% endif %}
>{{ action.label }}</button>
{% if action.onclick is defined and action.onclick %}onclick="{{ action.onclick }}"{% endif %}
>{{ action.label | default('') }}</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>
{% if cancel_href %}
<a
href="{{ cancel_href }}"
class="btn" data-variant="outline" data-size="sm"
data-toast-cancel
>{{ cancel.label | default('') }}</a>
{% else %}
<button
type="button"
class="btn" data-variant="outline" data-size="sm"
data-toast-cancel
{% if cancel.onclick is defined and cancel.onclick %}onclick="{{ cancel.onclick }}"{% endif %}
>{{ cancel.label | default('') }}</button>
{% endif %}
{% endif %}
</footer>
{% endif %}
</div>
</div>
{% endmacro %}
{% endmacro %}
+3 -3
View File
@@ -6,7 +6,6 @@
@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.
@@ -46,8 +45,9 @@
<dialog
id="{{ id }}"
class="dialog {{ dialog_attrs.class }}"
aria-labelledby="{{ id }}-title"
{% if title %}aria-labelledby="{{ id }}-title"{% endif %}
{% if description %}aria-describedby="{{ id }}-description"{% endif %}
{% if open %}open{% 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 %}
@@ -60,7 +60,7 @@
{{ key }}="{{ value }}"
{% endfor %}
>
<h2 id="{{ id }}-title">{{ title | safe }}</h2>
{% if title %}<h2 id="{{ id }}-title">{{ title | safe }}</h2>{% endif %}
{% if description %}<p id="{{ id }}-description">{{ description | safe }}</p>{% endif %}
</header>
{% endif %}
+2 -1
View File
@@ -7,6 +7,7 @@
@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.
@param menu_attrs {object} [optional] - Additional HTML attributes for the menu div.
#}
{% macro dropdown_menu(
trigger,
@@ -98,7 +99,7 @@
href="{{ item.url }}"
{% if item.attrs %}
{% for key, value in item.attrs %}
{% if key != "url" %} {{ key }}="{{ value }}" {% endif %}
{% if key != "href" and key != "url" %} {{ key }}="{{ value }}" {% endif %}
{% endfor %}
{% endif %}
>
+2 -2
View File
@@ -31,9 +31,9 @@
) %}
{% set id = id or ("select-" + (range(100000, 999999) | random | string)) %}
{% if selected is defined and selected is iterable and selected is not string %}
{% if selected is defined and selected is not none and selected is iterable and selected is not string %}
{% set selectedArray = selected %}
{% elif selected is defined %}
{% elif selected is defined and selected is not none %}
{% set selectedArray = [selected] %}
{% else %}
{% set selectedArray = [] %}
+1 -1
View File
@@ -7,7 +7,7 @@
@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 items {array} [optional] - Array of menu items 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 element.
@param content_attrs {object} [optional] - Additional HTML attributes for the content section.
+13 -1
View File
@@ -23,5 +23,17 @@
-%}
{%- 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 %}{% if key != 'class' %} {{ key }}="{{ value }}"{% endif %}{% endfor %}><nav role="tablist" aria-orientation="{{ orientation }}"{% for key, value in tablist_attrs %}{% 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 %} {{ 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 %} {{ key }}="{{ value }}"{% endfor %}{% endif %}>{{ tabset.panel | safe }}</div>{% endif %}{% endfor %}</div>
<div class="tabs {{ main_attrs.class }}" id="{{ id }}"{% for key, value in main_attrs %}{% if key != 'class' %} {{ key }}="{{ value }}"{% endif %}{% endfor %}>
<nav role="tablist" aria-orientation="{{ orientation }}"{% for key, value in tablist_attrs %}{% if key != 'aria-orientation' %} {{ key }}="{{ value }}"{% endif %}{% endfor %}>
{% for tabset in tabsets %}
{%- set tab_attrs = tabset.tab_attrs or {} -%}
<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 tab_attrs.disabled and tab_attrs['aria-disabled'] != 'true' else '-1' }}"{% if tab_attrs %}{% for key, value in tab_attrs %} {{ 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 %} {{ key }}="{{ value }}"{% endfor %}{% endif %}>{{ tabset.panel | safe }}</div>
{% endif %}
{% endfor %}
</div>
{%- endmacro -%}
+33 -13
View File
@@ -27,7 +27,9 @@
description=item.description,
action=item.action,
cancel=item.cancel,
attrs=item.attrs
duration=item.duration,
icon=item.icon,
attrs=item.attrs or {}
) }}
{% endfor %}
{{ caller() if caller }}
@@ -40,14 +42,16 @@
@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 duration {number} [optional] - Timeout in milliseconds. Use -1 to keep the toast open.
@param icon {string} [optional] - Custom icon HTML. Defaults to the category icon.
@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.
- href {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.
- href {string}: URL for an anchor link button.
@param attrs {object} [optional] - Additional HTML attributes for the toast div.
#}
{% macro toast(
@@ -56,20 +60,27 @@
description="",
action=None,
cancel=None,
duration=None,
icon=None,
attrs={}
) %}
{% set action_href = action.href if action and action.href else (action.url if action and action.url else None) %}
{% set cancel_href = cancel.href if cancel and cancel.href else (cancel.url if cancel and cancel.url else None) %}
<div
class="toast {{ attrs.class }}"
role="{{ 'alert' if category == 'error' else 'status' }}"
aria-atomic="true"
aria-hidden="false"
{% if category %}data-category="{{ category }}"{% endif %}
{% if duration is not none %}data-duration="{{ duration }}"{% endif %}
{% for key, value in attrs %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
>
<div class="toast-content">
{% if category in ["error", "success", "info", "warning"] %}
{% if icon %}
{{ icon | safe }}
{% elif 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" %}
@@ -87,9 +98,9 @@
{% if action or cancel %}
<footer>
{% if action %}
{% if action.href %}
{% if action_href %}
<a
href="{{ action.href }}"
href="{{ action_href }}"
class="btn" data-size="sm"
data-toast-action
>{{ action.label }}</a>
@@ -97,21 +108,30 @@
<button
type="button"
class="btn"
data-size="sm"
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>
{% if cancel_href %}
<a
href="{{ cancel_href }}"
class="btn" data-variant="outline" data-size="sm"
data-toast-cancel
>{{ cancel.label }}</a>
{% else %}
<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 %}
{% endif %}
</footer>
{% endif %}
</div>
</div>
{% endmacro %}
{% endmacro %}