Compare commits

...
Author SHA1 Message Date
grugbraindev d120e45be2 Lock page scroll while a modal dialog is open
Pure CSS: html:has(<dialog>:modal) gets overflow: hidden. A scroll
timeline sets scrollbar-gutter: stable only when the page can scroll,
so the layout doesn't shift.
2026-10-23 11:05:01 +08:00
Stefan Gründelandgrugbraindev 192f167c17 Fix tooltip ellipsis across style packs (#205)
Co-authored-by: grugbraindev <178141691+grugbraindev@users.noreply.github.com>
2026-10-23 09:52:36 +08:00
Stefan Gründel 64d219fdc6 Fix combobox Escape handling inside dialogs (#204) 2026-10-23 09:51:21 +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
Ronan Berder a9e1fe7b91 Prepare 1.0.0 release 2026-06-27 18:07:58 +08:00
Ronan Berder f254b92bd3 Checkpoint repo restructure 2026-06-27 09:01:35 +08:00
Ronan Berder c81900a58d Prepare 1.0.0 release 2026-06-27 08:55:48 +08:00
Ronan Berder 2be9606ce4 Align input group stacking with upstream 2026-06-26 18:13:18 +08:00
Ronan Berder be38645049 Fix input group dropdown stacking 2026-06-26 14:48:03 +08:00
Ronan Berder cc50da553f Use card surfaces for scroll area examples 2026-06-26 14:35:20 +08:00
Ronan Berder bf8f721662 Simplify select component selectors 2026-06-26 11:56:36 +08:00
Ronan Berder 7cd8a09de3 Narrow dark mode selectors 2026-06-25 18:15:35 +08:00
Ronan Berder 4942a79c95 Add border to command docs preview 2026-06-25 17:55:53 +08:00
Ronan Berder 3b49a0efc5 Fix rounded drawer side positioning 2026-06-25 17:22:57 +08:00
Ronan Berder 6c87a5cde8 Fix Sera command preview styling 2026-06-25 16:48:10 +08:00
Ronan Berder 77c5281f49 Fixing assets 2026-06-25 13:36:21 +08:00
Ronan Berder a0715d17ab Release beta 7 with Drawer component 2026-06-25 12:59:15 +08:00
Ronan Berder b3c5d16e28 Update ReallySimpleDocs to 1.0.1 2026-06-24 20:17:41 +08:00
Ronan Berder a8c066306b Update RSD docs integration 2026-06-24 19:07:23 +08:00
Ronan Berder bbdfe1d272 Disable HTMX boost on docs home link 2026-06-23 18:22:45 +08:00
Ronan Berder a17eaa20d3 Prepare 1.0.0-beta.6 2026-06-23 18:14:19 +08:00
Ronan Berder fbf3f47b75 Use published reallysimpledocs beta 2026-06-22 18:33:30 +08:00
Ronan Berder cb72d80558 Prepare 1.0.0-beta.5 2026-06-22 18:19:38 +08:00
Ronan Berder 9806d6af63 Prepare 1.0.0-beta.4 2026-06-20 18:18:11 +08:00
Ronan Berder 971b2e171b Prepare 1.0.0-beta.3 2026-06-20 13:58:42 +08:00
Ronan Berder 0ae97ae627 Expand chart examples and derived surfaces 2026-06-19 16:42:15 +08:00
Ronan Berder 1d33827162 Prepare 1.0 docs and style migration 2026-06-17 11:19:31 +08:00
Ronan Berder 815e3ffffe Prepare 1.0.0-beta.2 release 2026-06-14 14:34:41 +08:00
Ronan Berder b971bce153 Build Basecoat before docs 2026-06-13 22:53:10 +08:00
Ronan Berder 47efe50c5a Migrate docs to ReallySimpleDocs 2026-06-13 12:53:17 +08:00
Ronan Berder 349dac9cf1 Prepare beta release docs and exports 2026-06-13 11:55:08 +08:00
Ronan Berder 4973389fbe Prepare 1.0.0 beta release 2026-06-08 21:42:31 +08:00
Ronan Berder 4ccacc98a5 Remove temporary sidebar submenu 2026-06-06 18:51:39 +08:00
Ronan Berder 2e8daaef03 Fix sidebar nested item styling 2026-06-06 16:41:18 +08:00
Ronan Berder 1509cdadef Rename Cloudflare worker to basecoat 2026-06-04 15:45:13 +08:00
Ronan Berder 89b3942965 chore: bump packages to 1.0.0 2026-06-04 12:36:12 +08:00
Ronan Berder 0829ebe0bf Rename Wrangler service to basecoat 2026-06-04 12:33:01 +08:00
Ronan Berder 04ad999ce4 Regenerate Sera and Rhea docs styles 2026-06-04 12:25:46 +08:00
Ronan Berder 1e5011344f docs: tighten repo and agent guidance 2026-06-04 12:18:40 +08:00
Ronan Berder 1268f5a146 rename style pack source files 2026-06-03 23:20:23 +08:00
Ronan Berder c1cf01da45 prepare basecoat 1.0 migration 2026-06-03 23:08:41 +08:00
Ronan Berder 7e7d509e90 Migrate empty and item docs to Basecoat APIs 2026-06-03 18:59:21 +08:00
Ronan Berder 317c490e0c Align button styles across all eight variants 2026-06-01 15:10:35 +08:00
Ronan Berder a54d5fb4eb Bumping version to 0.3.11 2026-02-05 18:01:54 +08:00
Ronan Berder 8445f6d150 Removing current page code from sidebar.js 2026-02-05 17:57:07 +08:00
Ronan Berder 713c163fb8 Preparing 0.3.10 2026-01-13 19:33:59 +08:00
Ronan Berder a7f686e71b Fixing Jinja macro and removing button-group isolate 2026-01-13 16:37:14 +08:00
Ronan Berder 128f79c2e4 Fixing docs 2026-01-05 12:47:44 +08:00
Ronan Berder ee63071f87 Polishing 2026-01-05 12:15:43 +08:00
Ronan Berder def943d74e Moving to serialized array 2026-01-05 11:28:05 +08:00
Ronan Berder f43c20948b Forcing assets refresh on new version 2026-01-04 21:58:42 +08:00
Ronan Berder 3ec06205a0 Preparing 0.3.10-beta.1 2026-01-04 21:22:53 +08:00
Ronan Berder 06e7c1ec84 Refined multi-select 2026-01-04 19:21:07 +08:00
Ronan Berder 09f868d474 Stable multi-select implementation 2026-01-04 14:39:32 +08:00
Ronan Berder b9274702d3 Fixing text color in tooltip (see #97) 2026-01-03 14:24:08 +08:00
Ronan Berder 83c09c1197 Merge branch 'development' of github.com:hunvreus/basecoat into development 2026-01-03 14:21:35 +08:00
Ronan Berder 36bca0c6a7 Merge pull request #97 from PhilipdeRijk/fix/bg-tooltip
fix: change tooltip bg color to bg-foreground
2026-01-03 14:20:18 +08:00
Ronan Berder 167b98e573 Merge pull request #126 from tainguyenphat74/main
Correct typo
2025-12-30 18:41:15 +08:00
Ronan Berder 4035870578 Merge pull request #125 from wangbinyq/patch-1
fix links for select.njk documentation
2025-12-30 17:13:29 +08:00
Ronan Berder dea0602b03 Merge pull request #128 from hrbonz/wip/btn_groups_fix_typo
docs: fix small word mistake in button group
2025-12-30 17:12:55 +08:00
Stefan 'hr' Berder 19941561d3 docs: fix small word mistake in button group
The large button example was using "Small" "button" "group" list of
words. It makes more sense to use "Large" as the first word to stay
consistent with the other example.
2025-12-30 14:46:35 +07:00
tainguyenphat74 f708994df9 Correct typo 2025-12-28 00:16:38 +07:00
wangb 6fcbb31e30 Fix code preview rendering in select.njk 2025-12-24 15:07:51 +08:00
wangb 0dfc5e061c fix links for select.njk documentation 2025-12-24 15:05:06 +08:00
Ronan Berder c771c8256b Compiling CSS for site 2025-12-21 14:43:07 +08:00
Ronan Berder 2adb4c4667 Bumping up version unmber to 0.3.9 2025-12-21 14:39:44 +08:00
Ronan Berder cbb1119699 Addressing regression bug 2025-12-21 14:38:47 +08:00
Ronan Berder 88db2e84ce Bumping up version unmber to 0.3.8 2025-12-21 14:36:58 +08:00
Ronan Berder a7f6f717d1 Fix #112 and #61 2025-12-21 14:20:09 +08:00
Ronan Berder 91c3932fe7 Cleaning up 2025-12-18 09:00:05 +08:00
Ronan Berder f977492bf6 Removing FUNDING.yml 2025-12-18 08:57:48 +08:00
Ronan Berder 21ba8ecb9e Bumping version number 2025-12-10 15:00:14 +08:00
Ronan Berder c0bf373890 Fixing edge case for command and select with data-keywords filtering, updating macros for command and sidebar 2025-12-10 14:49:32 +08:00
Ronan Berder 77680f9e2b Bumping version number 2025-11-17 10:53:40 +08:00
Ronan Berder 2afa2ae02e Fixing and extending combobox and command 2025-11-17 10:45:39 +08:00
Ronan Berder 23585e3161 Fixing #107 2025-11-14 23:58:35 +08:00
Ronan Berder 7e3324edf2 Bumping up the version number 2025-11-14 19:51:01 +08:00
Ronan Berder cda08f3c95 Merge branch 'main' of github.com:hunvreus/basecoat 2025-11-14 19:38:29 +08:00
Ronan Berder 0b46e05a29 Draft llms.txt 2025-11-14 19:38:11 +08:00
Ronan Berder 7322220788 Merge pull request #106 from volfpeter/fix/add-command-to-combined-js-bundle
Fix: include command in combined JS bundle + fix command examples
2025-11-14 18:45:28 +08:00
Peter Volf 4c86ce9bfc fix: include command in combined JS bundle + fix command examples 2025-11-13 10:09:58 +01:00
Ronan Berder bff89833d4 Attempting to fix #84 2025-11-12 09:56:02 +08:00
Ronan Berder 770b12f82f 0.3.3 2025-11-05 11:40:56 +08:00
Ronan Berder b8ac469755 Merge branch 'development' 2025-11-05 11:37:34 +08:00
Ronan Berder 14abbdc013 Accepting theirs changes on docs/src/assets/styles.css to avoid conflicts from Tailwind 2025-11-05 11:37:28 +08:00
Ronan Berder f367ebac2a Finishing Field 2025-11-05 11:25:13 +08:00
Ronan Berder b8d8b1741c Fixing issues with field 2025-11-04 20:41:23 +08:00
Ronan Berder f82b1d79d4 Cleaning up Button Group 2025-11-04 12:46:00 +08:00
Ronan Berder eed1938d2f Adding Icons section 2025-11-03 14:14:56 +08:00
Ronan Berder e5aed697c8 Adding spinner 2025-11-03 13:51:40 +08:00
Ronan Berder 0c16664e4b Adding Input Group, cleaning up other new components 2025-11-03 11:51:39 +08:00
Ronan Berder ab44d6d48a Adding empty, field and item, working on input group 2025-11-02 11:56:40 +08:00
Ronan Berder 15473865b4 Cleaning up button group 2025-10-31 08:56:44 +08:00
Ronan Berder 6148716f68 Adding kbd and draft command 2025-10-30 19:09:21 +08:00
Ronan Berder 045d13cd49 Added kbd and command 2025-10-30 10:03:34 +07:00
Ronan Berder 2962e980eb Minor fixes 2025-10-29 10:37:49 +07:00
PhilipdeRijk 530224c587 fix: change tooltip bg color to bg-foreground 2025-09-26 11:53:09 +02:00
Ronan Berder 382b8f6fa2 Update popover.njk 2025-08-31 20:29:09 +08:00
Ronan Berder d644cb9efd Merge pull request #90 from dmjio/patch-1
Update to sidebar.njk
2025-08-21 22:59:31 +08:00
David M. Johnson 1c1a413114 Update sidebar.njk
alphabetize
2025-08-21 08:56:35 -05:00
Ronan Berder 1e1682df21 0.3.2 2025-08-18 17:12:47 +08:00
Ronan Berder d280e21aac Remove testing message 2025-08-18 10:01:17 +08:00
Ronan Berder a03e225bd7 Merge branch 'development' 2025-08-18 09:57:07 +08:00
Ronan Berder bd23745393 Solving merging conflict with #86 2025-08-18 09:57:00 +08:00
Ronan Berder 29600bd3ac Minor improvements to select 2025-08-18 09:52:09 +08:00
Ronan Berder 97a53f1a67 Merge pull request #59 from f15u/federico/fix-kitchen-sink
docs: fix label textarea
2025-08-18 09:39:30 +08:00
Ronan Berder ea4fd16b00 Merge pull request #68 from mtt-artis/patch-1
docs: typo
2025-08-18 09:02:23 +08:00
Ronan Berder c5db76d677 Merge pull request #86 from elanfranconi/elanfranconi/patch_closeToast_blur
Blur only occurs when the active element is inside the toast.
2025-08-18 08:48:50 +08:00
Elias Lanfranconi 246aa65860 Blur only occurs when the active element is inside the toast. 2025-08-10 14:09:05 +01:00
Ronan Berder 150c20fd67 Preventing change event from being triggered when initializing 2025-08-02 02:29:25 -06:00
Ronan Berder 81acdd18cc Fixing docs (typo and navigation) 2025-07-28 18:30:52 +08:00
Ronan Berder 16219ccae6 Improving registration and initialization of components, updating docs 2025-07-28 18:16:41 +08:00
Ronan Berder a2e9775a2c Fixing minor bugs and starting work on carousel 2025-07-13 10:39:13 +08:00
mtt 2a43d10da9 docs: typo 2025-07-12 15:54:10 +02:00
Federico Biccheddu 92ebe490b3 docs: fix label textarea 2025-07-01 23:18:14 +02:00
Ronan Berder 4a44656fd9 Bumping up version to 0.2.8 2025-06-30 19:48:12 +08:00
Ronan Berder 608e3b2197 Preventing change event from being dispatched if value hasn't changed 2025-06-30 19:46:59 +08:00
Ronan Berder a1a74c220f Adding sponsor link 2025-06-29 14:04:49 +08:00
Ronan Berder 55e3c78131 Adding contributing guidelines 2025-06-29 14:03:13 +08:00
Ronan Berder 113d34a842 Bumping version number to 0.2.7 after failed npm publish 2025-06-25 09:53:07 +08:00
Ronan Berder 972719f08f Fixing bug on hover for select 2025-06-25 09:49:01 +08:00
Ronan Berder d125153921 Bumping up package version 2025-06-25 09:40:59 +08:00
Ronan Berder 2389133e12 Handle mouse and keyboard navigation on select and dropdown 2025-06-25 09:27:29 +08:00
Ronan Berder 3f12f91765 Fixing form page 2025-06-24 22:18:37 +08:00
Ronan Berder ae4019a713 Adding theme switcher page to the docs 2025-06-24 21:14:19 +08:00
Ronan Berder 0d63543fa8 Bumping up the version number 2025-06-22 09:24:52 +08:00
Ronan Berder 14c3f85214 Fixing CSS errors 2025-06-22 09:21:22 +08:00
Ronan Berder 09bab0933e Fixing CSS errors 2025-06-22 09:15:20 +08:00
Ronan Berder 87730a1bfe Typos 2025-06-21 20:02:41 +08:00
Ronan Berder 79a61a2a2b Fixing typo 2025-06-21 17:35:55 +08:00
Ronan Berder 106914f3c4 Fixing typo 2025-06-21 15:29:19 +08:00
Ronan Berder cfa7dcbd54 Removing Alpine.js entirely 2025-06-20 08:29:29 +08:00
Ronan Berder 9c01b1db6d Removing mentions of Alpine.js (RIP) 2025-06-20 06:18:30 +08:00
Ronan Berder fca594d370 Updating docs 2025-06-19 21:38:32 +08:00
Ronan Berder 82ef9124e3 Bumping to 0.2.4 2025-06-19 20:27:26 +08:00
Ronan Berder 7b0200bca0 Fixing docs 2025-06-19 15:21:12 +08:00
Ronan Berder 850eda76d2 Moving to full 3 digits version number to avoid cache 2025-06-18 21:16:55 +08:00
295 changed files with 37208 additions and 18937 deletions
-46
View File
@@ -1,46 +0,0 @@
import eleventyLucideicons from "@grimlink/eleventy-plugin-lucide-icons";
import prettier from "prettier";
import Nunjucks from "nunjucks";
import fs from "fs";
import path from "path";
export default async function(eleventyConfig) {
eleventyConfig.addWatchTarget("src/nunjucks/");
eleventyConfig.setInputDirectory("./docs/src");
eleventyConfig.addPassthroughCopy({"docs/src/assets": "assets"});
eleventyConfig.addPassthroughCopy({ "src/js": "assets/js" });
eleventyConfig.addPlugin(eleventyLucideicons);
eleventyConfig.addShortcode("fetchCode", function(filePath) {
const absolutePath = path.resolve(process.cwd(), filePath);
try {
return fs.readFileSync(absolutePath, 'utf8');
} catch (error) {
console.error(`[Eleventy FetchCode Error] Failed to read: ${absolutePath}`, error);
return `<!-- Error fetching code for ${filePath} -->`;
}
});
eleventyConfig.addNunjucksAsyncFilter("prettyHtml", async (code, callback) => {
try {
const formattedCode = await prettier.format(code, {
parser: "html",
printWidth: 1000,
singleAttributePerLine: false,
bracketSameLine: true,
htmlWhitespaceSensitivity: "ignore"
});
callback(null, formattedCode);
} catch (err) {
console.error("Error formatting HTML with Prettier:", err);
callback(err, code);
}
});
let nunjucksEnvironment = new Nunjucks.Environment(
new Nunjucks.FileSystemLoader([
"src/nunjucks",
"docs/src/_includes"
]),
{ autoescape: true }
);
eleventyConfig.setLibrary("njk", nunjucksEnvironment);
}
-1
View File
@@ -1 +0,0 @@
github: [hunvreus]
+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"
+12 -5
View File
@@ -1,11 +1,18 @@
.*
!.gitignore
!.eleventy.js
!.gitattributes
!.github/
!.github/workflows/
!.github/workflows/*.yml
!.github/workflows/*.yaml
_site/
node_modules/
packages/cli/dist/
packages/css/dist/
# Logs
npm-debug.log*
npm-debug.log*
dist/
templates/
site/dist/
site/public/assets/js/
site/public/fragments/
+99
View File
@@ -0,0 +1,99 @@
# Agent Rules
## Communication
- Keep answers concise, technical, and to the point.
- Do not use filler or glazing openers.
- Keep explanations MECE: separate facts, decisions, risks, and open questions.
- If scope is partial, state exactly what was not included.
- Challenge incorrect assumptions with evidence from code, docs, or upstream sources.
## Project Intent
- Basecoat is a Tailwind CSS, vanilla HTML, CSS, and JavaScript implementation of shadcn/ui's visual language for non-React stacks.
- Basecoat maps upstream shadcn/ui concepts onto simpler semantic markup; it is not a React, Radix, Base UI, `cn-*`, or `data-slot` DOM port.
- Prefer the browser platform: native elements, semantic HTML, CSS state selectors, and small vanilla JS only when needed.
## Source Boundaries
- Do not hand-edit build outputs:
- `dist/**`
- `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
- Keep public component APIs small: usually one root class, semantic child elements, documented ARIA/data attributes only when needed, and minimal JS for behavior.
- Avoid required child classes when meaningful markup can identify children: `input`, `textarea`, `select`, `button`, `a`, `label`, `fieldset`, `legend`, `header`, `section`, `footer`, `svg`, `kbd`, `hr`.
- Add new child classes, wrappers, `data-*` attributes, macro arguments, or public selectors only as intentional API.
- Do not add roles for styling; ARIA must describe behavior or accessibility state.
- Prefer intentional divergence over awkward imitation when upstream markup does not map cleanly to Basecoat.
## Accessibility
- Start with the native element that matches the control.
- Use `<fieldset>` and `<legend>` for grouped form controls when appropriate.
- Use native `<dialog>` where possible.
- Keep composite-widget keyboard behavior predictable: skip disabled items, respect orientation, and handle Escape/Enter/Space according to role.
- Disabled custom items must be skipped by pointer and keyboard interactions.
- Use logical CSS utilities/properties for RTL; flip only icons whose meaning is directional.
## CSS
- Component CSS in `src/css/components/*.css` owns structure, layout primitives, accessibility selectors, and behavior hooks.
- Style-pack CSS in `src/css/styles/*.css` owns visual choices: color, radius, shadow/ring, typography, spacing, variants, and state visuals.
- Each style bundle must stand alone. Do not load Vega/default and then undo it in another style.
- Avoid defensive resets that only compensate for another style pack.
- Prefer Tailwind logical utilities (`ps`, `pe`, `ms`, `me`, `start`, `end`, `rounded-s`, `rounded-e`) over physical left/right utilities.
- Prefer `border` for lines and separators.
- Avoid `!important`; if unavoidable, keep the selector narrow and document why.
## 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.
- Emit custom events only for meaningful integration points.
## Upstream Migration
- Always check the upstream shadcn/ui repo before changing a migrated component: `https://github.com/shadcn-ui/ui`.
- Use a local copy of the upstream repo for migration work.
- Review these upstream repo paths:
- components: `apps/v4/registry/bases/base/ui/`
- docs: `apps/v4/content/docs/components/`
- examples: `apps/v4/registry/bases/base/examples/`
- styles: `apps/v4/registry/styles/style-{vega,nova,maia,lyra,mira,luma,sera,rhea}.css`
- Review Basecoat files in the same pass:
- 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/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`.
## Docs
- Docs are Basecoat API docs, not React docs.
- Update docs when supported markup, variants, states, JS behavior, templates, or macros change.
- Prefer concrete HTML examples over abstract composition trees.
- Do not include React-only APIs such as `asChild`.
- Do not add "Composition" sections unless they describe stable Basecoat markup.
- Do not add API Reference sections for native/CSS-only components unless there is a real Basecoat API.
- Structure component docs as: title and preview, `## Usage`, optional `### HTML structure` for stable markup requirements, optional `### JavaScript API` for components with Basecoat JS methods/events/options, then `## Examples`.
- Keep `Usage` focused on the minimal working snippet and important Basecoat/upstream differences; put variants, states, RTL, and richer patterns under `Examples`.
- Keep snippets minimal and aligned with current behavior.
- In HTML snippets, keep text-only elements on one line, such as `<p>Text</p>`; use multiline formatting when the element contains child elements or when line length would hurt readability.
- In HTML examples, keep icons as single-line `<svg ...>...</svg>` elements, keep simple text-only or inline-formatting elements on one line, and avoid blank separator lines between adjacent markup parts.
- Document intentional differences from upstream near the relevant example.
## Validation
- Run `npm run build` after source/package changes.
- Run `npm run docs:build` after docs or docs asset changes.
- Manually verify changed components in docs across light/dark mode, style packs, variants, RTL when supported, and relevant interaction states.
## Change Management
- Ask before removing intentional functionality or public API.
- Preserve backward compatibility when the repo already depends on it, especially package exports and default `basecoat.css` behavior.
- Keep generated outputs and docs consistent with source changes.
- Remove stale comments/docs during refactors.
- Keep commits focused and imperative.
- Keep release-visible changes in `CHANGELOG.md` under `## [Unreleased]`; put backlog or incomplete work in `TODO.md` or the migration tracker.
+191
View File
@@ -0,0 +1,191 @@
# Changelog
## [Unreleased]
### Fixed
- Prevented Escape from dismissing an enclosing dialog while closing an open Combobox popup.
- Fixed long Tooltip text clipping instead of showing an ellipsis across all style packs.
- Locked page scroll while a modal Dialog, Alert Dialog, Command Dialog or Drawer is open.
## [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
- Removed the CLI workspace from the repo. Template files now ship with `basecoat-css` under `templates/nunjucks` and `templates/jinja`; copy them from `node_modules/basecoat-css/templates/*` instead of installing `basecoat-cli`.
### 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
- Updated Scroll Area examples to use Card surfaces so framed examples inherit style-pack radius and border treatment.
## [1.0.0-beta.7] - 2026-06-25
### Added
- Added a beta Drawer component with native `<dialog>` markup, side placement, animated close behavior, backdrop and Escape handling, style-pack visuals, JavaScript entrypoints, and docs.
### Changed
- Updated the docs dependency to `reallysimpledocs@^1.0.2`.
- Marked Chart and Drawer as beta components in the docs navigation.
- Promoted the Chart docs warning from alpha to beta.
- Updated installation CDN examples for `basecoat-css@1.0.0-beta.7`.
### Fixed
- Aligned Drawer examples more closely with upstream shadcn/ui while avoiding Chart API coupling in Drawer docs.
## [1.0.0-beta.6] - 2026-06-23
### Changed
- Updated docs dependency to the published `reallysimpledocs@1.0.0-beta.5` package.
- Refined component documentation examples across the docs site, including Select, Table, Tabs, Switch, and Theme Switcher.
- Clarified CDN and npm installation guidance for default and named style bundles.
- Updated Table examples to avoid inline overlay menus inside scrollable table containers and documented the overflow limitation.
### Fixed
- Fixed dark-mode unchecked Switch thumb colors across all style packs to match upstream shadcn/ui behavior.
## [1.0.0-beta.5] - 2026-06-22
### Added
- Added a dedicated Accordion component with native `<details>` markup, single-item JavaScript behavior, `data-multiple`, disabled item handling, style-pack spacing, and docs.
- Added a dedicated Breadcrumb component with semantic `nav`/`ol` markup, style-pack visuals, collapsed and dropdown examples, and docs.
- Added documented Card action support with `.card-action`.
- Added Combobox clear button, popup trigger, and input-group support, with docs aligned to upstream examples.
### Fixed
- Fixed Combobox single selection reopening immediately after selection and filtering the reopened list to the selected value.
- Improved Combobox multiple chips and remove buttons to match style-pack button sizing more closely.
## [1.0.0-beta.4] - 2026-06-20
### Changed
- Renamed the compact scrollbar utility from `scrollbar-thin` to `scrollbar-sm`; `scrollbar-thin` remains available through the compatibility stylesheet.
## [1.0.0-beta.3] - 2026-06-20
### Breaking Changes
- Changed Button, Badge, Card, Avatar, and Alert visual APIs to use a canonical root class plus data attributes instead of composed visual classes. For example, use `class="btn" data-variant="outline"`, `class="badge" data-variant="secondary"`, `class="card" data-size="sm"`, `class="avatar" data-size="lg"`, and `class="alert" data-variant="destructive"`. Legacy aliases are available only through the optional compatibility stylesheet.
- Changed icon-only Button sizing from `data-icon="only"` plus optional `data-size` to upstream-aligned `data-size="icon|icon-xs|icon-sm|icon-lg"`.
### Added
- Added an opt-in `basecoat-css/compat` stylesheet for pre-1.0 default Basecoat class aliases.
- Added an optional Chart.js helper with `window.basecoat.chart()`, chart CSS, generated tooltips, generated legends, and docs.
- Added shared Alert action layout support for direct child `<footer>` action regions.
- Added dedicated Avatar and Avatar Group component styles and docs.
- Added dedicated Progress component styles and docs with label, controlled, and RTL examples.
## [1.0.0-beta.2] - 2026-06-14
### Breaking Changes
- Removed the `.form` convenience selector for Basecoat 1.0. Use explicit component classes (`label`, `input`, `textarea`, `select`) or compose fields with `.field` / `.fieldset`.
- Changed Combobox markup and behavior to an input-first structure. The visible input now filters options, the hidden input stores the submitted value, single select stores the selected value, and multiple select stores a JSON array.
- Removed Combobox `data-multiple`; use `aria-multiselectable="true"` on the Combobox listbox, matching Select.
- Changed Command markup to the migrated Basecoat structure: `.command-dialog` wraps `.command`, the search input lives in the command header, and items use role-based menu markup with `role="menuitem"`.
- Removed Combobox-specific search-header behavior from Select. Use the dedicated Combobox component for editable/filterable selection.
- Removed built-in document command events for Toast, Sidebar, and Theme. Use element methods instead: `toaster.toast(config)`, `sidebar.open()`, `sidebar.close()`, `sidebar.toggle()`, and `window.basecoat.theme.*`.
- Reworked style loading for style packs. Non-default styles are standalone bundles and should not be loaded on top of the default/Vega bundle.
### Added
- Added standalone style packs: Vega, Nova, Maia, Lyra, Mira, Luma, Sera, and Rhea.
- Added dedicated Empty component styles and docs.
- Added dedicated Item component styles and docs.
- Added dedicated Input Group component styles and docs.
- Added dedicated Spinner docs and examples using `animate-spin` and `size-4` patterns.
- Added style-specific package entrypoints such as `basecoat-css/nova` and styleless base entrypoints for custom themes.
- Added `window.basecoat.refresh(element)` as a generic dispatcher for components that expose `refresh()`.
- Added `refresh()` methods to Command, Select, Combobox, Dropdown Menu, and Tabs for dynamic child lists.
- Added method APIs for Sidebar, Toast, and Theme.
- Added `data-format="object"` to Select and Combobox for opt-in hidden input serialization as `{ value, label }` objects.
- Added `selected` details to Select and Combobox change events and JavaScript properties.
- Added `data-filter="manual"` to Command and Combobox for app-owned remote or local-search result filtering.
### Changed
- Changed `window.basecoat.init(name)` and `window.basecoat.initAll()` to initialize uninitialized components only, instead of forcing global reinitialization.
- Added internal destroy hooks for JavaScript components so removed component roots clean up event listeners and runtime state.
- Reworked component CSS so shared component files own structure and behavior hooks while style-pack files own visual treatment.
- Updated Button, Button Group, Input, Textarea, Select, Combobox, Command, Dialog, Dropdown Menu, Popover, Field, Tabs, Table, Card, Alert, Badge, Kbd, Label, Skeleton, Radio, Switch, Empty, Item, and Input Group toward current shadcn/ui styles.
- Split Native Select documentation from custom Select documentation.
- Updated docs to use Basecoat-specific HTML usage instead of React/shadcn composition APIs.
- Updated docs site styling, navigation, fonts, and style switcher for the 1.0 style system.
- Migrated the docs build to ReallySimpleDocs/Astro while keeping Basecoat's Nunjucks source examples as a pre-render step.
- Changed docs build scripts so local Basecoat package assets are generated before ReallySimpleDocs resolves `basecoat-css`.
### Removed
- Removed the old Form component page. Form layout should now be composed with Field, Fieldset, Input, Textarea, Select, Native Select, Checkbox, Radio, and Switch.
### Fixed
- Fixed destructive Alert descriptions/content using muted text instead of destructive text across all style packs.
### Migration Notes
To keep the previous `.form` wrapper behavior, define it in your own Tailwind CSS:
```css
.form label { @apply label; }
.form input { @apply input; }
.form textarea { @apply textarea; }
.form select { @apply select; }
```
To keep the previous document-event command APIs, add bridge listeners in your app:
```js
document.addEventListener('basecoat:toast', (event) => {
document.getElementById('toaster')?.toast(event.detail?.config || {});
});
document.addEventListener('basecoat:sidebar', (event) => {
const sidebar = document.getElementById(event.detail?.id || 'sidebar');
const action = event.detail?.action || 'toggle';
if (['open', 'close', 'toggle'].includes(action)) sidebar?.[action]();
});
document.addEventListener('basecoat:theme', (event) => {
const mode = event.detail?.mode;
mode ? window.basecoat.theme.set(mode) : window.basecoat.theme.toggle();
});
```
+42
View File
@@ -0,0 +1,42 @@
# Contributing to Basecoat
- Support development by sponsoring the author: https://github.com/sponsors/hunvreus
- 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.
- New features are worked in `feature/name-of-the-feature` branches.
- 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.
## Cloudflare Workers deploys
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`
- Preview version upload: `npm run workers:preview`
Workers Builds settings:
- Build command: `npm run docs:build`
- Deploy command: `npx wrangler deploy`
- Non-production branch deploy command: `npx wrangler versions upload`
- Production branch: `main`
- 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.
+104 -15
View File
@@ -1,29 +1,118 @@
# Basecoat
A collection of modern UI components built with Tailwind CSS that works with any web stack. Basecoat brings the magic of [shadcn/ui](https://ui.shadcn.com) to traditional web applications, no React required.
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](docs/src/assets/images/screenshot.png)
![screenshot](site/public/assets/images/screenshot.png)
## Features
- **Lightweight**: no runtime JS, just CSS and tiny Alpine.js scripts for the more interactive components.
- **Easy to use**: add classes like `btn` or `input` and you're done.
- **Framework-agnostic**: works with any backend or frontend stack.
- **Accessible**: components follow accessibility best practices.
- **Dark mode ready**: respects your Tailwind config.
- **Extendable**: tweak styles with Tailwind or CSS variables.
- **Themable**: fully compatible with shadcn/ui themes.
- **Readable**: no class soup, just clean markup.
- **Free and open source**: MIT licensed.
- Semantic HTML-first components.
- Tailwind CSS v4 source files and generated CSS bundles.
- Small vanilla JavaScript for components that need behavior.
- Standalone style packs: Vega, Nova, Maia, Lyra, Mira, Luma, Sera, and Rhea.
- Compatible with shadcn/ui themes.
- Dark mode.
- CDN, npm, and template usage paths.
- Nunjucks and Jinja templates.
## Documentation
Visit [basecoatui.com](https://basecoatui.com).
- Website: [basecoatui.com](https://basecoatui.com)
- Installation: [basecoatui.com/installation](https://basecoatui.com/installation)
- Customization: [basecoatui.com/customization](https://basecoatui.com/customization)
## Installation
## Install
Visit [basecoatui.com/installation](https://basecoatui.com/installation)
```bash
npm install basecoat-css
```
Use the default bundle:
```css
@import "tailwindcss";
@import "basecoat-css";
```
Use a specific style bundle:
```css
@import "tailwindcss";
@import "basecoat-css/nova";
```
Use the styleless base plus a custom style file:
```css
@import "tailwindcss";
@import "basecoat-css/base";
@import "./style-acme.css";
```
## Repository Layout
```text
.
├── dist/ Generated package output
├── scripts/ Build and generation scripts
├── 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
Basecoat separates structure from style:
- `src/css/base/base.css`: shared tokens and semantic utilities.
- `src/css/components/*.css`: component layout, structure, accessibility selectors, and behavior hooks.
- `src/css/styles/*.css`: style-pack visuals such as color, radius, shadow, typography, spacing, variants, and state styles.
Generated source entrypoints are committed for transparency and package imports:
- `src/css/basecoat.css`: default backward-compatible Vega bundle.
- `src/css/basecoat-base.css`: base plus components, no style pack.
- `src/css/basecoat-components.css`: component imports only.
- `src/css/basecoat-<style>.css`: base plus one style pack.
- `src/css/basecoat-<style>.cdn.css`: CDN-compatible wrapper.
These entrypoints are generated by `scripts/generate-css-entrypoints.js` and by the build scripts.
## JavaScript Lifecycle
- `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.
## Development
```bash
# Install dependencies.
npm i
npm --prefix site i
# Run the ReallySimpleDocs/Astro site.
npm run docs:dev
# Build package assets.
npm run build
# Build the static docs site.
npm run docs:build
```
## License
[MIT](/LICENSE.md)
[MIT](LICENSE.md)
+20
View File
@@ -0,0 +1,20 @@
## Now
- [ ] Add Drawer component
- [ ] Make utility components (e.g., Item) into proper components (i.e., .item)
- [ ] Split CSS into components
## Next
- [ ] Make Command load dynamic items (e.g. search results from remote endpoint)
- [ ] Make Combobox load dynamic items (e.g. search results from remote endpoint)
- [ ] Add Toggle component
- [ ] Add Carousel component
- [ ] Add Sheet component
- [ ] Add Separator component
- [ ] Adding Menubar component
## Later
- [ ] Add Calendar component (https://flatpickr.js.org/)
- [ ] Finish Chart component (https://www.chartjs.org/)
-92
View File
@@ -1,92 +0,0 @@
/* HTMX */
.htmx-indicator {
@apply invisible opacity-0 transition-discrete transition-all;
}
.htmx-indicator:not(.htmx-request) {
@apply hidden;
}
.htmx-request,
.htmx-request.htmx-indicator {
@apply visible opacity-100;
}
.content {
> h2 {
@apply mt-12 lg:mt-20 mb-6 scroll-m-22 text-2xl font-semibold tracking-tight first:mt-0;
}
> h3 {
@apply mt-8 mb-6 scroll-m-22 text-xl font-semibold tracking-tight;
}
> h4 {
@apply my-6 scroll-m-22 font-semibold tracking-tight;
}
.prose {
@apply my-6;
a {
@apply font-medium underline underline-offset-4;
}
code:not(pre > code) {
@apply relative rounded-md bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm;
}
blockquote {
@apply border-l-4 border-border pl-4;
}
ol {
@apply list-decimal ml-6;
}
ul {
@apply list-disc ml-6;
}
ol,
ul {
li {
@apply mt-2;
}
}
dl {
@apply border-b pb-3 my-6;
dt {
@apply border-t pt-3 mt-3 first:mt-0;
}
dd {
@apply mt-2;
}
dl {
@apply mt-4 border-b-0 pb-0 pl-8 my-0;
}
}
b,
strong {
@apply font-semibold;
}
> p,
> blockquote,
> ul,
> ol,
> dl,
> table,
> pre,
> p {
@apply my-6;
}
> *:first-child {
@apply !mt-0;
}
> *:last-child {
@apply !mb-0;
}
}
.step {
counter-increment: step;
&:before {
@apply absolute w-8 h-8 md:w-9 md:h-9 bg-muted rounded-full font-mono font-semibold text-center text-sm inline-flex items-center justify-center -indent-px border-4 mr-2 border-background;
@apply md:ml-[-50px] md:mt-[-4px];
content: counter(step);
}
> h3 {
@apply max-md:ml-10 min-h-8 flex items-center;
}
}
}
-151
View File
@@ -1,151 +0,0 @@
/*
Custom Highlight.js theme with CSS variables
derived from github.min.css and github-dark.min.css
*/
:root {
/* Base */
--hljs-bg: #fff;
--hljs-color: #24292e;
/* Tokens */
--hljs-keyword: #d73a49;
--hljs-title: #6f42c1;
--hljs-literal: #005cc5;
--hljs-string: #032f62;
--hljs-symbol: #e36209;
--hljs-comment: #6a737d;
--hljs-tag: #22863a;
--hljs-section: #005cc5;
--hljs-bullet: #735c0f;
/* Diffs */
--hljs-addition-color: #22863a;
--hljs-addition-bg: #f0fff4;
--hljs-deletion-color: #b31d28;
--hljs-deletion-bg: #ffeef0;
}
html.dark {
/* Base */
--hljs-bg: #0d1117;
--hljs-color: #c9d1d9;
/* Tokens */
--hljs-keyword: #ff7b72;
--hljs-title: #d2a8ff;
--hljs-literal: #79c0ff;
--hljs-string: #a5d6ff;
--hljs-symbol: #ffa657;
--hljs-comment: #8b949e;
--hljs-tag: #7ee787;
--hljs-section: #1f6feb;
--hljs-bullet: #f2cc60;
/* Diffs */
--hljs-addition-color: #aff5b4;
--hljs-addition-bg: #033a16;
--hljs-deletion-color: #ffdcd7;
--hljs-deletion-bg: #67060c;
}
/* --- Base --- */
pre code.hljs{
display:block;
overflow-x:auto;
padding:1em
}
code.hljs{
padding:3px 5px
}
/* --- Mapped styles --- */
.hljs {
color: var(--hljs-color);
background: var(--hljs-bg);
}
.hljs-doctag,
.hljs-keyword,
.hljs-meta .hljs-keyword,
.hljs-template-tag,
.hljs-template-variable,
.hljs-type,
.hljs-variable.language_ {
color: var(--hljs-keyword);
}
.hljs-title,
.hljs-title.class_,
.hljs-title.class_.inherited__,
.hljs-title.function_ {
color: var(--hljs-title);
}
.hljs-attr,
.hljs-attribute,
.hljs-literal,
.hljs-meta,
.hljs-number,
.hljs-operator,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-id,
.hljs-variable {
color: var(--hljs-literal);
}
.hljs-meta .hljs-string,
.hljs-regexp,
.hljs-string {
color: var(--hljs-string);
}
.hljs-built_in,
.hljs-symbol {
color: var(--hljs-symbol);
}
.hljs-code,
.hljs-comment,
.hljs-formula {
color: var(--hljs-comment);
}
.hljs-name,
.hljs-quote,
.hljs-selector-pseudo,
.hljs-selector-tag {
color: var(--hljs-tag);
}
.hljs-subst {
color: var(--hljs-color);
}
.hljs-section {
color: var(--hljs-section);
font-weight: 700;
}
.hljs-bullet {
color: var(--hljs-bullet);
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 700;
}
.hljs-addition {
color: var(--hljs-addition-color);
background-color: var(--hljs-addition-bg);
}
.hljs-deletion {
color: var(--hljs-deletion-color);
background-color: var(--hljs-deletion-bg);
}
-8
View File
@@ -1,8 +0,0 @@
@import "tailwindcss";
@import "../../src/css/basecoat.css";
@import "./custom.css";
@import "./highlight.css";
@import "./themes/claude.css";
@import "./themes/doom-64.css";
@import "./themes/supabase.css";
-95
View File
@@ -1,95 +0,0 @@
.theme-claude {
&:root {
--background: oklch(0.98 0.01 95.10);
--foreground: oklch(0.34 0.03 95.72);
--card: oklch(0.98 0.01 95.10);
--card-foreground: oklch(0.19 0.00 106.59);
--popover: oklch(1.00 0 0);
--popover-foreground: oklch(0.27 0.02 98.94);
--primary: oklch(0.62 0.14 39.04);
--primary-foreground: oklch(1.00 0 0);
--secondary: oklch(0.92 0.01 92.99);
--secondary-foreground: oklch(0.43 0.02 98.60);
--muted: oklch(0.93 0.02 90.24);
--muted-foreground: oklch(0.61 0.01 97.42);
--accent: oklch(0.92 0.01 92.99);
--accent-foreground: oklch(0.27 0.02 98.94);
--destructive: oklch(0.19 0.00 106.59);
--destructive-foreground: oklch(1.00 0 0);
--border: oklch(0.88 0.01 97.36);
--input: oklch(0.76 0.02 98.35);
--ring: oklch(0.59 0.17 253.06);
--chart-1: oklch(0.56 0.13 43.00);
--chart-2: oklch(0.69 0.16 290.41);
--chart-3: oklch(0.88 0.03 93.13);
--chart-4: oklch(0.88 0.04 298.18);
--chart-5: oklch(0.56 0.13 42.06);
--sidebar: oklch(0.97 0.01 98.88);
--sidebar-foreground: oklch(0.36 0.01 106.65);
--sidebar-primary: oklch(0.62 0.14 39.04);
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.92 0.01 92.99);
--sidebar-accent-foreground: oklch(0.33 0 0);
--sidebar-border: oklch(0.94 0 0);
--sidebar-ring: oklch(0.77 0 0);
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--radius: 0.5rem;
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
}
&.dark {
--background: oklch(0.27 0.00 106.64);
--foreground: oklch(0.81 0.01 93.01);
--card: oklch(0.27 0.00 106.64);
--card-foreground: oklch(0.98 0.01 95.10);
--popover: oklch(0.31 0.00 106.60);
--popover-foreground: oklch(0.92 0.00 106.48);
--primary: oklch(0.67 0.13 38.76);
--primary-foreground: oklch(1.00 0 0);
--secondary: oklch(0.98 0.01 95.10);
--secondary-foreground: oklch(0.31 0.00 106.60);
--muted: oklch(0.22 0.00 106.71);
--muted-foreground: oklch(0.77 0.02 99.07);
--accent: oklch(0.21 0.01 95.42);
--accent-foreground: oklch(0.97 0.01 98.88);
--destructive: oklch(0.64 0.21 25.33);
--destructive-foreground: oklch(1.00 0 0);
--border: oklch(0.36 0.01 106.89);
--input: oklch(0.43 0.01 100.22);
--ring: oklch(0.59 0.17 253.06);
--chart-1: oklch(0.56 0.13 43.00);
--chart-2: oklch(0.69 0.16 290.41);
--chart-3: oklch(0.21 0.01 95.42);
--chart-4: oklch(0.31 0.05 289.32);
--chart-5: oklch(0.56 0.13 42.06);
--sidebar: oklch(0.24 0.00 67.71);
--sidebar-foreground: oklch(0.81 0.01 93.01);
--sidebar-primary: oklch(0.33 0 0);
--sidebar-primary-foreground: oklch(0.99 0 0);
--sidebar-accent: oklch(0.17 0.00 106.62);
--sidebar-accent-foreground: oklch(0.81 0.01 93.01);
--sidebar-border: oklch(0.94 0 0);
--sidebar-ring: oklch(0.77 0 0);
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--radius: 0.5rem;
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
}
}
-95
View File
@@ -1,95 +0,0 @@
.theme-doom-64 {
&:root {
--background: oklch(0.85 0 0);
--foreground: oklch(0.24 0 0);
--card: oklch(0.76 0 0);
--card-foreground: oklch(0.24 0 0);
--popover: oklch(0.76 0 0);
--popover-foreground: oklch(0.24 0 0);
--primary: oklch(0.50 0.19 27.48);
--primary-foreground: oklch(1.00 0 0);
--secondary: oklch(0.50 0.09 126.19);
--secondary-foreground: oklch(1.00 0 0);
--muted: oklch(0.78 0 0);
--muted-foreground: oklch(0.41 0 0);
--accent: oklch(0.59 0.10 245.74);
--accent-foreground: oklch(1.00 0 0);
--destructive: oklch(0.71 0.20 46.46);
--destructive-foreground: oklch(0 0 0);
--border: oklch(0.43 0 0);
--input: oklch(0.43 0 0);
--ring: oklch(0.50 0.19 27.48);
--chart-1: oklch(0.50 0.19 27.48);
--chart-2: oklch(0.50 0.09 126.19);
--chart-3: oklch(0.59 0.10 245.74);
--chart-4: oklch(0.71 0.20 46.46);
--chart-5: oklch(0.57 0.04 40.43);
--sidebar: oklch(0.76 0 0);
--sidebar-foreground: oklch(0.24 0 0);
--sidebar-primary: oklch(0.50 0.19 27.48);
--sidebar-primary-foreground: oklch(1.00 0 0);
--sidebar-accent: oklch(0.59 0.10 245.74);
--sidebar-accent-foreground: oklch(1.00 0 0);
--sidebar-border: oklch(0.43 0 0);
--sidebar-ring: oklch(0.50 0.19 27.48);
--font-sans: "Oxanium", sans-serif;
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: "Source Code Pro", monospace;
--radius: 0px;
--shadow-2xs: 0px 2px 4px 0px hsl(0 0% 0% / 0.20);
--shadow-xs: 0px 2px 4px 0px hsl(0 0% 0% / 0.20);
--shadow-sm: 0px 2px 4px 0px hsl(0 0% 0% / 0.40), 0px 1px 2px -1px hsl(0 0% 0% / 0.40);
--shadow: 0px 2px 4px 0px hsl(0 0% 0% / 0.40), 0px 1px 2px -1px hsl(0 0% 0% / 0.40);
--shadow-md: 0px 2px 4px 0px hsl(0 0% 0% / 0.40), 0px 2px 4px -1px hsl(0 0% 0% / 0.40);
--shadow-lg: 0px 2px 4px 0px hsl(0 0% 0% / 0.40), 0px 4px 6px -1px hsl(0 0% 0% / 0.40);
--shadow-xl: 0px 2px 4px 0px hsl(0 0% 0% / 0.40), 0px 8px 10px -1px hsl(0 0% 0% / 0.40);
--shadow-2xl: 0px 2px 4px 0px hsl(0 0% 0% / 1.00);
}
&.dark {
--background: oklch(0.22 0 0);
--foreground: oklch(0.91 0 0);
--card: oklch(0.29 0 0);
--card-foreground: oklch(0.91 0 0);
--popover: oklch(0.29 0 0);
--popover-foreground: oklch(0.91 0 0);
--primary: oklch(0.61 0.21 27.03);
--primary-foreground: oklch(1.00 0 0);
--secondary: oklch(0.64 0.15 133.01);
--secondary-foreground: oklch(0 0 0);
--muted: oklch(0.26 0 0);
--muted-foreground: oklch(0.71 0 0);
--accent: oklch(0.75 0.12 244.75);
--accent-foreground: oklch(0 0 0);
--destructive: oklch(0.78 0.17 68.09);
--destructive-foreground: oklch(0 0 0);
--border: oklch(0.41 0 0);
--input: oklch(0.41 0 0);
--ring: oklch(0.61 0.21 27.03);
--chart-1: oklch(0.61 0.21 27.03);
--chart-2: oklch(0.64 0.15 133.01);
--chart-3: oklch(0.75 0.12 244.75);
--chart-4: oklch(0.78 0.17 68.09);
--chart-5: oklch(0.65 0.03 40.80);
--sidebar: oklch(0.19 0 0);
--sidebar-foreground: oklch(0.91 0 0);
--sidebar-primary: oklch(0.61 0.21 27.03);
--sidebar-primary-foreground: oklch(1.00 0 0);
--sidebar-accent: oklch(0.75 0.12 244.75);
--sidebar-accent-foreground: oklch(0 0 0);
--sidebar-border: oklch(0.41 0 0);
--sidebar-ring: oklch(0.61 0.21 27.03);
--font-sans: "Oxanium", sans-serif;
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: "Source Code Pro", monospace;
--radius: 0px;
--shadow-2xs: 0px 2px 5px 0px hsl(0 0% 0% / 0.30);
--shadow-xs: 0px 2px 5px 0px hsl(0 0% 0% / 0.30);
--shadow-sm: 0px 2px 5px 0px hsl(0 0% 0% / 0.60), 0px 1px 2px -1px hsl(0 0% 0% / 0.60);
--shadow: 0px 2px 5px 0px hsl(0 0% 0% / 0.60), 0px 1px 2px -1px hsl(0 0% 0% / 0.60);
--shadow-md: 0px 2px 5px 0px hsl(0 0% 0% / 0.60), 0px 2px 4px -1px hsl(0 0% 0% / 0.60);
--shadow-lg: 0px 2px 5px 0px hsl(0 0% 0% / 0.60), 0px 4px 6px -1px hsl(0 0% 0% / 0.60);
--shadow-xl: 0px 2px 5px 0px hsl(0 0% 0% / 0.60), 0px 8px 10px -1px hsl(0 0% 0% / 0.60);
--shadow-2xl: 0px 2px 5px 0px hsl(0 0% 0% / 1.50);
}
}
-100
View File
@@ -1,100 +0,0 @@
.theme-supabase {
&:root {
--background: oklch(0.99 0 0);
--foreground: oklch(0.20 0 0);
--card: oklch(0.99 0 0);
--card-foreground: oklch(0.20 0 0);
--popover: oklch(0.99 0 0);
--popover-foreground: oklch(0.44 0 0);
--primary: oklch(0.83 0.13 160.91);
--primary-foreground: oklch(0.26 0.01 166.46);
--secondary: oklch(0.99 0 0);
--secondary-foreground: oklch(0.20 0 0);
--muted: oklch(0.95 0 0);
--muted-foreground: oklch(0.24 0 0);
--accent: oklch(0.95 0 0);
--accent-foreground: oklch(0.24 0 0);
--destructive: oklch(0.55 0.19 32.73);
--destructive-foreground: oklch(0.99 0.00 17.21);
--border: oklch(0.90 0 0);
--input: oklch(0.90 0 0);
--ring: oklch(0.83 0.13 160.91);
--chart-1: oklch(0.83 0.13 160.91);
--chart-2: oklch(0.62 0.19 259.81);
--chart-3: oklch(0.61 0.22 292.72);
--chart-4: oklch(0.77 0.16 70.08);
--chart-5: oklch(0.70 0.15 162.48);
--sidebar: oklch(0.99 0 0);
--sidebar-foreground: oklch(0.55 0 0);
--sidebar-primary: oklch(0.83 0.13 160.91);
--sidebar-primary-foreground: oklch(0.26 0.01 166.46);
--sidebar-accent: oklch(0.95 0 0);
--sidebar-accent-foreground: oklch(0.24 0 0);
--sidebar-border: oklch(0.90 0 0);
--sidebar-ring: oklch(0.83 0.13 160.91);
--font-sans: Outfit, sans-serif;
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: monospace;
--radius: 0.5rem;
--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
--shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
--shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
--shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 2px 4px -1px hsl(0 0% 0% / 0.17);
--shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 4px 6px -1px hsl(0 0% 0% / 0.17);
--shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 8px 10px -1px hsl(0 0% 0% / 0.17);
--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.43);
--tracking-normal: 0.025em;
}
&.dark {
--background: oklch(0.18 0 0);
--foreground: oklch(0.93 0.01 255.51);
--card: oklch(0.20 0 0);
--card-foreground: oklch(0.93 0.01 255.51);
--popover: oklch(0.26 0 0);
--popover-foreground: oklch(0.73 0 0);
--primary: oklch(0.44 0.10 156.76);
--primary-foreground: oklch(0.92 0.01 167.16);
--secondary: oklch(0.26 0 0);
--secondary-foreground: oklch(0.99 0 0);
--muted: oklch(0.24 0 0);
--muted-foreground: oklch(0.71 0 0);
--accent: oklch(0.31 0 0);
--accent-foreground: oklch(0.99 0 0);
--destructive: oklch(0.31 0.09 29.79);
--destructive-foreground: oklch(0.94 0.00 34.31);
--border: oklch(0.28 0 0);
--input: oklch(0.28 0 0);
--ring: oklch(0.80 0.18 151.71);
--chart-1: oklch(0.80 0.18 151.71);
--chart-2: oklch(0.71 0.14 254.62);
--chart-3: oklch(0.71 0.16 293.54);
--chart-4: oklch(0.84 0.16 84.43);
--chart-5: oklch(0.78 0.13 181.91);
--sidebar: oklch(0.18 0 0);
--sidebar-foreground: oklch(0.63 0 0);
--sidebar-primary: oklch(0.44 0.10 156.76);
--sidebar-primary-foreground: oklch(0.92 0.01 167.16);
--sidebar-accent: oklch(0.31 0 0);
--sidebar-accent-foreground: oklch(0.99 0 0);
--sidebar-border: oklch(0.28 0 0);
--sidebar-ring: oklch(0.80 0.18 151.71);
--font-sans: Outfit, sans-serif;
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: monospace;
--radius: 0.5rem;
--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
--shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
--shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
--shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 2px 4px -1px hsl(0 0% 0% / 0.17);
--shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 4px 6px -1px hsl(0 0% 0% / 0.17);
--shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 8px 10px -1px hsl(0 0% 0% / 0.17);
--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.43);
}
body {
letter-spacing: var(--tracking-normal);
}
}
-11
View File
@@ -1,11 +0,0 @@
export default {
title: "Basecoat",
description: "A components library built with Tailwind CSS that works with any web stack.",
keywords: ["components", "component library", "component system", "UI", "UI kit", "shadcn", "shadcn/ui", "Tailwind CSS", "Tailwind", "CSS", "HTML", "Jinja", "Nunjucks", "Alpine.js"],
author: {
name: "Ronan Berder",
x: "@hunvreus"
},
url: process.env.ELEVENTY_ENV === 'development' ? "http://localhost:8080" : "https://basecoatui.com",
version: "0.2"
};
-107
View File
@@ -1,107 +0,0 @@
<!DOCTYPE html>
<html>
<head>
{# Dark mode #}
<script>
(() => {
try {
const stored = localStorage.getItem('themeMode');
if (stored ? stored === 'dark'
: matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark');
}
} catch (_) {}
const apply = dark => {
document.documentElement.classList.toggle('dark', dark);
try { localStorage.setItem('themeMode', dark ? 'dark' : 'light'); } catch (_) {}
};
document.addEventListener('basecoat:theme', (event) => {
const mode = event.detail?.mode;
apply(mode === 'dark' ? true
: mode === 'light' ? false
: !document.documentElement.classList.contains('dark'));
});
})();
</script>
{# Theme variant #}
<script>
(function() {
try {
const storedTheme = localStorage.getItem('themeVariant');
if (storedTheme) document.documentElement.classList.add(`theme-${storedTheme}`);
} catch (event) {
console.error('Could not apply theme variant from localStorage', event);
}
})();
</script>
{# Meta #}
<meta charset="utf-8">
<meta http-equiv="Content-Language" content="en">
<meta name="viewport" content="width=device-width, initial-scale=1">
{# Title & Description #}
{% set meta_title = title ~ ' | ' ~ site.title if title else site.title %}
{% set meta_description = description or site.description %}
<title>{{ meta_title }}</title>
<meta name="description" content="{{ meta_description }}"/>
<meta name="title" content="{{ meta_title }}">
{% if site.keywords %}<meta name="keywords" content="{{ site.keywords | join(',') }}" />{% endif %}
{# Favicon #}
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png">
{# Open Graph / Facebook #}
<meta property="og:type" content="website">
<meta property="og:url" content="{{ site.url }}{{ page.url }}">
<meta property="og:title" content="{{ meta_title }}">
<meta property="og:description" content="{{ meta_description }}">
<meta property="og:image" content="{{ site.url }}/assets/social-screenshot.png">
<meta property="og:site_name" content="{{ site.title }}">
<meta property="og:locale" content="en_US">
<meta property="og:author" content="{{ site.author.name }}">
{# X/Twitter #}
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:url" content="{{ site.url }}{{ page.url }}">
<meta name="twitter:title" content="{{ title if title else site.title }}">
<meta name="twitter:description" content="{{ meta_description }}">
<meta name="twitter:image" content="{{ site.url }}/assets/social-screenshot.png">
<meta name="twitter:creator" content="{{ site.author.x }}">
{# CSS #}
<link rel="stylesheet" href="/assets/styles.css">
{# JS #}
<script src="/assets/js/dropdown-menu.js" defer></script>
<script src="/assets/js/popover.js" defer></script>
<script src="/assets/js/select.js" defer></script>
<script src="/assets/js/sidebar.js" defer></script>
<script src="/assets/js/tabs.js" defer></script>
<script src="/assets/js/toast.js" defer></script>
{# Alpine.js #}
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
{# HTMX #}
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
{# Highlight.js #}
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/django.min.js"></script>
<script>
const highlight = () => {
if (!window.hljs) return;
document
.querySelectorAll('pre code:not([data-highlighted]), code.highlight:not([data-highlighted])')
.forEach(el => hljs.highlightElement(el));
};
if (!window._hljsInit) {
window._hljsInit = true;
document.addEventListener('DOMContentLoaded', highlight);
document.addEventListener('htmx:afterSwap', highlight);
}
</script>
{% if head %}{{ head | safe }}{% endif %}
</head>
<body>
{{ content | safe }}
{% from "toast.njk" import toaster %}
{{ toaster() }}
</body>
</html>
-15
View File
@@ -1,15 +0,0 @@
---
layout: layouts/base.njk
---
{% include "partials/sidebar.njk" %}
<main id="content">
<header class="bg-background sticky inset-x-0 top-0 isolate flex shrink-0 items-center gap-2 border-b z-10">
{% include "partials/header.njk" %}
</header>
<div class="p-4 md:p-6 xl:p-12">
{{ content | safe }}
</div>
</main>
-21
View File
@@ -1,21 +0,0 @@
---
layout: layouts/layout.njk
---
<main class="mx-auto relative flex w-full max-w-screen-lg gap-10">
<div class="mx-auto w-full flex-1 max-w-screen-md">
<header class="space-y-2">
<h1 class="text-2xl font-semibold tracking-tight sm:text-3xl xl:text-4xl">{{ title }}</h1>
<p class="text-muted-foreground text-[1.05rem] sm:text-base">{{ description }}</p>
</header>
<article class="pb-12 mt-8 content">
{{ content | safe }}
</article>
</div>
{% if toc %}
<div class="hidden text-sm xl:block w-full max-w-[300px]">
{% from "macros/toc.njk" import toc_nav %}
{{ toc_nav(toc) }}
</div>
{% endif %}
</main>
-28
View File
@@ -1,28 +0,0 @@
{% macro code_block(code, language="html", class="my-4") %}
<div
class="relative {{ class }}"
x-data="{
copied: false,
copy() {
const codeElement = $refs.code;
if (!codeElement) return;
navigator.clipboard.writeText(codeElement.textContent || '').then(() => {
this.copied = true;
setTimeout(() => { this.copied = false }, 2000);
}).catch(err => {
console.error('Failed to copy text: ', err);
});
}
}"
>
<pre x-ref="code" class="grid text-sm max-h-[650px] overflow-y-auto rounded-xl scrollbar"><code class="language-{{ language }} !bg-muted !p-3.5">{{- code | escape -}}</code></pre>
<button
@click="copy()"
class="btn-icon-ghost size-8 absolute right-2.5 top-2 text-muted-foreground hover:text-foreground"
>
{% lucide "clipboard", {"x-show": "!copied" } %}
{% lucide "check", {"x-show": "copied" } %}
</button>
</div>
{% endmacro %}
@@ -1,26 +0,0 @@
{% macro code_preview(id, code, class=None, default_code=false) %}
{% from "./code_block.njk" import code_block %}
{% set code_panel = code_block(code, class="") %}
{% set preview_panel %}
<div class="ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 relative rounded-md border">
<div class="preview flex min-h-[350px] w-full justify-center p-10 items-center">
<div {% if class %}class="{{ class }}"{% endif %}>
{{ code | safe }}
</div>
</div>
</div>
{% endset %}
<div class="relative my-6">
{% from "tabs.njk" import tabs %}
{% set tabsets = [
{ tab: "Preview", panel: preview_panel },
{ tab: "Code", panel: code_panel }
] %}
{{ tabs(
tabsets=tabsets,
default_tab_index=(2 if default_code else 1)
) }}
</div>
{% endmacro %}
-20
View File
@@ -1,20 +0,0 @@
{% macro toc_nav(items=[]) %}
<nav class="sticky top-22 space-y-2 [&_ul]:m-0 [&_ul]:list-none [&_ul_ul]:pl-4 [&_li]:mt-0 [&_li]:pt-2 [&_a]:inline-block [&_a]:no-underline [&_a]:transition-colors [&_a]:hover:text-foreground [&_a]:text-muted-foreground">
<h4 class="font-medium">On This Page</h4>
{% macro toc_list(items=[]) %}
{% if items and items.length %}
<ul>
{% for item in items %}
<li>
<a href="#{{ item.id }}">{{ item.label }}</a>
{% if item.children and item.children.length %}
{{ toc_list(item.children) }}
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
{% endmacro %}
{{ toc_list(items) }}
</nav>
{% endmacro %}
-64
View File
@@ -1,64 +0,0 @@
<div class="flex h-14 w-full items-center gap-2 px-4">
<button
type="button"
onclick="document.dispatchEvent(new CustomEvent('basecoat:sidebar'))"
aria-label="Toggle sidebar"
data-tooltip="Toggle sidebar"
data-side="bottom"
data-align="start"
class="btn-sm-icon-ghost mr-auto size-7 -ml-1.5"
>
{% lucide "panel-left" %}
</button>
<select
class="select h-8 leading-none"
id="theme-select"
>
<option value="">Default</option>
<option value="claude">Claude</option>
<option value="doom-64">Doom 64</option>
<option value="supabase">Supabase</option>
</select>
<script>
(() => {
const themeSelect = document.getElementById('theme-select');
const storedTheme = localStorage.getItem('themeVariant');
if (themeSelect && storedTheme) themeSelect.value = storedTheme;
themeSelect.addEventListener('change', () => {
const newTheme = themeSelect.value;
document.documentElement.classList.forEach(c => {
if (c.startsWith('theme-')) document.documentElement.classList.remove(c);
});
if (newTheme) {
document.documentElement.classList.add(`theme-${newTheme}`);
localStorage.setItem('themeVariant', newTheme);
} else {
localStorage.removeItem('themeVariant');
}
});
})();
</script>
<button
type="button"
aria-label="Toggle dark mode"
data-tooltip="Toggle dark mode"
data-side="bottom"
onclick="document.dispatchEvent(new CustomEvent('basecoat:theme'))"
class="btn-icon-outline size-8"
>
<span class="hidden dark:block">{% lucide "sun" %}</span>
<span class="block dark:hidden">{% lucide "moon" %}</span>
</button>
<a
href="https://github.com/hunvreus/basecoat"
class="btn-icon size-8"
target="_blank"
rel="noopener noreferrer"
data-tooltip="GitHub repository"
data-side="bottom"
data-align="end"
>
{% lucide "github" %}
</a>
</div>
@@ -1,97 +0,0 @@
<section id="accordion" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Accordion</h2>
<a href="/components/accordion" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="group grid w-full max-w-xl gap-4">
<section
x-init
@click="
const clickedSummary = $event.target.closest('summary');
if (!clickedSummary) return;
const clickedDetails = clickedSummary.closest('details');
if (!clickedDetails) return;
$el.querySelectorAll('details').forEach(detailsEl => {
if (detailsEl !== clickedDetails) {
detailsEl.removeAttribute('open');
}
});
"
>
<details class="group border-b last:border-b-0">
<summary class="w-full focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] transition-all outline-none rounded-md">
<h2 class="flex flex-1 items-start justify-between gap-4 py-4 text-left text-sm font-medium hover:underline ">
Is it accessible?
{% lucide "chevron-down", { "class": "text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200 group-open:rotate-180" } %}
</h2>
</summary>
<section class="pb-4">
<p class="text-sm">Yes. It adheres to the WAI-ARIA design pattern.</p>
</section>
</details>
<details class="group border-b last:border-b-0">
<summary class="w-full focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] transition-all outline-none rounded-md">
<h2 class="flex flex-1 items-start justify-between gap-4 py-4 text-left text-sm font-medium hover:underline ">
Is it styled?
{% lucide "chevron-down", { "class": "text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200 group-open:rotate-180" } %}
</h2>
</summary>
<section class="pb-4">
<p class="text-sm">Yes. It comes with default styles that matches the other components' aesthetic.</p>
</section>
</details>
<details class="group border-b last:border-b-0">
<summary class="w-full focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] transition-all outline-none rounded-md">
<h2 class="flex flex-1 items-start justify-between gap-4 py-4 text-left text-sm font-medium hover:underline ">
Is it animated?
{% lucide "chevron-down", { "class": "text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200 group-open:rotate-180" } %}
</h2>
</summary>
<section>
<p class="text-sm">Yes. It's animated by default, but you can disable it if you prefer.</p>
</section>
</details>
</section>
<section
x-init
@click="
const clickedSummary = $event.target.closest('summary');
if (!clickedSummary) return;
const clickedDetails = clickedSummary.closest('details');
if (!clickedDetails) return;
$el.querySelectorAll('details').forEach(detailsEl => {
if (detailsEl !== clickedDetails) {
detailsEl.removeAttribute('open');
}
});
"
>
<details class="group border-b last:border-b-0">
<summary class="w-full focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] transition-all outline-none rounded-md">
<h2 class="flex flex-1 items-start justify-between gap-4 py-4 text-left text-sm font-medium hover:underline ">
What are the key considerations when implementing a comprehensive enterprise-level authentication system?
{% lucide "chevron-down", { "class": "text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200 group-open:rotate-180" } %}
</h2>
</summary>
<section class="pb-4">
<p class="text-sm">Implementing a robust enterprise authentication system requires careful consideration of multiple factors. This includes secure password hashing and storage, multi-factor authentication (MFA) implementation, session management, OAuth2 and SSO integration, regular security audits, rate limiting to prevent brute force attacks, and maintaining detailed audit logs. Additionally, you'll need to consider scalability, performance impact, and compliance with relevant data protection regulations such as GDPR or HIPAA.</p>
</section>
</details>
<details class="group">
<summary class="w-full focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] transition-all outline-none rounded-md">
<h2 class="flex flex-1 items-start justify-between gap-4 py-4 text-left text-sm font-medium hover:underline ">
How does modern distributed system architecture handle eventual consistency and data synchronization across multiple regions?
{% lucide "chevron-down", { "class": "text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200 group-open:rotate-180" } %}
</h2>
</summary>
<section>
<p class="text-sm">Modern distributed systems employ various strategies to maintain data consistency across regions. This often involves using techniques like CRDT (Conflict-Free Replicated Data Types), vector clocks, and gossip protocols. Systems might implement event sourcing patterns, utilize message queues for asynchronous updates, and employ sophisticated conflict resolution strategies. Popular solutions like Amazon's DynamoDB and Google's Spanner demonstrate different approaches to solving these challenges, balancing between consistency, availability, and partition tolerance as described in the CAP theorem.</p>
</section>
</details>
</section>
</div>
</div>
</section>
@@ -1,26 +0,0 @@
{% from "dialog.njk" import dialog %}
<section id="alert-dialog" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Alert Dialog</h2>
<a href="/components/alert-dialog" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
{% set footer %}
<button class="btn-outline" @click="open = false">Cancel</button>
<button class="btn-primary" @click="open = false">Continue</button>
{% endset %}
{{ dialog(
id="alert-dialog-demo",
title="Are you absolutely sure?",
description="This action cannot be undone. This will permanently delete your account and remove your data from our servers.",
trigger="Open dialog",
trigger_attrs={"class": "btn-outline"},
footer=footer,
close_button=false,
close_on_overlay_click=false
) }}
</div>
</section>
@@ -1,72 +0,0 @@
<section id="alert" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Alert</h2>
<a href="/components/alert" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="grid max-w-xl items-start gap-4">
<div class="alert">
{% lucide "circle-check" %}
<h2>Success! Your changes have been saved</h2>
<section>This is an alert with icon, title and description.</section>
</div>
<div class="alert">
{% lucide "bookmark-check" %}
<section>This is an alert with icon, description and no title.</section>
</div>
<div class="alert">
<section>This one has a description only. No title. No icon.</section>
</div>
<div class="alert">
{% lucide "popcorn" %}
<h2>Let's try one with icon and title.</h2>
</div>
<div class="alert">
{% lucide "shield-alert" %}
<h2>This is a very long alert title that demonstrates how the component handles extended text content and potentially wraps across multiple lines</h2>
</div>
<div class="alert">
{% lucide "gift" %}
<section>This is a very long alert description that demonstrates how the component handles extended text content and potentially wraps across multiple lines</section>
</div>
<div class="alert">
{% lucide "circle-alert" %}
<h2>This is an extremely long alert title that spans multiple lines to demonstrate how the component handles very lengthy headings while maintaining readability and proper text wrapping behavior</h2>
<section>This is an equally long description that contains detailed information about the alert. It shows how the component can accommodate extensive content while preserving proper spacing, alignment, and readability across different screen sizes and viewport widths. This helps ensure the user experience remains consistent regardless of the content length.</section>
</div>
<div class="alert-destructive">
{% lucide "circle-alert" %}
<h2>Something went wrong!</h2>
<section>Your session has expired. Please log in again.</section>
</div>
<div class="alert-destructive">
{% lucide "circle-alert" %}
<h2>Something went wrong!</h2>
<section>
<p>Please verify your billing information and try again.</p>
<ul>
<li>Check your card details</li>
<li>Ensure sufficient funds</li>
<li>Verify billing address</li>
</ul>
</section>
</div>
<div class="alert border-amber-50 bg-amber-50 text-amber-900 dark:border-amber-950 dark:bg-amber-950 dark:text-amber-100">
{% lucide "circle-check" %}
<h2>Plot Twist: This Alert is Actually Amber!</h2>
<section>This one has custom colors for light and dark mode.</section>
</div>
</div>
</div>
</section>
@@ -1,37 +0,0 @@
<section id="avatar" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Avatar</h2>
<a href="/components/avatar" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-row flex-wrap items-center gap-4">
<img class="size-8 shrink-0 object-cover rounded-full" alt="@hunvreus" src="https://github.com/hunvreus.png">
<span class="size-8 shrink-0 bg-muted flex items-center justify-center rounded-full">CN</span>
<img class="size-12 shrink-0 object-cover rounded-full" alt="@hunvreus" src="https://github.com/hunvreus.png">
<img class="size-8 shrink-0 object-cover rounded-lg" alt="@hunvreus" src="https://github.com/hunvreus.png">
<div class="flex -space-x-2 [&_img]:ring-background [&_img]:ring-2 [&_img]:grayscale [&_img]:size-8 [&_img]:shrink-0 [&_img]:object-cover [&_img]:rounded-full">
<img alt="@hunvreus" src="https://github.com/hunvreus.png">
<img alt="@shadcn" src="https://github.com/shadcn.png">
<img alt="@adamwathan" src="https://github.com/adamwathan.png">
</div>
<div class="flex -space-x-2 [&_img]:ring-background [&_img]:ring-2 [&_img]:grayscale [&_img]:size-12 [&_img]:shrink-0 [&_img]:object-cover [&_img]:rounded-full">
<img alt="@hunvreus" src="https://github.com/hunvreus.png">
<img alt="@shadcn" src="https://github.com/shadcn.png">
<img alt="@adamwathan" src="https://github.com/adamwathan.png">
</div>
<div class="flex -space-x-2 hover:space-x-1 [&_img]:ring-background [&_img]:size-12 [&_img]:shrink-0 [&_img]:object-cover [&_img]:rounded-full [&_img]:ring-2 [&_img]:grayscale [&_img]:transition-all [&_img]:ease-in-out [&_img]:duration-300">
<img alt="@hunvreus" src="https://github.com/hunvreus.png">
<img alt="@shadcn" src="https://github.com/shadcn.png">
<img alt="@adamwathan" src="https://github.com/adamwathan.png">
</div>
</div>
</div>
</section>
@@ -1,47 +0,0 @@
<section id="badge" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Badge</h2>
<a href="/components/badge" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-col gap-2">
<div class="flex flex-wrap items-center gap-2 md:flex-row">
<span class="badge">Primary</span>
<span class="badge-secondary">Secondary</span>
<span class="badge-outline">Outline</span>
<span class="badge-destructive">Destructive</span>
<span class="badge-outline">
{% lucide "check" %}
Badge
</span>
<span class="badge-destructive">
{% lucide "circle-alert" %}
Alert
</span>
<span class="badge rounded-full min-w-5 px-1">8</span>
<span class="badge-destructive rounded-full min-w-5 px-1">99</span>
<span class="badge-outline rounded-full min-w-5 px-1 font-mono tabular-nums">20+</span>
</div>
<div class="flex flex-wrap items-center gap-2 md:flex-row">
<a href="#" class="badge">
Link
{% lucide "arrow-right" %}
</a>
<a href="#" class="badge-secondary">
Link
{% lucide "arrow-right" %}
</a>
<a href="#" class="badge-destructive">
Link
{% lucide "arrow-right" %}
</a>
<a href="#" class="badge-outline">
Link
{% lucide "arrow-right" %}
</a>
</div>
</div>
</div>
</section>
@@ -1,42 +0,0 @@
{% from "dropdown-menu.njk" import dropdown_menu %}
<section id="breadcrumb" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Breadcrumb</h2>
<a href="/components/breadcrumb" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<ol class="text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5">
<li class="inline-flex items-center gap-1.5">
<a href="#" class="hover:text-foreground transition-colors">Home</a>
</li>
<li>{% lucide "chevron-right", { "class": "size-3.5" } %}</li>
<li class="inline-flex items-center gap-1.5">
{% set trigger %}
{% lucide "ellipsis" %}
{% endset %}
{{ dropdown_menu(
id="demo-breadcrumb-menu",
trigger=trigger,
trigger_attrs={"class": "flex size-9 items-center justify-center h-4 w-4 hover:text-foreground cursor-pointer"},
popover_attrs={"class": "p-1"},
items=[
{ label: "Documentation" },
{ label: "Themes" },
{ label: "GitHub" }
]
) }}
</li>
<li>{% lucide "chevron-right", { "class": "size-3.5" } %}</li>
<li class="inline-flex items-center gap-1.5">
<a href="#" class="hover:text-foreground transition-colors">Components</a>
</li>
<li>{% lucide "chevron-right", { "class": "size-3.5" } %}</li>
<li class="inline-flex items-center gap-1.5">
<span class="text-foreground font-normal">Breadcrumb</span>
</li>
</ol>
</div>
</section>
@@ -1,101 +0,0 @@
<section id="button" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Button</h2>
<a href="/components/button" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-col gap-6">
<div class="flex flex-wrap items-center gap-2 md:flex-row">
<button class="btn-primary">Primary</button>
<button class="btn-outline">Outline</button>
<button class="btn-ghost">Ghost</button>
<button class="btn-destructive">
{% lucide "send" %}
Danger
</button>
<button class="btn-secondary">Secondary</button>
<button class="btn-link">Link</button>
<button class="btn-outline">
{% lucide "send" %}
Send
</button>
<button class="btn-outline">
Learn more
{% lucide "arrow-right" %}
</button>
<button class="btn-outline" disabled>
{% lucide "loader", { "class": "animate-spin" } %}
Loading
</button>
</div>
<div class="flex flex-wrap items-center gap-2 md:flex-row">
<button class="btn-sm-primary">Primary</button>
<button class="btn-sm-outline">Outline</button>
<button class="btn-sm-ghost">Ghost</button>
<button class="btn-sm-destructive">Danger</button>
<button class="btn-sm-secondary">Secondary</button>
<button class="btn-sm-link">Link</button>
<button class="btn-sm-outline">
{% lucide "send" %}
Send
</button>
<button class="btn-sm-outline">
Learn more
{% lucide "arrow-right" %}
</button>
<button class="btn-sm-outline" disabled>
{% lucide "loader", { "class": "animate-spin" } %}
Loading
</button>
</div>
<div class="flex flex-wrap items-center gap-2 md:flex-row">
<button class="btn-lg-primary">Primary</button>
<button class="btn-lg-outline">Outline</button>
<button class="btn-lg-ghost">Ghost</button>
<button class="btn-lg-destructive">
{% lucide "send" %}
Danger
</button>
<button class="btn-lg-secondary">Secondary</button>
<button class="btn-lg-link">Link</button>
<button class="btn-lg-outline">
{% lucide "send" %}
Send
</button>
<button class="btn-lg-outline">
Learn more
{% lucide "arrow-right" %}
</button>
<button class="btn-lg-outline" disabled>
{% lucide "loader", { "class": "animate-spin" } %}
Loading
</button>
</div>
<div class="flex flex-wrap items-center gap-2 md:flex-row">
<button class="btn-icon-primary">
{% lucide "download" %}
</button>
<button class="btn-icon-secondary">
{% lucide "upload" %}
</button>
<button class="btn-icon-outline">
{% lucide "arrow-right" %}
</button>
<button class="btn-icon-ghost">
{% lucide "ellipsis" %}
</button>
<button class="btn-icon-destructive">
{% lucide "trash-2" %}
</button>
<button class="btn-icon-outline" disabled>
{% lucide "loader", { "class": "animate-spin" } %}
</button>
</div>
</div>
</div>
</section>
@@ -1,147 +0,0 @@
<section id="card" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Card</h2>
<a href="/components/card" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-col items-start gap-4">
<div class="card w-full max-w-sm">
<header>
<h2>Login to your account</h2>
<p>Enter your details below to login to your account</p>
</header>
<section>
<form class="form grid gap-6">
<div class="grid gap-2">
<label for="demo-card-form-email">Email</label>
<input type="email" id="demo-card-form-email">
</div>
<div class="grid gap-2">
<div class="flex items-center gap-2">
<label for="demo-card-form-password">Password</label>
<a href="#" class="ml-auto inline-block text-sm underline-offset-4 hover:underline">Forgot your password?</a>
</div>
<input type="password" id="demo-card-form-password">
</div>
</form>
</section>
<footer class="flex flex-col items-center gap-2">
<button type="button" class="btn w-full">Login</button>
<button type="button" class="btn-outline w-full">Login with Google</button>
<p class="mt-4 text-center text-sm">Don't have an account? <a href="#" class="underline-offset-4 hover:underline">Sign up</a></p>
</footer>
</div>
<div class="card">
<header>
<h2>Meeting Notes</h2>
<p>Transcript from the meeting with the client.</p>
</header>
<section class="text-sm">
<p>Client requested dashboard redesign with focus on mobile responsiveness.</p>
<ol class="mt-4 flex list-decimal flex-col gap-2 pl-6">
<li>New analytics widgets for daily/weekly metrics</li>
<li>Simplified navigation menu</li>
<li>Dark mode support</li>
<li>Timeline: 6 weeks</li>
<li>Follow-up meeting scheduled for next Tuesday</li>
</ol>
</section>
<footer class="flex items-center">
<div class="flex -space-x-2 [&_img]:ring-background [&_img]:ring-2 [&_img]:grayscale [&_img]:size-8 [&_img]:shrink-0 [&_img]:object-cover [&_img]:rounded-full">
<img alt="@hunvreus" src="https://github.com/hunvreus.png">
<img alt="@shadcn" src="https://github.com/shadcn.png">
<img alt="@adamwathan" src="https://github.com/adamwathan.png">
</div>
</footer>
</div>
<div class="card">
<header>
<h2>Is this an image?</h2>
<p>This is a card with an image.</p>
</header>
<section class="px-0">
<img
alt="Photo by Drew Beamer"
loading="lazy"
width="500"
height="500"
class="aspect-video object-cover" style="color:transparent"
srcset="
https://images.unsplash.com/photo-1588345921523-c2dcdb7f1dcd?w=800&dpr=2&q=80&w=640&q=75 1x,
https://images.unsplash.com/photo-1588345921523-c2dcdb7f1dcd?w=800&dpr=2&q=80&w=1080&q=75 2x
"
src="https://images.unsplash.com/photo-1588345921523-c2dcdb7f1dcd?w=800&dpr=2&q=80&w=1080&q=75"
/>
</section>
<footer class="flex items-center gap-2">
<span class="badge-outline">
{% lucide "bed" %}
1
</span>
<span class="badge-outline">
{% lucide "bath" %}
2
</span>
<span class="badge-outline">
{% lucide "land-plot" %}
350m²
</span>
<span class="ml-auto font-medium tabular-nums">
$135,000
</span>
</footer>
</div>
<div class="flex w-full flex-wrap items-start gap-8 md:*:[.card]:basis-1/4">
<div class="card">
<section>Content Only</section>
</div>
<div class="card">
<header>
<h2>Header Only</h2>
<p>This is a card with a header and a description.</p>
</header>
</div>
<div class="card">
<header>
<h2>Header and Content</h2>
<p>This is a card with a header and a content.</p>
</header>
<section>Content only.</section>
</div>
<div class="card">
<footer>Footer Only</footer>
</div>
<div class="card">
<header>
<h2>Header + Footer</h2>
<p>This is a card with a header and a footer.</p>
</header>
<footer>Footer</footer>
</div>
<div class="card">
<section>Content</section>
<footer>Footer</footer>
</div>
<div class="card">
<header>
<h2>Header + Content + Footer</h2>
<p>This is a card with a header, content and footer.</p>
</header>
<section>Content</section>
<footer>Footer</footer>
</div>
</div>
</div>
</div>
</section>
@@ -1,34 +0,0 @@
<section id="checkbox" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Checkbox</h2>
<a href="/components/checkbox" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-col gap-6 max-w-lg">
<label class="label gap-3">
<input type="checkbox" class="input">
Accept terms and conditions
</label>
<div class="flex items-start gap-3">
<input type="checkbox" id="demo-checkbox-label-and-description" class="input">
<div class="grid gap-2">
<label for="demo-checkbox-label-and-description" class="label">Accept terms and conditions</label>
<p class="text-muted-foreground text-sm">By clicking this checkbox, you agree to the terms and conditions.</p>
</div>
</div>
<label class="label gap-3">
<input type="checkbox" class="input" disabled>
Enable notifications
</label>
<label class="flex items-start gap-3 border p-3 hover:bg-accent/50 rounded-lg has-[input[type='checkbox']:checked]:border-blue-600 has-[input[type='checkbox']:checked]:bg-blue-50 dark:has-[input[type='checkbox']:checked]:border-blue-900 dark:has-[input[type='checkbox']:checked]:bg-blue-950">
<input type="checkbox" class="input checked:bg-blue-600 checked:border-blue-600 dark:checked:bg-blue-700 dark:checked:border-blue-700 checked:after:bg-white" checked>
<div class="grid gap-2">
<h2 class="text-sm leading-none font-medium">Enable notifications</h2>
<p class="text-muted-foreground text-sm">You can enable or disable notifications at any time.</p>
</div>
</label>
</div>
</div>
</section>
@@ -1,83 +0,0 @@
{% from "select.njk" import select %}
<section id="combobox" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Combobox</h2>
<a href="/components/combobox" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-wrap items-start gap-4">
{% set options_frameworks = ["Next.js", "SvelteKit", "Nuxt.js", "Remix", "Astro"] %}
{% call select(
popover_attrs={"class": "w-48"},
listbox_attrs={"data-empty": "No framework found."},
is_combobox=true
) %}
{% for framework in options_frameworks %}
<div role="option" data-value="{{ framework }}">{{ framework }}</div>
{% endfor %}
{% endcall %}
{% set options_timezones = [
{
label: "Americas",
timezones: [
{ value: "America/New_York", label: "(GMT-5) New York" },
{ value: "America/Los_Angeles", label: "(GMT-8) Los Angeles" },
{ value: "America/Chicago", label: "(GMT-6) Chicago" },
{ value: "America/Toronto", label: "(GMT-5) Toronto" },
{ value: "America/Vancouver", label: "(GMT-8) Vancouver" },
{ value: "America/Sao_Paulo", label: "(GMT-3) São Paulo" }
]
},
{
label: "Europe",
timezones: [
{ value: "Europe/London", label: "(GMT+0) London" },
{ value: "Europe/Paris", label: "(GMT+1) Paris" },
{ value: "Europe/Berlin", label: "(GMT+1) Berlin" },
{ value: "Europe/Rome", label: "(GMT+1) Rome" },
{ value: "Europe/Madrid", label: "(GMT+1) Madrid" },
{ value: "Europe/Amsterdam", label: "(GMT+1) Amsterdam" }
]
},
{
label: "Asia/Pacific",
timezones: [
{ value: "Asia/Tokyo", label: "(GMT+9) Tokyo" },
{ value: "Asia/Shanghai", label: "(GMT+8) Shanghai" },
{ value: "Asia/Singapore", label: "(GMT+8) Singapore" },
{ value: "Asia/Dubai", label: "(GMT+4) Dubai" },
{ value: "Australia/Sydney", label: "(GMT+11) Sydney" },
{ value: "Asia/Seoul", label: "(GMT+9) Seoul" }
]
}
] %}
{% call select(
popover_attrs={"class": "w-72"},
listbox_attrs={"data-empty": "No timezone found."},
is_combobox=true
) %}
<div class="max-h-64 overflow-y-auto scrollbar">
{% for group in options_timezones %}
<div role="group" aria-labelledby="demo-combobox-timezones-group-{{ loop.index0 }}">
<span id="demo-combobox-timezones-group-{{ loop.index0 }}" role="heading">{{ group.label }}</span>
{% for timezone in group.timezones %}
<div role="option" data-value="{{ timezone.value}}">
{{ timezone.label }}
</div>
{% endfor %}
</div>
{% endfor %}
</div>
<hr role="separator">
<div role="option">
{% lucide "circle-plus" %}
Create timezone
</div>
{% endcall %}
</div>
</div>
</section>
@@ -1,65 +0,0 @@
{% from "dialog.njk" import dialog %}
<section id="dialog" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Dialog</h2>
<a href="/components/dialog" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-wrap items-center gap-4">
{% set footer %}
<button class="btn-outline" @click="open = false">Cancel</button>
<button class="btn" @click="open = false">Save changes</button>
{% endset %}
{% call dialog(
id="demo-dialog-edit-profile",
title="Edit profile",
description="Make changes to your profile here. Click save when you're done.",
trigger="Edit Profile",
trigger_attrs={"class": "btn-outline"},
dialog_attrs={"class": "w-full sm:max-w-[425px] max-h-[612px]"},
footer=footer
) %}
<form class="form grid gap-4">
<div class="grid gap-3">
<label for="demo-dialog-edit-profile-name">Name</label>
<input type="text" value="Pedro Duarte" id="demo-dialog-edit-profile-name" />
</div>
<div class="grid gap-3">
<label for="demo-dialog-edit-profile-username">Username</label>
<input type="text" value="@peduarte" id="demo-dialog-edit-profile-username" />
</div>
</form>
{% endcall %}
{% set footer %}
<button class="btn-outline" onclick="this.closest('dialog').close()">Close</button>
{% endset %}
{% call dialog(
id="dialog-example",
title="Scrollable Content",
description="This is a dialog with scrollable content.",
trigger="Scrollable Content",
trigger_attrs={"class": "btn-outline"},
dialog_attrs={"class": "w-full sm:max-w-[425px] max-h-[612px]"},
body_attrs={"class": "overflow-y-auto scrollbar"},
footer=footer
) %}
<div class="space-y-4 text-sm">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</div>
{% endcall %}
</div>
</div>
</section>
@@ -1,281 +0,0 @@
{% from "dropdown-menu.njk" import dropdown_menu %}
<section id="dropdown-menu" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Dropdown Menu</h2>
<a href="/components/dropdown-menu" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-wrap items-start gap-4">
{% call dropdown_menu(
id="dropdown-menu-default",
trigger="Open",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "min-w-56"}
) %}
<div role="group" aria-labelledby="account-options">
<div role="heading" id="account-options">My Account</div>
<div role="menuitem">
Profile
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
<div role="menuitem">
Billing
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
</div>
<div role="menuitem">
Settings
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
</div>
<div role="menuitem">
Keyboard shortcuts
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘K</span>
</div>
</div>
<hr role="separator">
<div role="menuitem">GitHub</div>
<div role="menuitem">Support</div>
<div role="menuitem" aria-disabled="true">API</div>
<hr role="separator">
<div role="menuitem">
Logout
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
{% endcall %}
{% call dropdown_menu(
id="dropdown-menu-checkboxes",
trigger="Checkboxes",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "min-w-56"}
) %}
<div role="group" aria-labelledby="account-options">
<div role="heading" id="account-options">Account Options</div>
<div role="menuitem">
{% lucide "user" %}
Profile
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
<div role="menuitem">
{% lucide "credit-card" %}
Billing
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
</div>
<div role="menuitem">
{% lucide "settings" %}
Settings
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
</div>
</div>
<hr role="separator">
<div role="group" aria-labelledby="appearance-options">
<div role="heading" id="appearance-options">Appearance</div>
<div
role="menuitemcheckbox"
aria-checked="true"
class="group"
x-data="{ checked: true }"
@click="checked = !checked"
:aria-checked="checked"
>
{% lucide "check", {
"class": "invisible group-aria-checked:visible",
"aria-hidden": "true",
"focusable": "false"
} %}
Status Bar
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
<div
role="menuitemcheckbox"
aria-checked="false"
class="group"
aria-disabled="true"
x-data="{ checked: false }"
@click="checked = !checked"
:aria-checked="checked"
>
{% lucide "check", {
"class": "invisible group-aria-checked:visible",
"aria-hidden": "true",
"focusable": "false"
} %}
Activity Bar
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
</div>
<div
role="menuitemcheckbox"
aria-checked="false"
class="group"
x-data="{ checked: false }"
@click="checked = !checked"
:aria-checked="checked"
>
{% lucide "check", {
"class": "invisible group-aria-checked:visible",
"aria-hidden": "true",
"focusable": "false"
} %}
Panel
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
</div>
</div>
<hr role="separator">
<div role="menuitem">
{% lucide "log-out" %}
Logout
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
{% endcall %}
{% set profile_menu %}
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<img alt="@hunvreus" src="https://github.com/hunvreus.png" class="size-8 shrink-0 rounded-full" alt="Ronan Berder"/>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">hunvreus</span>
<span class="text-muted-foreground truncate text-xs">hunvreus@example.com</span>
</div>
</div>
<hr role="separator">
<div role="menuitem">
{% lucide "star" %}
Upgrade to Pro
</div>
<hr role="separator">
<div role="menuitem">
{% lucide "user" %}
Account
</div>
<div role="menuitem">
{% lucide "credit-card" %}
Billing
</div>
<div role="menuitem">
{% lucide "bell" %}
Notifications
</div>
<hr role="separator">
<div role="menuitem">
{% lucide "log-out" %}
Signout
</div>
{% endset %}
{% call dropdown_menu(
trigger="Radio Group",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "min-w-56"}
) %}
<div role="group" aria-labelledby="position-options" x-data="{ checked: 'bottom' }">
<span id="position-options" role="heading">Panel Position</span>
<hr role="separator">
<div
role="menuitemradio"
aria-checked="false"
class="group"
@click="checked = 'top'"
:aria-checked="checked == 'top'"
>
<div class="size-4 flex items-center justify-center">
<div
class="size-2 rounded-full bg-foreground invisible group-aria-checked:visible"
aria-hidden="true",
focusable="false"
></div>
</div>
Status Bar
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
<div
role="menuitemradio"
aria-checked="false"
class="group"
@click="checked = 'bottom'"
:aria-checked="checked == 'bottom'"
>
<div class="size-4 flex items-center justify-center">
<div
class="size-2 rounded-full bg-foreground invisible group-aria-checked:visible"
aria-hidden="true",
focusable="false"
></div>
</div>
Activity Bar
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
</div>
<div
role="menuitemradio"
aria-checked="false"
class="group"
@click="checked = 'right'"
:aria-checked="checked == 'right'"
>
<div class="size-4 flex items-center justify-center">
<div
class="size-2 rounded-full bg-foreground invisible group-aria-checked:visible"
aria-hidden="true",
focusable="false"
></div>
</div>
Panel
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
</div>
</div>
{% endcall %}
{% set trigger %}
<img alt="@hunvreus" src="https://github.com/hunvreus.png" class="size-8 shrink-0 rounded-full" alt="Ronan Berder"/>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">hunvreus</span>
<span class="text-muted-foreground truncate text-xs">hunvreus@example.com</span>
</div>
{% lucide "chevrons-up-down", { "class": "text-muted-foreground ml-auto" } %}
{% endset %}
{% call dropdown_menu(
id="dropdown-checkboxes",
trigger=trigger,
trigger_attrs={"class": "btn-outline h-12 justify-start px-2 md:max-w-[200px]"}
) %}
{{ profile_menu | safe }}
{% endcall %}
{% set trigger %}
<img alt="@hunvreus" src="https://github.com/hunvreus.png" class="size-8 shrink-0 rounded-full" alt="Ronan Berder"/>
{% endset %}
{% call dropdown_menu(
id="dropdown-checkboxes",
trigger=trigger,
trigger_attrs={"class": "btn-icon-ghost rounded-full size-8"},
popover_attrs={"data-align": "end"}
) %}
{{ profile_menu | safe }}
{% endcall %}
{% set trigger %}
{% lucide "ellipsis" %}
{% endset %}
{% call dropdown_menu(
id="dropdown-checkboxes",
trigger=trigger,
trigger_attrs={"class": "btn-icon-ghost"},
popover_attrs={"class": "min-w-32"}
) %}
<div role="menuitem">
{% lucide "pencil" %}
Edit
</div>
<div role="menuitem">
{% lucide "share" %}
Share
</div>
<hr role="separator">
<div role="menuitem" class="text-destructive hover:bg-destructive/10 dark:hover:bg-destructive/20 focus:bg-destructive/10 dark:focus:bg-destructive/20 focus:text-destructive [&_svg]:!text-destructive">
{% lucide "trash-2" %}
Delete
</div>
{% endcall %}
</div>
</div>
</section>
@@ -1,106 +0,0 @@
<section id="form" class="w-full rounded-lg border scroll-mt-16">
<header class="border-b px-4 py-3">
<h2 class="text-sm font-medium">Form</h2>
</header>
<div class="p-4">
<form class="form grid w-full max-w-sm gap-6">
<div class="grid gap-2">
<label for="demo-form-text">Username</label>
<input type="text" id="demo-form-text" placeholder="hunvreus">
<p class="text-muted-foreground text-sm">This is your public display name.</p>
</div>
<div class="grid gap-2">
<label for="demo-form-select">Email</label>
<select id="demo-form-select">
<option value="bob@example.com">m@example.com</option>
<option value="alice@example.com">m@google.com</option>
<option value="john@example.com">m@support.com</option>
</select>
<p class="text-muted-foreground text-sm">You can manage email addresses in your email settings.</p>
</div>
<div class="grid gap-2">
<label for="demo-form-text">Bio</label>
<textarea id="demo-form-textarea" placeholder="I like to..." rows="3"></textarea>
<p class="text-muted-foreground text-sm">You can @mention other users and organizations.</p>
</div>
<div class="flex flex-col gap-3">
<label for="demo-form-radio">Notify me about...</label>
<fieldset id="demo-form-radio" class="grid gap-3">
<label class="font-normal"><input type="radio" name="demo-form-radio" value="1">All new messages</label>
<label class="font-normal"><input type="radio" name="demo-form-radio" value="2">Direct messages and mentions</label>
<label class="font-normal"><input type="radio" name="demo-form-radio" value="3" >Nothing</label>
</fieldset>
</div>
<div class="flex flex-row items-start gap-3 rounded-md border p-4 shadow-xs">
<input type="checkbox" id="demo-form-checkbox">
<div class="flex flex-col gap-1">
<label for="demo-form-checkbox" class="leading-snug">Use different settings for my mobile devices</label>
<p class="text-muted-foreground text-sm leading-snug">You can manage your mobile notifications in the mobile settings page.</p>
</div>
</div>
<div class="flex flex-col gap-4">
<header>
<label for="demo-form-checkboxes" class="text-base leading-normal">Sidebar</label>
<p class="text-muted-foreground text-sm">Select the items you want to display in the sidebar.</p>
</header>
<fieldset id="demo-form-checkboxes" class="flex flex-col gap-2">
<label class="font-normal leading-tight">
<input type="checkbox" name="demo-form-checkboxes" value="1" checked>
Recents
</label>
<label class="font-normal leading-tight">
<input type="checkbox" name="demo-form-checkboxes" value="2" checked>
Home
</label>
<label class="font-normal leading-tight">
<input type="checkbox" name="demo-form-checkboxes" value="3">
Applications
</label>
<label class="font-normal leading-tight">
<input type="checkbox" name="demo-form-checkboxes" value="4">
Desktop
</label>
<label class="font-normal leading-tight">
<input type="checkbox" name="demo-form-checkboxes" value="5">
Download
</label>
<label class="font-normal leading-tight">
<input type="checkbox" name="demo-form-checkboxes" value="6">
Documents
</label>
</fieldset>
</div>
<div class="grid gap-2">
<label for="demo-form-date">Date of birth</label>
<input type="date" id="demo-form-date">
<p class="text-muted-foreground text-sm">Your date of birth is used to calculate your age.</p>
</div>
<section class="grid gap-4">
<h3 class="text-lg font-medium">Email Notifications</h3>
<div class="gap-2 flex flex-row items-start justify-between rounded-lg border p-4 shadow-xs">
<div class="flex flex-col gap-0.5">
<label for="demo-form-switch" class="leading-normal">Marketing emails</label>
<p class="text-muted-foreground text-sm">Receive emails about new products, features, and more.</p>
</div>
<input type="checkbox" id="demo-form-switch" role="switch">
</div>
<div class="gap-2 flex flex-row items-start justify-between rounded-lg border p-4 shadow-xs">
<div class="flex flex-col gap-0.5 opacity-60">
<label for="demo-form-switch-disabled" class="leading-normal">Marketing emails</label>
<p class="text-muted-foreground text-sm">Receive emails about new products, features, and more.</p>
</div>
<input type="checkbox" id="demo-form-switch-disabled" role="switch" disabled>
</div>
</section>
<button type="submit" class="btn">Submit</button>
</form>
</div>
</section>
@@ -1,27 +0,0 @@
<section id="input" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Input</h2>
<a href="/components/input" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-col gap-y-4">
<input type="text" class="input" placeholder="Text">
<input type="text" class="input" disabled placeholder="Disabled">
<input type="text" class="input" aria-invalid="true" placeholder="Error">
<input type="email" class="input" placeholder="Email">
<input type="password" class="input" placeholder="Password">
<input type="number" class="input" placeholder="Number">
<input type="file" class="input">
<input type="tel" class="input" placeholder="Tel">
<input type="url" class="input" placeholder="URL">
<input type="search" class="input" placeholder="Search">
<input type="date" class="input">
<input type="datetime-local" class="input">
<input type="month" class="input">
<input type="week" class="input">
<input type="time" class="input">
</div>
</div>
</section>
@@ -1,30 +0,0 @@
<section id="label" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Label</h2>
<a href="/components/label" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-col gap-y-4">
<div class="grid w-full max-w-sm gap-6">
<div class="flex items-center gap-3">
<input type="checkbox" id="label-demo-terms" class="input">
<label for="label-demo-terms" class="label">Accept terms and conditions</label>
</div>
<div class="grid gap-3">
<label for="label-demo-text" class="label">Username</label>
<input type="text" id="label-demo-text" class="input" placeholder="Username">
</div>
<div class="grid gap-3">
<label for="label-demo-disabled" class="label">Disabled</label>
<input type="text" id="label-demo-disabled" class="peer input" placeholder="Disabled" disabled>
</div>
<div class="grid gap-3">
<label for="label-demo-textarea" class="label">Message</label>
<textarea id="label-demo-textarea" class="textarea" placeholder="Message"></textarea>
</div>
</div>
</div>
</div>
</section>
@@ -1,34 +0,0 @@
<section id="pagination" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Pagination</h2>
<a href="/components/pagination" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="inline-flex">
<nav role="navigation" aria-label="pagination" class="mx-auto flex w-full justify-center">
<ul class="flex flex-row items-center gap-1">
<li>
<a href="#" class="btn-ghost">{% lucide "chevron-left" %} Previous</a>
</li>
<li>
<a href="#" class="btn-ghost size-9">1</a>
</li>
<li>
<a href="#" class="btn-outline size-9">2</a>
</li>
<li>
<a href="#" class="btn-ghost size-9">3</a>
</li>
<li>
<a href="#" class="btn-icon-ghost">{% lucide "ellipsis" %}</a>
</li>
<li>
<a href="#" class="btn-ghost">Next {% lucide "chevron-right" %}</a>
</li>
</ul>
</nav>
</div>
</div>
</section>
@@ -1,43 +0,0 @@
{% from "popover.njk" import popover %}
<section id="popover" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Popover</h2>
<a href="/components/popover" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
{% call popover(
id="demo-popover",
trigger="Open popover",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "w-80"}
) %}
<div class="grid gap-4">
<header class="grid gap-1.5">
<h4 class="leading-none font-medium">Dimensions</h4>
<p class="text-muted-foreground text-sm">Set the dimensions for the layer.</p>
</header>
<form class="form grid gap-2">
<div class="grid grid-cols-3 items-center gap-4">
<label for="demo-popover-width">Width</label>
<input type="text" id="demo-popover-width" value="100%" class="col-span-2 h-8"/>
</div>
<div class="grid grid-cols-3 items-center gap-4">
<label for="demo-popover-max-width">Max. width</label>
<input type="text" id="demo-popover-max-width" value="300px" class="col-span-2 h-8"/>
</div>
<div class="grid grid-cols-3 items-center gap-4">
<label for="demo-popover-height">Height</label>
<input type="text" id="demo-popover-height" value="25px" class="col-span-2 h-8"/>
</div>
<div class="grid grid-cols-3 items-center gap-4">
<label for="demo-popover-max-height">Max. height</label>
<input type="text" id="demo-popover-max-height" value="none" class="col-span-2 h-8"/>
</div>
</form>
</div>
{% endcall %}
</div>
</section>
@@ -1,33 +0,0 @@
<section id="radio-group" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Radio Group</h2>
<a href="/components/radio-group" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-col gap-y-6">
<fieldset class="grid gap-3">
<label class="label"><input type="radio" name="demo-radio-group" value="default" class="input">Default</label>
<label class="label"><input type="radio" name="demo-radio-group" value="comfortable" class="input">Comfortable</label>
<label class="label"><input type="radio" name="demo-radio-group" value="compact" class="input">Compact</label>
</fieldset>
<fieldset class="grid gap-3 max-w-sm">
<label class="label gap-3 items-start hover:bg-accent/50 rounded-lg border p-4 has-[input[type='radio']:checked]:border-green-600 has-[input[type='radio']:checked]:bg-green-50 dark:has-[input[type='radio']:checked]:border-green-900 dark:has-[input[type='radio']:checked]:bg-green-950">
<input type="radio" name="demo-radio-group-styled" value="no" class="input checked:bg-green-600 checked:border-green-600 dark:checked:bg-input/30 checked:before:bg-background dark:checked:before:bg-primary">
<div class="grid gap-1 font-normal">
<h2 class="font-medium">Starter Plan</h2>
<p class="text-muted-foreground leading-snug">Perfect for small businesses getting started with our platform</p>
</div>
</label>
<label class="label gap-3 items-start hover:bg-accent/50 rounded-lg border p-4 has-[input[type='radio']:checked]:border-green-600 has-[input[type='radio']:checked]:bg-green-50 dark:has-[input[type='radio']:checked]:border-green-900 dark:has-[input[type='radio']:checked]:bg-green-950">
<input type="radio" name="demo-radio-group-styled" value="no" class="input checked:bg-green-600 checked:border-green-600 dark:checked:bg-input/30 checked:before:bg-background dark:checked:before:bg-primary">
<div class="grid gap-1 font-normal">
<h2 class="font-medium">Pro Plan</h2>
<p class="text-muted-foreground leading-snug">Advanced features for growing businesses with higher demands</p>
</div>
</label>
</fieldset>
</div>
</div>
</section>
@@ -1,99 +0,0 @@
{% from "select.njk" import select %}
<section id="select" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Select</h2>
<a href="/components/select" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4 gap">
<div class="flex flex-col gap-4">
<div class="flex flex-wrap items-center gap-2 md:flex-row">
<select class="select w-[180px]">
<optgroup label="Fruits">
<option>Apple</option>
<option>Banana</option>
<option>Blueberry</option>
</optgroup>
<optgroup label="Grapes">
<option>Pineapple</option>
</optgroup>
</select>
<select class="select w-[180px]" disabled>
<option>Disabled</option>
</select>
</div>
<div class="flex flex-wrap items-center gap-4">
<div class="flex flex-wrap items-center gap-2 md:flex-row">
{{ select(
id="select-default",
trigger_attrs={"class": "w-[180px]"},
selected="blueberry",
items=[
{
type: "group",
label: "Fruits",
items: [
{ type: "item", label: "Apple", value: "apple" },
{ type: "item", label: "Banana", value: "banana" },
{ type: "item", label: "Blueberry", value: "blueberry" }
]
},
{
type: "group",
label: "Grapes",
items: [
{ type: "item", label: "Pineapple", value: "pineapple" }
]
}
]
) }}
{% call select(
id="select-scrollbar",
trigger_attrs={"class": "w-[180px]"},
listbox_attrs={"class": "scrollbar overflow-y-auto max-h-64"}
) %}
{% for i in range(0, 99) %}
<div role="option" data-value="item-{{ i }}">Item {{ i }}</div>
{% endfor %}
{% endcall %}
{% call select(
id="select-disabled",
trigger_attrs={"disabled": "disabled", "class": "w-[180px]"}
) %}
<div role="option" data-value="disabled">Disabled</div>
{% endcall %}
{% call select(
id="select-with-icon",
name="chart-type",
trigger_attrs={'class': 'w-[180px]'}
) %}
<div type="button" role="option" data-value="bar">
<span class="flex items-center gap-2">
{% lucide "chart-bar", {"class": "text-muted-foreground"} %}
Bar
</span>
</div>
<div type="button" role="option" data-value="line">
<span class="flex items-center gap-2">
{% lucide "chart-line", {"class": "text-muted-foreground"} %}
Line
</span>
</div>
<div type="button" role="option" data-value="pie">
<span class="flex items-center gap-2">
{% lucide "chart-pie", {"class": "text-muted-foreground"} %}
Pie
</span>
</div>
{% endcall %}
</div>
</div>
</div>
</div>
</section>
@@ -1,40 +0,0 @@
<section id="skeleton" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Skeleton</h2>
<a href="/components/skeleton" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-col gap-4">
<div class="flex items-center gap-4">
<div class="bg-accent animate-pulse size-10 shrink-0 rounded-full"></div>
<div class="grid gap-2">
<div class="bg-accent animate-pulse rounded-md h-4 w-[150px]"></div>
<div class="bg-accent animate-pulse rounded-md h-4 w-[100px]"></div>
</div>
</div>
<div class="flex max-sm:flex-col gap-4 w-full">
<div class="card w-full @md:w-auto @md:min-w-sm">
<header>
<div class="bg-accent animate-pulse rounded-md h-4 w-2/3"></div>
<div class="bg-accent animate-pulse rounded-md h-4 w-1/2"></div>
</header>
<section>
<div class="bg-accent animate-pulse rounded-md aspect-square w-full"></div>
</section>
</div>
<div class="card w-full @md:w-auto @md:min-w-sm">
<header>
<div class="bg-accent animate-pulse rounded-md h-4 w-2/3"></div>
<div class="bg-accent animate-pulse rounded-md h-4 w-1/2"></div>
</header>
<section>
<div class="bg-accent animate-pulse rounded-md aspect-square w-full"></div>
</section>
</div>
</div>
</div>
</section>
@@ -1,36 +0,0 @@
<section id="slider" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Slider</h2>
<a href="/components/slider" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="max-w-sm">
<input
type="range"
class="input w-full"
min="0"
max="27"
value="12"
>
</div>
</div>
</section>
<script>
(() => {
const sliderEl = document.querySelector('input[type="range"].input');
if (!sliderEl) return;
const updateSlider = (el) => {
const min = parseFloat(el.min || 0);
const max = parseFloat(el.max || 100);
const value = parseFloat(el.value);
const percent = (max === min) ? 0 : ((value - min) / (max - min)) * 100;
el.style.setProperty('--slider-value', `${percent}%`);
};
updateSlider(sliderEl);
sliderEl.addEventListener('input', (event) => updateSlider(event.target));
})();
</script>
@@ -1,27 +0,0 @@
<section id="switch" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Switch</h2>
<a href="/components/switch" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="inline-flex flex-col gap-y-6">
<label class="label">
<input type="checkbox" name="switch" role="switch" class="input">
Airplane Mode
</label>
<label class="label">
<input type="checkbox" name="switch" role="switch" class="input checked:bg-blue-500 dark:checked:bg-blue-600" checked>
Bluetooth
</label>
<label class="label gap-6 leading-none border rounded-lg p-4 has-[input[type='checkbox']:checked]:border-blue-600">
<div class="grid gap-1">
<h2 class="font-medium text-sm">Share across devices</h2>
<p class="text-muted-foreground text-sm">Focus is shared across devices, and turns off when you leave the app.</p>
</div>
<input type="checkbox" name="switch" role="switch" class="input checked:bg-blue-500 dark:checked:bg-blue-600">
</label>
</div>
</div>
</section>
@@ -1,74 +0,0 @@
<section id="table" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Table</h2>
<a href="/components/table" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="relative w-full overflow-x-auto">
<table class="table">
<caption>A list of your recent invoices.</caption>
<thead>
<tr>
<th>Invoice</th>
<th>Status</th>
<th>Method</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<tr>
<td class="font-medium">INV001</td>
<td>Paid</td>
<td>Credit Card</td>
<td class="text-right">$250.00</td>
</tr>
<tr>
<td class="font-medium">INV002</td>
<td>Pending</td>
<td>PayPal</td>
<td class="text-right">$150.00</td>
</tr>
<tr>
<td class="font-medium">INV003</td>
<td>Unpaid</td>
<td>Bank Transfer</td>
<td class="text-right">$350.00</td>
</tr>
<tr>
<td class="font-medium">INV004</td>
<td>Paid</td>
<td>Paypal</td>
<td class="text-right">$450.00</td>
</tr>
<tr>
<td class="font-medium">INV005</td>
<td>Paid</td>
<td>Credit Card</td>
<td class="text-right">$550.00</td>
</tr>
<tr>
<td class="font-medium">INV006</td>
<td>Pending</td>
<td>Bank Transfer</td>
<td class="text-right">$200.00</td>
</tr>
<tr>
<td class="font-medium">INV007</td>
<td>Unpaid</td>
<td>Credit Card</td>
<td class="text-right">$300.00</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3">Total</td>
<td class="text-right">$2,500.00</td>
</tr>
</tfoot>
</table>
</div>
</div>
</section>
@@ -1,105 +0,0 @@
{% from "tabs.njk" import tabs %}
<section id="tabs" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Tabs</h2>
<a href="/components/tabs" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-col gap-6">
{% set account_panel %}
<div class="card">
<header>
<h2>Account</h2>
<p>Make changes to your account here. Click save when you're done.</p>
</header>
<section>
<form class="form grid gap-6">
<div class="grid gap-3">
<label for="demo-tabs-account-name">Name</label>
<input type="text" id="demo-tabs-account-name" value="Pedro Duarte" />
</div>
<div class="grid gap-3">
<label for="demo-tabs-account-username">Username</label>
<input type="text" id="demo-tabs-account-username" value="@peduarte" />
</div>
</form>
</section>
<footer>
<button type="button" class="btn">Save changes</button>
</footer>
</div>
{% endset %}
{% set password_panel %}
<div class="card">
<header>
<h2>Password</h2>
<p>Change your password here. After saving, you'll be logged out.</p>
</header>
<section>
<form class="form grid gap-6">
<div class="grid gap-3">
<label for="demo-tabs-password-current">Current password</label>
<input type="password" id="demo-tabs-password-current" />
</div>
<div class="grid gap-3">
<label for="demo-tabs-password-new">New password</label>
<input type="password" id="demo-tabs-password-new"/>
</div>
</form>
</section>
<footer>
<button type="button" class="btn">Save Password</button>
</footer>
</div>
{% endset %}
{% set tabsets = [
{ tab: "Account", panel: account_panel },
{ tab: "Password", panel: password_panel }
] %}
{{ tabs(
id='demo-tabs-with-panels',
tabsets=tabsets,
main_attrs={ "class": "max-w-[300px]" },
tablist_attrs={ "class": "w-full" }
) }}
{% set tabsets = [
{ tab: "Home" },
{ tab: "Settings" }
] %}
{{ tabs(
id='demo-tabs-without-panels',
tabsets=tabsets
) }}
{% set tabsets = [
{ tab: "Home" },
{ tab: "Disabled", tab_attrs: { "disabled": "disabled" } }
] %}
{{ tabs(
id='demo-tabs-disabled',
tabsets=tabsets
) }}
{% set label_preview %}
{% lucide 'app-window' %}
Preview
{% endset %}
{% set label_code %}
{% lucide 'code' %}
Code
{% endset %}
{% set tabsets = [
{ tab: label_preview },
{ tab: label_code }
] %}
{{ tabs(
id='demo-tabs-with-icons',
tabsets=tabsets
) }}
</div>
</div>
</section>
@@ -1,27 +0,0 @@
<section id="textarea" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Textarea</h2>
<a href="/components/textarea" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-col gap-y-10">
<textarea class="textarea" placeholder="Type your message here"></textarea>
<textarea class="textarea" placeholder="Type your message here" aria-invalid="true"></textarea>
<div class="grid gap-3">
<label for="textarea-demo-label" class="label">Label</label>
<textarea id="textarea-demo-label" class="textarea" placeholder="Type your message here"></textarea>
</div>
<div class="grid gap-3">
<label for="textarea-demo-label-and-description" class="label">With label and description</label>
<textarea id="textarea-demo-label-and-description" class="textarea" placeholder="Type your message here"></textarea>
<p class="text-muted-foreground text-sm">Type your message and press enter to send.</p>
</div>
<div class="grid gap-3">
<label for="textarea-demo-disabled" class="label">Disabled</label>
<textarea id="textarea-demo-disabled" class="textarea" placeholder="Type your message here" disabled></textarea>
</div>
</div>
</div>
</section>
@@ -1,25 +0,0 @@
<section id="toast" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Toast</h2>
<a href="/components/toast" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-wrap items-center gap-2">
<div class="flex flex-wrap items-center gap-2">
<button class="btn-outline" hx-trigger="click" hx-get="/fragments/toast/success" hx-target="#toaster" hx-swap="beforeend">
Success
</button>
<button class="btn-outline" hx-trigger="click" hx-get="/fragments/toast/error" hx-target="#toaster" hx-swap="beforeend">
Error
</button>
<button class="btn-outline" hx-trigger="click" hx-get="/fragments/toast/info" hx-target="#toaster" hx-swap="beforeend">
Info
</button>
<button class="btn-outline" hx-trigger="click" hx-get="/fragments/toast/warning" hx-target="#toaster" hx-swap="beforeend">
Warning
</button>
</div>
</div>
</section>
@@ -1,24 +0,0 @@
<section id="tooltip" class="w-full rounded-lg border scroll-mt-14">
<header class="border-b px-4 py-3 flex items-center justify-between">
<h2 class="text-sm font-medium">Tooltip</h2>
<a href="/components/tooltip" class="text-muted-foreground hover:text-foreground" data-tooltip="See documentation" data-side="left">
{% lucide "book-open", { "class": "size-4" } %}
</a>
</header>
<div class="p-4">
<div class="flex flex-wrap items-center gap-4">
<button type="button" class="btn-outline" data-tooltip="Top tooltip">
Top
</button>
<button type="button" class="btn-outline" data-tooltip="Right tooltip" data-side="right">
Right
</button>
<button type="button" class="btn-outline" data-tooltip="Bottom tooltip" data-side="bottom">
Bottom
</button>
<button type="button" class="btn-outline" data-tooltip="Left tooltip" data-side="left">
Left
</button>
</div>
</div>
</section>
-129
View File
@@ -1,129 +0,0 @@
{% from "sidebar.njk" import sidebar %}
{% from "popover.njk" import popover %}
{% set footer %}
{% set trigger %}
<img src="https://github.com/hunvreus.png" class="rounded-lg shrink-0 size-8">
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">Ronan Berder</span>
<span class="truncate text-xs">@hunvreus</span>
</div>
{% lucide "chevrons-up-down" %}
{% endset %}
{% call popover(
trigger=trigger,
trigger_attrs={
"class": "btn-ghost p-2 h-12 w-full flex items-center justify-start",
"data-keep-mobile-sidebar-open": ""
},
popover_attrs={
"data-side": "right",
"data-align": "end",
"class": "w-80",
"x-init": "
if (window.innerWidth < 768) {
$el.setAttribute('data-side', 'top');
$el.setAttribute('data-align', 'start');
$el.classList.add('w-[271px]');
}
"
}
) %}
<div class="grid gap-4">
<header class="grid gap-1.5">
<h2 class="font-semibold">I hope you like Basecoat...</h2>
<p class="text-muted-foreground text-sm">My name is <a href="https://ronanberder.com" target="_blank">Ronan</a> and I made this (and <a class="underline underline-offset-4" href="https://pagescms.org" target="_target">other things</a>). If you find it useful, please consider sponsoring me on GitHub or following me on X.</p>
</header>
<footer class="grid gap-2">
<a href="https://github.com/sponsors/hunvreus" class="btn-sm" target="_blank">Sponsor me on GitHub</a>
<a href="https://x.com/hunvreus" class="btn-sm-outline" target="_blank">Follow me on X</a>
</footer>
</div>
{% endcall %}
{% endset %}
{% set header %}
<a href="/" class="btn-ghost p-2 h-12 w-full justify-start">
<div class="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
<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>
</div>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">Basecoat</span>
<span class="truncate text-xs">v{{ pkg.version }}</span>
</div>
</a>
{# <div class="flex flex-col gap-2 px-2 text-sm">
<form class="form relative">
<label for="search" class="sr-only">Search</label>
<input type="text" placeholder="Search the docs..." class="h-8 w-full shadow-none pl-8">
{% lucide "search", { "class": "pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 opacity-50 select-none" } %}
</form>
</div> #}
{% endset %}
{% set link_attrs = {
"hx-boost": "true",
"hx-select": "#content",
"hx-target": "#content",
"hx-swap": "outerHTML"
} %}
{% set icon_home %}{% lucide "house" %}{% endset %}
{% set icon_info %}{% lucide "info" %}{% endset %}
{% set icon_square_terminal %}{% lucide "square-terminal" %}{% endset %}
{% set icon_layout_dashboard %}{% lucide "layout-dashboard" %}{% endset %}
{% set icon_github %}<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>{% endset %}
{% set icon_discord %}<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Discord</title><path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/></svg>{% endset %}
{% set icon_github %}<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>{% endset %}
{% set icon_discord %}<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Discord</title><path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/></svg>{% endset %}
{% set menu = [
{ type: "group", label: "Getting started", items: [
{ label: "Introduction", url: "/introduction", icon: icon_info, attrs: link_attrs },
{ label: "Installation", url: "/installation", icon: icon_square_terminal, attrs: link_attrs },
{ label: "Kitchen sink", url: "/kitchen-sink", icon: icon_layout_dashboard, attrs: link_attrs },
{ label: "GitHub", url: "https://github.com/hunvreus/basecoat", icon: icon_github, attrs: { "target": "_blank" } },
{ label: "Discord", url: "https://basecoatui.com/chat", icon: icon_discord, attrs: { "target": "_blank" } }
]},
{ type: 'group', label: 'Components', items: [
{ label: "Accordion", url: "/components/accordion", attrs: link_attrs },
{ label: "Alert", url: "/components/alert", attrs: link_attrs },
{ label: "Alert Dialog", url: "/components/alert-dialog", attrs: link_attrs },
{ label: "Avatar", url: "/components/avatar", attrs: link_attrs },
{ label: "Badge", url: "/components/badge", attrs: link_attrs },
{ label: "Breadcrumb", url: "/components/breadcrumb", attrs: link_attrs },
{ label: "Button", url: "/components/button", attrs: link_attrs },
{ label: "Card", url: "/components/card", attrs: link_attrs },
{ label: "Checkbox", url: "/components/checkbox", attrs: link_attrs },
{ label: "Combobox", url: "/components/combobox", attrs: link_attrs },
{ label: "Dialog", url: "/components/dialog", attrs: link_attrs },
{ label: "Dropdown Menu", url: "/components/dropdown-menu", attrs: link_attrs },
{ label: "Form", url: "/components/form", attrs: link_attrs },
{ label: "Input", url: "/components/input", attrs: link_attrs },
{ label: "Label", url: "/components/label", attrs: link_attrs },
{ label: "Pagination", url: "/components/pagination", attrs: link_attrs },
{ label: "Popover", url: "/components/popover", attrs: link_attrs },
{ label: "Radio Group", url: "/components/radio-group", attrs: link_attrs },
{ label: "Select", url: "/components/select", attrs: link_attrs },
{ label: "Skeleton", url: "/components/skeleton", attrs: link_attrs },
{ label: "Sidebar", url: "/components/sidebar", attrs: link_attrs },
{ label: "Slider", url: "/components/slider", attrs: link_attrs },
{ label: "Switch", url: "/components/switch", attrs: link_attrs },
{ label: "Table", url: "/components/table", attrs: link_attrs },
{ label: "Tabs", url: "/components/tabs", attrs: link_attrs },
{ label: "Textarea", url: "/components/textarea", attrs: link_attrs },
{ label: "Toast", url: "/components/toast", attrs: link_attrs },
{ label: "Tooltip", url: "/components/tooltip", attrs: link_attrs }
]}
] %}
{{ sidebar(
id="sidebar",
label="Sidebar navigation",
header=header,
footer=footer,
content_attrs={ "class": "scrollbar" },
menu=menu
) }}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because it is too large Load Diff
-70
View File
@@ -1,70 +0,0 @@
---
layout: layouts/page.njk
title: Accordion
description: A vertically stacked set of interactive headings that each reveal a section of content.
toc:
- label: Usage
id: usage
---
{% from "macros/code_preview.njk" import code_preview %}
<div class="alert mb-6">
{% lucide "circle-alert" %}
<h2>There is no dedicated accordion component in Basecoat.</h2>
</div>
{% set code %}<section
x-init
@click="
const clickedSummary = $event.target.closest('summary');
if (!clickedSummary) return;
const clickedDetails = clickedSummary.closest('details');
if (!clickedDetails) return;
$el.querySelectorAll('details').forEach(detailsEl => {
if (detailsEl !== clickedDetails) {
detailsEl.removeAttribute('open');
}
});
"
>
<details class="group border-b last:border-b-0 ">
<summary class="w-full focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] transition-all outline-none rounded-md">
<h2 class="flex flex-1 items-start justify-between gap-4 py-4 text-left text-sm font-medium hover:underline ">
Is it accessible?
{% lucide "chevron-down", { "class": "text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200 group-open:rotate-180" } %}
</h2>
</summary>
<section class="pb-4">
<p class="text-sm">Yes. It adheres to the WAI-ARIA design pattern.</p>
</section>
</details>
<details class="group border-b last:border-b-0">
<summary class="w-full focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] transition-all outline-none rounded-md">
<h2 class="flex flex-1 items-start justify-between gap-4 py-4 text-left text-sm font-medium hover:underline ">
Is it styled?
{% lucide "chevron-down", { "class": "text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200 group-open:rotate-180" } %}
</h2>
</summary>
<section class="pb-4">
<p class="text-sm">Yes. It comes with default styles that matches the other components' aesthetic.</p>
</section>
</details>
<details class="group border-b last:border-b-0">
<summary class="w-full focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] transition-all outline-none rounded-md">
<h2 class="flex flex-1 items-start justify-between gap-4 py-4 text-left text-sm font-medium hover:underline ">
Is it animated?
{% lucide "chevron-down", { "class": "text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200 group-open:rotate-180" } %}
</h2>
</summary>
<section>
<p class="text-sm whitespace-pre-wrap">Yes. It's animated by default, but you can disable it if you prefer.</p>
</section>
</details>
</section>
{% endset %}
{{ code_preview("accordion", code | prettyHtml, "w-full max-w-md") }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<p class="prose">Basecoat animates <code>&lt;details&gt;</code> elements by default. The example above uses Alpine.js to handle the open/close state along with some Tailwind CSS classes.</p>
-119
View File
@@ -1,119 +0,0 @@
---
layout: layouts/page.njk
title: Alert Dialog
description: A modal dialog that interrupts the user with important content and expects a response.
toc:
- label: Usage
id: usage
children:
- label: HTML
id: usage-html
- label: Jinja and Nunjucks
id: usage-macro
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% from "dialog.njk" import dialog %}
{% set code_html %}
{% set footer %}
<button class="btn-outline" onclick="document.getElementById('alert-dialog').close()">Cancel</button>
<button class="btn-primary" onclick="document.getElementById('alert-dialog').close()">Continue</button>
{% endset %}
{{ dialog(
id="alert-dialog",
title="Are you absolutely sure?",
description="This action cannot be undone. This will permanently delete your account and remove your data from our servers.",
trigger="Open alert dialog",
trigger_attrs={"class": "btn-outline"},
footer=footer,
close_button=false,
close_on_overlay_click=false
) }}
{% endset %}
{{ code_preview("alert-dialog", code_html | prettyHtml) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<h3 id="usage-default"><a href="#usage-html">HTML</a></h3>
<section class="prose">
<p>The Alert Dialog component is identical to <a href="/components/dialog" class="font-medium underline underline-offset-4">Dialog</a> except for two small differences: we do not display a close button and we do not let the user close the dialog when clicking on the backdrop (no <code>onclick</code> attribute on the <code>&lt;dialog&gt;</code> or the <code>&lt;article&gt;</code> elements).</p>
</section>
{{ code_block(code_html | prettyHtml, class="") }}
<section class="prose">
<p>The component has the following HTML structure:</p>
<dl>
<dt><code class="highlight language-html">&lt;button type="button"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The trigger button to open the alert dialog (using <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal" target="_blank">the native <code>showModal()</code> method</a>).</dd>
<dt><code class="highlight language-html">&lt;dialog class="dialog" id="{ DIALOG_ID }"&gt;</code></dt>
<dd>Wraps the whole component and acts as the backdrop. It can also have the following attributes:
<ul>
<li><code>aria-labelledby="{ TITLE_ID }"</code>: linked to by the <code>aria-labelledby</code> attribute of the dialog.</li>
<li><code>aria-describedby="{ DESCRIPTION_ID }"</code>: linked to by the <code>aria-describedby</code> attribute of the <code>&lt;dialog&gt;</code>.</li>
</ul>
<dl>
<dt><code class="highlight language-html">&lt;article&gt;</code></dt>
<dd>Wraps the content of the alert dialog.
<dl>
<dt><code class="highlight language-html">&lt;header&gt;</code></dt>
<dd>Contains the header for the alert dialog:
<dl>
<dt><code class="highlight language-html">&lt;h2&gt;</code></dt>
<dd>The title of the alert dialog, must have an <code>id</code> if you use the <code>aria-labelledby</code> attribute on the <code>&lt;dialog&gt;</code>.</dd>
<dt><code class="highlight language-html">&lt;p&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The description of the alert dialog, must have an <code>id</code> if you use the <code>aria-describedby</code> attribute on the <code>&lt;dialog&gt;</code>.</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>For the content.</dd>
<dt><code class="highlight language-html">&lt;footer&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Usually contains actions.</dd>
</dl>
</dd>
</dl>
</dd>
</dl>
</section>
<h3 id="usage-macro"><a href="#usage-macro">Jinja and Nunjucks</a></h3>
<div class="prose">
<p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">dialog()</code> Nunjucks or Jinja macro for this component. Make sure to set <code>close_on_overlay_click</code> and <code>close_button</code> to <code>False</code> (or <code>false</code> for Nunjucks).</p>
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/jinja/dialog.html.jinja" target="_blank">
Jinja macro
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/dialog.njk" target="_blank">
Nunjucks macro
{% lucide "arrow-right" %}
</a>
</div>
{% set raw_code %}{% raw %}{% set footer %}
<button class="btn-outline" onclick="document.getElementById('demo-alert-dialog').close()">Cancel</button>
<button class="btn-primary" onclick="document.getElementById('demo-alert-dialog').close()">Continue</button>
{% endset %}
{{ dialog(
id="demo-alert-dialog",
title="Are you absolutely sure?",
description="This action cannot be undone. This will permanently delete your account and remove your data from our servers.",
trigger="Open Alert Dialog",
trigger_attrs={"class": "btn-outline"},
footer=footer,
close_button=False,
close_on_overlay_click=False
) }}{% endraw %}
{% endset%}
{{ code_block(raw_code, "jinja") }}
-75
View File
@@ -1,75 +0,0 @@
---
layout: layouts/page.njk
title: Alert
description: Displays a callout for user attention.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Destructive
id: example-destructive
- label: No description
id: example-no-description
- label: No icon
id: example-no-icon
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<div class="alert">
{% lucide "circle-check" %}
<h2>Success! Your changes have been saved</h2>
<section>This is an alert with icon, title and description.</section>
</div>{% endset %}
{{ code_preview("alert", code) }}
<h2 id="usage">Usage</h2>
<section class="prose">
<p>You can use one of the two classes: <code>alert</code> or <code>alert-dialog</code>.</p>
</section>
{{ code_block(code) }}
<section class="prose">
<p>The component has the following HTML structure:</p>
<dl>
<dt><code class="highlight language-html">&lt;svg&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The icon.</dd>
<dt><code class="highlight language-html">&lt;h2&gt;</code></dt>
<dd>The title.</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The description.</dd>
</dl>
</section>
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-destructive"><a href="#example-destructive">Destructive</a></h3>
{% set code %}<div class="alert-destructive">
{% lucide "circle-alert" %}
<h2>Something went wrong!</h2>
<section>Your session has expired. Please log in again.</section>
</div>{% endset %}
{{ code_preview("alert-destructive", code) }}
<h3 id="example-no-description"><a href="#example-no-description">No description</a></h3>
{% set code %}
<div class="alert">
{% lucide "shield-alert" %}
<h2>This is a very long alert title that demonstrates how the component handles extended text content and potentially wraps across multiple lines</h2>
</div>{% endset %}
{{ code_preview("alert-no-description", code) }}
<h3 id="example-no-icon"><a href="#example-no-icon">No icon</a></h3>
{% set code %}<div class="alert">
<h2>Success! Your changes have been saved</h2>
<section>This is an alert with icon, title and description.</section>
</div>{% endset %}
{{ code_preview("alert-no-icon", code) }}
-23
View File
@@ -1,23 +0,0 @@
---
layout: layouts/page.njk
title: Avatar
description: An image element with a fallback for representing the user.
toc:
- label: Usage
id: usage
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
<div class="alert mb-6">
{% lucide "circle-alert" %}
<h2>There is no dedicated avatar component in Basecoat.</h2>
</div>
{% set code %}<img class="size-8 shrink-0 object-cover rounded-full" alt="@hunvreus" src="https://github.com/hunvreus.png">{% endset %}
{{ code_preview("avatar", code, "max-w-md") }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<p class="prose">Avatars are fairly simple to implement with Tailwind. The example above shows how to implement one with Tailwind and an <code>&lt;img&gt;</code> element. More examples can be found in the <a href="/kitchen-sink#avatar">kitchen sink</a>.</p>
-75
View File
@@ -1,75 +0,0 @@
---
layout: layouts/page.njk
title: Badge
description: Displays a badge or a component that looks like a badge.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Primary
id: example-primary
- label: Secondary
id: example-secondary
- label: Destructive
id: example-destructive
- label: Outline
id: example-outline
- label: With icon
id: example-with-icon
- label: Link
id: example-link
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<span class="badge">Badge</span>{% endset %}
{{ code_preview("badge", code) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Use one of the badge classes: <code>badge</code>, <code>badge-primary</code>, <code>badge-secondary</code>, <code>badge-destructive</code> or <code>badge-outline</code>.
</section>
{{ code_block(code) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-primary"><a href="#example-primary">Primary</a></h3>
{% set code %}<span class="badge">Primary</span>{% endset %}
{{ code_preview("badge-primary", code) }}
<h3 id="example-secondary"><a href="#example-secondary">Secondary</a></h3>
{% set code %}<span class="badge-secondary">Secondary</span>{% endset %}
{{ code_preview("badge-secondary", code) }}
<h3 id="example-destructive"><a href="#example-destructive">Destructive</a></h3>
{% set code %}<span class="badge-destructive">Destructive</span>{% endset %}
{{ code_preview("badge-destructive", code) }}
<h3 id="example-outline"><a href="#example-outline">Outline</a></h3>
{% set code %}<span class="badge-outline">Outline</span>{% endset %}
{{ code_preview("badge-outline", code) }}
<h3 id="example-with-icon"><a href="#example-with-icon">With icon</a></h3>
{% set code %}<span class="badge-destructive">
{% lucide "circle-alert" %}
With icon
</span>{% endset %}
{{ code_preview("badge-with-icon", code) }}
<h3 id="example-link"><a href="#example-link">Link</a></h3>
{% set code %}<a href="#" class="badge-outline">
Link
{% lucide "arrow-right" %}
</a>{% endset %}
{{ code_preview("badge-link", code) }}
-60
View File
@@ -1,60 +0,0 @@
---
layout: layouts/page.njk
title: Breadcrumb
description: Displays the path to the current resource using a hierarchy of links.
toc:
- label: Usage
id: usage
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% from "dropdown-menu.njk" import dropdown_menu %}
<div class="alert mb-6">
{% lucide "circle-alert" %}
<h2>There is no dedicated breadcrumb component in Basecoat.</h2>
</div>
{% set code_html %}
<ol class="text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5">
<li class="inline-flex items-center gap-1.5">
<a href="#" class="hover:text-foreground transition-colors">Home</a>
</li>
<li>{% lucide "chevron-right", { "class": "size-3.5" } %}</li>
<li class="inline-flex items-center gap-1.5">
{% set trigger %}
{% lucide "ellipsis" %}
{% endset %}
{% call dropdown_menu(
id="demo-breadcrumb-menu",
trigger=trigger,
trigger_attrs={"class": "flex size-9 items-center justify-center h-4 w-4 hover:text-foreground cursor-pointer"}
) %}
<nav role="menu">
<button type="button" role="menuitem">Documentation</button>
<button type="button" role="menuitem">Themes</button>
<button type="button" role="menuitem">GitHub</button>
</nav>
{% endcall %}
</li>
<li>{% lucide "chevron-right", { "class": "size-3.5" } %}</li>
<li class="inline-flex items-center gap-1.5">
<a href="#" class="hover:text-foreground transition-colors">Components</a>
</li>
<li>{% lucide "chevron-right", { "class": "size-3.5" } %}</li>
<li class="inline-flex items-center gap-1.5">
<span class="text-foreground font-normal">Breadcrumb</span>
</li>
</ol>
{% endset %}
{{ code_preview("dropdown-menu", code_html | prettyHtml) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>This example uses some simple Tailwind CSS styles along with the <a href="/components/dropdown-menu">Dropdown Menu</a> component (which may require JavaScript).</p>
</section>
{{ code_block(code_html | prettyHtml, class="") }}
-141
View File
@@ -1,141 +0,0 @@
---
layout: layouts/page.njk
title: Button
description: Displays a button or a component that looks like a button.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Primary
id: example-primary
- label: Secondary
id: example-secondary
- label: Destructive
id: example-destructive
- label: Outline
id: example-outline
- label: Ghost
id: example-ghost
- label: Link
id: example-link
- label: Icon
id: example-icon
- label: With Icon
id: example-with-icon
- label: Loading
id: example-loading
- label: Large
id: example-large
- label: Small
id: example-small
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<button class="btn">Button</button>{% endset %}
{{ code_preview("button", code) }}
<h2 id="usage">Usage</h2>
<section class="prose">
<p>You can use one of the <code>btn</code> classes to style any element as a button:</p>
<ul>
<li>
<b>Variants</b>:
<ul>
<li><code>btn</code> or <code>btn-primary</code> for primary
buttons,</li>
<li><code>btn-secondary</code> for secondary buttons.</li>
<li><code>btn-destructive</code> for destructive buttons.</li>
<li><code>btn-outline</code> for outline buttons.</li>
<li><code>btn-ghost</code> for ghost buttons.</li>
<li><code>btn-link</code> for link buttons.</li>
<li><code>btn-icon</code> for icon buttons. This can be combined with other variants, for example <code>btn-icon-destructive</code>.</li>
</ul>
</li>
<li>
<b>Sizes</b>:
<ul>
<li><code>btn-sm</code> for small buttons.</li>
<li><code>btn-lg</code> for large buttons.</li>
<li>You can combine sizes with any variant, for example <code>btn-lg-destructive</code> or <code>btn-sm-icon-outline</code>.</li>
</ul>
</li>
</ul>
</section>
{{ code_block(code) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-primary"><a href="#example-primary">Primary</a></h3>
{% set code %}<button class="btn">Primary</button>{% endset %}
{{ code_preview("button-primary", code) }}
<h3 id="example-secondary"><a href="#example-secondary">Secondary</a></h3>
{% set code %}<button class="btn-secondary">Button</button>{% endset %}
{{ code_preview("button-secondary", code) }}
<h3 id="example-destructive"><a href="#example-destructive">Destructive</a></h3>
{% set code %}<button class="btn-destructive">Destructive</button>{% endset %}
{{ code_preview("button-destructive", code) }}
<h3 id="example-outline"><a href="#example-outline">Outline</a></h3>
{% set code %}<button class="btn-outline">Outline</button>{% endset %}
{{ code_preview("button-outline", code) }}
<h3 id="example-ghost"><a href="#example-ghost">Ghost</a></h3>
{% set code %}<button class="btn-ghost">Ghost</button>{% endset %}
{{ code_preview("button-ghost", code) }}
<h3 id="example-link"><a href="#example-link">Link</a></h3>
{% set code %}<button class="btn-link">Link</button>{% endset %}
{{ code_preview("button-link", code) }}
<h3 id="example-icon"><a href="#example-icon">Icon</a></h3>
{% set code %}<button class="btn-icon-outline">
{% lucide "chevron-right" %}
</button>{% endset %}
{{ code_preview("button-icon", code) }}
<h3 id="example-with-icon"><a href="#example-with-icon">With Icon</a></h3>
{% set code %}<button class="btn">
{% lucide "send" %}
Send email
</button>{% endset %}
{{ code_preview("button-with-icon", code) }}
<h3 id="example-loading"><a href="#example-loading">Loading</a></h3>
{% set code %}<button class="btn-outline" disabled>
{% lucide "loader", { "class": "animate-spin" } %}
Loading
</button>{% endset %}
{{ code_preview("button-loading", code) }}
<h3 id="example-large"><a href="#example-large">Large</a></h3>
{% set code %}<button class="btn-lg">
{% lucide "shopping-cart" %}
Buy
</button>{% endset %}
{{ code_preview("button-large", code) }}
<h3 id="example-small"><a href="#example-small">Small</a></h3>
{% set code %}<button class="btn-sm-icon-destructive">
{% lucide "trash" %}
</button>{% endset %}
{{ code_preview("button-small", code) }}
-156
View File
@@ -1,156 +0,0 @@
---
layout: layouts/page.njk
title: Card
description: Displays a card with header, content, and footer.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: With content and footer
id: example-with-content-and-footer
- label: With image
id: example-with-image
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<div class="card w-full">
<header>
<h2>Login to your account</h2>
<p>Enter your details below to login to your account</p>
</header>
<section>
<form class="form grid gap-6">
<div class="grid gap-2">
<label for="demo-card-form-email">Email</label>
<input type="email" id="demo-card-form-email">
</div>
<div class="grid gap-2">
<div class="flex items-center gap-2">
<label for="demo-card-form-password">Password</label>
<a href="#" class="ml-auto inline-block text-sm underline-offset-4 hover:underline">Forgot your password?</a>
</div>
<input type="password" id="demo-card-form-password">
</div>
</form>
</section>
<footer class="flex flex-col items-center gap-2">
<button type="button" class="btn w-full">Login</button>
<button type="button" class="btn-outline w-full">Login with Google</button>
<p class="mt-4 text-center text-sm">Don't have an account? <a href="#" class="underline-offset-4 hover:underline">Sign up</a></p>
</footer>
</div>{% endset %}
{{ code_preview("card", code, "w-full max-w-sm") }}
<h2 id="usage">Usage</h2>
{% set code %}<div class="card">
<header>
<h2>Card Title</h2>
<p>Card Description</p>
</header>
<section>
<p>Card Content</p>
</section>
<footer>
<p>Card Footer</p>
</footer>
</div>{% endset %}
{{ code_block(code) }}
<section class="prose">
<p>The component has the following HTML structure:</p>
<dl>
<dt><code class="highlight language-html">&lt;div class="card"&gt;</code></dt>
<dd>The card container.
<dl>
<dt><code class="highlight language-html">&lt;header&gt;</code></dt>
<dd>The header of the card.
<dl>
<dt><code class="highlight language-html">&lt;h2&gt;</code></dt>
<dd>The title of the card.</dd>
<dt><code class="highlight language-html">&lt;p&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The description of the card.</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code></dt>
<dd>The content of the card.</dd>
<dt><code class="highlight language-html">&lt;footer&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The footer of the card.</dd>
</dl>
</dd>
</dl>
</section>
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-with-content-and-footer"><a href="#example-with-content-and-footer">With content and footer</a></h3>
{% set code %}<div class="card">
<header>
<h2>Meeting Notes</h2>
<p>Transcript from the meeting with the client.</p>
</header>
<section class="text-sm">
<p>Client requested dashboard redesign with focus on mobile responsiveness.</p>
<ol class="mt-4 flex list-decimal flex-col gap-2 pl-6">
<li>New analytics widgets for daily/weekly metrics</li>
<li>Simplified navigation menu</li>
<li>Dark mode support</li>
<li>Timeline: 6 weeks</li>
<li>Follow-up meeting scheduled for next Tuesday</li>
</ol>
</section>
<footer class="flex items-center">
<div class="flex -space-x-2 [&_img]:ring-card [&_img]:ring-2 [&_img]:grayscale [&_img]:size-8 [&_img]:shrink-0 [&_img]:object-cover [&_img]:rounded-full">
<img alt="@hunvreus" src="https://github.com/hunvreus.png">
<img alt="@shadcn" src="https://github.com/shadcn.png">
<img alt="@adamwathan" src="https://github.com/adamwathan.png">
</div>
</footer>
</div>{% endset %}
{{ code_preview("card-with-content-and-footer", code) }}
<h3 id="example-with-image"><a href="#example-with-image">With image</a></h3>
{% set code %}<div class="card">
<header>
<h2>Is this an image?</h2>
<p>This is a card with an image.</p>
</header>
<section class="px-0">
<img
alt="Photo by Drew Beamer"
loading="lazy"
width="500"
height="500"
class="aspect-video object-cover" style="color:transparent"
srcset="
https://images.unsplash.com/photo-1588345921523-c2dcdb7f1dcd?w=800&dpr=2&q=80&w=640&q=75 1x,
https://images.unsplash.com/photo-1588345921523-c2dcdb7f1dcd?w=800&dpr=2&q=80&w=1080&q=75 2x
"
src="https://images.unsplash.com/photo-1588345921523-c2dcdb7f1dcd?w=800&dpr=2&q=80&w=1080&q=75"
/>
</section>
<footer class="flex items-center gap-2">
<span class="badge-outline">
{% lucide "bed" %}
1
</span>
<span class="badge-outline">
{% lucide "bath" %}
2
</span>
<span class="badge-outline">
{% lucide "land-plot" %}
350m²
</span>
<span class="ml-auto font-medium tabular-nums">
$135,000
</span>
</footer>
</div>{% endset %}
{{ code_preview("card-with-image", code) }}
-814
View File
@@ -1,814 +0,0 @@
---
layout: layouts/page.njk
title: Chart
description: Displays the path to the current resource using a hierarchy of links.
toc:
- label: Usage
id: usage
---
<div class="grid gap-6">
<div class="grid grid-cols-2 gap-6">
<div class="card">
<header>
<h2>Area Chart</h2>
<p>Showing total visitors for the last 6 months</p>
</header>
<section>
<canvas id="chart-area-default"></canvas>
</section>
<footer>
<div class="grid gap-2 text-sm">
<div class="flex items-center gap-2 leading-none font-medium">
Trending up by 5.2% this month
{% lucide "trending-up", { "class": "size-4" } %}
</div>
<div class="text-muted-foreground flex items-center gap-2 leading-none">January - June 2024</div>
</div>
</footer>
</div>
<div class="card">
<header>
<h2>Area Chart - Stacked</h2>
<p>Showing total visitors for the last 6 months</p>
</header>
<section>
<canvas id="chart-area-stacked"></canvas>
</section>
<footer>
<div class="grid gap-2 text-sm">
<div class="flex items-center gap-2 leading-none font-medium">
Trending up by 5.2% this month
{% lucide "trending-up", { "class": "size-4" } %}
</div>
<div class="text-muted-foreground flex items-center gap-2 leading-none">January - June 2024</div>
</div>
</footer>
</div>
<div class="card">
<header>
<h2>Bar Chart - Multiple</h2>
<p>Showing total visitors for the last 6 months</p>
</header>
<section>
<canvas id="chart-bar-multiple"></canvas>
</section>
<footer>
<div class="grid gap-2 text-sm">
<div class="flex items-center gap-2 leading-none font-medium">
Trending up by 5.2% this month
{% lucide "trending-up", { "class": "size-4" } %}
</div>
<div class="text-muted-foreground flex items-center gap-2 leading-none">January - June 2024</div>
</div>
</footer>
</div>
<div class="card">
<header>
<h2>Doughnut Chart</h2>
<p>Showing total visitors for the last 6 months</p>
</header>
<section class="relative">
<div>
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center">
<div class="text-3xl font-semibold">1,125</div>
<div class="text-muted-foreground text-xs">Visitors</div>
</div>
<canvas id="chart-doughnut" class="max-h-[200px] mx-auto"></canvas>
</div>
</section>
</div>
</div>
<div class="card">
<header>
<h2>Bar Chart - Interactive</h2>
<p>Showing total visitors for the last 6 months</p>
</header>
<section>
<canvas id="chart-bar-interactive"></canvas>
</section>
</div>
<div class="card">
<header>
<h2>Bar Chart - Interactive</h2>
<p>Showing total visitors for the last 6 months</p>
</header>
<section class="max-h-[150px]">
<canvas id="chart-line-interactive"></canvas>
</section>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// Helper function to MULTIPLY the alpha channel of oklch colors by a factor
function setAlpha(colorString, alphaFactor) {
colorString = colorString.trim();
if (colorString.startsWith('oklch(')) {
const match = colorString.match(/^oklch\(([^\)]+?)\s*(?:\/\s*([\d.]+)(%?)\s*)?\)$/);
if (match && match[1]) {
const lchPart = match[1];
let currentAlpha = 1.0;
if (match[2]) {
currentAlpha = parseFloat(match[2]);
if (match[3] === '%') {
currentAlpha /= 100;
}
}
let newAlpha = currentAlpha * alphaFactor;
newAlpha = Math.max(0, Math.min(1, newAlpha));
return `oklch(${lchPart} / ${newAlpha.toFixed(3)})`;
} else {
console.warn('Failed to parse oklch string:', colorString);
return colorString;
}
}
return colorString;
}
const computedStyle = getComputedStyle(document.documentElement);
const themeColors = {
background: computedStyle.getPropertyValue('--background').trim(),
border: computedStyle.getPropertyValue('--border').trim(),
mutedForeground: computedStyle.getPropertyValue('--muted-foreground').trim(),
foreground: computedStyle.getPropertyValue('--foreground').trim(),
chart1: computedStyle.getPropertyValue('--chart-1').trim(),
chart2: computedStyle.getPropertyValue('--chart-2').trim(),
chart3: computedStyle.getPropertyValue('--chart-3').trim(),
chart4: computedStyle.getPropertyValue('--chart-4').trim(),
chart5: computedStyle.getPropertyValue('--chart-5').trim(),
};
const getOrCreateTooltip = (chart) => {
let tooltipEl = chart.canvas.parentNode.querySelector('div[role="tooltip"]');
if (!tooltipEl) {
const tooltipHTML = `
<div
role="tooltip"
class="border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl opacity-0 absolute pointer-events-none transition-all duration-400 space-y-1"
>
<h3 class="font-medium"></h3>
<ul class="grid gap-1"></ul>
</div>`;
const parser = new DOMParser();
const doc = parser.parseFromString(tooltipHTML.trim(), 'text/html');
tooltipEl = doc.body.firstChild;
chart.canvas.parentNode.appendChild(tooltipEl);
}
return tooltipEl;
};
const externalTooltipHandler = (context) => {
const {chart, tooltip} = context;
const chartType = chart.config._config.type;
const tooltipEl = getOrCreateTooltip(chart);
const titleEl = tooltipEl.querySelector('h3');
const bodyEl = tooltipEl.querySelector('ul');
// Hide if no tooltip
if (tooltip.opacity === 0) {
tooltipEl.classList.remove('opacity-100');
tooltipEl.classList.add('opacity-0');
return;
}
let bodyHtml = '';
if (tooltip.body) {
const titleLines = tooltip.title || [];
const bodyLines = tooltip.body.map(b => b.lines);
if (titleEl) {
if (chartType === 'doughnut') {
titleEl.remove();
} else {
titleEl.textContent = titleLines[0] || '';
}
}
bodyLines.forEach((body, i) => {
const colors = tooltip.labelColors[i];
const parts = body[0].split(': ');
const label = chartType === 'doughnut'
? titleLines[0] || ''
: parts[0] || '';
const value = parts[1] || '';
let color;
switch (chartType) {
case 'doughnut':
case 'pie':
case 'bar':
color = colors.backgroundColor;
break;
default:
color = colors.borderColor;
}
bodyHtml += `<li class="flex w-full flex-wrap items-center justify-between leading-none gap-x-2">
<div class="flex items-center gap-x-1.5">
<div class="shrink-0 rounded-[2px] size-2.5 block" style="background:${color}"></div>
<span class="text-muted-foreground">${label}</span>
</div>
<span class="text-foreground font-medium font-mono tabular-nums">${value}</span>
</li>`;
});
}
if (bodyEl) bodyEl.innerHTML = bodyHtml;
const {offsetLeft: positionX, offsetTop: positionY} = chart.canvas;
const tooltipHeight = tooltipEl.offsetHeight;
const tooltipWidth = tooltipEl.offsetWidth;
const caretX = tooltip.caretX;
const caretY = tooltip.caretY;
const xAlign = tooltip.xAlign;
const yAlign = tooltip.yAlign;
const tooltipOffset = 8;
let left = positionX + caretX;
let top = positionY + caretY;
if (xAlign === 'center') {
left -= tooltipWidth / 2;
} else if (xAlign === 'right') {
left -= (tooltipWidth + tooltipOffset);
} else { // xAlign === 'left'
left += tooltipOffset;
}
if (yAlign === 'center') {
top -= tooltipHeight / 2;
} else if (yAlign === 'top') {
top -= (tooltipHeight + tooltipOffset);
} else {
top += tooltipOffset;
}
const canvasBounds = chart.canvas.getBoundingClientRect();
if (left < 0) {
left = 0;
} else if (left + tooltipWidth > canvasBounds.width) {
left = canvasBounds.width - tooltipWidth;
}
if (top < 0) {
top = 0;
} else if (top + tooltipHeight > window.innerHeight) {
const flippedTop = positionY + caretY - tooltipHeight - tooltipOffset;
if (flippedTop >= 0) top = flippedTop;
}
tooltipEl.classList.remove('opacity-0');
tooltipEl.classList.add('opacity-100');
tooltipEl.style.left = left + 'px';
tooltipEl.style.top = top + 'px';
};
let ctx = document.getElementById('chart-area-default');
let data = {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
datasets: [{
label: 'Desktop',
data: [186, 305, 237, 73, 209, 214],
borderWidth: 1,
backgroundColor: setAlpha(themeColors.chart1, 0.2),
borderColor: themeColors.chart1,
pointRadius: 0,
pointHoverRadius: 4,
pointHoverBackgroundColor: themeColors.chart1,
pointHoverBorderColor: themeColors.chart1,
fill: 'origin'
}]
};
let config = {
type: 'line',
data: data,
options: {
tension: 0.4,
interaction: {
mode: 'nearest',
axis: 'x',
intersect: false
},
scales: {
y: {
border: { display: false },
grid: {
color: setAlpha(themeColors.border, 0.5),
lineWidth: 1
},
ticks: { display: false }
},
x: {
grid: { display: false },
ticks: { color: themeColors.mutedForeground }
}
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
position: 'nearest',
external: externalTooltipHandler,
}
}
}
};
new Chart(ctx, config);
ctx = document.getElementById('chart-area-stacked');
data = {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
datasets: [{
label: 'Desktop',
data: [186, 305, 237, 73, 209, 214],
borderWidth: 1,
backgroundColor: setAlpha(themeColors.chart1, 0.2),
borderColor: themeColors.chart1,
pointRadius: 0,
pointHoverRadius: 4,
pointHoverBackgroundColor: themeColors.chart1,
pointHoverBorderColor: themeColors.chart1,
fill: 'origin'
}, {
label: 'Mobile',
data: [80, 200, 120, 190, 130, 140],
borderWidth: 1,
backgroundColor: setAlpha(themeColors.chart2, 0.2),
borderColor: themeColors.chart2,
pointRadius: 0,
pointHoverRadius: 4,
pointHoverBackgroundColor: themeColors.chart2,
pointHoverBorderColor: themeColors.chart2,
fill: 'origin'
}]
};
config = {
type: 'line',
data: data,
options: {
tension: 0.4,
interaction: {
mode: 'nearest',
axis: 'x',
intersect: false
},
scales: {
y: {
border: { display: false },
grid: {
color: setAlpha(themeColors.border, 0.5),
lineWidth: 1
},
ticks: { display: false }
},
x: {
grid: { display: false },
ticks: { color: themeColors.mutedForeground }
}
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
position: 'nearest',
external: externalTooltipHandler,
}
}
}
};
new Chart(ctx, config);
ctx = document.getElementById('chart-bar-multiple');
data = {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
datasets: [{
label: 'Desktop',
data: [186, 305, 237, 73, 209, 214],
backgroundColor: themeColors.chart1,
borderWidth: 0,
borderRadius: 4,
barPercentage: 0.7,
categoryPercentage: 0.8
}, {
label: 'Mobile',
data: [80, 200, 120, 190, 130, 140],
backgroundColor: themeColors.chart2,
borderWidth: 0,
borderRadius: 4,
barPercentage: 0.7,
categoryPercentage: 0.8
}]
};
config = {
type: 'bar',
data: data,
options: {
interaction: {
mode: 'index',
axis: 'x',
intersect: false
},
scales: {
y: {
border: { display: false },
grid: {
color: setAlpha(themeColors.border, 0.5),
lineWidth: 1
},
ticks: { display: false }
},
x: {
grid: { display: false },
ticks: { color: themeColors.mutedForeground }
}
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
position: 'nearest',
external: externalTooltipHandler,
}
}
}
};
new Chart(ctx, config);
const chartData = [
{ date: "2024-04-01", desktop: 222, mobile: 150 },
{ date: "2024-04-02", desktop: 97, mobile: 180 },
{ date: "2024-04-03", desktop: 167, mobile: 120 },
{ date: "2024-04-04", desktop: 242, mobile: 260 },
{ date: "2024-04-05", desktop: 373, mobile: 290 },
{ date: "2024-04-06", desktop: 301, mobile: 340 },
{ date: "2024-04-07", desktop: 245, mobile: 180 },
{ date: "2024-04-08", desktop: 409, mobile: 320 },
{ date: "2024-04-09", desktop: 59, mobile: 110 },
{ date: "2024-04-10", desktop: 261, mobile: 190 },
{ date: "2024-04-11", desktop: 327, mobile: 350 },
{ date: "2024-04-12", desktop: 292, mobile: 210 },
{ date: "2024-04-13", desktop: 342, mobile: 380 },
{ date: "2024-04-14", desktop: 137, mobile: 220 },
{ date: "2024-04-15", desktop: 120, mobile: 170 },
{ date: "2024-04-16", desktop: 138, mobile: 190 },
{ date: "2024-04-17", desktop: 446, mobile: 360 },
{ date: "2024-04-18", desktop: 364, mobile: 410 },
{ date: "2024-04-19", desktop: 243, mobile: 180 },
{ date: "2024-04-20", desktop: 89, mobile: 150 },
{ date: "2024-04-21", desktop: 137, mobile: 200 },
{ date: "2024-04-22", desktop: 224, mobile: 170 },
{ date: "2024-04-23", desktop: 138, mobile: 230 },
{ date: "2024-04-24", desktop: 387, mobile: 290 },
{ date: "2024-04-25", desktop: 215, mobile: 250 },
{ date: "2024-04-26", desktop: 75, mobile: 130 },
{ date: "2024-04-27", desktop: 383, mobile: 420 },
{ date: "2024-04-28", desktop: 122, mobile: 180 },
{ date: "2024-04-29", desktop: 315, mobile: 240 },
{ date: "2024-04-30", desktop: 454, mobile: 380 },
{ date: "2024-05-01", desktop: 165, mobile: 220 },
{ date: "2024-05-02", desktop: 293, mobile: 310 },
{ date: "2024-05-03", desktop: 247, mobile: 190 },
{ date: "2024-05-04", desktop: 385, mobile: 420 },
{ date: "2024-05-05", desktop: 481, mobile: 390 },
{ date: "2024-05-06", desktop: 498, mobile: 520 },
{ date: "2024-05-07", desktop: 388, mobile: 300 },
{ date: "2024-05-08", desktop: 149, mobile: 210 },
{ date: "2024-05-09", desktop: 227, mobile: 180 },
{ date: "2024-05-10", desktop: 293, mobile: 330 },
{ date: "2024-05-11", desktop: 335, mobile: 270 },
{ date: "2024-05-12", desktop: 197, mobile: 240 },
{ date: "2024-05-13", desktop: 197, mobile: 160 },
{ date: "2024-05-14", desktop: 448, mobile: 490 },
{ date: "2024-05-15", desktop: 473, mobile: 380 },
{ date: "2024-05-16", desktop: 338, mobile: 400 },
{ date: "2024-05-17", desktop: 499, mobile: 420 },
{ date: "2024-05-18", desktop: 315, mobile: 350 },
{ date: "2024-05-19", desktop: 235, mobile: 180 },
{ date: "2024-05-20", desktop: 177, mobile: 230 },
{ date: "2024-05-21", desktop: 82, mobile: 140 },
{ date: "2024-05-22", desktop: 81, mobile: 120 },
{ date: "2024-05-23", desktop: 252, mobile: 290 },
{ date: "2024-05-24", desktop: 294, mobile: 220 },
{ date: "2024-05-25", desktop: 201, mobile: 250 },
{ date: "2024-05-26", desktop: 213, mobile: 170 },
{ date: "2024-05-27", desktop: 420, mobile: 460 },
{ date: "2024-05-28", desktop: 233, mobile: 190 },
{ date: "2024-05-29", desktop: 78, mobile: 130 },
{ date: "2024-05-30", desktop: 340, mobile: 280 },
{ date: "2024-05-31", desktop: 178, mobile: 230 },
{ date: "2024-06-01", desktop: 178, mobile: 200 },
{ date: "2024-06-02", desktop: 470, mobile: 410 },
{ date: "2024-06-03", desktop: 103, mobile: 160 },
{ date: "2024-06-04", desktop: 439, mobile: 380 },
{ date: "2024-06-05", desktop: 88, mobile: 140 },
{ date: "2024-06-06", desktop: 294, mobile: 250 },
{ date: "2024-06-07", desktop: 323, mobile: 370 },
{ date: "2024-06-08", desktop: 385, mobile: 320 },
{ date: "2024-06-09", desktop: 438, mobile: 480 },
{ date: "2024-06-10", desktop: 155, mobile: 200 },
{ date: "2024-06-11", desktop: 92, mobile: 150 },
{ date: "2024-06-12", desktop: 492, mobile: 420 },
{ date: "2024-06-13", desktop: 81, mobile: 130 },
{ date: "2024-06-14", desktop: 426, mobile: 380 },
{ date: "2024-06-15", desktop: 307, mobile: 350 },
{ date: "2024-06-16", desktop: 371, mobile: 310 },
{ date: "2024-06-17", desktop: 475, mobile: 520 },
{ date: "2024-06-18", desktop: 107, mobile: 170 },
{ date: "2024-06-19", desktop: 341, mobile: 290 },
{ date: "2024-06-20", desktop: 408, mobile: 450 },
{ date: "2024-06-21", desktop: 169, mobile: 210 },
{ date: "2024-06-22", desktop: 317, mobile: 270 },
{ date: "2024-06-23", desktop: 480, mobile: 530 },
{ date: "2024-06-24", desktop: 132, mobile: 180 },
{ date: "2024-06-25", desktop: 141, mobile: 190 },
{ date: "2024-06-26", desktop: 434, mobile: 380 },
{ date: "2024-06-27", desktop: 448, mobile: 490 },
{ date: "2024-06-28", desktop: 149, mobile: 200 },
{ date: "2024-06-29", desktop: 103, mobile: 160 },
{ date: "2024-06-30", desktop: 446, mobile: 400 },
]
data = {
labels: chartData.map(d => d.date),
datasets: [{
label: 'Desktop',
data: chartData.map(d => d.desktop),
backgroundColor: themeColors.chart2,
borderWidth: 0,
borderRadius: 0, // Stacking usually looks better without individual radius
}, {
label: 'Mobile',
data: chartData.map(d => d.mobile),
backgroundColor: themeColors.chart1,
borderWidth: 0,
borderRadius: 0, // Stacking usually looks better without individual radius
}]
};
ctx = document.getElementById('chart-bar-interactive');
// Restore helper function to format date for labels/tooltips
const formatDateLabel = (dateString) => {
try {
const date = new Date(dateString + 'T00:00:00');
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); // e.g., "Apr 1"
} catch (event) {
return dateString; // Fallback
}
};
// Restore data preparation with formatted date labels
data = {
labels: chartData.map(d => formatDateLabel(d.date)),
datasets: [{
label: 'Desktop',
data: chartData.map(d => d.desktop),
backgroundColor: themeColors.chart4,
borderWidth: 0,
borderRadius: 0,
}, {
label: 'Mobile',
data: chartData.map(d => d.mobile),
backgroundColor: themeColors.chart5,
borderWidth: 0,
borderRadius: 0,
}]
};
config = {
type: 'bar',
data: data,
options: {
interaction: {
mode: 'index',
axis: 'x',
intersect: false
},
scales: {
y: {
stacked: true,
border: { display: false },
grid: {
color: setAlpha(themeColors.border, 0.5),
lineWidth: 1
},
ticks: { display: false }
},
x: {
stacked: true,
grid: { display: false },
ticks: {
color: themeColors.mutedForeground,
callback: function(value, index, ticks) {
const label = this.getLabelForValue(value);
return index % 7 === 0 ? label : null;
},
maxRotation: 0,
minRotation: 0
}
}
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
position: 'nearest',
external: externalTooltipHandler,
}
}
}
};
new Chart(ctx, config);
// Same chart but with line chart
// We start off from the previous config and just change the type to line
ctx = document.getElementById('chart-line-interactive');
// Prepare data with formatted date labels AND offset for Mobile
const desktopData = chartData.map(d => d.desktop + 400); // Store raw desktop data
const mobileDataOffset = chartData.map(d => d.mobile); // Store offset mobile data
// Prepare data with gradient background
data = {
labels: chartData.map(d => formatDateLabel(d.date)),
datasets: [{
label: 'Desktop',
data: desktopData,
borderColor: themeColors.chart1,
// Make backgroundColor scriptable
backgroundColor: function(context) {
const chart = context.chart;
const { ctx, chartArea } = chart;
// Use the helper function defined above
return createGradient(ctx, chartArea, themeColors.chart1);
},
borderWidth: 1,
pointRadius: 0,
pointHoverRadius: 4,
pointHoverBackgroundColor: themeColors.chart1,
pointHoverBorderColor: themeColors.chart1,
fill: 'start' // Fill from the start (usually y=0 or axis baseline)
}, {
label: 'Mobile (Offset)', // Adjusted label
data: mobileDataOffset,
borderColor: themeColors.chart2,
// Make backgroundColor scriptable
backgroundColor: function(context) {
const chart = context.chart;
const { ctx, chartArea } = chart;
// Use the helper function defined above
return createGradient(ctx, chartArea, themeColors.chart2);
},
borderWidth: 1,
pointRadius: 0,
pointHoverRadius: 4,
pointHoverBackgroundColor: themeColors.chart2,
pointHoverBorderColor: themeColors.chart2,
fill: 'start'
}]
};
// Add the createGradient helper function before the data object
function createGradient(ctx, chartArea, color) {
if (!chartArea) {
return setAlpha(color, 0.1); // Fallback during initial setup
}
const gradient = ctx.createLinearGradient(0, chartArea.top, 0, chartArea.bottom);
gradient.addColorStop(0, setAlpha(color, 0.5)); // Start color at top
gradient.addColorStop(1, setAlpha(color, 0)); // End color (transparent) at bottom
return gradient;
}
// Calculate min/max for Y-axis scaling using the PLOTTED data
const allPlottedValues = [...desktopData, ...mobileDataOffset]; // Combine actual plotted values
const dataMin = Math.min(...allPlottedValues);
const dataMax = Math.max(...allPlottedValues);
const yPadding = (dataMax - dataMin) * 0.1; // Add 10% padding
config = {
type: 'line',
data: data,
options: {
maintainAspectRatio: false,
aspectRatio: 1,
tension: 0.4, // Keep tension from original line config
interaction: {
mode: 'index', // Use 'index' mode for better hover on multiple lines
axis: 'x',
intersect: false
},
scales: {
y: {
border: { display: false },
grid: {
color: setAlpha(themeColors.border, 0.5),
lineWidth: 1
},
ticks: { display: false },
// Set min and max for Y-axis
min: Math.max(0, dataMin - yPadding), // Ensure min is not negative
max: dataMax + yPadding
},
x: {
grid: { display: false },
ticks: {
color: themeColors.mutedForeground,
// Restore callback to reduce ticks
callback: function(value, index, ticks) {
const label = this.getLabelForValue(value);
return index % 7 === 0 ? label : null;
},
maxRotation: 0,
minRotation: 0
}
}
},
plugins: {
legend: { display: false },
tooltip: {
enabled: false,
position: 'nearest',
external: externalTooltipHandler,
}
}
}
};
new Chart(ctx, config);
const chartDataDoughnut = [
{ browser: "chrome", visitors: 275, fill: themeColors.chart1 }, // Use resolved colors
{ browser: "safari", visitors: 200, fill: themeColors.chart2 },
{ browser: "firefox", visitors: 287, fill: themeColors.chart3 },
{ browser: "edge", visitors: 173, fill: themeColors.chart4 },
{ browser: "other", visitors: 190, fill: themeColors.chart5 },
]
// Prepare data for the doughnut chart
const doughnutData = {
labels: chartDataDoughnut.map(d => d.browser),
datasets: [{
label: 'Visitors by Browser',
data: chartDataDoughnut.map(d => d.visitors),
backgroundColor: chartDataDoughnut.map(d => d.fill),
borderColor: themeColors.background, // Use background for border
borderWidth: 0, // Add border between segments
hoverBorderColor: themeColors.background,
hoverBorderWidth: 0
}]
};
// Configure the doughnut chart
const doughnutConfig = {
type: 'doughnut',
data: doughnutData,
options: {
cutout: '70%', // Adjust hole size
plugins: {
legend: {
display: false, // Hide default legend
},
tooltip: {
enabled: false, // Keep this false as we use external handler
position: 'nearest', // Keep this for external handler logic
external: externalTooltipHandler, // Use the existing handler
},
centerText: {} // Keep the custom plugin for center text
}
}
};
// Assume you have a canvas with id="chart-doughnut" in your HTML
const doughnutCtx = document.getElementById('chart-doughnut').getContext('2d');
new Chart(doughnutCtx, doughnutConfig);
</script>
-101
View File
@@ -1,101 +0,0 @@
---
layout: layouts/page.njk
title: Checkbox
description: A control that allows the user to toggle between checked and not checked.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Disabled
id: example-disabled
- label: With text
id: example-with-text
- label: Form
id: example-form
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<label class="label gap-3">
<input type="checkbox" class="input">
Accept terms and conditions
</label>{% endset %}
{{ code_preview("checkbox", code) }}
<h2 id="usage">Usage</h2>
<section class="prose">
<p>Add the <code>input</code> class to your <code>&lt;input type="radio"&gt;</code> elements or have a parent with the <code>form</code> class (<a href="/components/form">read more about form</a>). You can also set the <code>aria-invalid</code> attribute to <code>true</code> to make the input invalid.</p>
</section>
{% set code %}<input type="checkbox" class="input">{% endset %}
{{ code_block(code) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-disabled"><a href="#example-disabled">Disabled</a></h3>
{% set code %}<label class="label gap-3">
<input type="checkbox" class="input" disabled>
Accept terms and conditions
</label>{% endset %}
{{ code_preview("checkbox-disabled", code) }}
<h3 id="example-with-text"><a href="#example-with-text">With text</a></h3>
{% set code %}<div class="flex items-start gap-3">
<input type="checkbox" id="checkbox-with-text" class="input">
<div class="grid gap-2">
<label for="checkbox-with-text" class="label">Accept terms and conditions</label>
<p class="text-muted-foreground text-sm">By clicking this checkbox, you agree to the terms and conditions.</p>
</div>
</div>{% endset %}
{{ code_preview("checkbox-with-text", code) }}
<h3 id="example-form"><a href="#example-form">Form</a></h3>
{% set code %}<form class="form flex flex-row items-start gap-3 rounded-md border p-4 shadow-xs">
<input type="checkbox" id="checkbox-form-1">
<div class="flex flex-col gap-1">
<label for="checkbox-form-1" class="leading-snug">Use different settings for my mobile devices</label>
<p class="text-muted-foreground text-sm leading-snug">You can manage your mobile notifications in the mobile settings page.</p>
</div>
</form>{% endset %}
{{ code_preview("input-form", code) }}
{% set code %}<form class="form flex flex-col gap-4">
<header>
<label for="demo-form-checkboxes" class="text-base leading-normal">Sidebar</label>
<p class="text-muted-foreground text-sm">Select the items you want to display in the sidebar.</p>
</header>
<fieldset id="demo-form-checkboxes" class="flex flex-col gap-2">
<label class="font-normal leading-tight">
<input type="checkbox" name="demo-form-checkboxes" value="1" checked>
Recents
</label>
<label class="font-normal leading-tight">
<input type="checkbox" name="demo-form-checkboxes" value="2" checked>
Home
</label>
<label class="font-normal leading-tight">
<input type="checkbox" name="demo-form-checkboxes" value="3">
Applications
</label>
<label class="font-normal leading-tight">
<input type="checkbox" name="demo-form-checkboxes" value="4">
Desktop
</label>
<label class="font-normal leading-tight">
<input type="checkbox" name="demo-form-checkboxes" value="5">
Download
</label>
<label class="font-normal leading-tight">
<input type="checkbox" name="demo-form-checkboxes" value="6">
Documents
</label>
</fieldset>
</form>{% endset %}
{{ code_preview("input-form", code) }}
-249
View File
@@ -1,249 +0,0 @@
---
layout: layouts/page.njk
title: Combobox
description: Autocomplete input and command palette with a list of suggestions.
toc:
- label: Usage
id: usage
children:
- label: HTML + JavaScript
id: usage-html-js
children:
- label: "Step 1: Include the JavaScript files"
id: usage-html-js-1
- label: "Step 2: Add your combobox HTML"
id: usage-html-js-2
- label: HTML structure
id: usage-html-js-3
- label: Jinja and Nunjucks
id: usage-macro
- label: Examples
id: examples
children:
- label: Scrollable
id: example-scrollable
- label: Top side
id: example-top
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% from "select.njk" import select %}
{% set options_frameworks = ["Next.js", "SvelteKit", "Nuxt.js", "Remix", "Astro"] %}
{% set code_html %}
{% call select(
trigger_attrs={"class": "w-[200px]"},
listbox_attrs={"data-empty": "No framework found."},
is_combobox=true,
search_placeholder="Search framework..."
) %}
{% for framework in options_frameworks %}
<div role="option" data-value="{{ framework }}">{{ framework }}</div>
{% endfor %}
{% endcall %}
{% endset %}
{{ code_preview("combobox", code_html | prettyHtml) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<h3 id="usage-html-js"><a href="#usage-html-js">HTML + JavaScript</a></h3>
<h4 id="usage-html-js-1"><a href="#usage-html-js-1">Step 1: Include the JavaScript files</a></h4>
<section class="prose">
<p>You can either <a href="/installation/#install-cdn-all">include the JavaScript file for all the components</a>, or just the one for this component by adding this to the <code>&lt;head&gt;</code> of your page:</p>
</section>
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ site.version }}/dist/js/select.min.js" defer></script>{% endset %}
{{ code_block(code_script | prettyHtml, "html") }}
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-js">
Components with JavaScript
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="/installation/#install-cli">
Use the CLI
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/js/select.js" target="_blank">
select.js
{% lucide "arrow-right" %}
</a>
</div>
<h4 id="usage-html-js-2"><a href="#usage-html-js-2">Step 2: Add your combobox HTML</a></h4>
<section class="prose">
<p>Combobox uses the same markup and JavaScript as the <a href="/components/select#usage-html-js">Select component</a>, the only difference being the search box at the top of the listbox:</p>
</section>
{{ code_block(code_html | prettyHtml, "html") }}
<h4 id="usage-html-js-3"><a href="#usage-html-js-3">HTML structure</a></h4>
<section class="prose">
<dl>
<dt><code class="highlight language-html">&lt;div class="select"&gt;</code></dt>
<dd>Wraps around the entire component.
<dl>
<dt><code class="highlight language-html">&lt;button type="button" popovertarget="{ POPOVER_ID }"&gt;</code></dt>
<dd>
<p>The trigger to open the popover, can also have the following attributes:</p>
<ul>
<li><code>id="{BUTTON_ID}"</code>: linked to by the <code>aria-labelledby</code> attribute of the listbox.</li>
<li><code>aria-haspopup="listbox"</code>: indicates that the button opens a listbox.</li>
<li><code>aria-controls="{ LISTBOX_ID }"</code>: points to the listbox's id.</li>
<li><code>aria-expanded="false"</code>: tracks the popover's state.</li>
<li><code>aria-activedescendant="{ OPTION_ID }"</code>: points to the active option's id.</li>
</ul>
</dd>
<dt><code class="highlight language-html">&lt;div popover class="popover" id="{ POPOVER_ID }"&gt;</code></dt>
<dd>As with the <a href="/components/popover">Popover</a> component, you can set up the side and alignment of the popover using the <code>data-side</code> and <code>data-align</code> attributes.
<dl>
<dt><code class="highlight language-html">&lt;header&gt;</code></dt>
<dd>Contains the search input to filter the options in the listbox.</dd>
<dt><code class="highlight language-html">&lt;div role="listbox"&gt;</code></dt>
<dd>The listbox containing the options. Should have the following attributes:
<ul>
<li><code>id="{ LISTBOX_ID }"</code>: refered to by the <code>aria-controls</code> attribute of the trigger.</li>
<li><code>aria-labelledby="{ BUTTON_ID }"</code>: linked to by the button's <code>id</code> attribute.</li>
</ul>
<dl>
<dt><code class="highlight language-html">&lt;div role="option" data-value="{ VALUE }"&gt;</code></dt>
<dd>Option that can be selected.Should have a unique id if you use the <code>aria-activedescendant</code> attribute on the trigger.</dd>
<dt><code class="highlight language-html">&lt;hr role="separator"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Separator between groups/options.</dd>
<dt><code class="highlight language-html">&lt;div role="group"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Group of options, can have a <code>aria-labelledby</code> attribute to link to a heading.</dd>
<dt><code class="highlight language-html">&lt;span role="heading"&gt;</code></dt>
<dd>Group heading, must have an <code>id</code> attribute if you use the <code>aria-labelledby</code> attribute on the group.</dd>
</dl>
</dd>
</dl>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;input type="hidden" name="{ NAME }" value="{ VALUE }"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The hidden input that holds the value of the field (if needed).</dd>
</dl>
</section>
<h3 id="usage-macro"><a href="#usage-macro">Jinja and Nunjucks</a></h3>
<div class="prose">
<p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">select()</code> Nunjucks or Jinja macro for this component. If you use one of the macros, make sure to set <code>is_combobox</code> to <code>True</code> (or <code>true</code> for Nunjucks).</p>
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/jinja/select.html.jinja" target="_blank">
Jinja macro
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/select.njk" target="_blank">
Nunjucks macro
{% lucide "arrow-right" %}
</a>
</div>
{% set raw_code %}{% raw %}{% call select(
listbox_attrs={"data-empty": "No framework found."},
is_combobox=true
) %}
<div role="option" data-value="nextjs">Next.js</div>
<div role="option" data-value="sveltekit">SvelteKit</div>
<div role="option" data-value="nuxtjs">Nuxt.js</div>
<div role="option" data-value="remix">Remix</div>
<div role="option" data-value="astro">Astro</div>
{% endcall %}{% endraw %}{% endset %}
{{ code_block(raw_code, "jinja") }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-scrollable"><a href="#example-scrollable">Scrollable</a></h3>
{% set options_timezones = [
{
label: "Americas",
timezones: [
{ value: "America/New_York", label: "(GMT-5) New York" },
{ value: "America/Los_Angeles", label: "(GMT-8) Los Angeles" },
{ value: "America/Chicago", label: "(GMT-6) Chicago" },
{ value: "America/Toronto", label: "(GMT-5) Toronto" },
{ value: "America/Vancouver", label: "(GMT-8) Vancouver" },
{ value: "America/Sao_Paulo", label: "(GMT-3) São Paulo" }
]
},
{
label: "Europe",
timezones: [
{ value: "Europe/London", label: "(GMT+0) London" },
{ value: "Europe/Paris", label: "(GMT+1) Paris" },
{ value: "Europe/Berlin", label: "(GMT+1) Berlin" },
{ value: "Europe/Rome", label: "(GMT+1) Rome" },
{ value: "Europe/Madrid", label: "(GMT+1) Madrid" },
{ value: "Europe/Amsterdam", label: "(GMT+1) Amsterdam" }
]
},
{
label: "Asia/Pacific",
timezones: [
{ value: "Asia/Tokyo", label: "(GMT+9) Tokyo" },
{ value: "Asia/Shanghai", label: "(GMT+8) Shanghai" },
{ value: "Asia/Singapore", label: "(GMT+8) Singapore" },
{ value: "Asia/Dubai", label: "(GMT+4) Dubai" },
{ value: "Australia/Sydney", label: "(GMT+11) Sydney" },
{ value: "Asia/Seoul", label: "(GMT+9) Seoul" }
]
}
] %}
{% set code %}
{% call select(
trigger_attrs={"class": "w-[200px]"},
popover_attrs={"class": "w-72"},
listbox_attrs={"data-empty": "No timezone found."},
is_combobox=true,
search_placeholder="Search timezone..."
) %}
<div class="max-h-64 overflow-y-auto scrollbar">
{% for group in options_timezones %}
<div role="group" aria-labelledby="demo-combobox-timezones-group-{{ loop.index0 }}">
<div role="heading" id="demo-combobox-timezones-group-{{ loop.index0 }}">{{ group.label }}</div>
{% for timezone in group.timezones %}
<div role="option" data-value="{{ timezone.value}}">
{{ timezone.label }}
</div>
{% endfor %}
</div>
{% endfor %}
</div>
<hr role="separator">
<div role="option">
{% lucide "circle-plus" %}
Create timezone
</div>
{% endcall %}
{% endset %}
{{ code_preview("combobox-with-groups", code | prettyHtml) }}
<h3 id="example-top"><a href="#example-top">Top side</a></h3>
{% set options_frameworks = ["Next.js", "SvelteKit", "Nuxt.js", "Remix", "Astro"] %}
{% set code %}
{% call select(
trigger_attrs={"class": "w-[200px]"},
listbox_attrs={"data-empty": "No framework found."},
is_combobox=true,
search_placeholder="Search framework...",
popover_attrs={"data-side": "top"}
) %}
{% for framework in options_frameworks %}
<div role="option" data-value="{{ framework }}">{{ framework }}</div>
{% endfor %}
{% endcall %}{% endset %}
{{ code_preview("combobox", code | prettyHtml) }}
-177
View File
@@ -1,177 +0,0 @@
---
layout: layouts/page.njk
title: Dialog
description: A window overlaid on either the primary window or another dialog window, rendering the content underneath inert.
toc:
- label: Usage
id: usage
children:
- label: HTML
id: usage-html
- label: Jinja and Nunjucks
id: usage-macro
- label: Examples
id: examples
children:
- label: Scrollable content
id: example-scrollable-content
---
{% from "macros/code_block.njk" import code_block %}
{% from "macros/code_preview.njk" import code_preview %}
{% from "dialog.njk" import dialog %}
{% set code_html %}
{% set footer %}
<button class="btn-outline" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn" onclick="this.closest('dialog').close()">Save changes</button>
{% endset %}
{% call dialog(
id="demo-dialog-edit-profile",
title="Edit profile",
description="Make changes to your profile here. Click save when you're done.",
trigger="Edit Profile",
trigger_attrs={"class": "btn-outline"},
dialog_attrs={"class": "w-full sm:max-w-[425px] max-h-[612px]"},
footer=footer
) %}
<form class="form grid gap-4">
<div class="grid gap-3">
<label for="demo-dialog-edit-profile-name">Name</label>
<input type="text" value="Pedro Duarte" id="demo-dialog-edit-profile-name" autofocus/>
</div>
<div class="grid gap-3">
<label for="demo-dialog-edit-profile-username">Username</label>
<input type="text" value="@peduarte" id="demo-dialog-edit-profile-username" />
</div>
</form>
{% endcall %}
{% endset %}
{{ code_preview("dialog", code_html | prettyHtml) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<h3 id="usage-html"><a href="#usage-html">HTML</a></h3>
{{ code_block(code_html | prettyHtml, class="") }}
<section class="prose">
<p>The component has the following HTML structure:</p>
<dl>
<dt><code class="highlight language-html">&lt;button type="button"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The trigger button to open the dialog (using <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal" target="_blank">the native <code>showModal()</code> method</a>).</dd>
<dt><code class="highlight language-html">&lt;dialog class="dialog" id="{ DIALOG_ID }"&gt;</code></dt>
<dd>Wraps the whole component and acts as the backdrop. It can also have the following attributes:
<ul>
<li><code>aria-labelledby="{ TITLE_ID }"</code>: linked to by the <code>aria-labelledby</code> attribute of the <code>&lt;dialog&gt;</code>.</li>
<li><code>aria-describedby="{ DESCRIPTION_ID }"</code>: linked to by the <code>aria-describedby</code> attribute of the <code>&lt;dialog&gt;</code>.</li>
<li><code>onclick</code>: closes the dialog when the backdrop is clicked.</li>
</ul>
<dl>
<dt><code class="highlight language-html">&lt;article&gt;</code></dt>
<dd>Wraps the content of the dialog. It includes an <code>onclick</code> attribute to prevent from closing the dialog if we have an <code>onclick</code> event on <code>&lt;dialog&gt;</code>.
<dl>
<dt><code class="highlight language-html">&lt;header&gt;</code></dt>
<dd>Contains the header for the dialog:
<dl>
<dt><code class="highlight language-html">&lt;h2&gt;</code></dt>
<dd>The title of the dialog, must have an <code>id</code> if you use the <code>aria-labelledby</code> attribute on the <code>&lt;dialog&gt;</code>.</dd>
<dt><code class="highlight language-html">&lt;p&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The description of the dialog, must have an <code>id</code> if you use the <code>aria-describedby</code> attribute on the <code>&lt;dialog&gt;</code>.</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>For the content.</dd>
<dt><code class="highlight language-html">&lt;footer&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Usually contains actions.</dd>
<dt><code class="highlight language-html">&lt;form method="dialog"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>A form that contains a single button to close the dialog.</dd>
</dl>
</dd>
</dl>
</dd>
</dl>
</section>
<h3 id="usage-macro"><a href="#usage-macro">Jinja and Nunjucks</a></h3>
<div class="prose">
<p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">dialog()</code> Nunjucks or Jinja macro for this component.</p>
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/jinja/dialog.html.jinja" target="_blank">
Jinja macro
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/dialog.njk" target="_blank">
Nunjucks macro
{% lucide "arrow-right" %}
</a>
</div>
{% set raw_code %}{% raw %}{% set code_html %}
{% set footer %}
<button class="btn-outline" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn" onclick="this.closest('dialog').close()">Save changes</button>
{% endset %}
{% call dialog(
id="demo-dialog-edit-profile",
title="Edit profile",
description="Make changes to your profile here. Click save when you're done.",
trigger="Edit Profile",
trigger_attrs={"class": "btn-outline"},
dialog_attrs={"class": "w-full sm:max-w-[425px] max-h-[612px]"},
footer=footer
) %}
<form class="form grid gap-4">
<div class="grid gap-3">
<label for="demo-dialog-edit-profile-name">Name</label>
<input type="text" value="Pedro Duarte" id="demo-dialog-edit-profile-name" />
</div>
<div class="grid gap-3">
<label for="demo-dialog-edit-profile-username">Username</label>
<input type="text" value="@peduarte" id="demo-dialog-edit-profile-username" />
</div>
</form>
{% endcall %}{% endraw %}{% endset%}
{{ code_block(raw_code, "jinja") }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-scrollable-content"><a href="#example-scrollable-content">Scrollable content</a></h3>
{% set code %}
{% set footer %}
<button class="btn-outline" onclick="this.closest('dialog').close()">Close</button>
{% endset %}
{% call dialog(
id="dialog-example",
title="Scrollable Content",
description="This is a dialog with scrollable content.",
trigger="Scrollable Content",
trigger_attrs={"class": "btn-outline"},
dialog_attrs={"class": "w-full sm:max-w-[425px] max-h-[612px]"},
dialog_attrs={"class": "overflow-y-auto scrollbar"},
footer=footer
) %}
<div class="space-y-4 text-sm">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</div>
{% endcall %}
{% endset %}
{{ code_preview("dialog-scrollable-content", code | prettyHtml) }}
-376
View File
@@ -1,376 +0,0 @@
---
layout: layouts/page.njk
title: Dropdown Menu
description: Displays a menu to the user — such as a set of actions or functions — triggered by a button.
toc:
- label: Usage
id: usage
children:
- label: HTML + JavaScript
id: usage-html-js
children:
- label: "Step 1: Include the JavaScript files"
id: usage-html-js-1
- label: "Step 2: Add your dropdown menu HTML"
id: usage-html-js-2
- label: HTML structure
id: usage-html-js-3
- label: Jinja and Nunjucks
id: usage-macro
- label: Examples
id: examples
children:
- label: Checkboxes
id: example-checkboxes
- label: Radio Group
id: example-radio-group
---
{% from "macros/code_block.njk" import code_block %}
{% from "macros/code_preview.njk" import code_preview %}
{% from "dropdown-menu.njk" import dropdown_menu %}
{% set code_html %}
{% call dropdown_menu(
id="demo-dropdown-menu",
trigger="Open",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "min-w-56"}
) %}
<div role="group" aria-labelledby="account-options">
<div role="heading" id="account-options">My Account</div>
<div role="menuitem">
Profile
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
<div role="menuitem">
Billing
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
</div>
<div role="menuitem">
Settings
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
</div>
<div role="menuitem">
Keyboard shortcuts
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘K</span>
</div>
</div>
<hr role="separator">
<div role="menuitem">
GitHub
</div>
<div role="menuitem">
Support
</div>
<div role="menuitem" aria-disabled="true">
API
</div>
<hr role="separator">
<div role="menuitem">
Logout
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
{% endcall %}
{% endset %}
{{ code_preview("dropdown-menu", code_html | prettyHtml) }}
<h2 id="usage">Usage</h2>
<h3 id="usage-html-js"><a href="#usage-html-js">HTML + JavaScript</a></h3>
<h4 id="usage-html-js-1"><a href="#usage-html-js-1">Step 1: Include the JavaScript files</a></h4>
<section class="prose">
<p>You can either <a href="/installation/#install-cdn-all">include the JavaScript file for all the components</a>, or just the one for this component by adding this to the <code>&lt;head&gt;</code> of your page:</p>
</section>
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ site.version }}/dist/js/dropdown-menu.min.js" defer></script>{% endset %}
{{ code_block(code_script | prettyHtml, "html") }}
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-js">
Components with JavaScript
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="/installation/#install-cli">
Use the CLI
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/js/dropdown-menu.js" target="_blank">
dropdown-menu.js
{% lucide "arrow-right" %}
</a>
</div>
<h4 id="usage-html-js-2"><a href="#usage-html-js-2">Step 2: Add your dropdown menu HTML</a></h4>
{{ code_block(code_html | prettyHtml, "html") }}
<h4 id="usage-html-js-3"><a href="#usage-html-js-3">HTML structure</a></h4>
<section class="prose">
<dl>
<dt><code class="highlight language-html">&lt;div class="dropdown-menu"&gt;</code></dt>
<dd>Wraps around the entire component.
<dl>
<dt><code class="highlight language-html">&lt;button type="button" popovertarget="{ POPOVER_ID }"&gt;</code></dt>
<dd>
<p>The trigger to open the popover, can also have the following attributes:</p>
<ul>
<li><code>id="{BUTTON_ID}"</code>: linked to by the <code>aria-labelledby</code> attribute of the listbox.</li>
<li><code>aria-haspopup="menu"</code>: indicates that the button opens a menu.</li>
<li><code>aria-controls="{ MENU_ID }"</code>: points to the menu's id.</li>
<li><code>aria-expanded="false"</code>: tracks the popover's state.</li>
</ul>
</dd>
<dt><code class="highlight language-html">&lt;div popover class="popover" id="{ POPOVER_ID }"&gt;</code></dt>
<dd>As with the <a href="/components/popover">Popover</a> component, you can set up the side and alignment of the popover using the <code>data-side</code> and <code>data-align</code> attributes.
<dl>
<dt><code class="highlight language-html">&lt;div role="menu"&gt;</code></dt>
<dd>The menu containing the options. Should have the following attributes:
<ul>
<li><code>id="{ MENU_ID }"</code>: refered to by the <code>aria-controls</code> attribute of the trigger.</li>
<li><code>aria-labelledby="{ BUTTON_ID }"</code>: linked to by the button's <code>id</code> attribute.</li>
</ul>
<dl>
<dt><code class="highlight language-html">&lt;button role="menuitem"&gt;</code></dt>
<dd>Menu item.</dd>
<dt><code class="highlight language-html">&lt;button role="menuitemcheckbox"&gt;</code></dt>
<dd>Menu item with a checkbox.</dd>
<dt><code class="highlight language-html">&lt;button role="menuitemradio"&gt;</code></dt>
<dd>Menu item with a radio button.</dd>
<dt><code class="highlight language-html">&lt;hr role="separator"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Separator between groups/options.</dd>
<dt><code class="highlight language-html">&lt;div role="group"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Group of options, can have a <code>aria-labelledby</code> attribute to link to a heading.</dd>
<dt><code class="highlight language-html">&lt;span role="heading"&gt;</code></dt>
<dd>Group heading, must have an <code>id</code> attribute if you use the <code>aria-labelledby</code> attribute on the group.</dd>
</dl>
</dd>
</dl>
</dl>
</dd>
</dl>
</section>
<h3 id="usage-macro"><a href="#usage-macro">Jinja and Nunjucks</a></h3>
<div class="prose">
<p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">dropdown_menu()</code> Nunjucks or Jinja macro for this component.</p>
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/jinja/dropdown-menu.html.jinja" target="_blank">
Jinja macro
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/dropdown-menu.njk" target="_blank">
Nunjucks macro
{% lucide "arrow-right" %}
</a>
</div>
{% set raw_code %}{% raw %}{% call dropdown_menu(
id="dropdown-menu",
trigger="Open",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "min-w-56"}
) %}
<div role="group" aria-labelledby="account-options">
<div role="heading" id="account-options">My Account</div>
<div role="menuitem">
Profile
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
<div role="menuitem">
Billing
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
</div>
<div role="menuitem">
Settings
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
</div>
<div role="menuitem">
Keyboard shortcuts
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘K</span>
</div>
</div>
<hr role="separator">
<div role="menuitem">
GitHub
</div>
<div role="menuitem">
Support
</div>
<div role="menuitem" disabled>
API
</div>
<hr role="separator">
<div role="menuitem">
Logout
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
{% endcall %}{% endraw %}{% endset%}
{{ code_block(raw_code, "jinja") }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-checkboxes"><a href="#example-checkboxes">Checkboxes</a></h3>
{% set code %}{% call dropdown_menu(
id="dropdown-menu-checkboxes",
trigger="Open",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "min-w-56"}
) %}
<div role="group" aria-labelledby="account-options">
<div role="heading" id="account-options">Account Options</div>
<div role="menuitem">
{% lucide "user" %}
Profile
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
<div role="menuitem">
{% lucide "credit-card" %}
Billing
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
</div>
<div role="menuitem">
{% lucide "settings" %}
Settings
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
</div>
</div>
<hr role="separator">
<div role="group" aria-labelledby="appearance-options">
<div role="heading" id="appearance-options">Appearance</div>
<div
role="menuitemcheckbox"
aria-checked="true"
class="group"
x-data="{ checked: true }"
@click="checked = !checked"
:aria-checked="checked"
>
{% lucide "check", {
"class": "invisible group-aria-checked:visible",
"aria-hidden": "true",
"focusable": "false"
} %}
Status Bar
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
<div
role="menuitemcheckbox"
aria-checked="false"
class="group"
disabled
x-data="{ checked: false }"
@click="checked = !checked"
:aria-checked="checked"
>
{% lucide "check", {
"class": "invisible group-aria-checked:visible",
"aria-hidden": "true",
"focusable": "false"
} %}
Activity Bar
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
</div>
<div
role="menuitemcheckbox"
aria-checked="false"
class="group"
x-data="{ checked: false }"
@click="checked = !checked"
:aria-checked="checked"
>
{% lucide "check", {
"class": "invisible group-aria-checked:visible",
"aria-hidden": "true",
"focusable": "false"
} %}
Panel
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
</div>
</div>
<hr role="separator">
<div role="menuitem">
{% lucide "log-out" %}
Logout
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
{% endcall %}{% endset %}
{{ code_preview("dropdown-menu-checkboxes", code | prettyHtml) }}
<h3 id="example-radio-group"><a href="#example-radio-group">Radio Group</a></h3>
{% set code %}{% call dropdown_menu(
id="dropdown-menu-radio-group",
trigger="Open",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "min-w-56"}
) %}
<div role="group" aria-labelledby="position-options" x-data="{ checked: 'bottom' }">
<span id="position-options" role="heading">Panel Position</span>
<hr role="separator">
<div
role="menuitemradio"
aria-checked="false"
class="group"
@click="checked = 'top'"
:aria-checked="checked == 'top'"
>
<div class="size-4 flex items-center justify-center">
<div
class="size-2 rounded-full bg-foreground invisible group-aria-checked:visible"
aria-hidden="true",
focusable="false"
></div>
</div>
Status Bar
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</div>
<div
role="menuitemradio"
aria-checked="false"
class="group"
@click="checked = 'bottom'"
:aria-checked="checked == 'bottom'"
>
<div class="size-4 flex items-center justify-center">
<div
class="size-2 rounded-full bg-foreground invisible group-aria-checked:visible"
aria-hidden="true",
focusable="false"
></div>
</div>
Activity Bar
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
</div>
<div
role="menuitemradio"
aria-checked="false"
class="group"
@click="checked = 'right'"
:aria-checked="checked == 'right'"
>
<div class="size-4 flex items-center justify-center">
<div
class="size-2 rounded-full bg-foreground invisible group-aria-checked:visible"
aria-hidden="true",
focusable="false"
></div>
</div>
Panel
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
</div>
</div>
{% endcall %}{% endset %}
{{ code_preview("dropdown-menu", code | prettyHtml) }}
-79
View File
@@ -1,79 +0,0 @@
---
layout: layouts/page.njk
title: Input
description: Displays a form input field or a component that looks like an input field.
toc:
- label: Usage
id: usage
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<form class="form grid gap-6">
<div class="grid gap-2">
<label for="demo-form-text">Username</label>
<input type="text" id="demo-form-text" placeholder="hunvreus">
<p class="text-muted-foreground text-sm">This is your public display name.</p>
</div>
<div class="grid gap-2">
<label for="demo-form-select">Email</label>
<select id="demo-form-select">
<option value="bob@example.com">m@example.com</option>
<option value="alice@example.com">m@google.com</option>
<option value="john@example.com">m@support.com</option>
</select>
<p class="text-muted-foreground text-sm">You can manage email addresses in your email settings.</p>
</div>
<div class="grid gap-2">
<label for="demo-form-text">Bio</label>
<textarea id="demo-form-textarea" placeholder="I like to..." rows="3"></textarea>
<p class="text-muted-foreground text-sm">You can @mention other users and organizations.</p>
</div>
<div class="grid gap-2">
<label for="demo-form-date">Date of birth</label>
<input type="date" id="demo-form-date">
<p class="text-muted-foreground text-sm">Your date of birth is used to calculate your age.</p>
</div>
<div class="flex flex-col gap-3">
<label for="demo-form-radio">Notify me about...</label>
<fieldset id="demo-form-radio" class="grid gap-3">
<label class="font-normal"><input type="radio" name="demo-form-radio" value="1" checked>All new messages</label>
<label class="font-normal"><input type="radio" name="demo-form-radio" value="2">Direct messages and mentions</label>
<label class="font-normal"><input type="radio" name="demo-form-radio" value="3" >Nothing</label>
</fieldset>
</div>
<section class="grid gap-4">
<h3 class="text-lg font-medium">Email Notifications</h3>
<div class="gap-2 flex flex-row items-start justify-between rounded-lg border p-4 shadow-xs">
<div class="flex flex-col gap-0.5">
<label for="demo-form-switch" class="leading-normal">Marketing emails</label>
<p class="text-muted-foreground text-sm">Receive emails about new products, features, and more.</p>
</div>
<input type="checkbox" id="demo-form-switch" role="switch">
</div>
<div class="gap-2 flex flex-row items-start justify-between rounded-lg border p-4 shadow-xs">
<div class="flex flex-col gap-0.5 opacity-60">
<label for="demo-form-switch-disabled" class="leading-normal">Security emails</label>
<p class="text-muted-foreground text-sm">Receive emails about your account security.</p>
</div>
<input type="checkbox" id="demo-form-switch-disabled" role="switch" disabled>
</div>
</section>
<button type="submit" class="btn">Submit</button>
</form>{% endset %}
{{ code_preview("input", code, "w-full max-w-sm") }}
<h2 id="usage">Usage</h2>
<section class="prose">
<p>Adding the <code>form</code> class to the parent of any form element (<code>&lt;label&gt;</code>, <code>&lt;input&gt;</code>, <code>&lt;textarea&gt;</code>, <code>&lt;select&gt;</code>) will automatically apply the style you would get from adding <code>label</code>, <code>input</code>, <code>textarea</code>, or <code>select</code> classes. Read more about <a href="/components/label">Label</a>, <a href="/components/input">Input</a>, <a href="/components/textarea">Textarea</a>, and <a href="/components/select">Select</a>.</p>
</section>
{{ code_block(code) }}
-90
View File
@@ -1,90 +0,0 @@
---
layout: layouts/page.njk
title: Input
description: Displays a form input field or a component that looks like an input field.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Default
id: example-default
- label: Invalid
id: example-invalid
- label: Disabled
id: example-disabled
- label: With label
id: example-with-label
- label: With text
id: example-with-text
- label: With button
id: example-with-button
- label: Form
id: example-form
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<input class="input" type="email" placeholder="Email">{% endset %}
{{ code_preview("input", code) }}
<h2 id="usage">Usage</h2>
<section class="prose">
<p>Simply add the <code>input</code> class to your <code>&lt;input&gt;</code> element or have a parent with the <code>form</code> class (<a href="/components/form">read more about form</a>). You can also set the <code>aria-invalid</code> attribute to <code>true</code> to make the input invalid.</p>
</section>
{{ code_block(code) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
{{ code_preview("input-default", code) }}
<h3 id="example-invalid"><a href="#example-invalid">Invalid</a></h3>
{% set code %}<input class="input" type="email" placeholder="Email" aria-invalid="true">{% endset %}
{{ code_preview("input-invalid", code) }}
<h3 id="example-disabled"><a href="#example-disabled">Disabled</a></h3>
{% set code %}<input class="input" type="email" placeholder="Email" disabled>{% endset %}
{{ code_preview("input-disabled", code) }}
<h3 id="example-with-label"><a href="#example-with-label">With label</a></h3>
{% set code %}<div class="grid gap-3">
<label for="input-with-label" class="label">Label</label>
<input class="input" id="input-with-label" type="email" placeholder="Email">
</div>{% endset %}
{{ code_preview("input-with-label", code) }}
<h3 id="example-with-text"><a href="#example-with-text">With text</a></h3>
{% set code %}<div class="grid gap-3">
<label for="input-with-text" class="label">Label</label>
<input class="input" id="input-with-text" type="email" placeholder="Email">
<p class="text-muted-foreground text-sm">Fill in your email address.</p>
</div>{% endset %}
{{ code_preview("input-with-text", code) }}
<h3 id="example-with-button"><a href="#example-with-button">With button</a></h3>
{% set code %}<div class="flex items-center space-x-2">
<input class="input" type="email" placeholder="Email">
<button type="submit" class="btn">Submit</button>
</div>{% endset %}
{{ code_preview("textarea-form", code) }}
<h3 id="example-form"><a href="#example-form">Form</a></h3>
{% set code %}<form class="form space-y-6 w-full">
<div class="grid gap-3">
<label for="input-form" class="label">Username</label>
<input class="input" id="input-form" type="text" placeholder="hunvreus">
<p class="text-muted-foreground text-sm">This is your public display name.</p>
</div>
<button type="submit" class="btn">Submit</button>
</form>{% endset %}
{{ code_preview("input-form", code, "w-full max-w-sm") }}
-41
View File
@@ -1,41 +0,0 @@
---
layout: layouts/page.njk
title: Label
description: Renders an accessible label associated with controls.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Disabled
id: example-disabled
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<label class="label gap-3">
<input type="checkbox" class="input">
Accept terms and conditions
</label>{% endset %}
{{ code_preview("label", code) }}
<h2 id="usage">Usage</h2>
<section class="prose">
<p>Simply add the <code>label</code> class to your <code>&lt;label&gt;</code> element or have a parent with the <code>form</code> class (<a href="/components/form">read more about form</a>). Labels' style may be affected by the presence of a disabled peer or child element.</p>
</section>
{% set code %}<label class="label" for="email">Your email address</label>{% endset %}
{{ code_block(code) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-disabled"><a href="#example-disabled">Disabled</a></h3>
{% set code %}<div class="grid gap-3">
<label class="label" for="email">Email</label>
<input class="input" id="email" type="email" placeholder="Email" disabled>
</div>{% endset %}
{{ code_preview("label-disabled", code) }}
-48
View File
@@ -1,48 +0,0 @@
---
layout: layouts/page.njk
title: Pagination
description: Pagination with page navigation, next and previous links.
toc:
- label: Usage
id: usage
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
<div class="alert mb-6">
{% lucide "circle-alert" %}
<h2>There is no dedicated pagination component in Basecoat.</h2>
</div>
{% set code %}<nav role="navigation" aria-label="pagination" class="mx-auto flex w-full justify-center">
<ul class="flex flex-row items-center gap-1">
<li>
<a href="#" class="btn-ghost">{% lucide "chevron-left" %} Previous</a>
</li>
<li>
<a href="#" class="btn-icon-ghost">1</a>
</li>
<li>
<a href="#" class="btn-icon-outline">2</a>
</li>
<li>
<a href="#" class="btn-icon-ghost">3</a>
</li>
<li>
<div class="size-9 flex items-center justify-center">{% lucide "ellipsis", { "class": "size-4 shrink-0" } %}</div>
</li>
<li>
<a href="#" class="btn-ghost">Next {% lucide "chevron-right" %}</a>
</li>
</ul>
</nav>{% endset %}
{{ code_preview("pagination", code, "max-w-md") }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>A pagination is fairly simple to implement with Tailwind using the Basecoat button classes:</p>
</section>
{{ code_block(code) }}
-160
View File
@@ -1,160 +0,0 @@
---
layout: layouts/page.njk
title: Popover
description: Displays rich content in a portal, triggered by a button.
toc:
- label: Usage
id: usage
children:
- label: HTML + JavaScript
id: usage-html-js
children:
- label: "Step 1 (Optional): Include the JavaScript file"
id: usage-html-js-1
- label: "Step 2: Add your popover HTML"
id: usage-html-js-2
- label: HTML structure
id: usage-html-js-3
- label: Jinja and Nunjucks
id: usage-macro
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% from "popover.njk" import popover %}
{% set code_html %}
{% call popover(
id="demo-popover",
trigger="Open popover",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "w-80"}
) %}
<div class="grid gap-4">
<header class="grid gap-1.5">
<h4 class="leading-none font-medium">Dimensions</h4>
<p class="text-muted-foreground text-sm">Set the dimensions for the layer.</p>
</header>
<form class="form grid gap-2">
<div class="grid grid-cols-3 items-center gap-4">
<label for="demo-popover-width">Width</label>
<input type="text" id="demo-popover-width" value="100%" class="col-span-2 h-8" autofocus/>
</div>
<div class="grid grid-cols-3 items-center gap-4">
<label for="demo-popover-max-width">Max. width</label>
<input type="text" id="demo-popover-max-width" value="300px" class="col-span-2 h-8"/>
</div>
<div class="grid grid-cols-3 items-center gap-4">
<label for="demo-popover-height">Height</label>
<input type="text" id="demo-popover-height" value="25px" class="col-span-2 h-8"/>
</div>
<div class="grid grid-cols-3 items-center gap-4">
<label for="demo-popover-max-height">Max. height</label>
<input type="text" id="demo-popover-max-height" value="none" class="col-span-2 h-8"/>
</div>
</form>
</div>
{% endcall %}
{% endset %}
{{ code_preview("popover", code_html | prettyHtml) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<h3 id="usage-html-js"><a href="#usage-html-js">HTML + JavaScript</a></h3>
<h4 id="usage-html-js-1"><a href="#usage-html-js-1">Step 1: Include the JavaScript file</a></h4>
<section class="prose">
<p>You can either <a href="/installation/#install-cdn-all">include the JavaScript file for all the components</a>, or just the one for this component by adding this to the <code>&lt;head&gt;</code> of your page:</p>
</section>
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ site.version }}/dist/js/popover.min.js" defer></script>{% endset %}
{{ code_block(code_script | prettyHtml, "html") }}
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-js">
Components with JavaScript
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="/installation/#install-cli">
Use the CLI
{% lucide "arrow-right" %}
</a>
</div>
<h4 id="usage-html-js-2"><a href="#usage-html-js-2">Step 2: Add your popover HTML</a></h4>
{{ code_block(code_html | prettyHtml, "html") }}
<h4 id="usage-html-js-3"><a href="#usage-html-js-3">HTML structure</a></h4>
<section class="prose">
<dl>
<dt><code class="highlight language-html">&lt;button type="button" popovertarget="{ POPOVER_ID }"&gt;</code></dt>
<dd>
<p>The trigger to open the popover, can also have the following attributes:</p>
<ul>
<li><code>id="{BUTTON_ID}"</code>: linked to by the <code>aria-labelledby</code> attribute of the listbox.</li>
<li><code>aria-expanded="false"</code>: tracks the popover's state.</li>
<li><code>onclick</code>: a simple function to update <code>aria-expanded</code>.</li>
</ul>
</dd>
<dt><code class="highlight language-html">&lt;div popover class="popover" id="{ POPOVER_ID }"&gt;</code></dt>
<dd>As with the <a href="/components/popover">Popover</a> component, you can set up the side and alignment of the popover using the <code>data-side</code> and <code>data-align</code> attributes.</dd>
</dl>
</section>
<h3 id="usage-macro"><a href="#usage-macro">Jinja and Nunjucks</a></h3>
<div class="prose">
<p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">popover()</code> Nunjucks or Jinja macro for this component.</p>
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/jinja/popover.html.jinja" target="_blank">
Jinja macro
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/popover.njk" target="_blank">
Nunjucks macro
{% lucide "arrow-right" %}
</a>
</div>
{% set raw_code %}{% raw %}{% call popover(
id="demo-popover",
trigger="Open popover",
trigger_attrs={"class": "btn-outline"},
popover_attrs={"class": "w-80 p-4"}
) %}
<div class="grid gap-4">
<header class="grid gap-1.5">
<h4 class="leading-none font-medium">Dimensions</h4>
<p class="text-muted-foreground text-sm">Set the dimensions for the layer.</p>
</header>
<form class="form grid gap-2">
<div class="grid grid-cols-3 items-center gap-4">
<label for="demo-popover-width">Width</label>
<input type="text" id="demo-popover-width" value="100%" class="col-span-2 h-8"/>
</div>
<div class="grid grid-cols-3 items-center gap-4">
<label for="demo-popover-max-width">Max. width</label>
<input type="text" id="demo-popover-max-width" value="300px" class="col-span-2 h-8"/>
</div>
<div class="grid grid-cols-3 items-center gap-4">
<label for="demo-popover-height">Height</label>
<input type="text" id="demo-popover-height" value="25px" class="col-span-2 h-8"/>
</div>
<div class="grid grid-cols-3 items-center gap-4">
<label for="demo-popover-max-height">Max. height</label>
<input type="text" id="demo-popover-max-height" value="none" class="col-span-2 h-8"/>
</div>
</form>
</div>
{% endcall %}{% endraw %}{% endset %}
{{ code_block(raw_code, "jinja") }}
-48
View File
@@ -1,48 +0,0 @@
---
layout: layouts/page.njk
title: Radio Group
description: A set of checkable buttons—known as radio buttons—where no more than one of the buttons can be checked at a time.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Form
id: example-form
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<fieldset class="grid gap-3">
<label class="label"><input type="radio" name="radio-group" value="default" class="input">Default</label>
<label class="label"><input type="radio" name="radio-group" value="comfortable" class="input" checked>Comfortable</label>
<label class="label"><input type="radio" name="radio-group" value="compact" class="input">Compact</label>
</fieldset>{% endset %}
{{ code_preview("radio-group", code) }}
<h2 id="usage">Usage</h2>
<section class="prose">
<p>Simply add the <code>input</code> class to your <code>&lt;input type="radio"&gt;</code> elements or have a parent with the <code>form</code> class (<a href="/components/form">read more about form</a>). You can also set the <code>aria-invalid</code> attribute to <code>true</code> to make the input invalid.</p>
</section>
{{ code_block(code) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-form"><a href="#example-form">Form</a></h3>
{% set code %}<form class="form space-y-6 w-full">
<div class="flex flex-col gap-3">
<label for="demo-form-radio">Notify me about...</label>
<fieldset id="demo-form-radio" class="grid gap-3">
<label class="font-normal"><input type="radio" name="demo-form-radio" value="1">All new messages</label>
<label class="font-normal"><input type="radio" name="demo-form-radio" value="2">Direct messages and mentions</label>
<label class="font-normal"><input type="radio" name="demo-form-radio" value="3" >Nothing</label>
</fieldset>
</div>
<button type="submit" class="btn">Submit</button>
</form>{% endset %}
{{ code_preview("radio-group-form", code, "w-full max-w-sm") }}
-252
View File
@@ -1,252 +0,0 @@
---
layout: layouts/page.njk
title: Select
description: Displays a list of options for the user to pick from—triggered by a button.
toc:
- label: Usage
id: usage
children:
- label: HTML
id: usage-html
- label: HTML + JavaScript
id: usage-html-js
children:
- label: "Step 1: Include the JavaScript files"
id: usage-html-js-1
- label: "Step 2: Add your select HTML"
id: usage-html-js-2
- label: HTML structure
id: usage-html-js-3
- label: Jinja and Nunjucks
id: usage-macro
- label: Examples
id: examples
children:
- label: Scrollable
id: example-scrollable
- label: Disabled
id: example-disabled
- label: With icon
id: example-with-icon
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% from "select.njk" import select %}
{% set code_native %}<select class="select w-[180px]">
<optgroup label="Fruits">
<option>Apple</option>
<option>Banana</option>
<option>Blueberry</option>
<option>Grapes</option>
<option>Pineapple</option>
</optgroup>
</select>{% endset %}
{{ code_preview("select-native", code_native) }}
{% set code_html %}
{{ select(
trigger_attrs={"class": "w-[180px]"},
is_combobox=true,
items=[
{
type: "group",
label: "Fruits",
items: [
{ type: "item", value: "apple", label: "Apple" },
{ type: "item", value: "banana", label: "Banana" },
{ type: "item", value: "blueberry", label: "Blueberry" },
{ type: "item", value: "pineapple", label: "Grapes" },
{ type: "item", value: "pineapple", label: "Pineapple" }
]
}
]
) }}
{% endset %}
{% set code_script %}
{% endset %}
{{ code_preview("select", code_script | prettyHtml ~ code_html | prettyHtml) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<h3 id="usage-html"><a href="#usage-html">HTML</a></h3>
<section class="prose">
<p>If you use a <code>&lt;select&gt;</code> element, just add the <code>select</code> class to it or have a parent with the <code>form</code> class (<a href="/components/form">read more about form</a>).</p>
</section>
{{ code_block(code_native) }}
<h3 id="usage-html-js"><a href="#usage-html-js">HTML + JavaScript</a></h3>
<h4 id="usage-html-js-1"><a href="#usage-html-js-1">Step 1: Include the JavaScript files</a></h4>
<section class="prose">
<p>You can either <a href="/installation/#install-cdn-all">include the JavaScript file for all the components</a>, or just the one for this component by adding this to the <code>&lt;head&gt;</code> of your page:</p>
</section>
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ site.version }}/dist/js/select.min.js" defer></script>{% endset %}
{{ code_block(code_script | prettyHtml, "html") }}
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-js">
Components with JavaScript
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="/installation/#install-cli">
Use the CLI
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/js/select.js" target="_blank">
select.js
{% lucide "arrow-right" %}
</a>
</div>
<h4 id="usage-html-js-2"><a href="#usage-html-js-2">Step 2: Add your select HTML</a></h4>
{{ code_block(code_html | prettyHtml, "html") }}
<h4 id="usage-html-js-3"><a href="#usage-html-js-3">HTML structure</a></h4>
<section class="prose">
<dl>
<dt><code class="highlight language-html">&lt;div class="select"&gt;</code></dt>
<dd>Wraps around the entire component.
<dl>
<dt><code class="highlight language-html">&lt;button type="button" popovertarget="{ POPOVER_ID }"&gt;</code></dt>
<dd>
<p>The trigger to open the popover, can also have the following attributes:</p>
<ul>
<li><code>id="{BUTTON_ID}"</code>: linked to by the <code>aria-labelledby</code> attribute of the listbox.</li>
<li><code>aria-haspopup="listbox"</code>: indicates that the button opens a listbox.</li>
<li><code>aria-controls="{ LISTBOX_ID }"</code>: points to the listbox's id.</li>
<li><code>aria-expanded="false"</code>: tracks the popover's state.</li>
<li><code>aria-activedescendant="{ OPTION_ID }"</code>: points to the active option's id.</li>
</ul>
</dd>
<dt><code class="highlight language-html">&lt;div popover class="popover" id="{ POPOVER_ID }"&gt;</code></dt>
<dd>As with the <a href="/components/popover">Popover</a> component, you can set up the side and alignment of the popover using the <code>data-side</code> and <code>data-align</code> attributes.
<dl>
<dt><code class="highlight language-html">&lt;div role="listbox"&gt;</code></dt>
<dd>The listbox containing the options. Should have the following attributes:
<ul>
<li><code>id="{ LISTBOX_ID }"</code>: refered to by the <code>aria-controls</code> attribute of the trigger.</li>
<li><code>aria-labelledby="{ BUTTON_ID }"</code>: linked to by the button's <code>id</code> attribute.</li>
</ul>
<dl>
<dt><code class="highlight language-html">&lt;div role="option" data-value="{ VALUE }"&gt;</code></dt>
<dd>Option that can be selected.Should have a unique id if you use the <code>aria-activedescendant</code> attribute on the trigger.</dd>
<dt><code class="highlight language-html">&lt;hr role="separator"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Separator between groups/options.</dd>
<dt><code class="highlight language-html">&lt;div role="group"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>Group of options, can have a <code>aria-labelledby</code> attribute to link to a heading.</dd>
<dt><code class="highlight language-html">&lt;span role="heading"&gt;</code></dt>
<dd>Group heading, must have an <code>id</code> attribute if you use the <code>aria-labelledby</code> attribute on the group.</dd>
</dl>
</dd>
</dl>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;input type="hidden" name="{ NAME }" value="{ VALUE }"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The hidden input that holds the value of the field (if needed).</dd>
</dl>
</section>
<h3 id="usage-macro"><a href="#usage-macro">Jinja and Nunjucks</a></h3>
<div class="prose">
<p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">select()</code> Nunjucks or Jinja macro for this component.</p>
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/jinja/dialog.html.jinja" target="_blank">
Jinja macro
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/dialog.njk" target="_blank">
Nunjucks macro
{% lucide "arrow-right" %}
</a>
</div>
{% set raw_code %}{% raw %}{{ select(
items=[
{
type: "group",
label: "Fruits",
items: [
{ type: "item", value: "apple", label: "Apple" },
{ type: "item", value: "banana", label: "Banana" },
{ type: "item", value: "blueberry", label: "Blueberry" },
{ type: "item", value: "pineapple", label: "Grapes" },
{ type: "item", value: "pineapple", label: "Pineapple" }
]
}
]
) }}{% endraw %}{% endset %}
{{ code_block(raw_code, "jinja") }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-scrollable"><a href="#example-scrollable">Scrollable</a></h3>
{% set code %}
{% call select(
listbox_attrs={"class": "scrollbar overflow-y-auto max-h-64"}
) %}
{% for i in range(0, 99) %}
<div role="option" data-value="item-{{ i }}">Item {{ i }}</div>
{% endfor %}
{% endcall %}
{% endset %}
{{ code_preview("select-scrollable", code | prettyHtml) }}
<h3 id="example-disabled"><a href="#example-disabled">Disabled</a></h3>
{% set code %}
{% call select(
trigger_attrs={"disabled": "disabled"}
) %}
<div role="option" data-value="disabled">Disabled</div>
{% endcall %}
{% endset %}
{{ code_preview("select-disabled", code | prettyHtml) }}
<h3 id="example-with-icon"><a href="#example-with-icon">With icon</a></h3>
{% set code %}
{% call select(
trigger_attrs={'class': 'w-[180px]'}
) %}
<div type="button" role="option" data-value="bar">
<span class="flex items-center gap-2">
{% lucide "chart-bar", {"class": "text-muted-foreground"} %}
Bar
</span>
</div>
<div type="button" role="option" data-value="line">
<span class="flex items-center gap-2">
{% lucide "chart-line", {"class": "text-muted-foreground"} %}
Line
</span>
</div>
<div type="button" role="option" data-value="pie">
<span class="flex items-center gap-2">
{% lucide "chart-pie", {"class": "text-muted-foreground"} %}
Pie
</span>
</div>
{% endcall %}
{% endset %}
{{ code_preview("select-scrollable", code | prettyHtml) }}
-205
View File
@@ -1,205 +0,0 @@
---
layout: layouts/page.njk
title: Sidebar
description: A composable, themeable and customizable sidebar component.
toc:
- label: Usage
id: usage
children:
- label: HTML + JavaScript
id: usage-html-js
children:
- label: "Step 1: Include the JavaScript files"
id: usage-html-js-1
- label: "Step 2: Add your sidebar HTML"
id: usage-html-js-2
- label: HTML structure
id: usage-html-js-3
- label: Jinja and Nunjucks
id: usage-macro
---
{% from "macros/code_block.njk" import code_block %}
{% from "sidebar.njk" import sidebar %}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<h3 id="usage-html-js"><a href="#usage-html-js">HTML + JavaScript</a></h3>
<h4 id="usage-html-js-1"><a href="#usage-html-js-1">Step 1: Include the JavaScript files</a></h4>
<section class="prose">
<p>You can either <a href="/installation/#install-cdn-all">include the JavaScript file for all the components</a>, or just the one for this component by adding this to the <code>&lt;head&gt;</code> of your page:</p>
</section>
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ site.version }}/dist/js/sidebar.min.js" defer></script>{% endset %}
{{ code_block(code_script | prettyHtml, "html") }}
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-js">
Components with JavaScript
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="/installation/#install-cli">
Use the CLI
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/js/sidebar.js" target="_blank">
sidebar.js
{% lucide "arrow-right" %}
</a>
</div>
<h4 id="usage-html-js-2"><a href="#usage-html-js-2">Step 2: Add your sidebar HTML</a></h4>
{% set code_html %}
{% set icon_square_terminal %}{% lucide "square-terminal" %}{% endset %}
{% set icon_bot %}{% lucide "bot" %}{% endset %}
{% set icon_settings %}{% lucide "settings" %}{% endset %}
{% set menu = [
{ type: "group", label: "Getting started", items: [
{ label: "Playground", url: "#", icon: icon_square_terminal },
{ label: "Models", url: "#", icon: icon_bot },
{ label: "Settings", type: "submenu", icon: icon_settings, items: [
{ label: "General", url: "#" },
{ label: "Team", url: "#" },
{ label: "Billing", url: "#" },
{ label: "Limits", url: "#" }
] }
]}
] %}
{{ sidebar(
label="Sidebar navigation",
content_attrs={
"class": "scrollbar"
},
menu=menu
) }}
<main>
<button type="button" onclick="document.dispatchEvent(new CustomEvent('basecoat:sidebar'))">Toggle sidebar</button>
<h1>Content</h1>
</main>
{% endset %}
{{ code_block(code_html | prettyHtml, "html") }}
<section class="prose">
<p>Any element on the page can toggle, open or close the sidebar by dispatching the <code>basecoat:sidebar</code>. By default, the event will toggle the sidebar, but can be used to open or close if you add an <code>action</code> to the detail:</p>
</section>
{% set code_trigger %}<button type="button" onclick="document.dispatchEvent(new CustomEvent('basecoat:sidebar'));">Toggle sidebar</button>
<button type="button" onclick="document.dispatchEvent(new CustomEvent('basecoat:sidebar', { detail: { action: 'open' } }));">Open sidebar</button>
<button type="button" onclick="document.dispatchEvent(new CustomEvent('basecoat:sidebar', { detail: { action: 'close' } }));">Close sidebar</button>{% endset %}
{{ code_block(code_trigger | prettyHtml, "html") }}
<h4 id="usage-html-js-3"><a href="#usage-html-js-3">HTML structure</a></h4>
<section class="prose">
<dl>
<dt><code class="highlight language-html">&lt;aside class="sidebar" aria-hidden="false"&gt;</code></dt>
<dd>Wraps around the entire component. It can have the following attributes:
<ul>
<li><code>aria-hidden="true"</code>: controls the default state of the sidebar (hidden or visible).</li>
<li><code>data-side="left"</code>: specifies the side of the sidebar (<code>left</code> or <code>right</code>, defaults to <code>left</code>).</li>
</ul>
</dd>
<dl>
<dt><code class="highlight language-html">&lt;nav&gt;</code></dt>
<dd>
<p>The navigation element that contains the sidebar's content. It can have the following attributes:</p>
<ul>
<li><code>id="{BUTTON_ID}"</code>: linked to by the <code>aria-labelledby</code> attribute of the listbox.</li>
<li><code>aria-haspopup="menu"</code>: indicates that the button opens a menu.</li>
<li><code>aria-controls="{ MENU_ID }"</code>: points to the menu's id.</li>
<li><code>aria-expanded="false"</code>: tracks the popover's state.</li>
</ul>
<dl>
<dt><code class="highlight language-html">&lt;header&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The header of the sidebar.</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code></dt>
<dd>The main navigation list.
<dl>
<dt><code class="highlight language-html">&lt;div role="group"&gt;</code></dt>
<dd>Group of options, can have a <code>aria-labelledby</code> attribute to link to a heading.</dd>
<dt><code class="highlight language-html">&lt;span role="heading"&gt;</code></dt>
<dd>Group heading, must have an <code>id</code> attribute if you use the <code>aria-labelledby</code> attribute on the group.</dd>
<dt><code class="highlight language-html">&lt;ul&gt;</code></dt>
<dd>List of links or buttons.
<dl>
<dt><code class="highlight language-html">&lt;li&gt;</code></dt>
<dd>Individual item.
<dl>
<dt><code class="highlight language-html">&lt;a&gt;</code></dt>
<dd>A link. By default, clicking on a link will close the sidebar on mobile unless the <code>data-keep-mobile-sidebar-open</code> attribute is present.</dd>
<dt><code class="highlight language-html">&lt;button&gt;</code></dt>
<dd>A button. By default, clicking on a button will close the sidebar on mobile unless the <code>data-keep-mobile-sidebar-open</code> attribute is present.</dd>
<dt><code class="highlight language-html">&lt;details&gt;</code></dt>
<dd>Collapsible section.
<dl>
<dt><code class="highlight language-html">&lt;summary&gt;</code></dt>
<dd>Summary of the collapsible section.</dd>
<dt><code class="highlight language-html">&lt;ul&gt;</code></dt>
<dd>List of links or buttons.</dd>
</dl>
</dd>
</dl>
</dd>
</dl>
</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;footer&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The footer of the sidebar.</dd>
</dl>
</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;main&gt;</code></dt>
<dd>A wrapper for the content of the page.</dd>
<dt><code class="highlight language-html">&lt;button type="button" onclick="document.dispatchEvent(new CustomEvent('sidebar:toggle'))"&gt;</code></dt>
<dd>A button to toggle the sidebar. If you want to use multiple sidebars you will need to add unique ids to the sidebars (i.e. <code>&lt;aside class="sidebar" id="{SIDEBAR_ID}"&gt;</code>) and refer to them in the event <code>detail</code> (i.e. <code>document.dispatchEvent(new CustomEvent('sidebar:toggle', { detail: { id: '{SIDEBAR_ID}' } }));</code>).</dd>
</dl>
</section>
<h3 id="usage-macro"><a href="#usage-macro">Jinja and Nunjucks</a></h3>
<div class="prose">
<p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">sidebar()</code> Nunjucks or Jinja macro for this component.</p>
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/jinja/sidebar.html.jinja" target="_blank">
Jinja macro
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/sidebar.njk" target="_blank">
Nunjucks macro
{% lucide "arrow-right" %}
</a>
</div>
{% set raw_code %}{% raw %}{% set menu = [
{ type: "group", label: "Getting started", items: [
{ label: "Playground", url: "#" },
{ label: "Models", url: "#" },
{ label: "Settings", type: "submenu", items: [
{ label: "General", url: "#" },
{ label: "Team", url: "#" },
{ label: "Billing", url: "#" },
{ label: "Limits", url: "#" }
] }
]}
] %}
{{ sidebar(
label="Sidebar navigation",
menu=menu
) }}
<main>
<h1>Content</h1>
</main>
{% endraw %}{% endset%}
{{ code_block(raw_code, "jinja") }}
-51
View File
@@ -1,51 +0,0 @@
---
layout: layouts/page.njk
title: Skeleton
description: Used to show a placeholder while content is loading.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Card
id: example-card
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
<div class="alert mb-6">
{% lucide "circle-alert" %}
<h2>There is no dedicated skeleton component in Basecoat.</h2>
</div>
{% set code %}<div class="flex items-center gap-4">
<div class="bg-accent animate-pulse size-10 shrink-0 rounded-full"></div>
<div class="grid gap-2">
<div class="bg-accent animate-pulse rounded-md h-4 w-[150px]"></div>
<div class="bg-accent animate-pulse rounded-md h-4 w-[100px]"></div>
</div>
</div>{% endset %}
{{ code_preview("skeleton", code | prettyHtml) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<p class="prose">Simply use the <code>animate-pulse</code> class to create a skeleton.</p>
{{ code_block(code) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-card"><a href="#example-card">Card</a></h3>
{% set code %}<div class="card w-full @md:w-auto @md:min-w-sm">
<header>
<div class="bg-accent animate-pulse rounded-md h-4 w-2/3"></div>
<div class="bg-accent animate-pulse rounded-md h-4 w-1/2"></div>
</header>
<section>
<div class="bg-accent animate-pulse rounded-md aspect-square w-full"></div>
</section>
</div>{% endset %}
{{ code_preview("skeleton-card", code | prettyHtml, "w-full max-w-md") }}
-39
View File
@@ -1,39 +0,0 @@
---
layout: layouts/page.njk
title: Slider
description: An input where the user selects a value from within a given range.
toc:
- label: Usage
id: usage
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<input
type="range"
class="input w-full"
min="0"
max="100"
value="50"
x-data="{
updatePercent(el) {
const min = parseFloat(el.min || 0);
const max = parseFloat(el.max || 100);
const value = parseFloat(el.value);
const percent = (max === min) ? 0 : ((value - min) / (max - min)) * 100;
el.style.setProperty('--slider-value', `${percent}%`);
}
}"
x-init="updatePercent($el)"
@input="updatePercent($el)"
>{% endset %}
{{ code_preview("slider", code, "w-full max-w-sm") }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>You can style an <code>&lt;input type="range"&gt;</code> element by either adding the <code>input</code> class or having a parent with the <code>form</code> class (<a href="/components/form">read more about form</a>). However, the range input also requries some Javacript code to be able to paint the left side of the slider.</p>
</section>
{{ code_block(code) }}
-54
View File
@@ -1,54 +0,0 @@
---
layout: layouts/page.njk
title: Switch
description: A control that allows the user to toggle between checked and not checked.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Form
id: example-form
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<label class="label">
<input type="checkbox" name="switch" role="switch" class="input">
Airplane Mode
</label>{% endset %}
{{ code_preview("switch", code) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>You can style a <code>&lt;input type="checkbox"&gt;</code> element as a switch by adding a <code>role="switch"</code> attribute, and adding the <code>input</code> class or having a parent with the <code>form</code> class (<a href="/components/form">read more about form</a>).</p>
</section>
{% set code %}<input type="checkbox" role="switch" class="input">{% endset %}
{{ code_block(code) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-form"><a href="#example-form">Form</a></h3>
{% set code %}<form class="form grid gap-4">
<h3 class="text-lg font-medium">Email Notifications</h3>
<div class="gap-2 flex flex-row items-start justify-between rounded-lg border p-4 shadow-xs">
<div class="flex flex-col gap-0.5">
<label for="demo-form-switch" class="leading-normal">Marketing emails</label>
<p class="text-muted-foreground text-sm">Receive emails about new products, features, and more.</p>
</div>
<input type="checkbox" id="demo-form-switch" role="switch">
</div>
<div class="gap-2 flex flex-row items-start justify-between rounded-lg border p-4 shadow-xs">
<div class="flex flex-col gap-0.5 opacity-60">
<label for="demo-form-switch-disabled" class="leading-normal">Marketing emails</label>
<p class="text-muted-foreground text-sm">Receive emails about new products, features, and more.</p>
</div>
<input type="checkbox" id="demo-form-switch-disabled" role="switch" disabled>
</div>
</form>{% endset %}
{{ code_preview("switch-form", code, "w-full") }}
-87
View File
@@ -1,87 +0,0 @@
---
layout: layouts/page.njk
title: Table
description: A responsive table component.
toc:
- label: Usage
id: usage
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<div class="overflow-x-auto">
<table class="table">
<caption>A list of your recent invoices.</caption>
<thead>
<tr>
<th>Invoice</th>
<th>Status</th>
<th>Method</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<tr>
<td class="font-medium">INV001</td>
<td>Paid</td>
<td>Credit Card</td>
<td class="text-right">$250.00</td>
</tr>
<tr>
<td class="font-medium">INV002</td>
<td>Pending</td>
<td>PayPal</td>
<td class="text-right">$150.00</td>
</tr>
<tr>
<td class="font-medium">INV003</td>
<td>Unpaid</td>
<td>Bank Transfer</td>
<td class="text-right">$350.00</td>
</tr>
<tr>
<td class="font-medium">INV004</td>
<td>Paid</td>
<td>Paypal</td>
<td class="text-right">$450.00</td>
</tr>
<tr>
<td class="font-medium">INV005</td>
<td>Paid</td>
<td>Credit Card</td>
<td class="text-right">$550.00</td>
</tr>
<tr>
<td class="font-medium">INV006</td>
<td>Pending</td>
<td>Bank Transfer</td>
<td class="text-right">$200.00</td>
</tr>
<tr>
<td class="font-medium">INV007</td>
<td>Unpaid</td>
<td>Credit Card</td>
<td class="text-right">$300.00</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3">Total</td>
<td class="text-right">$2,500.00</td>
</tr>
</tfoot>
</table>
</div>{% endset %}
{{ code_preview("table", code, "w-full") }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<section class="prose">
<p>Simply add the <code>table</code> class to your table element.</p>
</section>
{% set code %}<table class="table">
<!-- Content of your table -->
</table>{% endset %}
{{ code_block(code) }}
-213
View File
@@ -1,213 +0,0 @@
---
layout: layouts/page.njk
title: Tabs
description: A set of layered sections of content—known as tab panels—that are displayed one at a time.
toc:
- label: Usage
id: usage
children:
- label: HTML + JavaScript
id: usage-default
- label: Jinja and Nunjucks
id: usage-macro
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% from "tabs.njk" import tabs %}
{% set code_html %}
{% set account_panel %}
<div class="card">
<header>
<h2>Account</h2>
<p>Make changes to your account here. Click save when you're done.</p>
</header>
<section>
<form class="form grid gap-6">
<div class="grid gap-3">
<label for="demo-tabs-account-name">Name</label>
<input type="text" id="demo-tabs-account-name" value="Pedro Duarte" />
</div>
<div class="grid gap-3">
<label for="demo-tabs-account-username">Username</label>
<input type="text" id="demo-tabs-account-username" value="@peduarte" />
</div>
</form>
</section>
<footer>
<button type="button" class="btn">Save changes</button>
</footer>
</div>
{% endset %}
{% set password_panel %}
<div class="card">
<header>
<h2>Password</h2>
<p>Change your password here. After saving, you'll be logged out.</p>
</header>
<section>
<form class="form grid gap-6">
<div class="grid gap-3">
<label for="demo-tabs-password-current">Current password</label>
<input type="password" id="demo-tabs-password-current" />
</div>
<div class="grid gap-3">
<label for="demo-tabs-password-new">New password</label>
<input type="password" id="demo-tabs-password-new"/>
</div>
</form>
</section>
<footer>
<button type="button" class="btn">Save Password</button>
</footer>
</div>
{% endset %}
{% set tabsets_demo = [
{ tab: "Account", panel: account_panel },
{ tab: "Password", panel: password_panel }
] %}
{{ tabs(
id='demo-tabs-with-panels',
tabsets=tabsets_demo,
main_attrs={ "class": "w-full" },
tablist_attrs={ "class": "w-full" }
) }}
{% endset %}
{{ code_preview("tabs", code_html | prettyHtml, "max-w-[300px] w-full") }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<h4 id="usage-html-js-1"><a href="#usage-html-js-1">Step 1: Include the JavaScript files</a></h4>
<section class="prose">
<p>You can either <a href="/installation/#install-cdn-all">include the JavaScript file for all the components</a>, or just the one for this component by adding this to the <code>&lt;head&gt;</code> of your page:</p>
</section>
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ site.version }}/dist/js/select.min.js" defer></script>{% endset %}
{{ code_block(code_script | prettyHtml, "html") }}
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-js">
Components with JavaScript
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="/installation/#install-cli">
Use the CLI
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/js/tabs.js" target="_blank">
tabs.js
{% lucide "arrow-right" %}
</a>
</div>
<h4 id="usage-html-js-2"><a href="#usage-html-js-2">Step 2: Add your select HTML</a></h4>
{{ code_block(code_html | prettyHtml, "html") }}
<h4 id="usage-html-js-3"><a href="#usage-html-js-3">HTML structure</a></h4>
<section class="prose">
<dl>
<dt><code class="highlight language-html">&lt;div class="tabs"&gt;</code></dt>
<dd>Wraps around the entire component.
<dl>
<dt><code class="highlight language-html">&lt;nav role="tablist" aria-orientation="horizontal"&gt;</code></dt>
<dd>The tablist containing the tab buttons
<dl>
<dt><code class="highlight language-html">&lt;button role="tab" id="{ TAB_ID }" aria-controls="{ PANEL_ID }" aria-selected="false" tabindex="0"&gt;</code></dt>
<dd>The tab button. When active, the <code>aria-selected</code> attribute is set to <code>true</code>. <code>tabindex="0"</code> is required for keyboard navigation.</dd>
</dl>
</dd>
</dl>
</dd>
<dl>
<dt><code class="highlight language-html">&lt;div role="tabpanel" id="{ PANEL_ID }" aria-labelledby="{ TAB_ID }" tabindex="-1" aria-selected="false"&gt;</code></dt>
<dd>The tab panel. When active, the <code>aria-selected</code> attribute is set to <code>true</code>.</dd>
</dl>
</dl>
</section>
<h3 id="usage-macro"><a href="#usage-macro">Jinja and Nunjucks</a></h3>
<div class="prose">
<p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">tabs()</code> Nunjucks or Jinja macro for this component.</p>
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/jinja/tabs.html.jinja" target="_blank">
Jinja macro
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/tabs.njk" target="_blank">
Nunjucks macro
{% lucide "arrow-right" %}
</a>
</div>
{% set raw_code %}{% raw %}{% set account_panel %}
<div class="card">
<header>
<h2>Account</h2>
<p>Make changes to your account here. Click save when you're done.</p>
</header>
<section>
<form class="form grid gap-6">
<div class="grid gap-3">
<label for="demo-tabs-account-name">Name</label>
<input type="text" id="demo-tabs-account-name" value="Pedro Duarte" />
</div>
<div class="grid gap-3">
<label for="demo-tabs-account-username">Username</label>
<input type="text" id="demo-tabs-account-username" value="@peduarte" />
</div>
</form>
</section>
<footer>
<button type="button" class="btn">Save changes</button>
</footer>
</div>
{% endset %}
{% set password_panel %}
<div class="card">
<header>
<h2>Password</h2>
<p>Change your password here. After saving, you'll be logged out.</p>
</header>
<section>
<form class="form grid gap-6">
<div class="grid gap-3">
<label for="demo-tabs-password-current">Current password</label>
<input type="password" id="demo-tabs-password-current" />
</div>
<div class="grid gap-3">
<label for="demo-tabs-password-new">New password</label>
<input type="password" id="demo-tabs-password-new"/>
</div>
</form>
</section>
<footer>
<button type="button" class="btn">Save Password</button>
</footer>
</div>
{% endset %}
{% set tabsets_demo = [
{ tab: "Account", panel: account_panel },
{ tab: "Password", panel: password_panel }
] %}
{{ tabs(
id='demo-tabs-with-panels',
tabsets=tabsets_demo,
main_attrs={ "class": "w-full" },
tablist_attrs={ "class": "w-full" }
) }}{% endraw %}{% endset%}
{{ code_block(raw_code, "jinja") }}
-90
View File
@@ -1,90 +0,0 @@
---
layout: layouts/page.njk
title: Textarea
description: Displays a form textarea or a component that looks like a textarea.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Default
id: example-default
- label: Invalid
id: example-invalid
- label: Disabled
id: example-disabled
- label: With label
id: example-with-label
- label: With text
id: example-with-text
- label: With button
id: example-with-button
- label: Form
id: example-form
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<textarea class="textarea" placeholder="Type your message here"></textarea>{% endset %}
{{ code_preview("button", code, "w-full") }}
<h2 id="usage">Usage</h2>
<section class="prose">
<p>Simply add the <code>textarea</code> class to your <code>&lt;textarea&gt;</code> element or have a parent with the <code>form</code> class (<a href="/components/form">read more about form</a>). You can also set the <code>aria-invalid</code> attribute to <code>true</code> to make the textarea invalid.</p>
</section>
{{ code_block(code) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
{{ code_preview("textarea-default", code, "w-full") }}
<h3 id="example-invalid"><a href="#example-invalid">Invalid</a></h3>
{% set code %}<textarea class="textarea" placeholder="Type your message here" aria-invalid="true"></textarea>{% endset %}
{{ code_preview("textarea-invalid", code, "w-full") }}
<h3 id="example-disabled"><a href="#example-disabled">Disabled</a></h3>
{% set code %}<textarea class="textarea" placeholder="Type your message here" disabled></textarea>{% endset %}
{{ code_preview("textarea-disabled", code, "w-full") }}
<h3 id="example-with-label"><a href="#example-with-label">With label</a></h3>
{% set code %}<div class="grid gap-3">
<label for="textarea-with-label" class="label">Label</label>
<textarea id="textarea-with-label" class="textarea" placeholder="Type your message here"></textarea>
</div>{% endset %}
{{ code_preview("textarea-with-label", code, "w-full") }}
<h3 id="example-with-text"><a href="#example-with-text">With text</a></h3>
{% set code %}<div class="grid gap-3">
<label for="textarea-with-label" class="label">Label</label>
<textarea id="textarea-with-label" class="textarea" placeholder="Type your message here"></textarea>
<p class="text-muted-foreground text-sm">Type your message and press enter to send.</p>
</div>{% endset %}
{{ code_preview("textarea-with-text", code, "w-full") }}
<h3 id="example-with-button"><a href="#example-with-button">With button</a></h3>
{% set code %}<div class="grid gap-3">
<textarea id="textarea-with-label" class="textarea" placeholder="Type your message here"></textarea>
<button type="submit" class="btn">Submit</button>
</div>{% endset %}
{{ code_preview("textarea-form", code, "w-full") }}
<h3 id="example-form"><a href="#example-form">Form</a></h3>
{% set code %}<form class="form space-y-6">
<div class="grid gap-3">
<label for="textarea-form" class="label">Bio</label>
<textarea id="textarea-form" placeholder="Tell us a bit about yourself"></textarea>
<p class="text-muted-foreground text-sm">You can @mention other users and organizations.</p>
</div>
<button type="submit" class="btn">Submit</button>
</form>{% endset %}
{{ code_preview("textarea-form", code, "w-full max-w-sm") }}
-53
View File
@@ -1,53 +0,0 @@
---
layout: layouts/page.njk
title: Theme Switcher
description: A component that allows the user to switch between light and dark mode.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Default
id: example-default
- label: Right
id: example-right
- label: Bottom + Start
id: example-bottom-start
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<button class="btn-outline" data-tooltip="Add to library">Hover</button>{% endset %}
{{ code_preview("button", code) }}
<h2 id="usage">Usage</h2>
<section class="prose">
<p>On any element, set the value of the <code>data-tooltip</code> attribute to the text you want to display in the tooltip. Additionally, you can set:</p>
<ul>
<li><code>data-side</code> attribute to <code>top</code>, <code>bottom</code>, <code>left</code>, or <code>right</code> to change the side of the tooltip.</li>
<li><code>data-align</code> attribute to <code>start</code>, <code>center</code>, or <code>end</code> to change the alignment of the tooltip.</li>
</ul>
</section>
{% set code %}<button class="btn-outline" data-tooltip="Tooltip text" data-side="bottom" data-align="center">Bottom left</button>{% endset %}
{{ code_block(code) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-default"><a href="#example-default">Default</a></h3>
{% set code %}<button class="btn-outline" data-tooltip="Default tooltip">Default</button>{% endset %}
{{ code_preview("button", code) }}
<h3 id="example-right"><a href="#example-right">Right</a></h3>
{% set code %}<button class="btn-outline" data-tooltip="Right side tooltip" data-side="right">Right</button>{% endset %}
{{ code_preview("button", code) }}
<h3 id="example-bottom-start"><a href="#example-bottom-start">Bottom + Start</a></h3>
{% set code %}<button class="btn-outline" data-tooltip="Bottom side and start aligned tooltip" data-side="bottom" data-align="start">Bottom + Start</button>{% endset %}
{{ code_preview("button", code) }}
-263
View File
@@ -1,263 +0,0 @@
---
layout: layouts/page.njk
title: Toast
description: A succinct message that is displayed temporarily.
toc:
- label: Usage
id: usage
children:
- label: HTML + JavaScript
id: usage-html-js
children:
- label: "Step 1: Include the JavaScript file"
id: usage-html-js-1
- label: "Step 2: Add the toaster HTML"
id: usage-html-js-2
- label: "Step 3: Add your toasts"
id: usage-html-js-3
- label: HTML structure
id: usage-html-js-4
- label: JavaScript config object
id: usage-html-js-5
- label: Jinja and Nunjucks
id: usage-macro
---
{% from "toast.njk" import toaster, toast %}
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code_server_side %}<button
class="btn-outline"
hx-trigger="click"
hx-get="/fragments/toast/success"
hx-target="#toaster"
hx-swap="beforeend"
>
Toast from backend (with HTMX)
</button>{% endset %}
{{ code_preview("toast-server-side", code_server_side, "max-w-md") }}
{% set code_client_side %}<button
class="btn-outline"
onclick="document.dispatchEvent(new CustomEvent('basecoat:toast', {
detail: {
config: {
category: 'success',
title: 'Success',
description: 'A success toast called from the front-end.',
cancel: {
label: 'Dismiss'
}
}
}
}))"
>
Toast from front-end
</button>{% endset %}
{{ code_preview("toast-client-side", code_client_side, "max-w-md") }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<h3 id="usage-html-js"><a href="#usage-html-js">HTML + JavaScript</a></h3>
<h4 id="usage-html-js-1"><a href="#usage-html-js-1">Step 1: Include the JavaScript file</a></h4>
<section class="prose">
<p>You can either <a href="/installation/#install-cdn-all">include the JavaScript file for all the components</a>, or just the one for this component by adding this to the <code>&lt;head&gt;</code> of your page:</p>
</section>
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ site.version }}/dist/js/toast.min.js" defer></script>{% endset %}
{{ code_block(code_script | prettyHtml, "html") }}
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-js">
Components with JavaScript
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="/installation/#install-cli">
Use the CLI
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/js/toast.js" target="_blank">
toast.js
{% lucide "arrow-right" %}
</a>
</div>
<h4 id="usage-html-js-2"><a href="#usage-html-js-2">Step 2: Add the toaster HTML</a></h4>
<section class="prose">
<p>Toasts are displayed in a parent element, the "toaster". Add this to this to at the end of your <code>&lt;body&gt;</code>:</p>
</section>
{% set code_toaster %}<div id="toaster" class="toaster"></div>{% endset %}
{{ code_block(code_toaster | prettyHtml, "html") }}
<section class="prose">
<p>You can set up the alignment of the toaster using the <code>data-align</code> attribute: <code>data-align="start"</code>, <code>data-align="center"</code>, or <code>data-align="end"</code> (default to <code>data-align="end"</code>).</p>
</section>
<h4 id="usage-html-js-3"><a href="#usage-html-js-3">Step 3: Add your toasts</a></h4>
<section class="prose">
<p>If you decide to server-render your toasts, or load them using asynchronoulsy with something like HTMX, you can just add the toast's markup to the toaster:</p>
</section>
{% set code_toast_html %}<div id="toaster" class="toaster">
{{ toast(
category="success",
title="Success",
description="A success toast called from the front-end.",
action={
label: "Dismiss",
click: "close()"
}
) }}
</div>{% endset %}
{{ code_block(code_toast_html | prettyHtml, "html") }}
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="#usage-html-js-4">
HTML structure
{% lucide "arrow-right" %}
</a>
</div>
<section class="prose">
<p>If you need to create a toast from the front-end, you can dispatch a custom <code>basecoat:toast</code> event as such:</p>
</section>
{{ code_block(code_client_side | prettyHtml, "html") }}
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="#usage-html-js-5">
JavaScript config object
{% lucide "arrow-right" %}
</a>
</div>
<h4 id="usage-html-js-4"><a href="#usage-html-js-4">HTML structure</a></h4>
<section class="prose">
<dl>
<dt><code class="highlight language-html">&lt;div class="toast"&gt;</code></dt>
<dd>Wraps around the toast component. You can add a <code>data-duration</code> attribute to set the duration of the toast in milliseconds (e.g. <code>data-duration="5000"</code> for 5 seconds). If not provided, the default duration is 3000ms (3 seconds) or 5000ms (5 seconds) for <code>error</code> toasts.
<dl>
<dt><code class="highlight language-html">&lt;div class="toast-content"&gt;</code></dt>
<dd>The content of the toast.
<dl>
<dt><code class="highlight language-html">&lt;svg aria-hidden="true"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The toast's icon.</dd>
<dt><code class="highlight language-html">&lt;section&gt;</code></dt>
<dd>The toast's message.
<dl>
<dt><code class="highlight language-html">&lt;h2&gt;</code></dt>
<dd>The toast's title.</dd>
<dt><code class="highlight language-html">&lt;p&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The toast's description.</dd>
</dl>
</dd>
<dt><code class="highlight language-html">&lt;footer&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The toast's buttons. When clicked, the toast will be closed (unless the button's <code>onclick</code> is set to <code>e.preventDefault()</code>).
<dl>
<dt><code class="highlight language-html">&lt;button type="button" class="btn" onclick="{ ONCLICK }"&gt;</code> or <code class="highlight language-html">&lt;a href="{ URL }" class="btn" &gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The toast's action button. This can either be a link (with a <code>href</code> attribute) or a button (with an <code>onclick</code> attribute).</dd>
</dl>
<dl>
<dt><code class="highlight language-html">&lt;button type="button" class="btn-outline" onclick="{ ONCLICK }"&gt;</code> <span class="badge-secondary">Optional</span></dt>
<dd>The toast's cancel button (with an optional <code>onclick</code> attribute).</dd>
</dl>
</dd>
</dl>
</dd>
</dl>
</dd>
</dl>
</section>
<h4 id="usage-html-js-5"><a href="#usage-html-js-5">JavaScript config object</a></h4>
<section class="prose">
<dl>
<dt><code>duration</code> <span class="badge-secondary">Optional</span></dt>
<dd>The duration of the toast in milliseconds. If not provided, the default duration is 3000ms (3 seconds) or 5000ms (5 seconds) for <code>error</code> toasts.</dd>
<dt><code>category</code> <span class="badge-secondary">Optional</span></dt>
<dd>Category of the toast, either <code>success</code>, <code>info</code>, <code>warning</code>, or <code>error</code>.</dd>
<dt><code>title</code></dt>
<dd>The title of the toast.</dd>
<dt><code>description</code> <span class="badge-secondary">Optional</span></dt>
<dd>The description of the toast.</dd>
<dt><code>action</code> <span class="badge-secondary">Optional</span></dt>
<dd>Action button.
<dl>
<dt><code>label</code> <span class="badge-secondary">Optional</span></dt>
<dd>The label of the cancel button. If not provided, the default label is "Dismiss".</dd>
<dt><code>onclick</code> <span class="badge-secondary">Optional</span></dt>
<dd>The onclick of the cancel button.</dd>
</dl>
</dd>
<dt><code>cancel</code> <span class="badge-secondary">Optional</span></dt>
<dd>Cancel button.
<dl>
<dt><code>label</code> <span class="badge-secondary">Optional</span></dt>
<dd>The label of the cancel button. If not provided, the default label is "Dismiss".</dd>
<dt><code>onclick</code> <span class="badge-secondary">Optional</span></dt>
<dd>The onclick of the cancel button.</dd>
</dl>
</dd>
</dl>
</section>
<h3 id="usage-macro"><a href="#usage-macro">Jinja and Nunjucks</a></h3>
<div class="prose">
<p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">toaster()</code> and <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">toast()</code> Nunjucks or Jinja macros for this component.</p>
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
Use Nunjucks or Jinja macros
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/jinja/toast.html.jinja" target="_blank">
Jinja macro
{% lucide "arrow-right" %}
</a>
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/toast.njk" target="_blank">
Nunjucks macro
{% lucide "arrow-right" %}
</a>
</div>
{% set raw_code_toaster %}{% raw %}{% from "toast.njk" import toaster %}
{{ toaster(
toasts=[
{
type: "success",
title: "Success",
description: "A success toast called from the front-end.",
action: { label: "Dismiss", click: "close()" }
},
{
type: "info",
title: "Info",
description: "An info toast called from the front-end.",
action: { label: "Dismiss", click: "close()" }
}
]
) }}{% endraw %}{% endset%}
{% set raw_code_toast %}{% raw %}{% from "toast.njk" import toast %}
{{ toast(
title="Event has been created",
description="Sunday, December 03, 2023 at 9:00 AM",
cancel={ label: "Undo" }
) }}{% endraw %}{% endset %}
{% from "tabs.njk" import tabs %}
{% set tabsets = [
{ tab: "toaster()", panel: code_block(raw_code_toaster, "jinja", class="") },
{ tab: "toast()", panel: code_block(raw_code_toast, "jinja", class="") }
] %}
{{ tabs(id="macros", tabsets=tabsets) }}
-53
View File
@@ -1,53 +0,0 @@
---
layout: layouts/page.njk
title: Tooltip
description: A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.
toc:
- label: Usage
id: usage
- label: Examples
id: examples
children:
- label: Default
id: example-default
- label: Right
id: example-right
- label: Bottom + Start
id: example-bottom-start
---
{% from "macros/code_preview.njk" import code_preview %}
{% from "macros/code_block.njk" import code_block %}
{% set code %}<button class="btn-outline" data-tooltip="Add to library">Hover</button>{% endset %}
{{ code_preview("button", code) }}
<h2 id="usage">Usage</h2>
<section class="prose">
<p>On any element, set the value of the <code>data-tooltip</code> attribute to the text you want to display in the tooltip. Additionally, you can set:</p>
<ul>
<li><code>data-side</code> attribute to <code>top</code>, <code>bottom</code>, <code>left</code>, or <code>right</code> to change the side of the tooltip.</li>
<li><code>data-align</code> attribute to <code>start</code>, <code>center</code>, or <code>end</code> to change the alignment of the tooltip.</li>
</ul>
</section>
{% set code %}<button class="btn-outline" data-tooltip="Tooltip text" data-side="bottom" data-align="center">Bottom left</button>{% endset %}
{{ code_block(code) }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-default"><a href="#example-default">Default</a></h3>
{% set code %}<button class="btn-outline" data-tooltip="Default tooltip">Default</button>{% endset %}
{{ code_preview("button", code) }}
<h3 id="example-right"><a href="#example-right">Right</a></h3>
{% set code %}<button class="btn-outline" data-tooltip="Right side tooltip" data-side="right">Right</button>{% endset %}
{{ code_preview("button", code) }}
<h3 id="example-bottom-start"><a href="#example-bottom-start">Bottom + Start</a></h3>
{% set code %}<button class="btn-outline" data-tooltip="Bottom side and start aligned tooltip" data-side="bottom" data-align="start">Bottom + Start</button>{% endset %}
{{ code_preview("button", code) }}
-330
View File
@@ -1,330 +0,0 @@
---
layout: layouts/layout.njk
---
{% from "select.njk" import select %}
<div class="max-w-screen-lg mx-auto">
<header class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<h1 class="text-2xl font-bold tracking-tight sm:text-3xl md:text-4xl">All of the shadcn/ui magic, none of the React</h1>
<p class="sm:text-lg text-muted-foreground">A components library built with Tailwind CSS that works with any web stack.</p>
</div>
<div class="flex w-full items-center justify-start gap-2 pt-2">
<a class="btn" href="/installation">Get Started</a>
<a class="btn-outline" href="/introduction">Learn more</a>
</div>
</header>
<section class="grid grid-cols-1 lg:grid-cols-2 gap-4 mt-8">
<div class="flex flex-col gap-4">
<div class="card">
<header>
<h2>Team Members</h2>
<p>Invite your team members to collaborate.</p>
</header>
<section>
<ul class="grid gap-4">
<li class="flex items-center gap-4">
<img src="/assets/images/avatar-1.png" alt="Sofia Davis" class="w-10 h-10 rounded-full" />
<div class="flex flex-col gap-1 mr-auto">
<h3 class="text-sm font-medium leading-none">Sofia Davis</h3>
<p class="text-sm text-muted-foreground">m@example.com</p>
</div>
{{ select(
is_combobox=true,
selected="owner",
popover_attrs={"data-align": "end"},
items=[
{ type: "item", value: "viewer", label: "Viewer" },
{ type: "item", value: "developer", label: "Developer" },
{ type: "item", value: "billing", label: "Billing" },
{ type: "item", value: "owner", label: "Owner" }
]
) }}
</li>
<li class="flex items-center gap-4">
<img src="/assets/images/avatar-2.png" alt="Jackson Lee" class="w-10 h-10 rounded-full" />
<div class="flex flex-col gap-1 mr-auto">
<h3 class="text-sm font-medium leading-none">Jackson Lee</h3>
<p class="text-sm text-muted-foreground">p@example.com</p>
</div>
{{ select(
is_combobox=true,
popover_attrs={"data-align": "end"},
items=[
{ type: "item", value: "", label: "Empty" },
{ type: "item", value: "viewer", label: "Viewer" },
{ type: "item", value: "developer", label: "Developer" },
{ type: "item", value: "billing", label: "Billing" },
{ type: "item", value: "owner", label: "Owner" }
]
) }}
</li>
<li class="flex items-center gap-4">
<img src="/assets/images/avatar-3.png" alt="Isabella Nguyen" class="w-10 h-10 rounded-full" />
<div class="flex flex-col gap-1 mr-auto">
<h3 class="text-sm font-medium leading-none">Isabella Nguyen</h3>
<p class="text-sm text-muted-foreground">i@example.com</p>
</div>
{{ select(
is_combobox=true,
popover_attrs={"data-align": "end"},
items=[
{ type: "item", value: "viewer", label: "Viewer" },
{ type: "item", value: "developer", label: "Developer" },
{ type: "item", value: "billing", label: "Billing" },
{ type: "item", value: "owner", label: "Owner" }
]
) }}
</li>
</ul>
</section>
</div>
<div class="card">
<header>
<h2>Cookie Settings</h2>
<p>Manage your cookie settings here.</p>
</header>
<section class="text-sm grid gap-6">
<label class="flex items-center justify-between gap-2">
<div class="flex flex-col gap-0.5">
<div class="font-medium">Strictly Necessary</div>
<div class="text-muted-foreground">These cookies are essential in order to use the website and use its features.</div>
</div>
<input type="checkbox" role="switch" class="input" checked />
</label>
<label class="flex items-center justify-between gap-2">
<div class="flex flex-col gap-0.5">
<div class="font-medium">Functional Cookies</div>
<div class="text-muted-foreground">These cookies allow the website to provide personalized functionality.</div>
</div>
<input type="checkbox" role="switch" class="input" />
</label>
<label class="flex items-center justify-between gap-2">
<div class="flex flex-col gap-0.5">
<div class="font-medium">Performance Cookies</div>
<div class="text-muted-foreground">These cookies help to improve the performance of the website.</div>
</div>
<input type="checkbox" role="switch" class="input" />
</label>
</section>
<footer>
<button type="button" class="btn-outline w-full">Save preferences</button>
</footer>
</div>
<div class="card">
<header>
<h2>Payment Method</h2>
<p>Add a new payment method to your account.</p>
</header>
<section>
<form class="form grid gap-6">
<ul class="flex gap-4">
<li class="flex-1">
<input type="radio" name="payment-method-type" id="payment-method-type-apple" class="hidden peer" />
<label for="payment-method-type-apple" class="text-sm font-medium leading-none flex flex-col items-center justify-between rounded-md border-2 border-muted p-4 hover:bg-muted peer-checked:border-primary [&>svg]:mb-3 [&>svg]:size-6">
{% lucide "credit-card" %}
Card
</label>
</li>
<li class="flex-1">
<input type="radio" name="payment-method-type" id="payment-method-type-card" class="hidden peer" />
<label for="payment-method-type-card" class="text-sm font-medium leading-none flex flex-col items-center justify-between rounded-md border-2 border-muted p-4 hover:bg-muted peer-checked:border-primary [&>svg]:mb-3 [&>svg]:size-6">
<svg role="img" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>PayPal</title><path d="M7.016 19.198h-4.2a.562.562 0 0 1-.555-.65L5.093.584A.692.692 0 0 1 5.776 0h7.222c3.417 0 5.904 2.488 5.846 5.5-.006.25-.027.5-.066.747A6.794 6.794 0 0 1 12.071 12H8.743a.69.69 0 0 0-.682.583l-.325 2.056-.013.083-.692 4.39-.015.087zM19.79 6.142c-.01.087-.01.175-.023.261a7.76 7.76 0 0 1-7.695 6.598H9.007l-.283 1.795-.013.083-.692 4.39-.134.843-.014.088H6.86l-.497 3.15a.562.562 0 0 0 .555.65h3.612c.34 0 .63-.249.683-.585l.952-6.031a.692.692 0 0 1 .683-.584h2.126a6.793 6.793 0 0 0 6.707-5.752c.306-1.95-.466-3.744-1.89-4.906z"/></svg>
Paypal
</label>
</li>
<li class="flex-1">
<input type="radio" name="payment-method-type" id="payment-method-type-paypal" class="hidden peer" />
<label for="payment-method-type-paypal" class="text-sm font-medium leading-none flex flex-col items-center justify-between rounded-md border-2 border-muted p-4 hover:bg-muted peer-checked:border-primary [&>svg]:mb-3 [&>svg]:size-6">
<svg role="img" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Apple</title><path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"/></svg>
Apple Pay
</label>
</li>
</ul>
<div class="grid gap-2">
<label for="payment-method-name">Name</label>
<input type="text" id="payment-method-name" placeholder="John Doe" />
</div>
<div class="grid gap-2">
<label for="payment-method-city">City</label>
<input type="text" id="payment-method-city" placeholder="New York" />
</div>
<div class="grid gap-2">
<label for="payment-method-card-number">Card Number</label>
<input type="text" id="payment-method-card-number" placeholder="1234 5678 9012 3456" />
</div>
<div class="flex gap-4">
<div class="grid gap-2 flex-1">
<label for="payment-method-expiration-month">Expires</label>
<select id="payment-method-expiration-month" class="w-full">
<option value="01">January</option>
<option value="02">February</option>
<option value="03">March</option>
<option value="04">April</option>
<option value="05">May</option>
<option value="06">June</option>
<option value="07">July</option>
<option value="08">August</option>
<option value="09">September</option>
<option value="10">October</option>
<option value="11">November</option>
<option value="12">December</option>
</select>
</div>
<div class="grid gap-2 flex-1">
<label for="payment-method-expiration-year">Year</label>
<select id="payment-method-expiration-year" class="w-full">
<option value="2024">2024</option>
<option value="2025">2025</option>
<option value="2026">2026</option>
<option value="2027">2027</option>
<option value="2028">2028</option>
<option value="2029">2029</option>
<option value="2030">2030</option>
<option value="2031">2031</option>
<option value="2032">2032</option>
<option value="2033">2033</option>
<option value="2034">2034</option>
</select>
</div>
<div class="grid gap-2 flex-1">
<label for="payment-method-cvv">CVV</label>
<input type="text" id="payment-method-cvv" placeholder="123" maxlength="4" class="w-full" />
</div>
</div>
<button type="button" class="btn w-full">Continue</button>
</form>
</section>
</div>
</div>
<div class="flex flex-col gap-4">
<div class="card">
<section class="space-y-6">
<header class="flex items-center gap-2">
<img src="/assets/images/avatar-1.png" alt="Sofia Davis" class="w-10 h-10 rounded-full" />
<div class="flex flex-col gap-1 mr-auto">
<h3 class="text-sm font-medium leading-none">Sofia Davis</h3>
<p class="text-sm text-muted-foreground">m@example.com</p>
</div>
<button type="button" class="btn-icon-outline rounded-full" data-tooltip="New message">
{% lucide "plus"%}
</button>
</header>
<section class="space-y-4">
<div class="flex w-max max-w-[75%] flex-col gap-2 rounded-lg px-3 py-2 text-sm bg-muted">
Hi, how can I help you today?
</div>
<div class="flex w-max max-w-[75%] flex-col gap-2 rounded-lg px-3 py-2 text-sm ml-auto bg-primary text-primary-foreground">
Hey, I'm having trouble with my account.
</div>
<div class="flex w-max max-w-[75%] flex-col gap-2 rounded-lg px-3 py-2 text-sm bg-muted">
What seems to be the problem?
</div>
<div class="flex w-max max-w-[75%] flex-col gap-2 rounded-lg px-3 py-2 text-sm ml-auto bg-primary text-primary-foreground">
I can't log in.
</div>
</section>
<footer class="flex items-center gap-2" x-data="{ message: '' }">
<input type="text" class="input w-full" placeholder="Type your message here..." x-model="message" />
<button type="button" class="btn-icon" :disabled="!message">
{% lucide "send"%}
</button>
</footer>
</section>
</div>
<div class="card">
<header>
<h2>Create an account</h2>
<p>Enter your email below to create your account</p>
</header>
<section class="grid gap-6">
<div class="flex gap-6">
<button type="button" class="btn-outline flex-1">
<svg role="img" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
GitHub
</button>
<button type="button" class="btn-outline flex-1">
<svg role="img" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google</title><path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"/></svg>
Google
</button>
</div>
<div class="relative">
<div class="absolute inset-0 flex items-center">
<span class="w-full border-t"></span>
</div>
<div class="relative flex justify-center text-xs uppercase">
<span class="bg-card px-2 text-muted-foreground">Or continue with</span>
</div>
</div>
<form class="form grid gap-6">
<div class="grid gap-2">
<label for="demo-card-form-email">Email</label>
<input type="email" id="demo-card-form-email">
</div>
<div class="grid gap-2">
<div class="flex items-center gap-2">
<label for="demo-card-form-password">Password</label>
<a href="#" class="ml-auto inline-block text-sm underline-offset-4 hover:underline">Forgot your password?</a>
</div>
<input type="password" id="demo-card-form-password">
</div>
<button type="button" class="btn w-full">Create an account</button>
</form>
</section>
</div>
<div class="card">
<header>
<h2>Report an issue</h2>
<p>What area are you having problems with?</p>
</header>
<section>
<form class="form grid gap-6">
<div class="flex gap-4">
<div class="grid gap-2 flex-1">
<label for="report-issue-area">Area</label>
<select id="report-issue-area" class="w-full">
<option value="team">Team</option>
<option value="billing">Billing</option>
<option value="account">Account</option>
<option value="deployments">Deployments</option>
<option value="support">Support</option>
</select>
</div>
<div class="grid gap-2 flex-1">
<label for="report-issue-security-level">Security Level</label>
<select id="report-issue-security-level" class="w-full">
<option value="1">Severity 1 (Highest)</option>
<option value="2">Severity 2</option>
<option value="3">Severity 3</option>
<option value="4">Severity 4 (Lowest)</option>
</select>
</div>
</div>
<div class="grid gap-2">
<label for="report-issue-subject">Subject</label>
<input type="text" id="report-issue-subject" placeholder="I need help with..." class="w-full" />
</div>
<div class="grid gap-2">
<label for="report-issue-description">Description</label>
<textarea id="report-issue-description" placeholder="Please include all information relevant to your issue." class="w-full"></textarea>
</div>
<footer class="flex items-center gap-4 justify-between">
<button type="button" class="btn-sm-ghost">Cancel</button>
<button type="button" class="btn-sm">Continue</button>
</footer>
</form>
</section>
</div>
</div>
</section>
</div>
-236
View File
@@ -1,236 +0,0 @@
---
layout: layouts/page.njk
title: Installation
description: How to install and use Basecoat in your app.
toc:
- label: Install using the CDN
id: install-cdn
- label: Installing using NPM
id: install-npm
children:
- label: Add Tailwind CSS
id: install-npm-tailwind
- label: Add Basecoat
id: install-npm-basecoat
- label: Import basecoat in your CSS
id: install-npm-import
- label: (Optional) Add JavaScript files
id: install-npm-js
- label: That's it
id: install-npm-done
- label: Components with JavaScript
id: install-js
- label: Use the CLI
id: install-cli
- label: Use Nunjucks or Jinja macros
id: install-macros
- label: Theming
id: install-theming
---
{% from "macros/code_block.njk" import code_block %}
{% from "tabs.njk" import tabs %}
<h2 id="install-cdn"><a href="#install-cdn">Install using the CDN</a></h2>
<h3 id="install-cdn-all"><a href="#install-cdn-all">Install all components</a></h3>
<section class="prose">
<p>Add the following to the <code>&lt;head&gt;</code> of your HTML file:</p>
</section>
{% set code %}<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/basecoat-css@{{ site.version }}/dist/basecoat.cdn.min.css">
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ site.version }}/dist/js/all.min.js" defer></script>{% endset %}
{{ code_block(code, "html") }}
<h3 id="install-cdn-all"><a href="#install-cdn-all">Install specific components</a></h3>
<section class="prose">
<p>While the JavaScript file for all components is small (around 3kB gzipped), you can cherry-pick the components you need as instructed in the component's page (<a href="/components/dropdown-menu">Dropdown Menu</a>, <a href="/components/popover">Popover</a>, <a href="/components/select">Select</a>, <a href="/components/sidebar">Sidebar</a>, <a href="/components/tabs">Tabs</a> and <a href="/components/toast">Toast</a>). For example, to add the <a href="/components/dropdown-menu">Dropdown Menu</a> component, add the following to the <code>&lt;head&gt;</code> of your HTML file:</p>
</section>
{% set code %}<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/basecoat-css@{{ site.version }}/dist/basecoat.cdn.min.css">
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ site.version }}/dist/js/dropdown-menu.min.js" defer></script>{% endset %}
{{ code_block(code, "html") }}
<h2 id="install-npm"><a href="#install-npm">Install using NPM</a></h2>
<ol class="[&>h3]:step steps mb-12 md:ml-4 md:border-l md:pl-8">
<li class="step">
<h3 class="mb-4 scroll-m-20 font-semibold tracking-tight" id="install-npm-tailwind"><a href="#install-npm-tailwind">Add Tailwind CSS</a></h3>
<div class="prose">
<p>Basecoat uses Tailwind CSS. You need to install it in your project.</p>
<p><a href="https://tailwindcss.com/docs/installation" target="_blank">Follow the Tailwind CSS installation instructions to get started.</a></p>
</div>
</li>
<li class="step mt-8">
<h3 class="mb-4 scroll-m-20 font-semibold tracking-tight" id="install-npm-basecoat"><a href="#install-npm-basecoat">Add Basecoat</a></h3>
<div class="prose">
<p>Add Basecoat to your Tailwind CSS file.</p>
</div>
{% set code_npm %}npm install basecoat-css{% endset %}
{% set code_pnpm %}pnpm add basecoat-css{% endset %}
{% set code_bun %}bun add basecoat-css{% endset %}
{% set code_yarn %}yarn add basecoat-css{% endset %}
{% set tabsets = [
{ tab: "npm", panel: code_block(code_npm, class="") },
{ tab: "pnpm", panel: code_block(code_pnpm, class="") },
{ tab: "bun", panel: code_block(code_bun, class="") },
{ tab: "yarn", panel: code_block(code_yarn, class="") }
] %}
{{ tabs(id="npm-install", tabsets=tabsets) }}
<div class="prose">
<p>Alternatively, you can directly <a href="https://github.com/hunvreus/basecoat/blob/main/src/css/basecoat.css" target="_blank">copy the <code>basecoat.css</code> file</a> into your project.</p>
</div>
</li>
<li class="step mt-8">
<h3 class="mb-4 scroll-m-20 font-semibold tracking-tight" id="install-npm-import"><a href="#install-npm-import">Import basecoat in your CSS</a></h3>
{% set code %}@import "tailwindcss";
@import "basecoat-css";{% endset %}
{{ code_block(code, "css") }}
</li>
<li class="step mt-8">
<h3 class="mb-4 scroll-m-20 font-semibold tracking-tight" id="install-npm-js"><a href="#install-npm-js">(Optional) Add JavaScript files</a></h3>
<div class="prose">
<p>Some components <a href="#install-js">need some JavaScript</a> (e.g. the <a href="/components/tabs">Tabs</a> component). There are two ways to get it into your project:</p>
<h4 class="mt-6 font-semibold">Using a build tool (Vite, Webpack…)</h4>
<p>If your project is ESM-aware you can directly import the scripts. To include all components:</p>
</div>
{% set code %}{% raw %}import 'basecoat-css/all';{% endraw %}{% endset %}
{{ code_block(code, "js") }}
<div class="prose">
<p>Or cherry-pick specific components, for example:</p>
</div>
{% set code %}{% raw %}import 'basecoat-css/popover';
import 'basecoat-css/tabs';{% endraw %}{% endset %}
{{ code_block(code, "js") }}
<div class="prose">
<h4 class="mt-6 font-semibold">Without a build tool</h4>
<p>Copy the pre-built files from <code>node_modules</code> into your public directory:</p>
</div>
{% set code %}npx copyfiles -u 1 "node_modules/basecoat-css/dist/js/**/*" public/js/basecoat{% endset %}
{{ code_block(code, "bash") }}
<div class="prose">
<p>Then reference the script you need:</p>
</div>
{% set code %}<script src="/js/basecoat/tabs.min.js" defer></script>{% endset %}
{{ code_block(code, "html") }}
<div class="prose">
<p>See each component page for the minimal script required.</p>
</div>
</li>
<li class="step mt-8">
<h3 class="mb-4 scroll-m-20 font-semibold tracking-tight" id="install-npm-done"><a href="#install-npm-done">That's it</a></h3>
<div class="prose">
<p>You can now use any of the Basecoat classes in your project (e.g. <code>btn</code>, <code>card</code>, <code>input</code>, etc). Read the documentation about each component to get started (e.g. <a href="/components/button">button</a>, <a href="/components/card">card</a>, <a href="/components/input">input</a>, etc).</p>
<p>Some components (e.g. <a href="/components/select">Select</a>) require <a href="#install-js">JavaScript code to work</a>.</p>
</div>
</li>
</ol>
<h2 id="install-js"><a href="#install-js">Components with JavaScript</a></h2>
<div class="prose">
<p><b>A handful of components require JavaScript code to work.</b>, specifically <a href="/components/dropdown-menu">Dropdown Menu</a>, <a href="/components/popover">Popover</a>, <a href="/components/select">Select</a>, <a href="/components/sidebar">Sidebar</a>, <a href="/components/tabs">Tabs</a> and <a href="/components/toast">Toast</a></p>
<p>If a component requires JavaScript, the documentation page will provide instructions. There are 2 options to add the JavaScript code to your project:</p>
<ul>
<li><b>CDN</b>: you either <a href="#install-cdn-all">add the code for all of the components</a> to the <code>&lt;head&gt;</code> of your HTML file, or just the file for the component you want to use as instructed on the component's page.</li>
<li><b>Local file</b>: you download the script as a separate file and include it in your project (<a href="#install-cli">or let the CLI do it for you</a>). Once again, you have the choice to include the fiel for all components at once (<a><code>all.min.js</code> or <code>all.js</code>).</p>
</ul>
</div>
<h2 id="install-cli"><a href="#install-cli">Use the CLI</a></h2>
<div class="prose">
<p>If you're using Nunjucks or Jinja, you can use the CLI to install the CSS and macros for any of the complex components(e.g. <a href="/components/dialog">Dialog</a>). It will copy the JS and <code>.html.jinja</code> or <code>.njk</code> macros into your code.</p>
<p>For example, to add the Dialog component, run one of the following commands:</p>
</div>
{% set code_npm_exec %}npx basecoat-cli add dialog{% endset %}
{% set code_pnpm_exec %}pnpm dlx basecoat-cli add dialog{% endset %}
{% set code_bun_exec %}bunx basecoat-cli add dialog{% endset %}
{% set code_yarn_exec %}yarn dlx basecoat-cli add dialog{% endset %}
{% set exec_tabsets = [
{ tab: "npm", panel: code_block(code_npm_exec, class="") },
{ tab: "pnpm", panel: code_block(code_pnpm_exec, class="") },
{ tab: "bun", panel: code_block(code_bun_exec, class="") },
{ tab: "yarn", panel: code_block(code_yarn_exec, class="") }
] %}
{{ tabs(id="cli-execution", tabsets=exec_tabsets, label="Execute CLI commands to add components:") }}
<div class="prose my-6">
<p>You will be asked to confirm the template engine and destination directory for the JavaScript and macros.</p>
</div>
<h2 id="install-macros"><a href="#install-macros">Use Nunjucks or Jinja macros</a></h2>
<div class="prose">
<p>For more complex components, you can use <a href="https://mozilla.github.io/nunjucks/" target="_blank">Nunjucks</a> or <a href="https://jinja.palletsprojects.com" target="_blank">Jinja</a> macros to help you generate the HTML and JavaScript code.</p>
<p>To install them, either <a href="https://github.com/hunvreus/basecoat/tree/main/src" target="_blank">copy them directly from the GitHub repository</a> or use <a href="#install-cli">the CLI</a> to do the work for you.</p>
<p>For example, here's how you could use the <code>select()</code> macro to generate the HTML and JavaScript code for a <a href="/components/select">Select</a> component:</p>
</div>
{% set code %}{% raw %}{% call select() %}
<div role="group" aria-labelledby="fruit-options-label">
<span id="fruit-options-label" role="heading">Fruits</span>
<div role="option" data-value="apple">Apple</div>
<div role="option" data-value="banana">Banana</div>
<div role="option" data-value="blueberry">Blueberry</div>
<div role="option" data-value="pineapple">Grapes</div>
<div role="option" data-value="pineapple">Pineapple</div>
</div>
{% endcall %}{% endraw %}{% endset %}
{{ code_block(code, "jinja") }}
<h2 id="install-theming"><a href="#install-theming">Theming</a></h2>
<div class="prose">
<p>You can import any <a href="https://shadcn-ui.com" target="_blank">shadcn/ui</a> compatible theme (e.g. <a href="https://tweakcn.com" target="_blank">tweakcn</a>). For example:</p>
<ul>
<li>Go to <a href="https://ui.shadcn.com/themes" target="_blank">ui.shadcn.com/themes</a> and select a theme.</li>
<li>Click "Copy code" and save the theme variables in a file (e.g. <code>theme.css</code>).</li>
<li>
Import the theme in your CSS file:
{% set code %}@import "tailwindcss";
@import "basecoat-css";
@import "./theme.css";{% endset %}
{{ code_block(code, "css") }}
</li>
</ul>
</div>
<div class="flex flex-wrap gap-2 my-6">
<a class="badge-outline" href="https://ui.shadcn.com/docs/theming" target="_blank">
About shadcn/ui theming
{% lucide "arrow-right" %}
</a>
</div>
<h2 id="install-customization"><a href="#install-manual">Customization</a></h2>
<div class="prose">
<section class="prose">
<p>You can override default styles using Tailwind:</p>
</section>
{% set code %}<button class="btn font-normal ">Click me</button>{% endset %}
{{ code_block(code, "html") }}
<section class="prose">
<p>You can also extend or override the existing styles in your own Tailwind files:</p>
</section>
{% set code %}@import "tailwindcss";
@import "basecoat-css";
@import "./custom.css";{% endset %}
{{ code_block(code, "css") }}
<section class="prose">
<p>More importantly, you can use the <a href="#install-theming">theme variables</a> to customize many aspects of the components (colors, fonts, sizes, etc).</p>
<p><b>If you want to make more drastic changes</b>, you can <a href="https://github.com/hunvreus/basecoat/blob/main/src/css/basecoat.css" target="_blank">copy the <code>basecoat.css</code> file</a> into your project and make your changes there.</p>
</section>
</div>
-63
View File
@@ -1,63 +0,0 @@
---
layout: layouts/page.njk
title: Introduction
description: Basecoat is a set of components built with Tailwind CSS. It is designed to be used with any traditional web stack
toc:
- label: Why Basecoat?
id: why
- label: How it works
id: how-it-works
- label: How can I help?
id: how-can-i-help
---
<h2 id="why"><a href="#why">Why Basecoat?</a></h2>
<div class="prose">
<p><b>Basecoat brings the magic of <a href="https://ui.shadcn.com/">shadcn/ui</a> to any traditional web stack: no React required.</b></p>
<p>Tailwind won. But building UIs with utility classes alone kinda sucks. Most Tailwind libraries like Flowbite, Preline, or even Tailwind UI ask you to copy walls of unreadable classes into your HTML. It works, but it's messy and hard to maintain.</p>
<p>shadcn/ui avoids that by wrapping everything in React components. It also gives you a killer design system, theme support, a CLI, and a growing ecosystem.</p>
<p>But maybe you're not using React. Maybe your app is built with plain HTML. Or Flask. Or Rails. Or Laravel. Or Django. Or whatever.</p>
<p><b>That's where Basecoat comes in.</b> It gives you modern, accessible components with the simplicity of plain HTML and Tailwind. Basecoat is:</p>
<ul>
<li><b>Lightweight</b>: no runtime JS, just CSS and tiny Alpine.js scripts for the more interactive components.</li>
<li><b>Easy to use</b>: add classes like <code>btn</code> or <code>input</code> and you're done.</li>
<li><b>Framework-agnostic</b>: works with any backend or frontend stack.</li>
<li><b>Accessible</b>: components follow accessibility best practices.</li>
<li><b>Dark mode ready</b>: respects your Tailwind config.</li>
<li><b>Extendable</b>: tweak styles with Tailwind or CSS variables.</li>
<li><b>Themable</b>: fully compatible with shadcn/ui themes.</li>
<li><b>Readable</b>: no class soup, just clean markup.</li>
<li><b>Free and open source</b>: MIT licensed.</li>
</ul>
</div>
<h2 id="how-it-works"><a href="#how-it-works">How it works</a></h2>
<div class="prose">
<p>Add a single CSS file to your Tailwind setup. Use components by dropping in simple classes like <code>btn</code>, <code>form</code>, or <code>card</code>.</p>
<p>Some components (like modals or dropdowns) use a tiny bit of Alpine.js. You can skip it if you don't need interactivity.</p>
<p>There's also a CLI to help you scaffold components and get set up fast.</p>
<p><a href="/installation">Check out the installation guide</a> to get started.</p>
</div>
<h2 id="how-can-i-help"><a href="#how-can-i-help">How can I help?</a></h2>
<div class="prose">
<p>Basecoat is 100% open source and free.</p>
<ul>
<li><a href="https://github.com/hunvreus/basecoat">Star it on GitHub</a></li>
<li><a href="https://github.com/hunvreus/basecoat/issues">Report bugs or request features</a></li>
<li><a href="https://github.com/hunvreus/basecoat/pulls">Submit a pull request</a></li>
<li><a href="https://github.com/sponsors/hunvreus">Sponsor the project</a></li>
</ul>
</div>
-52
View File
@@ -1,52 +0,0 @@
---
layout: layouts/layout.njk
title: Kitchen Sink
description: A collection of all the components available in Basecoat.
---
{% set components = [
{ label: "Accordion", id: "accordion" },
{ label: "Alert", id: "alert" },
{ label: "Alert Dialog", id: "alert-dialog" },
{ label: "Avatar", id: "avatar" },
{ label: "Badge", id: "badge" },
{ label: "Breadcrumb", id: "breadcrumb" },
{ label: "Button", id: "button" },
{ label: "Card", id: "card" },
{ label: "Checkbox", id: "checkbox" },
{ label: "Combobox", id: "combobox" },
{ label: "Dialog", id: "dialog" },
{ label: "Dropdown Menu", id: "dropdown-menu" },
{ label: "Form", id: "form" },
{ label: "Input", id: "input" },
{ label: "Label", id: "label" },
{ label: "Pagination", id: "pagination" },
{ label: "Popover", id: "popover" },
{ label: "Radio Group", id: "radio-group" },
{ label: "Select", id: "select" },
{ label: "Skeleton", id: "skeleton" },
{ label: "Slider", id: "slider" },
{ label: "Switch", id: "switch" },
{ label: "Table", id: "table" },
{ label: "Tabs", id: "tabs" },
{ label: "Textarea", id: "textarea" },
{ label: "Toast", id: "toast" },
{ label: "Tooltip", id: "tooltip" }
] %}
<div class="flex gap-x-10 justify-center">
<div>
<header class="space-y-2 mb-8">
<h1 class="text-3xl font-semibold tracking-tight">{{ title }}</h1>
<p class="text-muted-foreground">{{ description }}</p>
</header>
<div class="grid gap-4 flex-1">
{% for component in components %}
{% include "partials/kitchen-sink/" ~ component.id ~ ".njk" %}
{% endfor %}
</div>
</div>
<div class="hidden text-sm xl:block w-full max-w-[200px]">
{% from "macros/toc.njk" import toc_nav %}
{{ toc_nav(components) }}
</div>
</div>
-7
View File
@@ -1,7 +0,0 @@
---
permalink: /robots.txt
eleventyExcludeFromCollections: true
---
Sitemap: {{ site.url }}/sitemap.xml
User-agent: *
Disallow:
-15
View File
@@ -1,15 +0,0 @@
---
permalink: /sitemap.xml
eleventyExcludeFromCollections: true
---
<?xml version="1.0" encoding="utf-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{% for page in collections.all %}
{% if not page.url.startsWith('/fragments/') %}
<url>
<loc>{{ site.url }}{{ page.url | url }}</loc>
<lastmod>{{ page.date.toISOString() }}</lastmod>
</url>
{% endif %}
{% endfor %}
</urlset>
-71
View File
@@ -1,71 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Anchor Positioning Test (Top, End)</title>
<script type="module">
if (!("anchorName" in document.documentElement.style)) {
console.log("Loading CSS anchor positioning polyfill...");
import("https://unpkg.com/@oddbird/css-anchor-positioning");
}
</script>
<style>
body {
font-family: sans-serif;
display: grid;
place-items: center;
height: 100vh;
margin: 0;
background: #222;
color: #eee;
}
button {
padding: 0.5em 1em;
font-size: 1rem;
border-radius: 6px;
border: 1px solid #888;
background: #444;
color: #eee;
cursor: pointer;
}
[popover] {
border: 1px solid #888;
border-radius: 6px;
background: #333;
color: #eee;
padding: 1em;
margin: 0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}
/* The core anchor positioning logic */
#my-popover {
position: absolute;
inset: auto; /* Required to override default popover centering */
bottom: anchor(top); /* Position bottom of popover to top of anchor */
right: anchor(right); /* Align right edge of popover with right edge of anchor */
margin-bottom: 8px; /* A little space between popover and button */
}
</style>
</head>
<body>
<button
popovertarget="my-popover"
id="my-anchor-button"
style="anchor-name: --my-anchor;">
Click Me
</button>
<div
popover
id="my-popover"
style="position-anchor: --my-anchor;">
I should be anchored above the button, aligned to the right.
</div>
</body>
</html>
+233 -3363
View File
File diff suppressed because it is too large Load Diff
+107 -20
View File
@@ -1,12 +1,18 @@
{
"name": "basecoat",
"version": "0.2.3",
"description": "shadcn/ui for the rest of us",
"author": "hunvreus",
"name": "basecoat-css",
"version": "1.0.2",
"description": "Tailwind CSS for Basecoat components",
"author": {
"name": "hunvreus",
"url": "https://x.com/hunvreus"
},
"license": "MIT",
"type": "module",
"private": true,
"workspaces": [
"packages/*"
"main": "dist/basecoat.css",
"style": "dist/basecoat.css",
"files": [
"dist/",
"templates/"
],
"keywords": [
"components",
@@ -21,10 +27,8 @@
"css",
"html",
"jinja",
"nunjucks",
"alpinejs"
"nunjucks"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/hunvreus/basecoat.git"
@@ -32,21 +36,104 @@
"bugs": {
"url": "https://github.com/hunvreus/basecoat/issues"
},
"homepage": "https://github.com/hunvreus/basecoat#readme",
"homepage": "https://basecoatui.com/installation#npm",
"exports": {
".": "./dist/basecoat.css",
"./css": "./dist/basecoat.css",
"./basecoat": "./dist/js/basecoat.js",
"./basecoat.min": "./dist/js/basecoat.min.js",
"./all": "./dist/js/all.js",
"./all.min": "./dist/js/all.min.js",
"./accordion": "./dist/js/accordion.js",
"./accordion.min": "./dist/js/accordion.min.js",
"./chart": "./dist/js/chart.js",
"./chart.min": "./dist/js/chart.min.js",
"./combobox": "./dist/js/combobox.js",
"./combobox.min": "./dist/js/combobox.min.js",
"./command": "./dist/js/command.js",
"./command.min": "./dist/js/command.min.js",
"./dropdown-menu": "./dist/js/dropdown-menu.js",
"./dropdown-menu.min": "./dist/js/dropdown-menu.min.js",
"./drawer": "./dist/js/drawer.js",
"./drawer.min": "./dist/js/drawer.min.js",
"./popover": "./dist/js/popover.js",
"./popover.min": "./dist/js/popover.min.js",
"./range": "./dist/js/range.js",
"./range.min": "./dist/js/range.min.js",
"./select": "./dist/js/select.js",
"./select.min": "./dist/js/select.min.js",
"./sidebar": "./dist/js/sidebar.js",
"./sidebar.min": "./dist/js/sidebar.min.js",
"./tabs": "./dist/js/tabs.js",
"./tabs.min": "./dist/js/tabs.min.js",
"./toast": "./dist/js/toast.js",
"./toast.min": "./dist/js/toast.min.js",
"./all.css": "./dist/basecoat.all.css",
"./base": "./dist/basecoat-base.css",
"./base.css": "./dist/basecoat-base.css",
"./base.cdn.css": "./dist/basecoat-base.cdn.css",
"./compat": "./dist/basecoat-compat.css",
"./compat.css": "./dist/basecoat-compat.css",
"./compat.cdn.css": "./dist/basecoat-compat.cdn.css",
"./compat/legacy.css": "./dist/compat/legacy.css",
"./components": "./dist/basecoat-components.css",
"./components.css": "./dist/basecoat-components.css",
"./components/*": "./dist/components/*.css",
"./components/*.css": "./dist/components/*.css",
"./base/base.css": "./dist/base/base.css",
"./vega": "./dist/basecoat-vega.css",
"./vega.css": "./dist/basecoat-vega.css",
"./nova": "./dist/basecoat-nova.css",
"./nova.css": "./dist/basecoat-nova.css",
"./maia": "./dist/basecoat-maia.css",
"./maia.css": "./dist/basecoat-maia.css",
"./lyra": "./dist/basecoat-lyra.css",
"./lyra.css": "./dist/basecoat-lyra.css",
"./mira": "./dist/basecoat-mira.css",
"./mira.css": "./dist/basecoat-mira.css",
"./luma": "./dist/basecoat-luma.css",
"./luma.css": "./dist/basecoat-luma.css",
"./sera": "./dist/basecoat-sera.css",
"./sera.css": "./dist/basecoat-sera.css",
"./rhea": "./dist/basecoat-rhea.css",
"./rhea.css": "./dist/basecoat-rhea.css",
"./styles/vega": "./dist/styles/vega.css",
"./styles/nova": "./dist/styles/nova.css",
"./styles/maia": "./dist/styles/maia.css",
"./styles/lyra": "./dist/styles/lyra.css",
"./styles/mira": "./dist/styles/mira.css",
"./styles/luma": "./dist/styles/luma.css",
"./styles/sera": "./dist/styles/sera.css",
"./styles/rhea": "./dist/styles/rhea.css",
"./styles/*": "./dist/styles/*.css",
"./styles/*.css": "./dist/styles/*.css",
"./package.json": "./package.json",
"./styles/style-vega.css": "./dist/styles/vega.css",
"./styles/style-nova.css": "./dist/styles/nova.css",
"./styles/style-maia.css": "./dist/styles/maia.css",
"./styles/style-lyra.css": "./dist/styles/lyra.css",
"./styles/style-mira.css": "./dist/styles/mira.css",
"./styles/style-luma.css": "./dist/styles/luma.css",
"./styles/style-sera.css": "./dist/styles/sera.css",
"./styles/style-rhea.css": "./dist/styles/rhea.css",
"./styles/style-*.css": "./dist/styles/*.css",
"./templates/nunjucks/*": "./templates/nunjucks/*",
"./templates/jinja/*": "./templates/jinja/*"
},
"scripts": {
"build": "node ./scripts/build.js",
"docs:build": "tailwindcss -i ./docs/css/styles.css -o ./docs/src/assets/styles.css --minify && eleventy",
"docs:dev:css": "tailwindcss -i ./docs/css/styles.css -o ./docs/src/assets/styles.css --watch",
"docs:dev:eleventy": "ELEVENTY_ENV=prod eleventy --serve",
"docs:dev": "concurrently \"npm:docs:dev:css\" \"npm:docs:dev:eleventy\""
"prepack": "node ./scripts/build.js",
"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 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": {
"@11ty/eleventy": "^3.0.0",
"@grimlink/eleventy-plugin-lucide-icons": "^2.1.4",
"@tailwindcss/cli": "^4.1.3",
"concurrently": "^9.1.2",
"@tailwindcss/cli": "^4.1.17",
"prettier": "^3.5.3",
"tailwindcss": "^4.1.3",
"tailwindcss": "^4.1.17",
"terser": "^5.42.0"
}
}
-56
View File
@@ -1,56 +0,0 @@
# basecoat-cli
This package provides a Command Line Interface (CLI) to help you quickly scaffold [Basecoat](https://basecoatui.com) components into your project.
## Prerequisites
- Ensure you have Node.js and npm (or your preferred package manager) installed.
- For the components to function correctly, your project should have:
- [Tailwind CSS](https://tailwindcss.com/docs/installation) installed and configured.
- The `basecoat-css` package installed and imported (or `basecoat.css` manually added).
- [Alpine.js](https://alpinejs.dev/docs/installation) installed and initialized, as Basecoat components rely on it for interactivity.
## Usage
Use the `add` command to add components to your project:
```bash
# npx
npx basecoat-cli add [component]
# pnpm
pnpm dlx basecoat-cli add [component]
# bun
bunx basecoat-cli add [component]
# yarn
yarn dlx basecoat-cli add [component]
```
Replace `[component]` with the name of the component you wish to add (e.g., `dialog`, `select`) or leave it out if you want to pick from the list of available components
or install all of them at once.
The CLI will prompt you for:
1. **Template Engine:** Choose between Nunjucks (`.njk`) or Jinja (`.html.jinja`).
2. **Assets Directory:** The path where component assets (like JavaScript files) should be placed (e.g., `src/assets/js/components`).
3. **Macros Directory:** The path where component macros should be placed (e.g., `src/_includes/macros/components`).
The CLI will then copy the necessary JavaScript files and Nunjucks/Jinja macros for the selected component into the specified directories.
### Example
To add the `dialog` component:
```bash
npx basecoat-cli add dialog
```
## Documentation
For more detailed information on components, their usage, and customization options, please refer to the [Basecoat documentation](https://basecoatui.com/installation/#install-cli).
## License
[MIT](https://github.com/hunvreus/basecoat/blob/main/LICENSE.md)
-193
View File
@@ -1,193 +0,0 @@
#!/usr/bin/env node
import { Command } from 'commander';
import inquirer from 'inquirer';
import fs from 'fs-extra';
import path from 'path';
import { fileURLToPath } from 'url';
const program = new Command();
// Attempt to read version from the CLI's own package.json
let packageVersion = 'unknown';
try {
const cliScriptRunningDir = path.dirname(fileURLToPath(import.meta.url)); // .../packages/cli/dist
const cliPackageRootDir = path.resolve(cliScriptRunningDir, '..'); // .../packages/cli
const packageJsonPath = path.join(cliPackageRootDir, 'package.json');
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
packageVersion = JSON.parse(packageJsonContent).version;
} catch (error) {
console.warn('Could not read CLI package version:', error.message);
}
const DEFAULT_CONFIG = {
templateEngine: null,
templateDest: './components/basecoat',
scriptDest: './static/js/basecoat'
};
let config = { ...DEFAULT_CONFIG }; // User-specific config, potentially updated by prompts
// Discovers available components from the CLI's bundled assets.
async function getAvailableComponents() {
try {
const cliScriptRunningDir = path.dirname(fileURLToPath(import.meta.url));
const assetsDir = path.resolve(cliScriptRunningDir, 'assets');
const jsSourceDir = path.join(assetsDir, 'js');
const jsFiles = await fs.readdir(jsSourceDir);
return jsFiles
.filter(file => file.endsWith('.js'))
.map(file => path.basename(file, '.js'));
} catch (error) {
console.error("Error reading component source directories from CLI assets:", error);
return [];
}
}
// Ensures necessary configuration (template engine, destination paths) is set, prompting the user if needed.
async function ensureConfiguration() {
if (!config.templateEngine) {
const engineChoice = await inquirer.prompt([
{
type: 'list',
name: 'engine',
message: 'Which template engine are you using?',
choices: ['nunjucks', 'jinja'],
}
]);
config.templateEngine = engineChoice.engine;
}
const pathPrompts = [];
if (!config.templateDest || config.templateDest === DEFAULT_CONFIG.templateDest) {
pathPrompts.push({
type: 'input',
name: 'templateDest',
message: 'Where should template files be placed?',
default: DEFAULT_CONFIG.templateDest,
});
}
if (!config.scriptDest || config.scriptDest === DEFAULT_CONFIG.scriptDest) {
pathPrompts.push({
type: 'input',
name: 'scriptDest',
message: 'Where should script files be placed?',
default: DEFAULT_CONFIG.scriptDest,
});
}
if (pathPrompts.length > 0) {
const pathChoices = await inquirer.prompt(pathPrompts);
if (pathChoices.templateDest) config.templateDest = pathChoices.templateDest;
if (pathChoices.scriptDest) config.scriptDest = pathChoices.scriptDest;
}
}
// Copies a component's template and script files to the user's project.
async function addComponent(componentName) {
console.log(`\nProcessing component: ${componentName}...`);
const cliScriptRunningDir = path.dirname(fileURLToPath(import.meta.url));
const assetsDir = path.resolve(cliScriptRunningDir, 'assets');
const templateExt = config.templateEngine === 'jinja' ? '.html.jinja' : '.njk';
const templateSource = path.join(assetsDir, config.templateEngine, `${componentName}${templateExt}`);
const scriptSource = path.join(assetsDir, 'js', `${componentName}.js`);
const templateDestPath = path.resolve(process.cwd(), config.templateDest, `${componentName}${templateExt}`);
const scriptDestPath = path.resolve(process.cwd(), config.scriptDest, `${componentName}.js`);
try {
const templateExists = await fs.pathExists(templateSource);
const scriptExists = await fs.pathExists(scriptSource);
if (!templateExists) {
console.error(` Error: Template file for component '${componentName}' not found in CLI assets. Searched: ${templateSource}`);
// Optionally, list available template engines or files for that component if helpful
}
if (!scriptExists) {
console.error(` Error: Script file for component '${componentName}' not found in CLI assets. Searched: ${scriptSource}`);
}
if (!templateExists || !scriptExists) return; // Skip this component if sources are missing
await fs.ensureDir(path.dirname(templateDestPath));
await fs.ensureDir(path.dirname(scriptDestPath));
await fs.copyFile(templateSource, templateDestPath);
console.log(` -> Copied template to: ${templateDestPath}`);
await fs.copyFile(scriptSource, scriptDestPath);
console.log(` -> Copied script to: ${scriptDestPath}`);
} catch (error) {
console.error(` Error processing component ${componentName}:`, error);
}
}
// Setup main CLI program
program
.name('basecoat-cli')
.description('Add Basecoat components to your project')
.version(packageVersion);
// Define the 'add' command
program
.command('add')
.description('Add one or more Basecoat components to your project')
.argument('[components...]', 'Names of components to add (e.g., dialog select)')
.action(async (componentsArg) => {
let componentsToAdd = []; // Initialize as an empty array
try {
if (!componentsArg || componentsArg.length === 0) {
const availableComponents = await getAvailableComponents();
if (availableComponents.length === 0) {
console.error('Error: No components available in the CLI. Build might be corrupted.');
return;
}
const allComponentsChoice = 'All components';
const choices = [allComponentsChoice, ...availableComponents];
const answers = await inquirer.prompt([
{
type: 'list', // Changed from checkbox to list
name: 'selectedComponent', // Changed name for clarity
message: 'Which component(s) would you like to add?',
choices: choices,
}
]);
if (answers.selectedComponent === allComponentsChoice) {
componentsToAdd = availableComponents; // Add all
} else {
componentsToAdd = [answers.selectedComponent]; // Add the single selected one
}
} else {
// If components were provided as arguments, use them directly
// (This part of the logic remains, ensuring componentsArg is an array)
componentsToAdd = Array.isArray(componentsArg) ? componentsArg : [componentsArg];
}
if (!componentsToAdd || componentsToAdd.length === 0) {
console.log('No components selected. Exiting.');
return;
}
await ensureConfiguration(); // Confirm/gather destination paths
for (const componentName of componentsToAdd) {
await addComponent(componentName);
}
console.log('\nComponent addition process finished.');
} catch (error) {
if (error.isTtyError || error.constructor?.name === 'ExitPromptError') { // Handle inquirer cancellation
console.log('\nOperation cancelled by user.');
} else {
console.error('\nAn unexpected error occurred during the add command:', error);
}
process.exit(1); // Exit with error code for unexpected errors
}
});
program.parse(process.argv);
-45
View File
@@ -1,45 +0,0 @@
{
"name": "basecoat-cli",
"version": "0.2.3",
"description": "Add Basecoat components to your project",
"author": "hunvreus",
"license": "MIT",
"type": "module",
"bin": {
"basecoat-cli": "./dist/index.js"
},
"files": [
"dist/"
],
"dependencies": {
"commander": "^13.1.0",
"fs-extra": "^11.3.0",
"inquirer": "^12.6.0"
},
"keywords": [
"cli",
"components",
"component library",
"component system",
"ui",
"ui kit",
"shadcn",
"shadcn/ui",
"tailwind",
"tailwindcss",
"css",
"html",
"jinja",
"nunjucks",
"alpinejs"
],
"repository": {
"type": "git",
"url": "git+https://github.com/hunvreus/basecoat.git",
"directory": "packages/cli"
},
"bugs": {
"url": "https://github.com/hunvreus/basecoat/issues"
},
"homepage": "https://basecoatui.com/installation#install-cli"
}
-63
View File
@@ -1,63 +0,0 @@
# basecoat-css
This package provides the core CSS styles for [Basecoat](https://basecoatui.com), a component library built with Tailwind CSS.
## Prerequisites
Your project must have [Tailwind CSS](https://tailwindcss.com/docs/installation) installed and configured, as Basecoat relies on Tailwind utility classes and theming.
## Installation
Install with any package manager:
```bash
npm install basecoat-css # or pnpm add / yarn add / bun add
```
## Usage
Add it just after Tailwind in your stylesheet:
```css
@import "tailwindcss";
@import "basecoat-css";
```
That's it, you can use any Basecoat class (`btn`, `card`, `input`, etc) in your markup.
### (Optional) JavaScript files
Some interactive components (Dropdown Menu, Popover, Select, Sidebar, Tabs, Toast) need some JavaScript.
With a build tool (ESM):
```js
import 'basecoat-css/all';
```
Or cherry-pick the components you need:
```js
import 'basecoat-css/tabs';
import 'basecoat-css/popover';
```
Without a build tool, copy the files from `node_modules`:
```bash
npx copyfiles -u 1 "node_modules/basecoat-css/dist/js/**/*" public/js/basecoat
```
Then reference what you need, e.g.
```html
<script src="/js/basecoat/tabs.min.js" defer></script>
```
## Documentation
For more detailed information on components, their usage, and customization options, please refer to the [Basecoat documentation](https://basecoatui.com/installation/#install-css).
## License
[MIT](https://github.com/hunvreus/basecoat/blob/main/LICENSE.md)
-60
View File
@@ -1,60 +0,0 @@
{
"name": "basecoat-css",
"version": "0.2.3",
"description": "Tailwind CSS for Basecoat components",
"author": {
"name": "hunvreus",
"url": "https://x.com/hunvreus"
},
"license": "MIT",
"type": "module",
"main": "dist/basecoat.css",
"style": "dist/basecoat.css",
"files": [
"dist/"
],
"keywords": [
"components",
"component library",
"component system",
"ui",
"ui kit",
"shadcn",
"shadcn/ui",
"tailwind",
"tailwindcss",
"css",
"html",
"jinja",
"nunjucks",
"alpinejs"
],
"repository": {
"type": "git",
"url": "git+https://github.com/hunvreus/basecoat.git",
"directory": "packages/css"
},
"bugs": {
"url": "https://github.com/hunvreus/basecoat/issues"
},
"homepage": "https://basecoatui.com/installation#install-npm",
"exports": {
".": "./dist/basecoat.css",
"./css": "./dist/basecoat.css",
"./all": "./dist/js/all.js",
"./all.min": "./dist/js/all.min.js",
"./dropdown-menu": "./dist/js/dropdown-menu.js",
"./dropdown-menu.min": "./dist/js/dropdown-menu.min.js",
"./popover": "./dist/js/popover.js",
"./popover.min": "./dist/js/popover.min.js",
"./select": "./dist/js/select.js",
"./select.min": "./dist/js/select.min.js",
"./sidebar": "./dist/js/sidebar.js",
"./sidebar.min": "./dist/js/sidebar.min.js",
"./tabs": "./dist/js/tabs.js",
"./tabs.min": "./dist/js/tabs.min.js",
"./toast": "./dist/js/toast.js",
"./toast.min": "./dist/js/toast.min.js",
"./package.json": "./package.json"
}
}

Some files were not shown because too many files have changed in this diff Show More