Compare commits

...
Author SHA1 Message Date
grugbraindev a6acba6ace Scope sidebar nav rules to the direct child nav
The sidebar rules matched every nav inside .sidebar, so a nested
tablist nav became fixed, column, sidebar-width. Use .sidebar > nav
in the component CSS, all style packs and sidebar.js.
2026-10-23 09:49:29 +08:00
Grug b7d33eee04 Add single-file .bundle.css outputs for Tailwind CLI without npm (#206)
Emit dist/basecoat-{style}.bundle.css for every style pack, plus
dist/basecoat.bundle.css (Vega alias) and dist/basecoat-base.bundle.css.
Each is the uncompiled source with relative @imports inlined, so it can
be downloaded as one file (e.g. from jsDelivr) and imported after
tailwindcss. Compiling a bundle produces byte-identical output to the
split entrypoint.

Document the bundles on the installation page and fix the description
of basecoat-css/all.css, which is an alias for the default stylesheet.

Refs #150
2026-10-23 09:23:51 +08:00
GrugandMaurits van Riezen 30e5bdcf3a Escape toast text by default, add html option (#203)
Toast title, description and action labels were inserted into the page
as HTML, so passing user input could inject markup or script. They are
now escaped by default, and `html: true` keeps the old raw-HTML
behaviour. The action href, onclick strings, category and duration are
escaped as attribute values. `icon` stays raw markup.

Documents the `html` and `icon` options, adds a warning about `html`
and `onclick`, and notes that the Jinja and Nunjucks macros rely on
template autoescaping.

Co-authored-by: Maurits van Riezen <12109031+mousetail@users.noreply.github.com>
2026-10-23 09:23:05 +08:00
GrugandPhilipdeRijk cafffa0597 Don't close dialogs when a drag ends on the backdrop (#202)
Only close a dialog or command dialog on backdrop click when the press
also started on the backdrop, so selecting text inside and releasing
outside no longer closes it.

Co-authored-by: PhilipdeRijk <derijk.pjtm@gmail.com>
2026-10-23 09:22:42 +08:00
Grugandboratatouille 2a2c64c651 Deploy the site from a release branch moved on each published release (#207)
Co-authored-by: boratatouille <178141691+boratatouille@users.noreply.github.com>
2026-10-23 09:09:26 +08:00
Alexander Posztosandboratatouille 0b28009e38 Fix iOS auto-zoom on combobox input (#185)
* Fix iOS auto-zoom on select and command search inputs

* Match shadcn/ui input sizing for combobox and keep command input as is

---------

Co-authored-by: boratatouille <178141691+boratatouille@users.noreply.github.com>
2026-10-22 23:32:56 +08:00
Радоје Јездић d942c8b31d fix(toast): prevent htmx from pushing toast fragment path (#187)
close #186
2026-10-22 23:27:46 +08:00
Ronan Berder d826637eb9 Merge pull request #201 from hunvreus/docs/contributing-target-main
Point contributors at main instead of development
2026-10-22 22:36:45 +08:00
boratatouille 2ad4f6a43b Point contributors at main instead of development 2026-10-22 21:09:07 +08:00
Ronan Berder 6953a4adbe Prepare 1.0.2 release 2026-07-06 15:21:53 +07:00
Ronan Berder f49eaa8513 Update ReallySimpleDocs dependency 2026-06-30 15:33:34 +08:00
Ronan Berder fcea07fc3d Update docs dependency 2026-06-29 22:39:24 +08:00
Ronan Berder aa09ddb271 Add manual release announcement dispatch 2026-06-28 10:46:28 +08:00
Ronan Berder 05ea49ccc5 Prepare 1.0.1 release 2026-06-28 10:06:30 +08:00
Ronan Berder 5f84c842f7 Harden Discord release workflow 2026-06-28 09:54:30 +08:00
Ronan Berder 7dc70daa83 Add force init lifecycle option 2026-06-28 09:54:05 +08:00
Ronan Berder b4424e9e8b Add Discord release announcement workflow 2026-06-28 09:51:48 +08:00
Ronan Berder d431873be7 Fix docs sidebar after history restore 2026-06-27 21:12:05 +08:00
Ronan Berder 537f77507e Install site dependencies during docs build 2026-06-27 19:45:17 +08:00
Ronan Berder 521bd93705 Regenerate lockfiles for deploy 2026-06-27 19:33:57 +08:00
38 changed files with 4230 additions and 871 deletions
+33
View File
@@ -0,0 +1,33 @@
name: Deploy site on release
# Cloudflare Workers Builds deploys the site from the `release` branch.
# Publishing a GitHub release moves `release` to the released tag.
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: Tag to deploy (for example 1.1.0)
required: true
type: string
permissions:
contents: write
jobs:
move-release-branch:
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' || github.event.release.prerelease == false
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Point release branch at tag
env:
TAG: ${{ inputs.tag || github.event.release.tag_name }}
run: |
SHA=$(git rev-parse "refs/tags/$TAG^{commit}")
git push --force origin "$SHA:refs/heads/release"
+72
View File
@@ -0,0 +1,72 @@
name: Announce release on Discord
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: Release tag to announce
required: true
type: string
permissions:
contents: read
jobs:
announce:
runs-on: ubuntu-latest
steps:
- name: Send Discord announcement
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
RELEASE_TAG_INPUT: ${{ inputs.tag }}
REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
run: |
test -n "$DISCORD_WEBHOOK_URL"
if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then
test -n "$RELEASE_TAG_INPUT"
curl --fail-with-body \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GH_TOKEN" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"$GITHUB_API_URL/repos/$REPO/releases/tags/$RELEASE_TAG_INPUT" \
> release.json
else
jq '.release' "$GITHUB_EVENT_PATH" > release.json
fi
if [ "$(jq -r '.prerelease' release.json)" = "true" ]; then
echo "Skipping prerelease."
exit 0
fi
jq -n \
--arg repo "$REPO" \
--arg name "$(jq -r '.name // ""' release.json)" \
--arg tag "$(jq -r '.tag_name' release.json)" \
--arg url "$(jq -r '.html_url' release.json)" \
--arg body "$(jq -r '.body // ""' release.json)" \
'($body | if length > 1500 then .[:1500] + "...\n\n[Read more](" + $url + ")" else . end) as $description |
{
username: "Basecoat Releases",
allowed_mentions: { parse: [] },
embeds: [{
title: ($repo + " " + $tag),
url: $url,
description: ((if $name != "" then "**" + $name + "**\n\n" else "" end) + $description),
color: 5793266
}]
}' > payload.json
curl --fail-with-body \
--retry 3 \
--retry-all-errors \
--retry-delay 2 \
-H "Content-Type: application/json" \
-X POST \
-d @payload.json \
"$DISCORD_WEBHOOK_URL"
+4
View File
@@ -1,6 +1,10 @@
.*
!.gitignore
!.gitattributes
!.github/
!.github/workflows/
!.github/workflows/*.yml
!.github/workflows/*.yaml
_site/
node_modules/
+1
View File
@@ -48,6 +48,7 @@
## JavaScript
- JavaScript is behavior only; do not use JS to paper over CSS architecture issues.
- Register JS components with `window.basecoat.register()` and make initialization idempotent with `data-*-initialized` flags.
- Component init functions must return before attaching listeners or mutating DOM when the root already has its initialized flag.
- Keep generated DOM minimal and aligned with documented HTML.
- Prefer native browser APIs and events.
- Keep scroll management scoped to component containers. Do not use `scrollIntoView()` when it can scroll the page unexpectedly.
+20
View File
@@ -2,6 +2,26 @@
## Unreleased
## [1.0.2] - 2026-07-06
### Added
- Added `data-size="sm"` support to Alert Dialog for the compact upstream size.
### Changed
- Removed global font smoothing from Basecoat's package CSS so `antialiased` remains an application-level choice.
## [1.0.1] - 2026-06-28
### Added
- Added explicit `force: true` support to `window.basecoat.init()` and `window.basecoat.initAll()` for rehydrating restored DOM after framework or navigation cache restores.
### Fixed
- Reinitialized Basecoat components after HTMX history restores in the docs site.
## [1.0.0] - 2026-06-27
### Breaking Changes
+15 -4
View File
@@ -1,13 +1,12 @@
# Contributing to Basecoat
- Support development by sponsoring the author: https://github.com/sponsors/hunvreus
- Submit pull requests (PRs) against the `development` branch, not `main`.
- Submit pull requests (PRs) against the `main` branch.
- For branches, we have:
- `main` is our production and default branch. This is what we deploy to https://basecoatui.com and publish to https://www.npmjs.com/package/basecoat-css.
- `development` is for staging. We will deploy it soon at https://dev.basecoatui.com.
- New features are worked in `feature/name-of-the-feature` branches.
- Issues are addressed in `issue/123-main-isse` branches.
- When ready, we PR against `development`, test it and then finally merge to `main`.
- Issues are addressed in `issue/123-main-issue` branches.
- When ready, open a PR against `main`.
- Keep changes focused: one feature or fix per PR.
- Test locally before submitting.
- Follow existing code style.
@@ -29,3 +28,15 @@ Workers Builds settings:
- Enable builds for non-production branches for branch preview URLs.
Thanks for helping!
## Releases and the docs site
The site at basecoatui.com deploys from the `release` branch, not `main`, so the docs always match the version on npm. Nobody commits to `release` directly.
To release:
1. Bump the version in `package.json`, commit, tag (for example `1.1.0`) and push.
2. Run `npm publish`.
3. Publish a GitHub release for the tag, for example `gh release create 1.1.0 --generate-notes`.
Publishing the release runs `.github/workflows/deploy-site.yml`, which points `release` at the tag. Cloudflare then builds and deploys the site. To redeploy or roll back, run that workflow manually with the tag you want live.
+1
View File
@@ -92,6 +92,7 @@ These entrypoints are generated by `scripts/generate-css-entrypoints.js` and by
- `window.basecoat.init(name)` initializes uninitialized components for one registered component.
- `window.basecoat.initAll()` initializes all uninitialized registered components.
- `window.basecoat.init(name, { force: true })` and `window.basecoat.initAll({ force: true })` destroy and reinitialize matching components. Use this when a framework or navigation library restores previously initialized DOM from a cache. Force mode resets transient component state such as open menus, focus state, and active interactions.
- `window.basecoat.refresh(element)` asks an initialized component to rescan dynamic children when it supports refresh.
- Components use internal destroy hooks to clean up listeners when initialized roots are removed from the DOM.
+800 -70
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "basecoat-css",
"version": "1.0.0",
"version": "1.0.2",
"description": "Tailwind CSS for Basecoat components",
"author": {
"name": "hunvreus",
@@ -123,12 +123,12 @@
"scripts": {
"build": "node ./scripts/build.js",
"prepack": "node ./scripts/build.js",
"docs:build": "npm run build && npm --prefix site run build",
"docs:build": "npm run build && npm --prefix site clean-install --progress=false && 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"
"workers:dev": "npm run build && npm --prefix site clean-install --progress=false && npm --prefix site run workers:dev",
"workers:deploy": "npm run build && npm --prefix site clean-install --progress=false && npm --prefix site run workers:deploy",
"workers:preview": "npm run build && npm --prefix site clean-install --progress=false && npm --prefix site run workers:preview"
},
"devDependencies": {
"@tailwindcss/cli": "^4.1.17",
+45
View File
@@ -64,6 +64,37 @@ async function copyDirRecursive(src, dest) {
}
}
// Recursively inlines relative `@import "./...";` lines into a single CSS string.
// Each file is inlined at most once per bundle (tracked in `seen`).
async function inlineCssImports(filePath, seen) {
const resolved = path.resolve(filePath);
if (seen.has(resolved)) return '';
seen.add(resolved);
const source = await fs.readFile(resolved, 'utf-8');
const lines = source.split('\n');
const out = [];
for (const line of lines) {
const match = line.match(/^\s*@import\s+["'](\.{1,2}\/[^"']+)["']\s*;\s*$/);
if (match) {
const inlined = await inlineCssImports(path.join(path.dirname(resolved), match[1]), seen);
if (inlined) out.push(inlined.replace(/\n+$/, ''));
} else if (/^\s*@import\s+["']\.{1,2}\//.test(line)) {
throw new Error(`Unsupported relative @import (with modifiers) in ${resolved}: ${line.trim()}`);
} else {
out.push(line);
}
}
return out.join('\n');
}
// Writes an uncompiled single-file bundle for a dist CSS entrypoint.
async function writeCssBundle(entryPath, outPath, label, version) {
const body = await inlineCssImports(entryPath, new Set());
const header = `/* Basecoat v${version}, ${label}, single-file source. Import after tailwindcss. */\n`;
await fs.writeFile(outPath, `${header}${body.replace(/\n+$/, '')}\n`);
}
// Main build function to prepare the package for publishing.
async function build() {
console.log('Starting build process...');
@@ -155,6 +186,20 @@ async function build() {
await copyDirRecursive(srcCssCompatDir, cssCompatDistDir);
console.log(`Copied split CSS folders to ${cssDistDir}`);
// Create uncompiled single-file bundles for Tailwind users without a package manager.
const { version } = JSON.parse(await fs.readFile(path.join(projectRoot, 'package.json'), 'utf-8'));
for (const style of styles) {
await writeCssBundle(
path.join(cssDistDir, `basecoat-${style}.css`),
path.join(cssDistDir, `basecoat-${style}.bundle.css`),
style,
version,
);
}
await writeCssBundle(path.join(cssDistDir, 'basecoat.css'), path.join(cssDistDir, 'basecoat.bundle.css'), 'vega', version);
await writeCssBundle(path.join(cssDistDir, 'basecoat-base.css'), path.join(cssDistDir, 'basecoat-base.bundle.css'), 'base (no style pack)', version);
console.log(`Generated single-file CSS bundles in ${cssDistDir}`);
await copyDirRecursive(srcNunjucksDir, path.join(cssTemplatesDir, 'nunjucks'));
await copyDirRecursive(srcJinjaDir, path.join(cssTemplatesDir, 'jinja'));
console.log(`Copied template assets to ${cssTemplatesDir}`);
+1
View File
@@ -31,6 +31,7 @@ export default defineConfig({
style: 'vega',
css: false,
bodyAttrs: {
class: 'antialiased',
'hx-boost': 'true',
'hx-target': '#content',
'hx-select': '#content',
+2764 -485
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -15,7 +15,7 @@
"htmx.org": "^2.0.10",
"lucide-static": "^1.17.0",
"nunjucks": "^3.2.4",
"reallysimpledocs": "^1.0.6"
"reallysimpledocs": "^1.0.12"
},
"devDependencies": {
"astro": "^6.4.6",
+26
View File
@@ -159,7 +159,33 @@ const styleUrls = {
}
};
const clearSerializedRuntimeState = (root) => {
for (const element of root.querySelectorAll?.("*") || []) {
delete element.dataset.basecoatComponent;
for (const attribute of Array.from(element.attributes)) {
if (/^data-[a-z0-9-]+-initialized$/.test(attribute.name)) {
element.removeAttribute(attribute.name);
}
}
}
};
const cleanHistoryItem = (event) => {
const item = event.detail?.item;
if (typeof item?.content !== "string") return;
const template = document.createElement("template");
template.innerHTML = item.content;
clearSerializedRuntimeState(template.content);
item.content = template.innerHTML;
};
document.addEventListener("DOMContentLoaded", syncSidebar);
document.addEventListener("htmx:historyItemCreated", cleanHistoryItem);
document.addEventListener("htmx:historyRestore", () => {
window.basecoat?.initAll?.({ force: true });
syncSidebar();
});
document.addEventListener("htmx:afterSettle", () => {
window.basecoat?.initAll?.();
syncSidebar();
+22 -1
View File
@@ -74,7 +74,7 @@ Alert Dialog uses a native `<dialog class="alert-dialog">` element. Omit the clo
<dd>Trigger button. Basecoat uses the native <code>HTMLDialogElement.showModal()</code> method.</dd>
<dt><code>&lt;dialog class="alert-dialog" id="&#123; DIALOG_ID &#125;"&gt;</code></dt>
<dd>
Native modal dialog. Add <code>aria-labelledby</code> and <code>aria-describedby</code> when title and description are present.
Native modal dialog. Add <code>aria-labelledby</code> and <code>aria-describedby</code> when title and description are present. Use <code>data-size="sm"</code> for the compact upstream size.
<dl>
<dt><code>&lt;div&gt;</code></dt>
<dd>
@@ -164,6 +164,27 @@ Use a destructive action button when the alert dialog confirms a destructive ope
</dialog>
</Preview>
### Small
Use `data-size="sm"` for a compact alert dialog. The small size keeps media, title, and description stacked and centers the action buttons in a two-column footer.
<Preview>
<button type="button" onclick="document.getElementById('alert-dialog-small').showModal()" class="btn" data-variant="outline">Invite Members</button>
<dialog id="alert-dialog-small" class="alert-dialog" data-size="sm" aria-labelledby="alert-dialog-small-title" aria-describedby="alert-dialog-small-description">
<div>
<header>
<figure><svg class="lucide lucide-users" 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="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><path d="M16 3.128a4 4 0 0 1 0 7.744" /><path d="M22 21v-2a4 4 0 0 0-3-3.87" /><circle cx="9" cy="7" r="4" /></svg></figure>
<h2 id="alert-dialog-small-title">Invite team members?</h2>
<p id="alert-dialog-small-description">Send an invitation link to your selected teammates.</p>
</header>
<footer>
<button class="btn" data-variant="outline" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn" onclick="this.closest('dialog').close()">Invite</button>
</footer>
</div>
</dialog>
</Preview>
### RTL
Alert Dialog supports document direction. Set `dir="rtl"` on the dialog or a parent element.
+4 -4
View File
@@ -197,7 +197,7 @@ A simple command menu in a dialog.
<Preview>
<button type="button" class="btn" data-variant="outline" onclick="document.getElementById('command-basic').showModal()">Open Menu</button>
<dialog id="command-basic" class="command-dialog" aria-label="Command menu" onclick="if (event.target === this) this.close()">
<dialog id="command-basic" class="command-dialog" aria-label="Command menu" onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()">
<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="command-basic-input" placeholder="Type a command or search..." autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" role="combobox" aria-expanded="true" aria-controls="command-basic-menu" />
@@ -218,7 +218,7 @@ A simple command menu in a dialog.
<Preview>
<button type="button" class="btn" data-variant="outline" onclick="document.getElementById('command-shortcuts-dialog').showModal()">Open Menu</button>
<dialog id="command-shortcuts-dialog" class="command-dialog" aria-label="Command menu" onclick="if (event.target === this) this.close()">
<dialog id="command-shortcuts-dialog" class="command-dialog" aria-label="Command menu" onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()">
<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="command-shortcuts-input" placeholder="Type a command or search..." autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" role="combobox" aria-expanded="true" aria-controls="command-shortcuts-menu" />
@@ -248,7 +248,7 @@ A simple command menu in a dialog.
<Preview>
<button type="button" class="btn" data-variant="outline" onclick="document.getElementById('command-groups-dialog').showModal()">Open Menu</button>
<dialog id="command-groups-dialog" class="command-dialog" aria-label="Command menu" onclick="if (event.target === this) this.close()">
<dialog id="command-groups-dialog" class="command-dialog" aria-label="Command menu" onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()">
<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="command-groups-input" placeholder="Type a command or search..." autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" role="combobox" aria-expanded="true" aria-controls="command-groups-menu" />
@@ -291,7 +291,7 @@ A simple command menu in a dialog.
<Preview>
<button type="button" class="btn" data-variant="outline" onclick="document.getElementById('command-scrollable-dialog').showModal()">Open Menu</button>
<dialog id="command-scrollable-dialog" class="command-dialog" aria-label="Command menu" onclick="if (event.target === this) this.close()">
<dialog id="command-scrollable-dialog" class="command-dialog" aria-label="Command menu" onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()">
<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="command-scrollable-input" placeholder="Type a command or search..." autocomplete="off" autocorrect="off" spellcheck="false" aria-autocomplete="list" role="combobox" aria-expanded="true" aria-controls="command-scrollable-menu" />
+7 -7
View File
@@ -2,7 +2,7 @@
<Preview>
<button type="button" onclick="document.getElementById('demo-dialog-edit-profile').showModal()" class="btn" data-variant="outline">Open Dialog</button>
<dialog id="demo-dialog-edit-profile" class="dialog" aria-labelledby="demo-dialog-edit-profile-title" aria-describedby="demo-dialog-edit-profile-description" onclick="if (event.target === this) this.close()">
<dialog id="demo-dialog-edit-profile" class="dialog" aria-labelledby="demo-dialog-edit-profile-title" aria-describedby="demo-dialog-edit-profile-description" onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()">
<div class="sm:max-w-sm">
<header>
<h2 id="demo-dialog-edit-profile-title">Edit profile</h2>
@@ -63,7 +63,7 @@ Basecoat uses the native `<dialog>` element and `showModal()`. This differs from
```html
<button type="button" onclick="document.getElementById('demo-dialog-edit-profile').showModal()" class="btn" data-variant="outline">Open Dialog</button>
<dialog id="demo-dialog-edit-profile" class="dialog" aria-labelledby="demo-dialog-edit-profile-title" aria-describedby="demo-dialog-edit-profile-description" onclick="if (event.target === this) this.close()">
<dialog id="demo-dialog-edit-profile" class="dialog" aria-labelledby="demo-dialog-edit-profile-title" aria-describedby="demo-dialog-edit-profile-description" onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()">
<div class="sm:max-w-sm">
<header>
<h2 id="demo-dialog-edit-profile-title">Edit profile</h2>
@@ -134,7 +134,7 @@ Basecoat uses the native `<dialog>` element and `showModal()`. This differs from
<Preview>
<button type="button" onclick="document.getElementById('dialog-custom-close').showModal()" class="btn" data-variant="outline">Share</button>
<dialog id="dialog-custom-close" class="dialog" aria-labelledby="dialog-custom-close-title" aria-describedby="dialog-custom-close-description" onclick="if (event.target === this) this.close()">
<dialog id="dialog-custom-close" class="dialog" aria-labelledby="dialog-custom-close-title" aria-describedby="dialog-custom-close-description" onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()">
<div class="sm:max-w-md">
<header>
<h2 id="dialog-custom-close-title">Share link</h2>
@@ -157,7 +157,7 @@ Basecoat uses the native `<dialog>` element and `showModal()`. This differs from
<Preview>
<button type="button" onclick="document.getElementById('dialog-no-close').showModal()" class="btn" data-variant="outline">No Close Button</button>
<dialog id="dialog-no-close" class="dialog" aria-labelledby="dialog-no-close-title" aria-describedby="dialog-no-close-description" onclick="if (event.target === this) this.close()">
<dialog id="dialog-no-close" class="dialog" aria-labelledby="dialog-no-close-title" aria-describedby="dialog-no-close-description" onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()">
<div>
<header>
<h2 id="dialog-no-close-title">No Close Button</h2>
@@ -171,7 +171,7 @@ Basecoat uses the native `<dialog>` element and `showModal()`. This differs from
<Preview>
<button type="button" onclick="document.getElementById('dialog-sticky-footer').showModal()" class="btn" data-variant="outline">Sticky Footer</button>
<dialog id="dialog-sticky-footer" class="dialog" aria-labelledby="dialog-sticky-footer-title" aria-describedby="dialog-sticky-footer-description" onclick="if (event.target === this) this.close()">
<dialog id="dialog-sticky-footer" class="dialog" aria-labelledby="dialog-sticky-footer-title" aria-describedby="dialog-sticky-footer-description" onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()">
<div>
<header>
<h2 id="dialog-sticky-footer-title">Sticky Footer</h2>
@@ -199,7 +199,7 @@ Basecoat uses the native `<dialog>` element and `showModal()`. This differs from
<Preview>
<button type="button" onclick="document.getElementById('dialog-scrollable').showModal()" class="btn" data-variant="outline">Scrollable Content</button>
<dialog id="dialog-scrollable" class="dialog" aria-labelledby="dialog-scrollable-title" aria-describedby="dialog-scrollable-description" onclick="if (event.target === this) this.close()">
<dialog id="dialog-scrollable" class="dialog" aria-labelledby="dialog-scrollable-title" aria-describedby="dialog-scrollable-description" onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()">
<div>
<header>
<h2 id="dialog-scrollable-title">Scrollable Content</h2>
@@ -229,7 +229,7 @@ Dialog positioning and close button placement use logical properties. Set `dir="
<Preview>
<div dir="rtl">
<button type="button" onclick="document.getElementById('dialog-rtl').showModal()" class="btn" data-variant="outline">فتح الحوار</button>
<dialog id="dialog-rtl" class="dialog" aria-labelledby="dialog-rtl-title" aria-describedby="dialog-rtl-description" onclick="if (event.target === this) this.close()">
<dialog id="dialog-rtl" class="dialog" aria-labelledby="dialog-rtl-title" aria-describedby="dialog-rtl-description" onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()">
<div class="sm:max-w-sm">
<header>
<h2 id="dialog-rtl-title">تعديل الملف الشخصي</h2>
+11
View File
@@ -10,6 +10,7 @@
hx-get="/fragments/toast/success"
hx-target="#toaster"
hx-swap="beforeend"
hx-push-url="false"
>Toast from backend (with HTMX)</button>
</Preview>
@@ -163,6 +164,10 @@ Append server-rendered toast markup to the toaster, or create one from JavaScrip
### Config object
<Callout type="warning" title="Text is escaped by default" icon="triangle-alert">
`title`, `description`, and action labels are inserted as plain text. With `html: true` they are inserted as HTML, so never pass untrusted user input with it. `onclick` is JavaScript code and must never contain user input. `icon` is always inserted as raw HTML (it is meant for SVG markup).
</Callout>
<dl>
<dt><code>duration</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Timeout in milliseconds. Defaults to <code>3000</code>, or <code>5000</code> for error toasts. Use <code>-1</code> to keep it open.</dd>
@@ -176,4 +181,10 @@ Append server-rendered toast markup to the toaster, or create one from JavaScrip
<dd>Primary action. Supports <code>label</code> with either <code>href</code> or <code>onclick</code>.</dd>
<dt><code>cancel</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Secondary action. Supports <code>label</code> and optional <code>onclick</code>.</dd>
<dt><code>icon</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Custom icon markup, usually an SVG. Inserted as raw HTML. Defaults to the category icon.</dd>
<dt><code>html</code> <span class="badge" data-variant="secondary">Optional</span></dt>
<dd>Set to <code>true</code> to insert <code>title</code>, <code>description</code>, and action labels as HTML instead of plain text. Defaults to <code>false</code>. Only use it with trusted content.</dd>
</dl>
The Jinja and Nunjucks `toast()` macros render on your server, so your template engine's autoescaping applies to `title`, `description`, and labels there when it is enabled. Nunjucks enables it by default. With Jinja, make sure your environment's `autoescape` setting covers the Basecoat template files (for example `.html.jinja`). `icon` is output with `safe`, and `onclick` is output as an attribute, so neither should contain user input.
+15 -1
View File
@@ -64,6 +64,8 @@ Or load the Basecoat runtime once, then only the component scripts you use.
| `dist/basecoat-{style}.cdn.min.css` | Standalone CDN stylesheet for `vega`, `nova`, `maia`, `lyra`, `mira`, `luma`, `sera`, or `rhea`. |
| `dist/basecoat-base.cdn.min.css` | Base tokens, utilities, and component structure without a style pack. |
| `dist/basecoat-compat.cdn.min.css` | Optional pre-1.0 compatibility aliases. Load after Basecoat. |
| `dist/basecoat.bundle.css` | Uncompiled single-file source for use with your own Tailwind build. Alias for the Vega style. |
| `dist/basecoat-{style}.bundle.css` | Uncompiled single-file source for use with your own Tailwind build, for `vega`, `nova`, `maia`, `lyra`, `mira`, `luma`, `sera`, or `rhea`. |
| `dist/js/all.min.js` | All auto-initialized JavaScript components except Chart. |
| `dist/js/basecoat.min.js` | Runtime only. Load once before individual component scripts. |
| `dist/js/{component}.min.js` | Individual JavaScript component file. |
@@ -129,6 +131,10 @@ Or import a named style bundle.
@import "basecoat-css/maia";
```
<Callout title="Using the Tailwind CLI without npm?" icon="info">
Download a single-file bundle, for example <a href={`https://cdn.jsdelivr.net/npm/basecoat-css@${basecoatVersion}/dist/basecoat-maia.bundle.css`}>{`basecoat-maia.bundle.css`}</a>. Save it next to your CSS and import it after Tailwind: `@import "tailwindcss"; @import "./basecoat-maia.bundle.css";`. Use `basecoat.bundle.css` for the default Vega style.
</Callout>
<Callout type="warning" title="Stylesheet order matters" icon="triangle-alert">
Load Basecoat after any stylesheet that includes Tailwind base/preflight. Loading another full Tailwind build after Basecoat can reset borders, inputs, and other component defaults.
</Callout>
@@ -195,7 +201,7 @@ See [Templates](/templates) for the available macros and arguments.
| `basecoat-css/components` | All component structure without a style pack. |
| `basecoat-css/components/button` | Individual component CSS. Replace `button` with any component file. |
| `basecoat-css/styles/maia` | Individual style-pack CSS. Use with `basecoat-css/base` or `basecoat-css/components`. |
| `basecoat-css/all.css` | Full default stylesheet plus all style-pack CSS. Use only when you need every style pack available at runtime. |
| `basecoat-css/all.css` | Alias for `basecoat-css`, the default Vega stylesheet. It does not include the other style packs. |
| `basecoat-css/compat` | Optional pre-1.0 compatibility aliases. Load after Basecoat. |
### Template files
@@ -243,6 +249,14 @@ Chart uses a separate helper and is not included in `basecoat-css/all`.
Each component page shows the minimal scripts or imports required for that component.
### JavaScript lifecycle
Basecoat initializes registered components on page load and when new DOM is inserted.
Use `window.basecoat.initAll()` after manually inserting HTML that contains Basecoat components. It only initializes components that are not already initialized.
Use `window.basecoat.initAll({ force: true })` when a framework or navigation library restores previously initialized DOM from a cache, such as an HTMX history restore. Force mode destroys existing component instances before reinitializing them, so it also resets transient state such as open menus, focus state, and active interactions.
## Compatibility
Basecoat 1.0 introduces new style packs and a smaller public API based on root classes plus semantic HTML and documented attributes. If you need pre-1.0 class aliases while migrating, load the compatibility stylesheet after your Basecoat stylesheet.
+2 -3
View File
@@ -1,6 +1,3 @@
import "../../../src/js/basecoat.js";
import "../../../src/js/accordion.js";
const initPriceRange = () => {
const input = document.getElementById("price-range");
const output = document.getElementById("price-range-output");
@@ -104,3 +101,5 @@ if (document.readyState === "loading") {
} else {
initDemos();
}
document.addEventListener("htmx:afterSettle", initDemos);
+1 -1
View File
@@ -131,6 +131,6 @@
@apply scroll-smooth;
}
body {
@apply bg-background text-foreground overscroll-none antialiased;
@apply bg-background text-foreground overscroll-none;
}
}
+20
View File
@@ -77,5 +77,25 @@
> * > footer {
@apply flex flex-col-reverse sm:flex-row sm:justify-end;
}
&:is([data-size='sm']) > * > header,
> [data-size='sm'] > header {
@apply sm:place-items-center sm:text-center;
}
&:is([data-size='sm']) > * > header:has(> figure),
> [data-size='sm'] > header:has(> figure) {
@apply sm:grid-rows-[auto_auto_1fr];
}
&:is([data-size='sm']) > * > header:has(> figure) > :is(h2, p),
> [data-size='sm'] > header:has(> figure) > :is(h2, p) {
@apply sm:col-start-auto;
}
&:is([data-size='sm']) > * > footer,
> [data-size='sm'] > footer {
@apply grid grid-cols-2 sm:grid sm:grid-cols-2;
}
}
}
+6 -6
View File
@@ -8,7 +8,7 @@
&[aria-hidden=false] {
@apply max-md:bg-black/50 max-md:fixed max-md:inset-0 max-md:z-40;
}
nav {
> nav {
@apply fixed inset-y-0 z-50 flex w-(--sidebar-mobile-width) flex-col transition-transform duration-300 ease-in-out md:w-(--sidebar-width);
}
& + * {
@@ -16,14 +16,14 @@
}
&:not([data-side]),
&[data-side=left] {
nav {
> nav {
@apply left-0 border-r;
}
& + * {
@apply relative md:ml-(--sidebar-width);
}
&[aria-hidden=true] {
nav {
> nav {
@apply -translate-x-full;
}
& + * {
@@ -32,14 +32,14 @@
}
}
&[data-side=right] {
nav {
> nav {
@apply right-0 border-l;
}
& + * {
@apply relative md:mr-(--sidebar-width);
}
&[aria-hidden=true] {
nav {
> nav {
@apply translate-x-full;
}
& + * {
@@ -47,7 +47,7 @@
}
}
}
nav {
> nav {
> header,
> footer {
@apply flex flex-col;
+38 -33
View File
@@ -415,7 +415,7 @@
/* Combobox */
.combobox > input[role='combobox'] {
@apply bg-input/50 border-transparent focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-3xl border py-2 ps-3 pe-8 text-sm transition-[color,box-shadow,background-color] focus-visible:ring-3 aria-invalid:ring-3;
@apply bg-input/50 border-transparent focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-3xl border py-2 ps-3 pe-8 text-base md:text-sm transition-[color,box-shadow,background-color] focus-visible:ring-3 aria-invalid:ring-3;
}
.combobox > svg,
@@ -540,6 +540,11 @@
@apply bg-popover text-popover-foreground ring-foreground/5 dark:ring-foreground/10 max-w-xs gap-6 rounded-4xl p-6 shadow-xl ring-1 duration-100 sm:max-w-md;
}
.alert-dialog:is([data-size='sm']) > *,
.alert-dialog > [data-size='sm'] {
@apply sm:max-w-xs;
}
.dialog > * {
@apply bg-popover text-popover-foreground ring-foreground/5 dark:ring-foreground/10 max-w-[calc(100%-2rem)] gap-6 rounded-4xl p-6 text-sm shadow-xl ring-1 duration-100 sm:max-w-md;
}
@@ -1177,92 +1182,92 @@
}
/* Sidebar */
.sidebar nav {
.sidebar > nav {
@apply bg-sidebar text-sidebar-foreground;
}
.sidebar nav > header,
.sidebar nav > footer {
.sidebar > nav > header,
.sidebar > nav > footer {
@apply gap-2 p-2 [--radius:var(--radius-xl)];
}
.sidebar nav > section {
.sidebar > nav > section {
@apply gap-2 [--radius:var(--radius-xl)];
}
.sidebar nav [role=separator] {
.sidebar > nav [role=separator] {
@apply border-sidebar-border mx-2;
}
.sidebar nav > section > [role=group] {
.sidebar > nav > section > [role=group] {
@apply p-2;
}
.sidebar nav h3 {
.sidebar > nav h3 {
@apply text-sidebar-foreground/70 ring-sidebar-ring h-8 rounded-xl px-3 text-xs font-medium focus-visible:ring-2 [&>svg]:size-4 transition-[margin,opacity] duration-200 ease-linear;
}
.sidebar nav > section > [role=group] > ul {
.sidebar > nav > section > [role=group] > ul {
@apply gap-0.5;
}
.sidebar nav > section > [role=group] {
.sidebar > nav > section > [role=group] {
@apply text-sm;
}
.sidebar nav li > :is(a, button),
.sidebar nav li > details > summary {
.sidebar > nav li > :is(a, button),
.sidebar > nav li > details > summary {
@apply ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground gap-2 rounded-xl px-3 py-2 text-sm focus-visible:ring-2 text-left focus-visible:outline-hidden data-[open]:hover:bg-sidebar-accent data-[open]:hover:text-sidebar-accent-foreground [&>svg]:shrink-0 [&>svg]:size-4;
}
.sidebar nav li > :is(a, button):is([aria-current=page], [data-active=true]),
.sidebar nav li > details > summary:is([aria-current=page], [data-active=true]) {
.sidebar > nav li > :is(a, button):is([aria-current=page], [data-active=true]),
.sidebar > nav li > details > summary:is([aria-current=page], [data-active=true]) {
@apply bg-sidebar-accent text-sidebar-accent-foreground font-medium;
}
.sidebar nav li > :is(a, button):not([data-variant]),
.sidebar nav li > details > summary:not([data-variant]),
.sidebar nav li > :is(a, button)[data-variant=default],
.sidebar nav li > details > summary[data-variant=default] {
.sidebar > nav li > :is(a, button):not([data-variant]),
.sidebar > nav li > details > summary:not([data-variant]),
.sidebar > nav li > :is(a, button)[data-variant=default],
.sidebar > nav li > details > summary[data-variant=default] {
@apply hover:bg-sidebar-accent hover:text-sidebar-accent-foreground;
}
.sidebar nav li > :is(a, button)[data-variant=outline],
.sidebar nav li > details > summary[data-variant=outline] {
.sidebar > nav li > :is(a, button)[data-variant=outline],
.sidebar > nav li > details > summary[data-variant=outline] {
@apply bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:shadow-[0_0_0_1px_var(--sidebar-accent)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground;
}
.sidebar nav li > :is(a, button):not([data-size]),
.sidebar nav li > details > summary:not([data-size]),
.sidebar nav li > :is(a, button)[data-size=default],
.sidebar nav li > details > summary[data-size=default] {
.sidebar > nav li > :is(a, button):not([data-size]),
.sidebar > nav li > details > summary:not([data-size]),
.sidebar > nav li > :is(a, button)[data-size=default],
.sidebar > nav li > details > summary[data-size=default] {
@apply h-9 text-sm;
}
.sidebar nav li > :is(a, button)[data-size=sm],
.sidebar nav li > details > summary[data-size=sm] {
.sidebar > nav li > :is(a, button)[data-size=sm],
.sidebar > nav li > details > summary[data-size=sm] {
@apply h-8 text-xs;
}
.sidebar nav li > :is(a, button)[data-size=lg],
.sidebar nav li > details > summary[data-size=lg] {
.sidebar > nav li > :is(a, button)[data-size=lg],
.sidebar > nav li > details > summary[data-size=lg] {
@apply h-14 px-3 text-sm;
}
.sidebar nav li > details > summary::after {
.sidebar > nav li > details > summary::after {
@apply size-4;
}
.sidebar nav li > details::details-content {
.sidebar > nav li > details::details-content {
@apply px-3.5;
}
.sidebar nav ul ul {
.sidebar > nav ul ul {
@apply border-sidebar-border translate-x-px gap-1 border-s px-2.5 py-0.5 rtl:-translate-x-px;
}
.sidebar nav ul ul > li > :is(a, button),
.sidebar nav ul ul > li > details > summary {
.sidebar > nav ul ul > li > :is(a, button),
.sidebar > nav ul ul > li > details > summary {
@apply text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground h-7 gap-2 rounded-xl px-3 text-sm focus-visible:ring-2 [&>svg]:size-4;
}
/* Progress */
+37 -32
View File
@@ -520,6 +520,11 @@
@apply bg-popover text-popover-foreground ring-foreground/10 max-w-xs gap-4 rounded-none p-4 ring-1 duration-100 sm:max-w-sm;
}
.alert-dialog:is([data-size='sm']) > *,
.alert-dialog > [data-size='sm'] {
@apply sm:max-w-xs;
}
.dialog > * {
@apply bg-popover text-popover-foreground ring-foreground/10 max-w-[calc(100%-2rem)] gap-4 rounded-none p-4 text-xs/relaxed ring-1 duration-100 sm:max-w-sm;
}
@@ -1180,92 +1185,92 @@
}
/* Sidebar */
.sidebar nav {
.sidebar > nav {
@apply bg-sidebar text-sidebar-foreground;
}
.sidebar nav > header,
.sidebar nav > footer {
.sidebar > nav > header,
.sidebar > nav > footer {
@apply gap-2 p-2;
}
.sidebar nav > section {
.sidebar > nav > section {
@apply gap-0;
}
.sidebar nav [role=separator] {
.sidebar > nav [role=separator] {
@apply border-sidebar-border mx-2;
}
.sidebar nav > section > [role=group] {
.sidebar > nav > section > [role=group] {
@apply p-2;
}
.sidebar nav h3 {
.sidebar > nav h3 {
@apply text-sidebar-foreground/70 ring-sidebar-ring h-8 rounded-none px-2 text-xs focus-visible:ring-2 [&>svg]:size-4 transition-[margin,opacity] duration-200 ease-linear;
}
.sidebar nav > section > [role=group] > ul {
.sidebar > nav > section > [role=group] > ul {
@apply gap-0;
}
.sidebar nav > section > [role=group] {
.sidebar > nav > section > [role=group] {
@apply text-xs;
}
.sidebar nav li > :is(a, button),
.sidebar nav li > details > summary {
.sidebar > nav li > :is(a, button),
.sidebar > nav li > details > summary {
@apply ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground gap-2 rounded-none p-2 text-xs focus-visible:ring-2 text-left focus-visible:outline-hidden data-[open]:hover:bg-sidebar-accent data-[open]:hover:text-sidebar-accent-foreground [&>svg]:shrink-0 [&>svg]:size-4;
}
.sidebar nav li > :is(a, button):is([aria-current=page], [data-active=true]),
.sidebar nav li > details > summary:is([aria-current=page], [data-active=true]) {
.sidebar > nav li > :is(a, button):is([aria-current=page], [data-active=true]),
.sidebar > nav li > details > summary:is([aria-current=page], [data-active=true]) {
@apply bg-sidebar-accent text-sidebar-accent-foreground font-medium;
}
.sidebar nav li > :is(a, button):not([data-variant]),
.sidebar nav li > details > summary:not([data-variant]),
.sidebar nav li > :is(a, button)[data-variant=default],
.sidebar nav li > details > summary[data-variant=default] {
.sidebar > nav li > :is(a, button):not([data-variant]),
.sidebar > nav li > details > summary:not([data-variant]),
.sidebar > nav li > :is(a, button)[data-variant=default],
.sidebar > nav li > details > summary[data-variant=default] {
@apply hover:bg-sidebar-accent hover:text-sidebar-accent-foreground;
}
.sidebar nav li > :is(a, button)[data-variant=outline],
.sidebar nav li > details > summary[data-variant=outline] {
.sidebar > nav li > :is(a, button)[data-variant=outline],
.sidebar > nav li > details > summary[data-variant=outline] {
@apply bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:shadow-[0_0_0_1px_var(--sidebar-accent)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground;
}
.sidebar nav li > :is(a, button):not([data-size]),
.sidebar nav li > details > summary:not([data-size]),
.sidebar nav li > :is(a, button)[data-size=default],
.sidebar nav li > details > summary[data-size=default] {
.sidebar > nav li > :is(a, button):not([data-size]),
.sidebar > nav li > details > summary:not([data-size]),
.sidebar > nav li > :is(a, button)[data-size=default],
.sidebar > nav li > details > summary[data-size=default] {
@apply h-8 text-xs;
}
.sidebar nav li > :is(a, button)[data-size=sm],
.sidebar nav li > details > summary[data-size=sm] {
.sidebar > nav li > :is(a, button)[data-size=sm],
.sidebar > nav li > details > summary[data-size=sm] {
@apply h-7 text-xs;
}
.sidebar nav li > :is(a, button)[data-size=lg],
.sidebar nav li > details > summary[data-size=lg] {
.sidebar > nav li > :is(a, button)[data-size=lg],
.sidebar > nav li > details > summary[data-size=lg] {
@apply h-12 text-xs;
}
.sidebar nav li > details > summary::after {
.sidebar > nav li > details > summary::after {
@apply size-4;
}
.sidebar nav li > details::details-content {
.sidebar > nav li > details::details-content {
@apply px-3.5;
}
.sidebar nav ul ul {
.sidebar > nav ul ul {
@apply border-sidebar-border translate-x-px gap-1 border-s px-2.5 py-0.5 rtl:-translate-x-px;
}
.sidebar nav ul ul > li > :is(a, button),
.sidebar nav ul ul > li > details > summary {
.sidebar > nav ul ul > li > :is(a, button),
.sidebar > nav ul ul > li > details > summary {
@apply text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground h-7 gap-2 rounded-none px-2 text-xs focus-visible:ring-2 [&>svg]:size-4;
}
/* Progress */
+38 -33
View File
@@ -407,7 +407,7 @@
/* Combobox */
.combobox > input[role='combobox'] {
@apply border-input bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-4xl border py-2 ps-3 pe-8 text-sm transition-colors focus-visible:ring-[3px] aria-invalid:ring-[3px];
@apply border-input bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-4xl border py-2 ps-3 pe-8 text-base md:text-sm transition-colors focus-visible:ring-[3px] aria-invalid:ring-[3px];
}
.combobox > svg,
@@ -532,6 +532,11 @@
@apply bg-popover text-popover-foreground ring-foreground/5 max-w-xs gap-6 rounded-4xl p-6 ring-1 duration-100 sm:max-w-md;
}
.alert-dialog:is([data-size='sm']) > *,
.alert-dialog > [data-size='sm'] {
@apply sm:max-w-xs;
}
.dialog > * {
@apply bg-popover text-popover-foreground ring-foreground/5 max-w-[calc(100%-2rem)] gap-6 rounded-4xl p-6 text-sm ring-1 duration-100 sm:max-w-md;
}
@@ -1165,92 +1170,92 @@
}
/* Sidebar */
.sidebar nav {
.sidebar > nav {
@apply bg-sidebar text-sidebar-foreground;
}
.sidebar nav > header,
.sidebar nav > footer {
.sidebar > nav > header,
.sidebar > nav > footer {
@apply gap-2 p-2 [--radius:var(--radius-xl)];
}
.sidebar nav > section {
.sidebar > nav > section {
@apply gap-2 [--radius:var(--radius-xl)];
}
.sidebar nav [role=separator] {
.sidebar > nav [role=separator] {
@apply border-sidebar-border mx-2;
}
.sidebar nav > section > [role=group] {
.sidebar > nav > section > [role=group] {
@apply p-2;
}
.sidebar nav h3 {
.sidebar > nav h3 {
@apply text-sidebar-foreground/70 ring-sidebar-ring h-8 rounded-md px-3 text-xs font-medium focus-visible:ring-2 [&>svg]:size-4 transition-[margin,opacity] duration-200 ease-linear;
}
.sidebar nav > section > [role=group] > ul {
.sidebar > nav > section > [role=group] > ul {
@apply gap-1;
}
.sidebar nav > section > [role=group] {
.sidebar > nav > section > [role=group] {
@apply text-sm;
}
.sidebar nav li > :is(a, button),
.sidebar nav li > details > summary {
.sidebar > nav li > :is(a, button),
.sidebar > nav li > details > summary {
@apply ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground gap-2 rounded-lg px-3 py-2 text-sm focus-visible:ring-2 text-left focus-visible:outline-hidden data-[open]:hover:bg-sidebar-accent data-[open]:hover:text-sidebar-accent-foreground [&>svg]:shrink-0 [&>svg]:size-4;
}
.sidebar nav li > :is(a, button):is([aria-current=page], [data-active=true]),
.sidebar nav li > details > summary:is([aria-current=page], [data-active=true]) {
.sidebar > nav li > :is(a, button):is([aria-current=page], [data-active=true]),
.sidebar > nav li > details > summary:is([aria-current=page], [data-active=true]) {
@apply bg-sidebar-accent text-sidebar-accent-foreground font-medium;
}
.sidebar nav li > :is(a, button):not([data-variant]),
.sidebar nav li > details > summary:not([data-variant]),
.sidebar nav li > :is(a, button)[data-variant=default],
.sidebar nav li > details > summary[data-variant=default] {
.sidebar > nav li > :is(a, button):not([data-variant]),
.sidebar > nav li > details > summary:not([data-variant]),
.sidebar > nav li > :is(a, button)[data-variant=default],
.sidebar > nav li > details > summary[data-variant=default] {
@apply hover:bg-sidebar-accent hover:text-sidebar-accent-foreground;
}
.sidebar nav li > :is(a, button)[data-variant=outline],
.sidebar nav li > details > summary[data-variant=outline] {
.sidebar > nav li > :is(a, button)[data-variant=outline],
.sidebar > nav li > details > summary[data-variant=outline] {
@apply bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:shadow-[0_0_0_1px_var(--sidebar-accent)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground;
}
.sidebar nav li > :is(a, button):not([data-size]),
.sidebar nav li > details > summary:not([data-size]),
.sidebar nav li > :is(a, button)[data-size=default],
.sidebar nav li > details > summary[data-size=default] {
.sidebar > nav li > :is(a, button):not([data-size]),
.sidebar > nav li > details > summary:not([data-size]),
.sidebar > nav li > :is(a, button)[data-size=default],
.sidebar > nav li > details > summary[data-size=default] {
@apply h-9 text-sm;
}
.sidebar nav li > :is(a, button)[data-size=sm],
.sidebar nav li > details > summary[data-size=sm] {
.sidebar > nav li > :is(a, button)[data-size=sm],
.sidebar > nav li > details > summary[data-size=sm] {
@apply h-8 text-xs;
}
.sidebar nav li > :is(a, button)[data-size=lg],
.sidebar nav li > details > summary[data-size=lg] {
.sidebar > nav li > :is(a, button)[data-size=lg],
.sidebar > nav li > details > summary[data-size=lg] {
@apply h-14 px-3 text-sm;
}
.sidebar nav li > details > summary::after {
.sidebar > nav li > details > summary::after {
@apply size-4;
}
.sidebar nav li > details::details-content {
.sidebar > nav li > details::details-content {
@apply px-3.5;
}
.sidebar nav ul ul {
.sidebar > nav ul ul {
@apply border-sidebar-border translate-x-px gap-1 border-s px-2.5 py-0.5 rtl:-translate-x-px;
}
.sidebar nav ul ul > li > :is(a, button),
.sidebar nav ul ul > li > details > summary {
.sidebar > nav ul ul > li > :is(a, button),
.sidebar > nav ul ul > li > details > summary {
@apply text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground h-7 gap-2 rounded-md px-2 text-sm focus-visible:ring-2 [&>svg]:size-4;
}
/* Progress */
+38 -33
View File
@@ -407,7 +407,7 @@
/* Combobox */
.combobox > input[role='combobox'] {
@apply border-input bg-input/20 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-7 rounded-md border py-1 ps-2 pe-6 text-xs/relaxed transition-colors focus-visible:ring-2 aria-invalid:ring-2;
@apply border-input bg-input/20 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-7 rounded-md border py-1 ps-2 pe-6 text-sm md:text-xs/relaxed transition-colors focus-visible:ring-2 aria-invalid:ring-2;
}
.combobox > svg,
@@ -536,6 +536,11 @@
@apply bg-popover text-popover-foreground ring-foreground/10 max-w-xs gap-3 rounded-xl p-4 ring-1 duration-100 sm:max-w-sm;
}
.alert-dialog:is([data-size='sm']) > *,
.alert-dialog > [data-size='sm'] {
@apply max-w-64 sm:max-w-64;
}
.dialog > * {
@apply bg-popover text-popover-foreground ring-foreground/10 max-w-[calc(100%-2rem)] gap-4 rounded-xl p-4 text-xs/relaxed ring-1 duration-100 sm:max-w-sm;
}
@@ -1174,92 +1179,92 @@
}
/* Sidebar */
.sidebar nav {
.sidebar > nav {
@apply bg-sidebar text-sidebar-foreground;
}
.sidebar nav > header,
.sidebar nav > footer {
.sidebar > nav > header,
.sidebar > nav > footer {
@apply gap-2 p-2;
}
.sidebar nav > section {
.sidebar > nav > section {
@apply gap-0;
}
.sidebar nav [role=separator] {
.sidebar > nav [role=separator] {
@apply border-sidebar-border mx-2;
}
.sidebar nav > section > [role=group] {
.sidebar > nav > section > [role=group] {
@apply px-2 py-1;
}
.sidebar nav h3 {
.sidebar > nav h3 {
@apply text-sidebar-foreground/70 ring-sidebar-ring h-8 rounded-md px-2 text-xs focus-visible:ring-2 [&>svg]:size-4 transition-[margin,opacity] duration-200 ease-linear;
}
.sidebar nav > section > [role=group] > ul {
.sidebar > nav > section > [role=group] > ul {
@apply gap-px;
}
.sidebar nav > section > [role=group] {
.sidebar > nav > section > [role=group] {
@apply text-xs;
}
.sidebar nav li > :is(a, button),
.sidebar nav li > details > summary {
.sidebar > nav li > :is(a, button),
.sidebar > nav li > details > summary {
@apply ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground gap-2 rounded-[calc(var(--radius-sm)+2px)] p-2 text-xs focus-visible:ring-2 text-left focus-visible:outline-hidden data-[open]:hover:bg-sidebar-accent data-[open]:hover:text-sidebar-accent-foreground [&>svg]:shrink-0 [&>svg]:size-4;
}
.sidebar nav li > :is(a, button):is([aria-current=page], [data-active=true]),
.sidebar nav li > details > summary:is([aria-current=page], [data-active=true]) {
.sidebar > nav li > :is(a, button):is([aria-current=page], [data-active=true]),
.sidebar > nav li > details > summary:is([aria-current=page], [data-active=true]) {
@apply bg-sidebar-accent text-sidebar-accent-foreground font-medium;
}
.sidebar nav li > :is(a, button):not([data-variant]),
.sidebar nav li > details > summary:not([data-variant]),
.sidebar nav li > :is(a, button)[data-variant=default],
.sidebar nav li > details > summary[data-variant=default] {
.sidebar > nav li > :is(a, button):not([data-variant]),
.sidebar > nav li > details > summary:not([data-variant]),
.sidebar > nav li > :is(a, button)[data-variant=default],
.sidebar > nav li > details > summary[data-variant=default] {
@apply hover:bg-sidebar-accent hover:text-sidebar-accent-foreground;
}
.sidebar nav li > :is(a, button)[data-variant=outline],
.sidebar nav li > details > summary[data-variant=outline] {
.sidebar > nav li > :is(a, button)[data-variant=outline],
.sidebar > nav li > details > summary[data-variant=outline] {
@apply bg-muted/20 dark:bg-muted/30 border-input shadow-[0_0_0_1px_var(--sidebar-border)] hover:shadow-[0_0_0_1px_var(--sidebar-accent)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground;
}
.sidebar nav li > :is(a, button):not([data-size]),
.sidebar nav li > details > summary:not([data-size]),
.sidebar nav li > :is(a, button)[data-size=default],
.sidebar nav li > details > summary[data-size=default] {
.sidebar > nav li > :is(a, button):not([data-size]),
.sidebar > nav li > details > summary:not([data-size]),
.sidebar > nav li > :is(a, button)[data-size=default],
.sidebar > nav li > details > summary[data-size=default] {
@apply h-8 text-xs;
}
.sidebar nav li > :is(a, button)[data-size=sm],
.sidebar nav li > details > summary[data-size=sm] {
.sidebar > nav li > :is(a, button)[data-size=sm],
.sidebar > nav li > details > summary[data-size=sm] {
@apply h-7 text-xs;
}
.sidebar nav li > :is(a, button)[data-size=lg],
.sidebar nav li > details > summary[data-size=lg] {
.sidebar > nav li > :is(a, button)[data-size=lg],
.sidebar > nav li > details > summary[data-size=lg] {
@apply h-12 text-xs;
}
.sidebar nav li > details > summary::after {
.sidebar > nav li > details > summary::after {
@apply size-4;
}
.sidebar nav li > details::details-content {
.sidebar > nav li > details::details-content {
@apply px-3.5;
}
.sidebar nav ul ul {
.sidebar > nav ul ul {
@apply border-sidebar-border translate-x-px gap-1 border-s px-2.5 py-0.5 rtl:-translate-x-px;
}
.sidebar nav ul ul > li > :is(a, button),
.sidebar nav ul ul > li > details > summary {
.sidebar > nav ul ul > li > :is(a, button),
.sidebar > nav ul ul > li > details > summary {
@apply text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground h-7 gap-2 rounded-md px-2 text-xs focus-visible:ring-2 [&>svg]:size-4;
}
/* Progress */
+38 -33
View File
@@ -403,7 +403,7 @@
/* Combobox */
.combobox > input[role='combobox'] {
@apply border-input dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent py-1 ps-2.5 pe-8 text-sm transition-colors focus-visible:ring-3 aria-invalid:ring-3;
@apply border-input dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent py-1 ps-2.5 pe-8 text-base md:text-sm transition-colors focus-visible:ring-3 aria-invalid:ring-3;
}
.combobox > svg,
@@ -528,6 +528,11 @@
@apply bg-popover text-popover-foreground ring-foreground/10 max-w-xs gap-4 rounded-xl p-4 ring-1 duration-100 sm:max-w-sm;
}
.alert-dialog:is([data-size='sm']) > *,
.alert-dialog > [data-size='sm'] {
@apply sm:max-w-xs;
}
.dialog > * {
@apply bg-popover text-popover-foreground ring-foreground/10 max-w-[calc(100%-2rem)] gap-4 rounded-xl p-4 text-sm ring-1 duration-100 sm:max-w-sm;
}
@@ -1188,92 +1193,92 @@
}
/* Sidebar */
.sidebar nav {
.sidebar > nav {
@apply bg-sidebar text-sidebar-foreground;
}
.sidebar nav > header,
.sidebar nav > footer {
.sidebar > nav > header,
.sidebar > nav > footer {
@apply gap-2 p-2;
}
.sidebar nav > section {
.sidebar > nav > section {
@apply gap-0;
}
.sidebar nav [role=separator] {
.sidebar > nav [role=separator] {
@apply border-sidebar-border mx-2;
}
.sidebar nav > section > [role=group] {
.sidebar > nav > section > [role=group] {
@apply p-2;
}
.sidebar nav h3 {
.sidebar > nav h3 {
@apply text-sidebar-foreground/70 ring-sidebar-ring h-8 rounded-md px-2 text-xs font-medium focus-visible:ring-2 [&>svg]:size-4 transition-[margin,opacity] duration-200 ease-linear;
}
.sidebar nav > section > [role=group] > ul {
.sidebar > nav > section > [role=group] > ul {
@apply gap-0;
}
.sidebar nav > section > [role=group] {
.sidebar > nav > section > [role=group] {
@apply text-sm;
}
.sidebar nav li > :is(a, button),
.sidebar nav li > details > summary {
.sidebar > nav li > :is(a, button),
.sidebar > nav li > details > summary {
@apply ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground gap-2 rounded-md p-2 text-sm focus-visible:ring-2 text-left focus-visible:outline-hidden data-[open]:hover:bg-sidebar-accent data-[open]:hover:text-sidebar-accent-foreground [&>svg]:shrink-0 [&>svg]:size-4;
}
.sidebar nav li > :is(a, button):is([aria-current=page], [data-active=true]),
.sidebar nav li > details > summary:is([aria-current=page], [data-active=true]) {
.sidebar > nav li > :is(a, button):is([aria-current=page], [data-active=true]),
.sidebar > nav li > details > summary:is([aria-current=page], [data-active=true]) {
@apply bg-sidebar-accent text-sidebar-accent-foreground font-medium;
}
.sidebar nav li > :is(a, button):not([data-variant]),
.sidebar nav li > details > summary:not([data-variant]),
.sidebar nav li > :is(a, button)[data-variant=default],
.sidebar nav li > details > summary[data-variant=default] {
.sidebar > nav li > :is(a, button):not([data-variant]),
.sidebar > nav li > details > summary:not([data-variant]),
.sidebar > nav li > :is(a, button)[data-variant=default],
.sidebar > nav li > details > summary[data-variant=default] {
@apply hover:bg-sidebar-accent hover:text-sidebar-accent-foreground;
}
.sidebar nav li > :is(a, button)[data-variant=outline],
.sidebar nav li > details > summary[data-variant=outline] {
.sidebar > nav li > :is(a, button)[data-variant=outline],
.sidebar > nav li > details > summary[data-variant=outline] {
@apply bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:shadow-[0_0_0_1px_var(--sidebar-accent)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground;
}
.sidebar nav li > :is(a, button):not([data-size]),
.sidebar nav li > details > summary:not([data-size]),
.sidebar nav li > :is(a, button)[data-size=default],
.sidebar nav li > details > summary[data-size=default] {
.sidebar > nav li > :is(a, button):not([data-size]),
.sidebar > nav li > details > summary:not([data-size]),
.sidebar > nav li > :is(a, button)[data-size=default],
.sidebar > nav li > details > summary[data-size=default] {
@apply h-8 text-sm;
}
.sidebar nav li > :is(a, button)[data-size=sm],
.sidebar nav li > details > summary[data-size=sm] {
.sidebar > nav li > :is(a, button)[data-size=sm],
.sidebar > nav li > details > summary[data-size=sm] {
@apply h-7 text-xs;
}
.sidebar nav li > :is(a, button)[data-size=lg],
.sidebar nav li > details > summary[data-size=lg] {
.sidebar > nav li > :is(a, button)[data-size=lg],
.sidebar > nav li > details > summary[data-size=lg] {
@apply h-12 text-sm;
}
.sidebar nav li > details > summary::after {
.sidebar > nav li > details > summary::after {
@apply size-4;
}
.sidebar nav li > details::details-content {
.sidebar > nav li > details::details-content {
@apply px-3.5;
}
.sidebar nav ul ul {
.sidebar > nav ul ul {
@apply border-sidebar-border translate-x-px gap-1 border-s px-2.5 py-0.5 rtl:-translate-x-px;
}
.sidebar nav ul ul > li > :is(a, button),
.sidebar nav ul ul > li > details > summary {
.sidebar > nav ul ul > li > :is(a, button),
.sidebar > nav ul ul > li > details > summary {
@apply text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground h-7 gap-2 rounded-md px-2 text-sm focus-visible:ring-2 [&>svg]:size-4;
}
/* Progress */
+38 -33
View File
@@ -415,7 +415,7 @@
/* Combobox */
.combobox > input[role='combobox'] {
@apply bg-input/50 border-transparent focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-8 rounded-2xl border py-1 ps-2.5 pe-8 text-sm transition-[color,box-shadow] duration-200 focus-visible:ring-3 aria-invalid:ring-3;
@apply bg-input/50 border-transparent focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-8 rounded-2xl border py-1 ps-2.5 pe-8 text-base md:text-sm transition-[color,box-shadow] duration-200 focus-visible:ring-3 aria-invalid:ring-3;
}
.combobox > svg,
@@ -540,6 +540,11 @@
@apply bg-popover text-popover-foreground ring-foreground/5 dark:ring-foreground/10 max-w-xs gap-6 rounded-3xl p-6 shadow-xl ring-1 duration-100 sm:max-w-md;
}
.alert-dialog:is([data-size='sm']) > *,
.alert-dialog > [data-size='sm'] {
@apply sm:max-w-xs;
}
.dialog > * {
@apply bg-popover text-popover-foreground ring-foreground/5 dark:ring-foreground/10 max-w-[calc(100%-2rem)] gap-6 rounded-3xl p-6 text-sm shadow-xl ring-1 duration-100 sm:max-w-md;
}
@@ -1182,92 +1187,92 @@
}
/* Sidebar */
.sidebar nav {
.sidebar > nav {
@apply bg-sidebar text-sidebar-foreground;
}
.sidebar nav > header,
.sidebar nav > footer {
.sidebar > nav > header,
.sidebar > nav > footer {
@apply gap-2 p-2 [--radius:var(--radius-xl)];
}
.sidebar nav > section {
.sidebar > nav > section {
@apply gap-2 [--radius:var(--radius-xl)];
}
.sidebar nav [role=separator] {
.sidebar > nav [role=separator] {
@apply border-sidebar-border mx-2;
}
.sidebar nav > section > [role=group] {
.sidebar > nav > section > [role=group] {
@apply p-2;
}
.sidebar nav h3 {
.sidebar > nav h3 {
@apply text-sidebar-foreground/70 ring-sidebar-ring h-8 rounded-xl px-3 text-xs font-medium focus-visible:ring-3 [&>svg]:size-4 transition-[margin,opacity] duration-200 ease-linear;
}
.sidebar nav > section > [role=group] > ul {
.sidebar > nav > section > [role=group] > ul {
@apply gap-0.5;
}
.sidebar nav > section > [role=group] {
.sidebar > nav > section > [role=group] {
@apply text-sm;
}
.sidebar nav li > :is(a, button),
.sidebar nav li > details > summary {
.sidebar > nav li > :is(a, button),
.sidebar > nav li > details > summary {
@apply ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground gap-2 rounded-xl px-3 py-2 text-sm duration-200 focus-visible:ring-3 whitespace-nowrap has-[>svg:first-child]:ps-2.5 has-[>svg:last-child]:pe-2.5 text-left focus-visible:outline-hidden data-[open]:hover:bg-sidebar-accent data-[open]:hover:text-sidebar-accent-foreground [&>svg]:shrink-0 [&>svg]:size-4;
}
.sidebar nav li > :is(a, button):is([aria-current=page], [data-active=true]),
.sidebar nav li > details > summary:is([aria-current=page], [data-active=true]) {
.sidebar > nav li > :is(a, button):is([aria-current=page], [data-active=true]),
.sidebar > nav li > details > summary:is([aria-current=page], [data-active=true]) {
@apply bg-sidebar-accent text-sidebar-accent-foreground font-medium;
}
.sidebar nav li > :is(a, button):not([data-variant]),
.sidebar nav li > details > summary:not([data-variant]),
.sidebar nav li > :is(a, button)[data-variant=default],
.sidebar nav li > details > summary[data-variant=default] {
.sidebar > nav li > :is(a, button):not([data-variant]),
.sidebar > nav li > details > summary:not([data-variant]),
.sidebar > nav li > :is(a, button)[data-variant=default],
.sidebar > nav li > details > summary[data-variant=default] {
@apply hover:bg-sidebar-accent hover:text-sidebar-accent-foreground;
}
.sidebar nav li > :is(a, button)[data-variant=outline],
.sidebar nav li > details > summary[data-variant=outline] {
.sidebar > nav li > :is(a, button)[data-variant=outline],
.sidebar > nav li > details > summary[data-variant=outline] {
@apply bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:shadow-[0_0_0_1px_var(--sidebar-accent)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground;
}
.sidebar nav li > :is(a, button):not([data-size]),
.sidebar nav li > details > summary:not([data-size]),
.sidebar nav li > :is(a, button)[data-size=default],
.sidebar nav li > details > summary[data-size=default] {
.sidebar > nav li > :is(a, button):not([data-size]),
.sidebar > nav li > details > summary:not([data-size]),
.sidebar > nav li > :is(a, button)[data-size=default],
.sidebar > nav li > details > summary[data-size=default] {
@apply h-8 text-sm;
}
.sidebar nav li > :is(a, button)[data-size=sm],
.sidebar nav li > details > summary[data-size=sm] {
.sidebar > nav li > :is(a, button)[data-size=sm],
.sidebar > nav li > details > summary[data-size=sm] {
@apply h-7 text-xs;
}
.sidebar nav li > :is(a, button)[data-size=lg],
.sidebar nav li > details > summary[data-size=lg] {
.sidebar > nav li > :is(a, button)[data-size=lg],
.sidebar > nav li > details > summary[data-size=lg] {
@apply h-12 px-3 text-sm;
}
.sidebar nav li > details > summary::after {
.sidebar > nav li > details > summary::after {
@apply size-4;
}
.sidebar nav li > details::details-content {
.sidebar > nav li > details::details-content {
@apply px-3.5;
}
.sidebar nav ul ul {
.sidebar > nav ul ul {
@apply border-sidebar-border translate-x-px gap-1 border-s px-2.5 py-0.5 rtl:-translate-x-px;
}
.sidebar nav ul ul > li > :is(a, button),
.sidebar nav ul ul > li > details > summary {
.sidebar > nav ul ul > li > :is(a, button),
.sidebar > nav ul ul > li > details > summary {
@apply text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground h-7 gap-2 rounded-xl px-3 text-sm focus-visible:ring-3 [&>svg]:size-4;
}
/* Progress */
+38 -33
View File
@@ -419,7 +419,7 @@
/* Combobox */
.combobox > input[role='combobox'] {
@apply border-transparent border-b-input bg-transparent focus-visible:border-b-ring aria-invalid:border-b-destructive dark:aria-invalid:border-b-destructive/50 h-10 rounded-none border py-2 ps-0 pe-8 text-sm transition-[color,border-color];
@apply border-transparent border-b-input bg-transparent focus-visible:border-b-ring aria-invalid:border-b-destructive dark:aria-invalid:border-b-destructive/50 h-10 rounded-none border py-2 ps-0 pe-8 text-base md:text-sm transition-[color,border-color];
}
.combobox > svg,
@@ -544,6 +544,11 @@
@apply bg-popover text-popover-foreground ring-foreground/10 max-w-xs gap-6 rounded-none p-6 shadow-md ring-1 duration-100 sm:max-w-md;
}
.alert-dialog:is([data-size='sm']) > *,
.alert-dialog > [data-size='sm'] {
@apply sm:max-w-xs;
}
.dialog > * {
@apply bg-popover text-popover-foreground ring-foreground/10 max-w-[calc(100%-2rem)] gap-6 rounded-none p-6 text-sm shadow-md ring-1 duration-100 sm:max-w-md;
}
@@ -1198,92 +1203,92 @@
}
/* Sidebar */
.sidebar nav {
.sidebar > nav {
@apply bg-sidebar text-sidebar-foreground;
}
.sidebar nav > header,
.sidebar nav > footer {
.sidebar > nav > header,
.sidebar > nav > footer {
@apply gap-2 p-2 [--radius:0];
}
.sidebar nav > section {
.sidebar > nav > section {
@apply gap-2 [--radius:0];
}
.sidebar nav [role=separator] {
.sidebar > nav [role=separator] {
@apply border-sidebar-border mx-2;
}
.sidebar nav > section > [role=group] {
.sidebar > nav > section > [role=group] {
@apply p-2;
}
.sidebar nav h3 {
.sidebar > nav h3 {
@apply text-sidebar-foreground/70 ring-sidebar-ring h-8 rounded-none px-3 text-xs font-semibold uppercase tracking-wider focus-visible:ring-2 [&>svg]:size-3.5 transition-[margin,opacity] duration-200 ease-linear;
}
.sidebar nav > section > [role=group] > ul {
.sidebar > nav > section > [role=group] > ul {
@apply gap-0.5;
}
.sidebar nav > section > [role=group] {
.sidebar > nav > section > [role=group] {
@apply text-sm;
}
.sidebar nav li > :is(a, button),
.sidebar nav li > details > summary {
.sidebar > nav li > :is(a, button),
.sidebar > nav li > details > summary {
@apply ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground gap-2 rounded-none px-3 py-2 text-sm focus-visible:ring-2 text-left focus-visible:outline-hidden data-[open]:hover:bg-sidebar-accent data-[open]:hover:text-sidebar-accent-foreground [&>svg]:shrink-0 [&>svg]:size-3.5;
}
.sidebar nav li > :is(a, button):is([aria-current=page], [data-active=true]),
.sidebar nav li > details > summary:is([aria-current=page], [data-active=true]) {
.sidebar > nav li > :is(a, button):is([aria-current=page], [data-active=true]),
.sidebar > nav li > details > summary:is([aria-current=page], [data-active=true]) {
@apply bg-sidebar-accent text-sidebar-accent-foreground font-medium;
}
.sidebar nav li > :is(a, button):not([data-variant]),
.sidebar nav li > details > summary:not([data-variant]),
.sidebar nav li > :is(a, button)[data-variant=default],
.sidebar nav li > details > summary[data-variant=default] {
.sidebar > nav li > :is(a, button):not([data-variant]),
.sidebar > nav li > details > summary:not([data-variant]),
.sidebar > nav li > :is(a, button)[data-variant=default],
.sidebar > nav li > details > summary[data-variant=default] {
@apply hover:bg-sidebar-accent hover:text-sidebar-accent-foreground;
}
.sidebar nav li > :is(a, button)[data-variant=outline],
.sidebar nav li > details > summary[data-variant=outline] {
.sidebar > nav li > :is(a, button)[data-variant=outline],
.sidebar > nav li > details > summary[data-variant=outline] {
@apply bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:shadow-[0_0_0_1px_var(--sidebar-accent)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground;
}
.sidebar nav li > :is(a, button):not([data-size]),
.sidebar nav li > details > summary:not([data-size]),
.sidebar nav li > :is(a, button)[data-size=default],
.sidebar nav li > details > summary[data-size=default] {
.sidebar > nav li > :is(a, button):not([data-size]),
.sidebar > nav li > details > summary:not([data-size]),
.sidebar > nav li > :is(a, button)[data-size=default],
.sidebar > nav li > details > summary[data-size=default] {
@apply h-9 text-sm;
}
.sidebar nav li > :is(a, button)[data-size=sm],
.sidebar nav li > details > summary[data-size=sm] {
.sidebar > nav li > :is(a, button)[data-size=sm],
.sidebar > nav li > details > summary[data-size=sm] {
@apply h-8 text-xs;
}
.sidebar nav li > :is(a, button)[data-size=lg],
.sidebar nav li > details > summary[data-size=lg] {
.sidebar > nav li > :is(a, button)[data-size=lg],
.sidebar > nav li > details > summary[data-size=lg] {
@apply h-14 px-3 text-sm;
}
.sidebar nav li > details > summary::after {
.sidebar > nav li > details > summary::after {
@apply size-3.5;
}
.sidebar nav li > details::details-content {
.sidebar > nav li > details::details-content {
@apply px-3.5;
}
.sidebar nav ul ul {
.sidebar > nav ul ul {
@apply border-sidebar-border translate-x-px gap-1 border-s px-2.5 py-0.5 rtl:-translate-x-px;
}
.sidebar nav ul ul > li > :is(a, button),
.sidebar nav ul ul > li > details > summary {
.sidebar > nav ul ul > li > :is(a, button),
.sidebar > nav ul ul > li > details > summary {
@apply text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground h-7 gap-2 rounded-none px-3 text-sm focus-visible:ring-2 [&>svg]:size-3.5;
}
/* Progress */
+38 -33
View File
@@ -403,7 +403,7 @@
/* Combobox */
.combobox > input[role='combobox'] {
@apply border-input dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-md border bg-transparent py-2 ps-2.5 pe-8 text-sm shadow-xs transition-[color,box-shadow] focus-visible:ring-3 aria-invalid:ring-3;
@apply border-input dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-md border bg-transparent py-2 ps-2.5 pe-8 text-base md:text-sm shadow-xs transition-[color,box-shadow] focus-visible:ring-3 aria-invalid:ring-3;
}
.combobox > svg,
@@ -528,6 +528,11 @@
@apply bg-popover text-popover-foreground ring-foreground/10 max-w-xs gap-6 rounded-xl p-6 ring-1 duration-100 sm:max-w-lg;
}
.alert-dialog:is([data-size='sm']) > *,
.alert-dialog > [data-size='sm'] {
@apply sm:max-w-xs;
}
.dialog > * {
@apply bg-popover text-popover-foreground ring-foreground/10 max-w-[calc(100%-2rem)] gap-6 rounded-xl p-6 text-sm ring-1 duration-100 sm:max-w-md;
}
@@ -1187,92 +1192,92 @@
}
/* Sidebar */
.sidebar nav {
.sidebar > nav {
@apply bg-sidebar text-sidebar-foreground;
}
.sidebar nav > header,
.sidebar nav > footer {
.sidebar > nav > header,
.sidebar > nav > footer {
@apply gap-2 p-2;
}
.sidebar nav > section {
.sidebar > nav > section {
@apply gap-2;
}
.sidebar nav [role=separator] {
.sidebar > nav [role=separator] {
@apply border-sidebar-border mx-2;
}
.sidebar nav > section > [role=group] {
.sidebar > nav > section > [role=group] {
@apply p-2;
}
.sidebar nav h3 {
.sidebar > nav h3 {
@apply text-sidebar-foreground/70 ring-sidebar-ring h-8 rounded-md px-2 text-xs font-medium focus-visible:ring-2 [&>svg]:size-4 transition-[margin,opacity] duration-200 ease-linear;
}
.sidebar nav > section > [role=group] > ul {
.sidebar > nav > section > [role=group] > ul {
@apply gap-1;
}
.sidebar nav > section > [role=group] {
.sidebar > nav > section > [role=group] {
@apply text-sm;
}
.sidebar nav li > :is(a, button),
.sidebar nav li > details > summary {
.sidebar > nav li > :is(a, button),
.sidebar > nav li > details > summary {
@apply ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground gap-2 rounded-md p-2 text-sm focus-visible:ring-2 text-left focus-visible:outline-hidden data-[open]:hover:bg-sidebar-accent data-[open]:hover:text-sidebar-accent-foreground [&>svg]:shrink-0 [&>svg]:size-4;
}
.sidebar nav li > :is(a, button):is([aria-current=page], [data-active=true]),
.sidebar nav li > details > summary:is([aria-current=page], [data-active=true]) {
.sidebar > nav li > :is(a, button):is([aria-current=page], [data-active=true]),
.sidebar > nav li > details > summary:is([aria-current=page], [data-active=true]) {
@apply bg-sidebar-accent text-sidebar-accent-foreground font-medium;
}
.sidebar nav li > :is(a, button):not([data-variant]),
.sidebar nav li > details > summary:not([data-variant]),
.sidebar nav li > :is(a, button)[data-variant=default],
.sidebar nav li > details > summary[data-variant=default] {
.sidebar > nav li > :is(a, button):not([data-variant]),
.sidebar > nav li > details > summary:not([data-variant]),
.sidebar > nav li > :is(a, button)[data-variant=default],
.sidebar > nav li > details > summary[data-variant=default] {
@apply hover:bg-sidebar-accent hover:text-sidebar-accent-foreground;
}
.sidebar nav li > :is(a, button)[data-variant=outline],
.sidebar nav li > details > summary[data-variant=outline] {
.sidebar > nav li > :is(a, button)[data-variant=outline],
.sidebar > nav li > details > summary[data-variant=outline] {
@apply bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:shadow-[0_0_0_1px_var(--sidebar-accent)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground;
}
.sidebar nav li > :is(a, button):not([data-size]),
.sidebar nav li > details > summary:not([data-size]),
.sidebar nav li > :is(a, button)[data-size=default],
.sidebar nav li > details > summary[data-size=default] {
.sidebar > nav li > :is(a, button):not([data-size]),
.sidebar > nav li > details > summary:not([data-size]),
.sidebar > nav li > :is(a, button)[data-size=default],
.sidebar > nav li > details > summary[data-size=default] {
@apply h-8 text-sm;
}
.sidebar nav li > :is(a, button)[data-size=sm],
.sidebar nav li > details > summary[data-size=sm] {
.sidebar > nav li > :is(a, button)[data-size=sm],
.sidebar > nav li > details > summary[data-size=sm] {
@apply h-7 text-xs;
}
.sidebar nav li > :is(a, button)[data-size=lg],
.sidebar nav li > details > summary[data-size=lg] {
.sidebar > nav li > :is(a, button)[data-size=lg],
.sidebar > nav li > details > summary[data-size=lg] {
@apply h-12 text-sm;
}
.sidebar nav li > details > summary::after {
.sidebar > nav li > details > summary::after {
@apply size-4;
}
.sidebar nav li > details::details-content {
.sidebar > nav li > details::details-content {
@apply px-3.5;
}
.sidebar nav ul ul {
.sidebar > nav ul ul {
@apply border-sidebar-border translate-x-px gap-1 border-s px-2.5 py-0.5 rtl:-translate-x-px;
}
.sidebar nav ul ul > li > :is(a, button),
.sidebar nav ul ul > li > details > summary {
.sidebar > nav ul ul > li > :is(a, button),
.sidebar > nav ul ul > li > details > summary {
@apply text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground h-7 gap-2 rounded-md px-2 text-sm focus-visible:ring-2 [&>svg]:size-4;
}
/* Progress */
+26 -6
View File
@@ -63,9 +63,24 @@
node.querySelectorAll('[data-basecoat-component]').forEach(destroyComponent);
};
const initAllComponents = () => {
const uniqueElements = (elements) => Array.from(new Set(elements));
const getComponentElements = (componentName, selector, force = false) => {
const elements = Array.from(document.querySelectorAll(selector));
if (force) {
elements.push(...document.querySelectorAll(`[data-basecoat-component="${componentName}"]`));
}
return uniqueElements(elements);
};
const initAllComponents = (options = {}) => {
const force = options.force === true;
Object.entries(componentRegistry).forEach(([name, { selector }]) => {
document.querySelectorAll(selector).forEach(element => initComponent(element, name));
getComponentElements(name, selector, force).forEach((element) => {
const wasComponent = element.dataset?.basecoatComponent === name;
if (force) destroyComponent(element);
if (wasComponent || element.matches(selector)) initComponent(element, name);
});
});
};
@@ -111,18 +126,23 @@
observer = null;
};
const initRegisteredComponent = (componentName) => {
const initRegisteredComponent = (componentName, options = {}) => {
const component = componentRegistry[componentName];
if (!component) {
console.warn(`Component '${componentName}' not found in registry`);
return;
}
document.querySelectorAll(component.selector).forEach(element => initComponent(element, componentName));
const force = options.force === true;
getComponentElements(componentName, component.selector, force).forEach((element) => {
const wasComponent = element.dataset?.basecoatComponent === componentName;
if (force) destroyComponent(element);
if (wasComponent || element.matches(component.selector)) initComponent(element, componentName);
});
};
const initAllRegisteredComponents = () => {
initAllComponents();
const initAllRegisteredComponents = (options = {}) => {
initAllComponents(options);
};
const setTheme = (mode) => {
+3 -3
View File
@@ -1,6 +1,6 @@
(() => {
const initSidebar = (sidebarComponent) => {
if (sidebarComponent.dataset.sidebarInitialized) return;
if (sidebarComponent.dataset.sidebarInitialized && typeof sidebarComponent.toggle === 'function') return;
const initialOpen = sidebarComponent.dataset.initialOpen !== 'false';
const initialMobileOpen = sidebarComponent.dataset.initialMobileOpen === 'true';
@@ -30,7 +30,7 @@
const handleClick = (event) => {
const target = event.target;
const nav = sidebarComponent.querySelector('nav');
const nav = sidebarComponent.querySelector(':scope > nav');
const isMobile = window.innerWidth < breakpoint;
if (isMobile && target.closest('a, button') && !target.closest('[data-keep-mobile-sidebar-open]')) {
@@ -60,6 +60,6 @@
};
if (window.basecoat) {
window.basecoat.register('sidebar', '.sidebar:not([data-sidebar-initialized])', initSidebar);
window.basecoat.register('sidebar', '.sidebar', initSidebar);
}
})();
+18 -7
View File
@@ -131,6 +131,13 @@
element.addEventListener('transitionend', () => element.remove(), { once: true });
}
const escapeHtml = (value) => String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
function createToast(config) {
const {
category = 'info',
@@ -140,18 +147,22 @@
cancel,
duration,
icon,
html: allowHtml = false,
} = config;
// Text is escaped unless `html: true`. `icon` is always raw markup (SVG).
const text = (value) => (allowHtml ? String(value) : escapeHtml(value));
const iconHtml = icon || (category && ICONS[category]) || '';
const titleHtml = title ? `<h2>${title}</h2>` : '';
const descriptionHtml = description ? `<p>${description}</p>` : '';
const titleHtml = title ? `<h2>${text(title)}</h2>` : '';
const descriptionHtml = description ? `<p>${text(description)}</p>` : '';
const actionHtml = action?.href
? `<a href="${action.href}" class="btn" data-toast-action>${action.label}</a>`
? `<a href="${escapeHtml(action.href)}" class="btn" data-toast-action>${text(action.label)}</a>`
: action?.onclick
? `<button type="button" class="btn" data-toast-action onclick="${action.onclick}">${action.label}</button>`
? `<button type="button" class="btn" data-toast-action onclick="${escapeHtml(action.onclick)}">${text(action.label)}</button>`
: '';
const cancelHtml = cancel
? `<button type="button" class="btn h-6 text-xs px-2.5 rounded-sm" data-variant="outline" data-toast-cancel onclick="${cancel?.onclick || ''}">${cancel.label}</button>`
? `<button type="button" class="btn h-6 text-xs px-2.5 rounded-sm" data-variant="outline" data-toast-cancel onclick="${escapeHtml(cancel?.onclick || '')}">${text(cancel.label)}</button>`
: '';
const footerHtml = actionHtml || cancelHtml ? `<footer>${actionHtml}${cancelHtml}</footer>` : '';
@@ -161,8 +172,8 @@
class="toast"
role="${category === 'error' ? 'alert' : 'status'}"
aria-atomic="true"
${category ? `data-category="${category}"` : ''}
${duration !== undefined ? `data-duration="${duration}"` : ''}
${category ? `data-category="${escapeHtml(category)}"` : ''}
${duration !== undefined ? `data-duration="${escapeHtml(duration)}"` : ''}
>
<div class="toast-content">
${iconHtml}
+1 -1
View File
@@ -98,7 +98,7 @@
class="command-dialog {{ dialog_attrs.class | default('') }}"
aria-label="Command menu"
{% if open %}open{% endif %}
onclick="if (event.target === this) this.close()"
onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()"
{% for key, value in dialog_attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
+1 -1
View File
@@ -53,7 +53,7 @@
{% 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 %}
{% if close_on_overlay_click %}onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()"{% endif %}
{% for key, value in dialog_attrs.items() %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
+1 -1
View File
@@ -92,7 +92,7 @@
class="command-dialog {{ dialog_attrs.class }}"
aria-label="Command menu"
{% if open %}open{% endif %}
onclick="if (event.target === this) this.close()"
onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()"
{% for key, value in dialog_attrs %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}
+1 -1
View File
@@ -48,7 +48,7 @@
{% 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 %}
{% if close_on_overlay_click %}onpointerdown="this.dataset.backdropPress = event.target === this" onclick="if (event.target === this && this.dataset.backdropPress === 'true') this.close()"{% endif %}
{% for key, value in dialog_attrs %}
{% if key != 'class' %}{{ key }}="{{ value }}"{% endif %}
{% endfor %}