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
Ronan Berder 7b02f64831 Bug fixes 2025-06-18 20:57:20 +08:00
Ronan Berder 9b59f43a67 Fixing more issues with popovers 2025-06-18 20:55:32 +08:00
Ronan Berder 46bf212f41 Fixing docs 2025-06-18 19:54:15 +08:00
Ronan Berder be9c6e537d Removing unused files 2025-06-18 19:42:39 +08:00
Ronan Berder 218c16ba6b Fixing bugs and docs issues 2025-06-18 19:42:26 +08:00
Ronan Berder a5c1b8df57 Fixes to docs 2025-06-17 16:25:18 +08:00
Ronan Berder fda413a5af Fixing README for basecoat-css 2025-06-16 21:28:32 +08:00
Ronan Berder 40c700b861 Bumping version number for 0.2.1 2025-06-16 21:14:28 +08:00
Ronan Berder 43673ad316 Updated Jinja templates, fixed Nunjucks 2025-06-16 21:11:36 +08:00
Ronan Berder 1513075cb6 Fixing highlight 2025-06-16 19:54:37 +08:00
Ronan Berder 7084ed25e7 Resolve merging conflict 2025-06-16 19:43:03 +08:00
Ronan Berder 0da20133f6 Final polish 2025-06-16 19:41:10 +08:00
Ronan Berder 8a7341e345 Polishing 2025-06-16 19:14:39 +08:00
Ronan Berder c066a392f4 Cleaning up installation 2025-06-16 18:28:18 +08:00
Ronan Berder 17a18c1871 Getting ready to clean up the package and release 0.2.0 2025-06-16 17:58:01 +08:00
Ronan Berder f51a03da79 Removing wrangler config 2025-06-15 10:36:04 +08:00
Ronan Berder feab320e4c Attempting deployment with Workers 2025-06-15 10:25:10 +08:00
Ronan Berder 7cd0362254 Typo 2025-06-15 09:40:08 +08:00
Ronan Berder fed3dd209f Fixing comments 2025-06-15 08:57:30 +08:00
Ronan Berder e0ff2caf3c Fixing comments 2025-06-15 08:53:35 +08:00
Ronan Berder 553612ccad Fixing wrong dependency 2025-06-15 08:39:03 +08:00
Ronan Berder 9f5ffe6f51 Updated build script 2025-06-15 08:11:00 +08:00
Ronan Berder 386d139fe2 Finalized all component docs 2025-06-14 16:48:01 +08:00
Ronan Berder 7e3773276c Revamped the select docs 2025-06-13 14:14:38 +08:00
Ronan Berder 056c025628 Working on docs 2025-06-12 18:44:27 +08:00
Ronan Berder ebf5b6e33f Working on docs 2025-06-12 18:44:17 +08:00
Ronan Berder d37dc99923 Update sidebar.njk 2025-06-12 13:37:22 +08:00
Ronan Berder 3b0b38a4cf PoC for new components, including select 2025-06-12 10:37:19 +08:00
Ronan Berder cdc52a810e Fixing bigs in macros 2025-06-08 12:32:52 +08:00
Ronan Berder 526cfab55c Merge pull request #12 from HugoDF/main
fix: typo in textarea docs title
2025-05-21 18:18:23 -07:00
HugoDF 40d468816a fix typo 2025-05-21 21:18:54 +01:00
Ronan Berder e2dcbcb4dd Merge branch 'main' into development 2025-05-19 00:15:39 -07:00
Ronan Berder ef50b3b463 Merge branch 'main' into development 2025-05-07 15:12:48 +08:00
Ronan Berder 061d0d8e48 Adding chart to dev 2025-05-07 10:07:14 +08:00
298 changed files with 38285 additions and 18017 deletions
-45
View File
@@ -1,45 +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.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/)
-76
View File
@@ -1,76 +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-16 mb-6 scroll-m-22 border-b pb-4 text-xl font-semibold tracking-tight first:mt-0;
}
> h3 {
@apply mt-8 mb-6 scroll-m-22 text-lg 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 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;
}
}
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;
}
.step:before {
@apply md:absolute w-8 h-8 md:w-9 md:h-9 bg-muted rounded-full font-mono font-medium text-center text-base inline-flex items-center justify-center -indent-px border-4 mr-2 border-background;
@apply md:ml-[-50px] md:mt-[-4px];
content: counter(step);
}
}
-15
View File
@@ -1,15 +0,0 @@
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Geist+Mono&display=swap");
@import "tailwindcss";
@import "../../src/css/basecoat.css";
@import "./custom.css";
@theme {
--font-sans: 'Geist';
--font-mono: 'Geist Mono';
}
@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);
}
}
-10
View File
@@ -1,10 +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"
};
-122
View File
@@ -1,122 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<!-- Theme -->
<script>
function isDark() {
return localStorage.getItem('darkMode') === 'dark' || (!localStorage.getItem('darkMode') && window.matchMedia('(prefers-color-scheme: dark)').matches);
}
if (isDark()) {
document.documentElement.classList.add('dark');
}
if (localStorage.getItem('themeVariant')) {
document.documentElement.classList.add(`theme-${localStorage.getItem('themeVariant')}`);
}
document.addEventListener('alpine:init', () => {
Alpine.store('darkMode', {
isDark: isDark(),
toggle() {
this.isDark = document.documentElement.classList.toggle('dark');
localStorage.setItem('darkMode', this.isDark ? 'dark' : 'light');
}
});
Alpine.store('themeVariant', {
name: localStorage.getItem('themeVariant'),
setThemeVariant(name) {
this.name = name;
localStorage.setItem('themeVariant', name);
document.documentElement.classList.forEach(className => {
if (className.startsWith('theme-')) {
document.documentElement.classList.remove(className);
}
});
document.documentElement.classList.add(`theme-${name}`);
}
});
});
</script>
<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/dialog.js" defer></script>
<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>
<script>
document.addEventListener('alpine:init', () => {
window.basecoat.registerDialog(Alpine);
window.basecoat.registerDropdownMenu(Alpine);
window.basecoat.registerPopover(Alpine);
window.basecoat.registerSelect(Alpine);
window.basecoat.registerSidebar(Alpine);
window.basecoat.registerTabs(Alpine);
window.basecoat.registerToast(Alpine);
});
</script>
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<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 applyHighlighting = () => { if (window.hljs) hljs.highlightAll(); };
if (!window._hljsInitialized) {
window._hljsInitialized = true;
document.addEventListener('DOMContentLoaded', applyHighlighting);
document.addEventListener('htmx:afterSwap', applyHighlighting);
}
</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 max-w-screen-md">
<header class="space-y-2">
<h1 class="text-3xl font-bold tracking-tight">{{ title }}</h1>
<p class="text-muted-foreground">{{ 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="text-sm max-h-[650px] overflow-y-auto rounded-xl scrollbar"><code class="language-{{ language }} !bg-neutral-900 p-0">{{- code | escape -}}</code></pre>
<button
@click="copy()"
class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 z-10 h-6 w-6 text-zinc-50 hover:bg-zinc-700 hover:text-zinc-50 [&_svg]:h-3 [&_svg]:w-3 absolute right-3 top-3"
>
{% lucide "clipboard", { "class": "size-4", "x-show": "!copied" } %}
{% lucide "check", { "class": "size-4", "x-show": "copied" } %}
</button>
</div>
{% endmacro %}
@@ -1,28 +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(
id="code-block-{{ id }}",
tabsets=tabsets,
default_tab_index=(2 if default_code else 1),
register_on=["alpine:init", "htmx:afterSwap"]
) }}
</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 %}
-53
View File
@@ -1,53 +0,0 @@
<div class="flex h-14 w-full items-center gap-2 px-4">
<button
type="button"
aria-label="Hide sidebar"
data-tooltip="Hide sidebar"
:data-tooltip="sidebarOpen ? 'Hide sidebar' : 'Show sidebar'"
data-side="bottom"
data-align="start"
class="btn-sm-icon-ghost mr-auto size-7 -ml-1.5"
x-init
x-data="{ sidebarOpen: true }"
@click="
sidebarOpen = !sidebarOpen;
$dispatch('sidebar:toggle');
"
>
{% lucide "panel-left" %}
</button>
<select
class="select h-8 leading-none"
x-init
@change="$store.themeVariant.setThemeVariant($event.target.value)"
:value="$store.themeVariant.name"
>
<option value="">Default</option>
<option value="claude">Claude</option>
<option value="doom-64">Doom 64</option>
<option value="supabase">Supabase</option>
</select>
<button
type="button"
:aria-label="$store.darkMode.isDark ? 'Switch to light mode' : 'Switch to dark mode'"
:data-tooltip="$store.darkMode.isDark ? 'Light mode' : 'Dark mode'"
data-side="bottom"
class="btn-icon-outline size-8"
x-init
@click="$store.darkMode.toggle()"
>
<span x-show="$store.darkMode.isDark">{% lucide "sun" %}</span>
<span x-show="!$store.darkMode.isDark">{% 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,28 +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">
<div class="flex flex-col gap-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>
</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, title and description.</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 "popover.njk" import popover %}
<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 %}
{% call popover(
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>
</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(
content_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(
content_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,76 +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.",
main_attrs={"x-ref": "myDialog"},
trigger="Edit Profile",
trigger_attrs={"class": "btn-outline"},
content_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" @click="open = false">Close</button>
{% endset %}
{% call dialog(
id="dialog-example",
title="Scrollable Content",
description="This is a dialog with scrollable content.",
main_attrs={"x-ref": "myDialog"},
trigger="Scrollable Content",
trigger_attrs={"class": "btn-outline"},
content_attrs={"class": "w-full sm:max-w-[425px] max-h-[612px]"},
content_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 %}
<button
x-init
@click="$dispatch('dialog:open', { id: 'dialog-example' })"
type="button"
class="btn-outline"
>
Trigger dialog:open
</button>
</div>
</div>
</section>
@@ -1,292 +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",
trigger="Open",
trigger_attrs={"class": "btn-outline"},
content_attrs={"class": "min-w-56"}
) %}
<div role="group" aria-labelledby="account-options">
<span id="account-options" role="heading">My Account</span>
<button type="button" role="menuitem">
Profile
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</button>
<button type="button" role="menuitem">
Billing
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
</button>
<button type="button" role="menuitem">
Settings
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
</button>
<button type="button" role="menuitem">
Keyboard shortcuts
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘K</span>
</button>
</div>
<hr role="separator">
<button type="button" role="menuitem">GitHub</button>
<button type="button" role="menuitem">Support</button>
<button type="button" role="menuitem" disabled>API</button>
<hr role="separator">
<button type="button" role="menuitem">
Logout
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</button>
{% endcall %}
{% call dropdown_menu(
id="dropdown-menu-checkboxes",
trigger="Checkboxes",
trigger_attrs={"class": "btn-outline"},
content_attrs={"class": "min-w-56"},
register_on=["alpine:init", "htmx:afterSwap"]
) %}
<div role="group" aria-labelledby="account-options">
<span id="account-options" role="heading">Account Options</span>
<button type="button" role="menuitem">
{% lucide "user" %}
Profile
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</button>
<button type="button" role="menuitem">
{% lucide "credit-card" %}
Billing
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
</button>
<button type="button" role="menuitem">
{% lucide "settings" %}
Settings
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
</button>
</div>
<hr role="separator">
<div role="group" aria-labelledby="appearance-options">
<span id="appearance-options" role="heading">Appearance</span>
<button
type="button"
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>
</button>
<button
type="button"
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>
</button>
<button
type="button"
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>
</button>
</div>
<hr role="separator">
<button type="button" role="menuitem">
{% lucide "log-out" %}
Logout
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</button>
{% 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">
<button type="button" role="menuitem">
{% lucide "star" %}
Upgrade to Pro
</button>
<hr role="separator">
<button type="button" role="menuitem">
{% lucide "user" %}
Account
</button>
<button type="button" role="menuitem">
{% lucide "credit-card" %}
Billing
</button>
<button type="button" role="menuitem">
{% lucide "bell" %}
Notifications
</button>
<hr role="separator">
<button type="button" role="menuitem">
{% lucide "log-out" %}
Signout
</button>
{% endset %}
{% call dropdown_menu(
id="dropdown-menu-radio-group",
trigger="Radio Group",
trigger_attrs={"class": "btn-outline"},
content_attrs={"class": "min-w-56"},
register_on=["alpine:init", "htmx:afterSwap"]
) %}
<div role="group" aria-labelledby="position-options" x-data="{ checked: 'bottom' }">
<span id="position-options" role="heading">Panel Position</span>
<hr role="separator">
<button
type="button"
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>
</button>
<button
type="button"
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>
</button>
<button
type="button"
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>
</button>
</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"},
content_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"},
content_attrs={"class": "min-w-32"}
) %}
<button type="button" role="menuitem">
{% lucide "pencil" %}
Edit
</button>
<button type="button" role="menuitem">
{% lucide "share" %}
Share
</button>
<hr role="separator">
<button type="button" 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
</button>
{% 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"},
content_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 %}
</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,76 +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">
{% call select(trigger_attrs={"class": "w-[180px]"}) %}
<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>
<div role="group" aria-labelledby="vegetable-options-label">
<span id="vegetable-options-label" role="heading">Grapes</span>
<div role="option" data-value="pineapple">Pineapple</div>
</div>
{% endcall %}
{% call select(
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(trigger_attrs={"disabled": "disabled", "class": "w-[180px]"}) %}
<div role="option" data-value="disabled">Disabled</div>
{% endcall %}
{% call select(name="just-testing", trigger_attrs={'class': 'w-[180px]'}) %}
<div type="button" role="option" data-value="bar">
{% lucide "chart-bar", {"class": "text-muted-foreground"} %}
Bar
</div>
<div type="button" role="option" data-value="line">
{% lucide "chart-line", {"class": "text-muted-foreground"} %}
Line
</div>
<div type="button" role="option" data-value="pie">
{% lucide "chart-pie", {"class": "text-muted-foreground"} %}
Pie
</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,30 +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"
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)"
>
</div>
</div>
</section>
@@ -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>
-146
View File
@@ -1,146 +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"},
content_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-full');
}
"
}
) %}
<div class="grid gap-4 p-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",
"@click": "if (window.innerWidth < 768) { open = false }"
} %}
{% 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 %}{% lucide "github" %}{% 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",
"@click": "if (window.innerWidth < 768) { open = false }"
}
}
]},
{ 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(
label="Sidebar navigation",
header=header,
footer=footer,
content_attrs={
"x-data": "{
updateCurrentPage() {
$el.querySelectorAll('a').forEach(link => {
normalizedCurrentPathname = window.location.pathname.endsWith('/') ?
window.location.pathname.slice(0, -1) :
window.location.pathname;
normalizedLinkPathname = link.pathname.endsWith('/') ?
link.pathname.slice(0, -1) :
link.pathname;
if (normalizedCurrentPathname === normalizedLinkPathname) {
link.setAttribute('aria-current', 'page');
} else {
link.setAttribute('aria-current', 'false');
}
});
}
}",
"class": "scrollbar",
"x-init": "updateCurrentPage()",
"@htmx:pushed-into-history.window": "updateCurrentPage()"
},
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>
-122
View File
@@ -1,122 +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 + 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 "dialog.njk" import dialog %}
{% set code_html %}
{% 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",
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,
register_on=["alpine:init", "htmx:afterSwap"]
) }}
{% endset %}
{% set code_script %}
<script>
{% fetchCode "src/js/dialog.js" %}
document.addEventListener('alpine:init', () => {
window.basecoat.registerDialog(Alpine);
});
</script>
{% endset %}
{{ code_preview("alert-dialog", code_script | prettyHtml ~ code_html | prettyHtml) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<h3 id="usage-default"><a href="#usage-default">HTML + Javascript</a></h3>
<div class="alert my-6">
{% lucide "circle-alert" %}
<h2>This component requires Javascript.</h2>
</div>
<section class="prose">
<p>The Alert Dialog component uses the same Alpine.js code as the <a href="/components/dialog" class="font-medium underline underline-offset-4">Dialog</a> component with one small difference: we set <code>initialCloseOnOverlayClick</code> to <code>false</code> to prevent the dialog from closing when the overlay is clicked.</p>
<p>You can include the Javascript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. <code class="text-xs">x-bind</code>, <code class="text-xs">x-data</code>, <code class="text-xs">@click</code>).</p>
</section>
<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/dialog.js" target="_blank">
Individual JS file (dialog.js)
{% lucide "arrow-right" %}
</a>
</div>
{% from "tabs.njk" import tabs %}
{% set tabsets = [
{ tab: "HTML", panel: code_block(code_html | prettyHtml, class="") },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml, class="") }
] %}
{{ tabs(id="alert-dialog", tabsets=tabsets) }}
<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. If you use one of the macros, 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" @click="open = false">Cancel</button>
<button class="btn-primary" @click="open = false">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") }}
-68
View File
@@ -1,68 +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> with any of the following children:</p>
<ul>
<li>A svg icon,</li>
<li>A <code>&lt;h2&gt;</code> for the title.</li>
<li>A <code>&lt;section&gt;</code> for the description. This can include paragraphs, lists, etc.</li>
</ul>
</section>
{{ code_block(code) }}
<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) }}
-98
View File
@@ -1,98 +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_script %}
<script>
{% fetchCode "src/js/dropdown-menu.js" %}
document.addEventListener('alpine:init', () => {
window.basecoat.registerDropdownMenu(Alpine);
});
</script>
{% endset %}
{% 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"},
register_on=["alpine:init", "htmx:afterSwap"]
) %}
<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_script | prettyHtml ~ code_html | prettyHtml) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<div class="alert my-6">
{% lucide "circle-alert" %}
<h2>This component requires Javascript.</h2>
</div>
<section class="prose">
<p>This example uses some simple Tailwind CSS styles along with the <a href="/components/dropdown-menu">Dropdown Menu</a>.</p>
<p>You can include the Javascript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. <code class="text-xs">x-bind</code>, <code class="text-xs">x-data</code>, <code class="text-xs">@click</code>).</p>
</section>
<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">
Individual JS file (select.js)
{% lucide "arrow-right" %}
</a>
</div>
{% from "tabs.njk" import tabs %}
{% set tabsets = [
{ tab: "HTML", panel: code_block(code_html | prettyHtml, class="") },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml, class="") }
] %}
{{ tabs(id="alert-dialog", tabsets=tabsets) }}
-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) }}
-141
View File
@@ -1,141 +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>
<section class="prose">
<p>Add the <code>card</code> class to any element and then define any of the children elements:</p>
<ul>
<li>A <code>&lt;header&gt;</code> for the card header, which can contain a <code>&lt;h2&gt;</code> for the title and a <code>&lt;p&gt;</code> for the description.</li>
<li>A <code>&lt;section&gt;</code> for the card content.</li>
<li>A <code>&lt;footer&gt;</code> for the card footer.</li>
</ul>
</section>
{% 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) }}
<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) }}
-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) }}
-214
View File
@@ -1,214 +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-default
- 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...",
register_on=["alpine:init", "htmx:afterSwap"]
) %}
{% for framework in options_frameworks %}
<div role="option" data-value="{{ framework }}">{{ framework }}</div>
{% endfor %}
{% endcall %}
{% endset %}
{% set code_script %}
<script>
{% fetchCode "src/js/select.js" %}
document.addEventListener('alpine:init', () => {
window.basecoat.registerSelect(Alpine);
});
</script>
{% endset %}
{{ code_preview("combobox", code_script | prettyHtml ~ code_html | prettyHtml) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<h3 id="usage-default"><a href="#usage-default">HTML + Javascript</a></h3>
<div class="alert my-6">
{% lucide "circle-alert" %}
<h2>This component requires Javascript.</h2>
</div>
<div class="prose">
<p>Combobox uses the same Alpine.js code and markup as the <a href="/components/select">Select</a> component, the only difference being the search box at the top of the listbox.</p>
<p>You can set up a few additional options on the <code>&lt;div data-popover&gt;</code> element:</p>
<ul>
<li><code>data-side</code> can be set to <code>top</code>, <code>bottom</code>, <code>left</code>, or <code>right</code> to change the side of the popover.</li>
<li><code>data-align</code> can be set to <code>start</code>, <code>center</code>, or <code>end</code> to change the alignment of the popover.</li>
<li><code>data-empty</code> to set up the message when the search is empty.</li>
</ul>
<p>You can include the Javascript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. <code class="text-xs">x-bind</code>, <code class="text-xs">x-data</code>, <code class="text-xs">@click</code>).</p>
</div>
<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">
Individual JS file (select.js)
{% lucide "arrow-right" %}
</a>
</div>
{% from "tabs.njk" import tabs %}
{% set tabsets = [
{ tab: "HTML", panel: code_block(code_html | prettyHtml, class="") },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml, class="") }
] %}
{{ tabs(id="combobox", tabsets=tabsets) }}
<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]"},
content_attrs={"class": "w-72"},
listbox_attrs={"data-empty": "No timezone found."},
is_combobox=true,
search_placeholder="Search timezone...",
register_on=["alpine:init", "htmx:afterSwap"]
) %}
<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 %}
{% 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...",
register_on=["alpine:init", "htmx:afterSwap"],
content_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) }}
-206
View File
@@ -1,206 +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 + Javascript
id: usage-default
- 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" @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.",
main_attrs={"x-ref": "myDialog"},
trigger="Edit Profile",
trigger_attrs={"class": "btn-outline"},
content_attrs={"class": "w-full sm:max-w-[425px] max-h-[612px]"},
footer=footer,
register_on=["alpine:init", "htmx:afterSwap"]
) %}
<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" x-ref="focusOnOpen" />
</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 %}
{% set code_script %}
<script>
{% fetchCode "src/js/dialog.js" %}
document.addEventListener('alpine:init', () => {
window.basecoat.registerDialog(Alpine);
});
</script>
{% endset %}
{{ code_preview("dialog", code_script | prettyHtml ~ code_html | prettyHtml) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<h3 id="usage-default"><a href="#usage-default">HTML + Javascript</a></h3>
<div class="alert my-6">
{% lucide "circle-alert" %}
<h2>This component requires Javascript.</h2>
</div>
<section class="prose">
<p>The component is structured as such:</p>
<ul>
<li>A <code>&lt;div class="dialog"&gt;</code> which wraps around the entire component and holds it state (e.g. open/close).</li>
<li>A <code>&lt;button&gt;</code> that acts as the trigger to open or close the popover.</li>
<li>A <code>&lt;div role="dialog"&gt;</code> that holds the dialog itself. It also acts as the backdrop for the dialog.</li>
<li>Inside of it, we have:
<ul>
<li>A <code>&lt;header&gt;</code> that contains the title (<code>&lt;h2&gt;</code>) and description (<code>&lt;p&gt;</code>) of the dialog.</li>
<li>A <code>&lt;section&gt;</code> for the content.</li>
<li>A <code>&lt;footer&gt;</code> that usually contains actions.</li>
<li>A close <code>&lt;button&gt;</code>.</li>
</ul>
</li>
</ul>
<p>You can include the Javascript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. <code class="text-xs">x-bind</code>, <code class="text-xs">x-data</code>, <code class="text-xs">@click</code>).</p>
</section>
<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/dialog.js" target="_blank">
Individual JS file (dialog.js)
{% lucide "arrow-right" %}
</a>
</div>
{% from "tabs.njk" import tabs %}
{% set tabsets = [
{ tab: "HTML", panel: code_block(code_html | prettyHtml, class="") },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml, class="") }
] %}
{{ tabs(id="combobox", tabsets=tabsets) }}
<section class="prose">
<p>The component will dispatch <code>dialog:opened</code> and <code>dialog:closed</code> events when the dialog is opened or closed. You can also dispatch <code>dialog:open</code> and <code>dialog:close</code> events to open or close the dialog programmatically. You will need the element to have an <code>id</code> to be able to target it (e.g. <code>&lt;div id="my-dialog" class="dialog"&gt;</code>):</p>
</section>
{% set code_dispatch %}<button type="button" x-init @click="$dispatch('dialog:open', { id: 'my-dialog' })">
Open #my-dialog
</button>{% endset %}
{{ code_block(code_dispatch | prettyHtml, "html") }}
<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 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.",
main_attrs={"x-ref": "myDialog"},
trigger="Edit Profile",
trigger_attrs={"class": "btn-outline"},
content_attrs={"class": "w-full sm:max-w-[425px] max-h-[612px]"},
footer=footer,
register_on=["alpine:init", "htmx:afterSwap"]
) %}
<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" @click="open = false">Close</button>
{% endset %}
{% call dialog(
id="dialog-example",
title="Scrollable Content",
description="This is a dialog with scrollable content.",
main_attrs={"x-ref": "myDialog"},
trigger="Scrollable Content",
trigger_attrs={"class": "btn-outline"},
content_attrs={"class": "w-full sm:max-w-[425px] max-h-[612px]"},
content_body_attrs={"class": "overflow-y-auto scrollbar"},
footer=footer,
register_on=["alpine:init", "htmx:afterSwap"]
) %}
<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) }}
-366
View File
@@ -1,366 +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-default
- 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="dropdown-menu",
trigger="Open",
trigger_attrs={"class": "btn-outline"},
content_attrs={"class": "min-w-56"},
register_on=["alpine:init", "htmx:afterSwap"]
) %}
<div role="group" aria-labelledby="account-options">
<span id="account-options" role="heading">My Account</span>
<button type="button" role="menuitem">
Profile
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</button>
<button type="button" role="menuitem">
Billing
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
</button>
<button type="button" role="menuitem">
Settings
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
</button>
<button type="button" role="menuitem">
Keyboard shortcuts
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘K</span>
</button>
</div>
<hr role="separator">
<button type="button" role="menuitem">
GitHub
</button>
<button type="button" role="menuitem">
Support
</button>
<button type="button" role="menuitem" disabled>
API
</button>
<hr role="separator">
<button type="button" role="menuitem">
Logout
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</button>
{% endcall %}
{% endset %}
{% set code_script %}
<script>
{% fetchCode "src/js/dropdown-menu.js" %}
document.addEventListener('alpine:init', () => {
window.basecoat.registerDropdownMenu(Alpine);
});
</script>
{% endset %}
{{ code_preview("dropdown-menu", code_script | prettyHtml ~ code_html | prettyHtml) }}
<h2 id="usage">Usage</h2>
<h3 id="usage-default"><a href="#usage-default">HTML + Javascript</a></h3>
<div class="alert my-6">
{% lucide "circle-alert" %}
<h2>This component requires Javascript.</h2>
</div>
<section class="prose">
<p>The component is structured as such:</p>
<ul>
<li>A <code>&lt;div class="popover"&gt;</code> which wraps around the entire component and holds it state (e.g. open/close).</li>
<li>A <code>&lt;button&gt;</code> that acts as the trigger to open or close the popover.</li>
<li>A <code>&lt;div data-popover&gt;</code> that holds the content of the popover.</li>
<li>Inside of the <code>&lt;div data-popover&gt;</code> is a <code>&lt;nav role="menu"&gt;</code> that contains a combination of:
<ul>
<li><code>&lt;button role="menuitem"&gt;</code> for menu items.</li>
<li><code>&lt;button role="menuitemcheckbox"&gt;</code> for menu items with checkboxes.</li>
<li><code>&lt;button role="menuitemradio"&gt;</code> for menu items with radio buttons.</li>
<li><code>&lt;hr role="separator"&gt;</code> to display a horizontal separator.</li>
<li><code>&lt;div role="group"&gt;</code> to group options.</li>
<li><code>&lt;span role="heading"&gt;</code> for group headings.</li>
</ul>
</ul>
<p>As with the <a href="/components/popover">Popover</a> component, you can set up a few additional options on the <code>&lt;div data-popover&gt;</code> element:</p>
<ul>
<li><code>data-side</code> can be set to <code>top</code>, <code>bottom</code>, <code>left</code>, or <code>right</code> to change the side of the popover.</li>
<li><code>data-align</code> can be set to <code>start</code>, <code>center</code>, or <code>end</code> to change the alignment of the popover.</li>
</ul>
<p>You can include the Javascript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. <code class="text-xs">x-bind</code>, <code class="text-xs">x-data</code>, <code class="text-xs">@click</code>).</p>
</section>
<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">
Individual JS file (dropdown-menu.js)
{% lucide "arrow-right" %}
</a>
</div>
{% from "tabs.njk" import tabs %}
{% set tabsets = [
{ tab: "HTML", panel: code_block(code_html | prettyHtml, class="") },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml, class="") }
] %}
{{ tabs(id="dropdown-menu", tabsets=tabsets) }}
<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"},
content_attrs={"class": "min-w-56"},
register_on=["alpine:init", "htmx:afterSwap"]
) %}
<div role="group" aria-labelledby="account-options">
<span id="account-options" role="heading">My Account</span>
<button type="button" role="menuitem">
Profile
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</button>
<button type="button" role="menuitem">
Billing
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
</button>
<button type="button" role="menuitem">
Settings
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
</button>
<button type="button" role="menuitem">
Keyboard shortcuts
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘K</span>
</button>
</div>
<hr role="separator">
<button type="button" role="menuitem">
GitHub
</button>
<button type="button" role="menuitem">
Support
</button>
<button type="button" role="menuitem" disabled>
API
</button>
<hr role="separator">
<button type="button" role="menuitem">
Logout
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</button>
{% 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"},
content_attrs={"class": "min-w-56"},
register_on=["alpine:init", "htmx:afterSwap"]
) %}
<div role="group" aria-labelledby="account-options">
<span id="account-options" role="heading">Account Options</span>
<button type="button" role="menuitem">
{% lucide "user" %}
Profile
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</button>
<button type="button" role="menuitem">
{% lucide "credit-card" %}
Billing
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘B</span>
</button>
<button type="button" role="menuitem">
{% lucide "settings" %}
Settings
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⌘S</span>
</button>
</div>
<hr role="separator">
<div role="group" aria-labelledby="appearance-options">
<span id="appearance-options" role="heading">Appearance</span>
<button
type="button"
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>
</button>
<button
type="button"
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>
</button>
<button
type="button"
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>
</button>
</div>
<hr role="separator">
<button type="button" role="menuitem">
{% lucide "log-out" %}
Logout
<span class="text-muted-foreground ml-auto text-xs tracking-widest">⇧⌘P</span>
</button>
{% 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"},
content_attrs={"class": "min-w-56"},
register_on=["alpine:init", "htmx:afterSwap"]
) %}
<div role="group" aria-labelledby="position-options" x-data="{ checked: 'bottom' }">
<span id="position-options" role="heading">Panel Position</span>
<hr role="separator">
<button
type="button"
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>
</button>
<button
type="button"
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>
</button>
<button
type="button"
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>
</button>
</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) }}
-169
View File
@@ -1,169 +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-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 "popover.njk" import popover %}
{% set code_html %}
{% call popover(
id="demo-popover",
trigger="Open popover",
trigger_attrs={"class": "btn-outline"},
content_attrs={"class": "w-80 p-4"},
register_on=["alpine:init", "htmx:afterSwap"]
) %}
<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 %}
{% endset %}
{% set code_script %}
<script>
{% fetchCode "src/js/popover.js" %}
document.addEventListener('alpine:init', () => {
window.basecoat.registerPopover(Alpine);
});
</script>
{% endset %}
{{ code_preview("popover", code_script | prettyHtml ~ code_html | prettyHtml) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<h3 id="usage-default"><a href="#usage-default">HTML + Javascript</a></h3>
<div class="alert my-6">
{% lucide "circle-alert" %}
<h2>This component requires Javascript.</h2>
</div>
<section class="prose">
<p>The component is structured as such:</p>
<ul>
<li>A <code>&lt;div class="popover"&gt;</code> which wraps around the entire component and holds it state (e.g. open/close).</li>
<li>A <code>&lt;button&gt;</code> that acts as the trigger to open or close the popover.</li>
<li>A <code>&lt;div data-popover&gt;</code> that holds the content of the popover.</li>
</ul>
<p>You can set up a few additional options on the <code>&lt;div data-popover&gt;</code> element:</p>
<ul>
<li><code>data-side</code> can be set to <code>top</code>, <code>bottom</code>, <code>left</code>, or <code>right</code> to change the side of the popover.</li>
<li><code>data-align</code> can be set to <code>start</code>, <code>center</code>, or <code>end</code> to change the alignment of the popover.</li>
</ul>
<p>You can include the Javascript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. <code class="text-xs">x-bind</code>, <code class="text-xs">x-data</code>, <code class="text-xs">@click</code>).</p>
</section>
<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/popover.js" target="_blank">
Individual JS file (popover.js)
{% lucide "arrow-right" %}
</a>
</div>
{% from "tabs.njk" import tabs %}
{% set tabsets = [
{ tab: "HTML", panel: code_block(code_html | prettyHtml, class="") },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml, class="") }
] %}
{{ tabs(id="popover", tabsets=tabsets) }}
<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"},
content_attrs={"class": "w-80 p-4"},
register_on=["alpine:init", "htmx:afterSwap"]
) %}
<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") }}
-220
View File
@@ -1,220 +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-js
- 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>
</select>{% endset %}
{{ code_preview("select-native", code_native) }}
{% set code_html %}
{% call select(
name="select-js",
trigger_attrs={"class": "w-[180px]"},
register_on=["alpine:init", "htmx:afterSwap"]
) %}
<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 %}
{% endset %}
{% set code_script %}
<script>
{% fetchCode "src/js/select.js" %}
document.addEventListener('alpine:init', () => {
window.basecoat.registerSelect(Alpine);
});
</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-js"><a href="#usage-js">HTML + Javascript</a></h3>
<section class="prose">
<p>If you need to do more than what <code>&lt;select&gt;</code> allows (e.g. HTML options), you can use this component. It is structured as such:</p>
<ul>
<li>A <code>&lt;div class="popover"&gt;</code> which wraps around the entire component and holds it state (e.g. open/close).</li>
<li>A <code>&lt;button&gt;</code> that acts as the trigger to open or close the popover.</li>
<li>A <code>&lt;div data-popover&gt;</code> that holds the content of the popover.</li>
<li>Inside of the <code>&lt;div data-popover&gt;</code> is a <code>&lt;nav role="listbox"&gt;</code> that contains a combination of:
<ul>
<li><code>&lt;div role="option"&gt;</code> for the options with a <code>data-value</code> attribute.</li>
<li><code>&lt;hr role="separator"&gt;</code> to display a horizontal separator.</li>
<li><code>&lt;div role="group"&gt;</code> to group options.</li>
<li><code>&lt;span role="heading"&gt;</code> for group headings.</li>
</ul>
</li>
<li>An <code>&lt;input type="hidden"&gt;</code> to hold the value of the field (if needed).</li>
</ul>
<p>As with the <a href="/components/popover">Popover</a> component, you can set up a few additional options on the <code>&lt;div data-popover&gt;</code> element:</p>
<ul>
<li><code>data-side</code> can be set to <code>top</code>, <code>bottom</code>, <code>left</code>, or <code>right</code> to change the side of the popover.</li>
<li><code>data-align</code> can be set to <code>start</code>, <code>center</code>, or <code>end</code> to change the alignment of the popover.</li>
</ul>
<p>You can include the Javascript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. <code class="text-xs">x-bind</code>, <code class="text-xs">x-data</code>, <code class="text-xs">@click</code>).</p>
</section>
<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">
Individual JS file (select.js)
{% lucide "arrow-right" %}
</a>
</div>
{% from "tabs.njk" import tabs %}
{% set tabsets = [
{ tab: "HTML", panel: code_block(code_html | prettyHtml, class="") },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml, class="") }
] %}
{{ tabs(id="select", tabsets=tabsets) }}
<section class="prose">
<p>The component will dispatch a <code>select:change</code> event when a value is selected, along with the value and label of the selected option in the <code>detail</code> object.</p>
</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/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 %}{% call select(name="select-js") %}
<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(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"},
register_on=["alpine:init", "htmx:afterSwap"]
) %}
{% 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"},
register_on=["alpine:init", "htmx:afterSwap"]
) %}
<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]'},
register_on=["alpine:init", "htmx:afterSwap"]
) %}
<div type="button" role="option" data-value="bar">
{% lucide "chart-bar", {"class": "text-muted-foreground"} %}
Bar
</div>
<div type="button" role="option" data-value="line">
{% lucide "chart-line", {"class": "text-muted-foreground"} %}
Line
</div>
<div type="button" role="option" data-value="pie">
{% lucide "chart-pie", {"class": "text-muted-foreground"} %}
Pie
</div>
{% endcall %}
{% endset %}
{{ code_preview("select-scrollable", code | prettyHtml) }}
-151
View File
@@ -1,151 +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-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 "sidebar.njk" import sidebar %}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<h3 id="usage-default"><a href="#usage-default">HTML + Javascript</a></h3>
<div class="alert my-6">
{% lucide "circle-alert" %}
<h2>This component requires Javascript.</h2>
</div>
<section class="prose">
<p>To use a sidebar, you will need the sidebar itself and a sibling container for the content of the page (e.g. <code>&lt;main&gt;</code>), with the following structure for the sidebar:</p>
<ul>
<li>A <code>&lt;div class="sidebar"&gt;</code> which wraps around the entire component and holds it state (e.g. open/close). It accepts an optional <code>data-side</code> set to <code>left</code>, or <code>right</code> to specify the side of the sidebar (defaults to <code>left</code>).</li>
<li>A <code>&lt;nav&gt;</code> that contains the actual sidebar with the following children:
<ul>
<li>A <code>&lt;header&gt;</code> and <code>&lt;footer&gt;</code> for the header and footer of the sidebar (fixed position).</li>
<li>A <code>&lt;section&gt;</code> for the main navigation list. To add links, you <b>must</b> wrap them in a <code>&lt;div role="group"&gt;</code> first. These groups can contain <code>&lt;h3"&gt;</code> for group headings and lists (i.e. <code>&lt;ul&gt;</code>) of links or buttons. You can also use <code>&lt;details&gt;</code> to wrap around collapsbile sections.</li>
</ul>
</li>
</ul>
<p>Any elememt on the page can toggle, open or close the sidebar by dispatching the <code>sidebar:toggle</code>, <code>sidebar:open</code> or <code>sidebar:close</code> events.</p>
<p>You can include the Javascript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. <code class="text-xs">x-bind</code>, <code class="text-xs">x-data</code>, <code class="text-xs">@click</code>).</p>
</section>
<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">
Individual JS file (sidebar.js)
{% lucide "arrow-right" %}
</a>
</div>
{% 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" @click="$dispatch('sidebar:toggle')">Toggle sidebar</button>
<h1>Content</h1>
</main>
{% endset %}
{% set code_script %}
<script>
{% fetchCode "src/js/sidebar.js" %}
document.addEventListener('alpine:init', () => {
window.basecoat.registerSidebar(Alpine);
});
</script>
{% endset %}
{% from "tabs.njk" import tabs %}
{% set tabsets = [
{ tab: "HTML", panel: code_block(code_html | prettyHtml, class="") },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml, class="") }
] %}
{{ tabs(id="sidebar", tabsets=tabsets) }}
<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") }}
-85
View File
@@ -1,85 +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 %}<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>{% 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) }}
-215
View File
@@ -1,215 +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" },
register_on=["alpine:init", "htmx:afterSwap"]
) }}
{% endset %}
{% set code_script %}
<script>
{% fetchCode "src/js/tabs.js" %}
document.addEventListener('alpine:init', () => {
window.basecoat.registerTabs(Alpine);
});
</script>
{% endset %}
{{ code_preview("tabs", code_script | prettyHtml ~ code_html | prettyHtml, "max-w-[300px] w-full") }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<h3 id="usage-default"><a href="#usage-default">HTML + Javascript</a></h3>
<div class="alert my-6">
{% lucide "circle-alert" %}
<h2>This component requires Javascript.</h2>
</div>
<section class="prose">
<p>The component is structured as such:</p>
<ul>
<li>A <code>&lt;div class="tabs"&gt;</code> which wraps around the entire component and holds it state (e.g. open/close).</li>
<li>A <code>&lt;nav role="tablist"&gt;</code> that holds the tab buttons (<code>&lt;button role="tab"&gt;</code>).</li>
<li>A series of <code>&lt;div role="tabpanel"&gt;</code> that holds the tab panels corresponding to each tab button.</li>
</ul>
<p>You can include the Javascript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. <code class="text-xs">x-bind</code>, <code class="text-xs">x-data</code>, <code class="text-xs">@click</code>).</p>
</section>
<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">
Individual JS file (tabs.js)
{% lucide "arrow-right" %}
</a>
</div>
{% from "tabs.njk" import tabs %}
{% set tabsets = [
{ tab: "HTML", panel: code_block(code_html | prettyHtml, class="") },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml, class="") }
] %}
{{ tabs(id="combobox", tabsets=tabsets) }}
<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" },
register_on=["alpine:init", "htmx:afterSwap"]
) }}{% endraw %}{% endset%}
{{ code_block(raw_code, "jinja") }}
-90
View File
@@ -1,90 +0,0 @@
---
layout: layouts/page.njk
title: Teaxtarea
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") }}
-253
View File
@@ -1,253 +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-default
children:
- label: Append a toast
id: usage-append-toast
- label: Use $toast()
id: usage-use-magic-method
- 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_htmx %}<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-htmx", code_htmx, "max-w-md") }}
{% set code_alpine %}<button
class="btn-outline"
@click='
$toast({
category: "success",
title: "Success",
description: "A success toast called from the front-end.",
action: {
label: "Dismiss",
click: "open =false"
}
})
'>
Toast from front-end
</button>
{% endset %}
{{ code_preview("toast-alpine", code_alpine, "max-w-md") }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
<h3 id="usage-default"><a href="#usage-default">HTML + Javascript</a></h3>
<div class="alert my-6">
{% lucide "circle-alert" %}
<h2>This component requires Javascript.</h2>
</div>
<section class="prose">
<p>To use toasts, you will first need to set up the toaster code to the end of your <code>&lt;body&gt;</code> (see HTML below).</p>
<p>The <code>&lt;template id="toast-template"&gt;</code> bit is only required if you plan on using the <code>$toast()</code> method (see below).</p>
<p>You will also need to include the Javascript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. <code class="text-xs">x-bind</code>, <code class="text-xs">x-data</code>, <code class="text-xs">@click</code>).</p>
</section>
<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">
Individual JS file (toast.js)
{% lucide "arrow-right" %}
</a>
</div>
{% set code_script %}
<script>
{% fetchCode "src/js/popover.js" %}
document.addEventListener('alpine:init', () => {
window.basecoat.registerPopover(Alpine);
});
</script>
{% endset %}
{% set code_html_toaster %}{{ toaster() }}{% endset %}
{% from "tabs.njk" import tabs %}
{% set tabsets = [
{ tab: "HTML", panel: code_block(code_html_toaster | prettyHtml, class="") },
{ tab: "Javascipt (Alpine.js)", panel: code_block(code_script | prettyHtml, class="") }
] %}
{{ tabs(id="popover", tabsets=tabsets) }}
<section class="prose">
<p>Once that's in place, we can do one of two things:</p>
<ul>
<li>Append a <code>&lt;div class="toast"&gt;</code> to the toaster.</li>
<li>Use the <code>$toast()</code> Alpine.js "magic method".</li>
</ul>
</section>
<h4 id="usage-append-toast"><a href="#usage-append-toast">Append a toast</a></h4>
<section class="prose">
<p>To display a toast, just append the following to the toaster:</p>
</section>
{% set code_toast_simple %}<div class="toast" x-data="toast" x-bind="$toastBindings">
<div class="toast-content">
<p>This is a toast.</p>
</div>
</div>{% endset %}
{{ code_block(code_toast_simple | prettyHtml, "html") }}
<section class="prose">
<p>You can implement the layout you want within the toast, but here is the standard format used in the examples above:</p>
</section>
{% set code_toast %}{{ toast(
category="success",
title="Success",
description="A success toast called from the front-end.",
action={
label: "Dismiss",
click: "close()"
}
) }}{% endset %}
{{ code_block(code_toast | prettyHtml, "html") }}
<section class="prose">
<p>When setting up <code>x-data</code>, you can pass a config object with `category` (e.g. "success", "error", "info", "warning") and `duration` (in milliseconds). By default, the duration will be 3 seconds, and 5 seconds for errors.</p>
<p>This approach is used in the first example with HTMX: when the button is clicked, we retrieve the a fragment that contains the markup for the toast and append it to <code>#toaster</code>.</p>
</section>
{% set code_htmx %}<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 %}
<h4 id="usage-use-magic-method"><a href="#usage-use-magic-method">Use $toast()</a></h4>
<section class="prose">
<p>If you want to display a toast directly from the front-end, you can use the <code>$toast()</code> Alpine.js magic method. This is what we're doing in the second example above:</p>
</section>
{% set code_magic_method %}<button
class="btn-outline"
@click='
$toast({
category: "success",
title: "Success",
description: "A success toast called from the front-end.",
action: {
label: "Dismiss",
click: "open =false"
}
})
'>
Toast from front-end
</button>
{% endset %}
{{ code_block(code_magic_method, "html") }}
<section class="prose">
<p>The <code>$toast()</code> method accepts a config object with the following properties:</p>
<ul>
<li><code>duration</code> (optional): the duration of the toast in milliseconds. By default, the duration will be 3 seconds, or 5 seconds if <code>category</code> is "error".</li>
<li><code>category</code> (optional): "success", "error", "info", "warning" or any arbitrary string.</li>
<li><code>title</code> (optional): the title of the toast.</li>
<li><code>description</code> (optional): the description of the toast.</li>
<li><code>action</code> (optional): an object to define an action button:
<ul>
<li><code>label</code>: the label of the action button.</li>
<li><code>click</code> (optional): a string of Javascript to execute when the button is clicked.</li>
<li><code>url</code> (optional): a URL to link to when the button is clicked.</li>
</ul>
</li>
<li><code>cancel</code> (optional): an object to define a cancel button (similar structure to action).</li>
</ul>
</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", click: "close()" }
) }}{% 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) }}
-326
View File
@@ -1,326 +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>
{% call select(
is_combobox=true,
selected="owner",
content_attrs={"data-align": "end"}
) %}
<div role="option" data-value="viewer">Viewer</div>
<div role="option" data-value="developer">Developer</div>
<div role="option" data-value="billing">Billing</div>
<div role="option" data-value="owner">Owner</div>
{% endcall %}
</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>
{% call select(
is_combobox=true,
content_attrs={"data-align": "end"}
) %}
<div role="option" data-value="viewer">Viewer</div>
<div role="option" data-value="developer">Developer</div>
<div role="option" data-value="billing">Billing</div>
<div role="option" data-value="owner">Owner</div>
{% endcall %}
</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>
{% call select(
is_combobox=true,
content_attrs={"data-align": "end"}
) %}
<div role="option" data-value="viewer">Viewer</div>
<div role="option" data-value="developer">Developer</div>
<div role="option" data-value="billing">Billing</div>
<div role="option" data-value="owner">Owner</div>
{% endcall %}
</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>
-188
View File
@@ -1,188 +0,0 @@
---
layout: layouts/page.njk
title: Installation
description: How to install and use Basecoat in your app.
toc:
- label: Installing the CSS
id: install-css
children:
- label: Add Tailwind CSS
id: install-css
- label: Add Basecoat
id: install-css-basecoat
- label: Import basecoat in your CSS
id: install-css-import
- label: That's it
id: install-css-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-css">Install the CSS</h2>
<ol class="[&>h3]:step steps mb-12 md:ml-4 md:border-l md:pl-8">
<li class="step">
<h4 class="mb-4 scroll-m-20 font-semibold tracking-tight" id="install-css-tailwind"><a href="#install-css-tailwind">Add Tailwind CSS</a></h4>
<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-css-basecoat"><a href="#install-css-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-css-import"><a href="#install-css-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-css-done"><a href="#install-css-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>Some components require Javascript code to work.</b></p>
<p>The documentation provides Alpine.js code snippets to cover that. <a href="https://alpinejs.dev/docs/installation" target="_blank">Make sure to install Alpine.js if you decide to use them.</a></p>
<p>For simple components like <a href="/components/accordion">Accordion</a> or <a href="/components/slider">Slider</a>, you will notice some inline Alpine.js code that you can just copy along with the rest of the HTML.</p>
<p>For more complex components (<a href="/components/dialog">Dialog</a>, <a href="/components/dropdown-menu">Dropdown Menu</a>, <a href="/components/popover">Popover</a>, <a href="/components/select">Select</a>, <a href="/components/tabs">Tabs</a> and <a href="/components/tooltip">Toast</a>), you have one of 2 options:
<ul>
<li>Copy the Javascript snippet provided in the documentation page into your project.</li>
<li>
<p><a href="https://github.com/hunvreus/basecoat/tree/main/src/js" target="_blank">Download the script as a separate file</a> and include it in your project (<a href="#install-cli">or let the CLI do it for you</a>). <b>If you use this approach, make sure to load the file before the Alpine.js core JS file and register the component on the <code>alpine:init</code> event.</b> For example:</p>
{% set code %}<script src="/assets/js/dialog.js" defer></script>
<script>
document.addEventListener('alpine:init', () => {
window.basecoat.registerDialog(Alpine);
});
</script>
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>{% endset %}
{{ code_block(code, "html") }}
</li>
</ul>
<p>You are also welcome to write your own Javascript.</p>
<p>All of the components try and implement WAI-ARIA standards to make them accessible.</p>
</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>
{% 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") }}
<p>Make sure to include the Javascript code <b>before</b> Alpine.js core JS file:</p>
{% set code %}<script src="/assets/js/select.js" defer></script>
<script>
document.addEventListener('alpine:init', () => {
window.basecoat.registerSelect(Alpine);
});
</script>
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>{% endset %}
{{ code_block(code, "jinja") }}
</div>
<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">
<p>You can override default styles using Tailwind:</p>
{% set code %}<button class="btn font-normal ">Click me</button>{% endset %}
{{ code_block(code, "html") }}
<p>You can also obviously extend and/or override the existing styles with your own Tailwind files:</p>
{% set code %}@import "tailwindcss";
@import "basecoat-css";
@import "theme.css";{% endset %}
{{ code_block(code, "css") }}
<p>You can also use 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>, I recommend <a href="https://github.com/hunvreus/basecoat/blob/main/src/css/basecoat.css" target="_blank">copying the <code>basecoat.css</code> file</a> into your project and making your changes there.</p>
</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>
+267 -3267
View File
File diff suppressed because it is too large Load Diff
+109 -30
View File
@@ -1,22 +1,19 @@
{
"name": "basecoat",
"version": "0.1.0",
"description": "shadcn/ui for the rest of us",
"author": "hunvreus",
"type": "module",
"private": true,
"workspaces": [
"packages/*"
],
"files": [
"src/css",
"src/js",
"src/nunjucks",
"src/jinja"
],
"exports": {
".": "./src/css/basecoat.css"
"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",
"main": "dist/basecoat.css",
"style": "dist/basecoat.css",
"files": [
"dist/",
"templates/"
],
"keywords": [
"components",
"component library",
@@ -30,10 +27,8 @@
"css",
"html",
"jinja",
"nunjucks",
"alpinejs"
"nunjucks"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/hunvreus/basecoat.git"
@@ -41,20 +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.1.1",
"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"
}
-54
View File
@@ -1,54 +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
You can install `basecoat-css` using your preferred package manager:
```bash
# npm
npm install basecoat-css
# yarn
yarn add basecoat-css
# pnpm
pnpm add basecoat-css
# bun
bun add basecoat-css
```
Alternatively, you can download the `basecoat.css` file directly from the [GitHub repository](https://github.com/hunvreus/basecoat/blob/main/src/css/basecoat.css) (or the `dist/basecoat.css` file from this package after installation) and include it in your project.
## Usage
After installation, import `basecoat-css` into your main CSS file. Make sure to import it **after** Tailwind CSS and **before** any custom theme overrides.
```css
/* Your main CSS file (e.g., styles.css) */
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
@import "basecoat-css"; /* Or the path to basecoat.css if manually added */
/* Your custom styles or theme overrides (optional) */
/* @import "your-theme.css"; */
```
Once imported, you can start using Basecoat's component classes in your HTML.
## 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)
-40
View File
@@ -1,40 +0,0 @@
{
"name": "basecoat-css",
"version": "0.1.2",
"description": "Tailwind CSS for Basecoat components",
"author": {
"name": "hunvreus",
"url": "https://x.com/hunvreus"
},
"license": "MIT",
"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-css"
}
+140 -25
View File
@@ -1,6 +1,11 @@
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import { exec } from 'child_process';
import { promisify } from 'util';
import { generateCssEntrypoints } from './generate-css-entrypoints.js';
const execPromise = promisify(exec);
// Resolve project root, assuming this script is in <project_root>/scripts/
const __filename = fileURLToPath(import.meta.url);
@@ -59,49 +64,159 @@ async function copyDirRecursive(src, dest) {
}
}
// Main build function to prepare packages for publishing.
// Recursively inlines relative `@import "./...";` lines into a single CSS string.
// Each file is inlined at most once per bundle (tracked in `seen`).
async function inlineCssImports(filePath, seen) {
const resolved = path.resolve(filePath);
if (seen.has(resolved)) return '';
seen.add(resolved);
const source = await fs.readFile(resolved, 'utf-8');
const lines = source.split('\n');
const out = [];
for (const line of lines) {
const match = line.match(/^\s*@import\s+["'](\.{1,2}\/[^"']+)["']\s*;\s*$/);
if (match) {
const inlined = await inlineCssImports(path.join(path.dirname(resolved), match[1]), seen);
if (inlined) out.push(inlined.replace(/\n+$/, ''));
} else if (/^\s*@import\s+["']\.{1,2}\//.test(line)) {
throw new Error(`Unsupported relative @import (with modifiers) in ${resolved}: ${line.trim()}`);
} else {
out.push(line);
}
}
return out.join('\n');
}
// Writes an uncompiled single-file bundle for a dist CSS entrypoint.
async function writeCssBundle(entryPath, outPath, label, version) {
const body = await inlineCssImports(entryPath, new Set());
const header = `/* Basecoat v${version}, ${label}, single-file source. Import after tailwindcss. */\n`;
await fs.writeFile(outPath, `${header}${body.replace(/\n+$/, '')}\n`);
}
// Main build function to prepare the package for publishing.
async function build() {
console.log('Starting build process...');
await generateCssEntrypoints();
// Define all necessary paths
const cliPackageDir = path.join(projectRoot, 'packages', 'cli');
const cliDistDir = path.join(cliPackageDir, 'dist');
const cliDistAssetsDir = path.join(cliDistDir, 'assets');
const cssPackageDir = path.join(projectRoot, 'packages', 'css');
const cssDistDir = path.join(cssPackageDir, 'dist');
const cssDistDir = path.join(projectRoot, 'dist');
const cssTemplatesDir = path.join(projectRoot, 'templates');
const srcDir = path.join(projectRoot, 'src');
const srcCssDir = path.join(srcDir, 'css');
const srcCssStylesDir = path.join(srcCssDir, 'styles');
const srcCssCompatDir = path.join(srcCssDir, 'compat');
const srcJsDir = path.join(srcDir, 'js');
const srcNunjucksDir = path.join(srcDir, 'nunjucks');
const srcJinjaDir = path.join(srcDir, 'jinja');
const srcNunjucksDir = path.join(srcDir, 'templates', 'nunjucks');
const srcJinjaDir = path.join(srcDir, 'templates', 'jinja');
// Clean previous build artifacts
console.log('Cleaning distribution directories...');
await cleanDir(cliDistDir);
await cleanDir(cssDistDir);
await cleanDir(cssTemplatesDir);
// JS files minification and copy
const jsFiles = await fs.readdir(srcJsDir);
const cssJsDistDir = path.join(cssDistDir, 'js');
await ensureDir(cssJsDistDir);
console.log('Copying and minifying JS files...');
for (const jsFile of jsFiles) {
if (path.extname(jsFile) === '.js') {
const srcFile = path.join(srcJsDir, jsFile);
const baseName = path.basename(jsFile, '.js');
const minifiedFileName = `${baseName}.min.js`;
// Copy original file to destination
await fs.copyFile(srcFile, path.join(cssJsDistDir, jsFile));
// Create minified file
await execPromise(`npx terser ${srcFile} -o ${path.join(cssJsDistDir, minifiedFileName)} --compress --mangle`);
}
}
// Create combined component files
console.log('Creating combined component files...');
const componentsToCombine = ['basecoat.js', 'accordion.js', 'command.js', 'combobox.js', 'drawer.js', 'dropdown-menu.js', 'popover.js', 'range.js', 'select.js', 'sidebar.js', 'tabs.js', 'toast.js'];
const componentPaths = componentsToCombine.map(f => path.join(srcJsDir, f));
// Create non-minified bundle
let combinedContent = '';
for (const p of componentPaths) {
combinedContent += await fs.readFile(p, 'utf-8') + '\n';
}
const allJsPath = path.join(cssJsDistDir, 'all.js');
await fs.writeFile(allJsPath, combinedContent);
// Build CLI package
console.log('Building CLI package...');
await ensureDir(cliDistDir); // Ensure base dist dir exists
await ensureDir(cliDistAssetsDir); // Ensure assets dir within dist exists
// Create minified bundle
const allMinJsPath = path.join(cssJsDistDir, 'all.min.js');
await execPromise(`npx terser ${componentPaths.join(' ')} -o ${allMinJsPath} --compress --mangle`);
await fs.copyFile(path.join(cliPackageDir, 'index.js'), path.join(cliDistDir, 'index.js'));
console.log(`Copied CLI index.js to ${cliDistDir}`);
await copyDirRecursive(srcJsDir, path.join(cliDistAssetsDir, 'js'));
console.log(`Copied JS assets to ${path.join(cliDistAssetsDir, 'js')}`);
await copyDirRecursive(srcNunjucksDir, path.join(cliDistAssetsDir, 'nunjucks'));
console.log(`Copied Nunjucks assets to ${path.join(cliDistAssetsDir, 'nunjucks')}`);
await copyDirRecursive(srcJinjaDir, path.join(cliDistAssetsDir, 'jinja'));
console.log(`Copied Jinja assets to ${path.join(cliDistAssetsDir, 'jinja')}`);
console.log(`Copied and minified JS to ${cssJsDistDir}`);
// Build CSS package
console.log('Building CSS package...');
await ensureDir(cssDistDir); // Ensure dist dir exists for css package
await ensureDir(cssDistDir);
const styles = ['vega', 'nova', 'maia', 'lyra', 'mira', 'luma', 'sera', 'rhea'];
await fs.copyFile(path.join(srcCssDir, 'basecoat.css'), path.join(cssDistDir, 'basecoat.css'));
console.log(`Copied basecoat.css to ${cssDistDir}`);
await fs.copyFile(path.join(srcCssDir, 'basecoat.all.css'), path.join(cssDistDir, 'basecoat.all.css'));
await fs.copyFile(path.join(srcCssDir, 'basecoat-base.css'), path.join(cssDistDir, 'basecoat-base.css'));
await fs.copyFile(path.join(srcCssDir, 'basecoat-base.cdn.css'), path.join(cssDistDir, 'basecoat-base.cdn.css'));
await fs.copyFile(path.join(srcCssDir, 'basecoat-compat.css'), path.join(cssDistDir, 'basecoat-compat.css'));
await fs.copyFile(path.join(srcCssDir, 'basecoat-compat.cdn.css'), path.join(cssDistDir, 'basecoat-compat.cdn.css'));
await fs.copyFile(path.join(srcCssDir, 'basecoat-components.css'), path.join(cssDistDir, 'basecoat-components.css'));
for (const style of styles) {
await fs.copyFile(path.join(srcCssDir, `basecoat-${style}.css`), path.join(cssDistDir, `basecoat-${style}.css`));
await fs.copyFile(path.join(srcCssDir, `basecoat-${style}.cdn.css`), path.join(cssDistDir, `basecoat-${style}.cdn.css`));
}
console.log(`Copied basecoat CSS entrypoints to ${cssDistDir}`);
// Copy split CSS folders used by basecoat.css imports.
const cssBaseSrcDir = path.join(srcCssDir, 'base');
const cssComponentsSrcDir = path.join(srcCssDir, 'components');
const cssBaseDistDir = path.join(cssDistDir, 'base');
const cssComponentsDistDir = path.join(cssDistDir, 'components');
const cssStylesDistDir = path.join(cssDistDir, 'styles');
const cssCompatDistDir = path.join(cssDistDir, 'compat');
await copyDirRecursive(cssBaseSrcDir, cssBaseDistDir);
await copyDirRecursive(cssComponentsSrcDir, cssComponentsDistDir);
await copyDirRecursive(srcCssStylesDir, cssStylesDistDir);
await copyDirRecursive(srcCssCompatDir, cssCompatDistDir);
console.log(`Copied split CSS folders to ${cssDistDir}`);
// Create uncompiled single-file bundles for Tailwind users without a package manager.
const { version } = JSON.parse(await fs.readFile(path.join(projectRoot, 'package.json'), 'utf-8'));
for (const style of styles) {
await writeCssBundle(
path.join(cssDistDir, `basecoat-${style}.css`),
path.join(cssDistDir, `basecoat-${style}.bundle.css`),
style,
version,
);
}
await writeCssBundle(path.join(cssDistDir, 'basecoat.css'), path.join(cssDistDir, 'basecoat.bundle.css'), 'vega', version);
await writeCssBundle(path.join(cssDistDir, 'basecoat-base.css'), path.join(cssDistDir, 'basecoat-base.bundle.css'), 'base (no style pack)', version);
console.log(`Generated single-file CSS bundles in ${cssDistDir}`);
await copyDirRecursive(srcNunjucksDir, path.join(cssTemplatesDir, 'nunjucks'));
await copyDirRecursive(srcJinjaDir, path.join(cssTemplatesDir, 'jinja'));
console.log(`Copied template assets to ${cssTemplatesDir}`);
// Create Tailwind CSS builds for the CSS package.
const cdnEntries = ['basecoat.cdn.css', 'basecoat-base.cdn.css', 'basecoat-compat.cdn.css', ...styles.map((style) => `basecoat-${style}.cdn.css`)];
for (const entry of cdnEntries) {
const cdnCssSrc = path.join(srcCssDir, entry);
const baseName = path.basename(entry, '.css');
const cssDistCdnPath = path.join(cssDistDir, `${baseName}.css`);
const cssDistCdnMinPath = path.join(cssDistDir, `${baseName}.min.css`);
await execPromise(`npx tailwindcss -i "${cdnCssSrc}" -o "${cssDistCdnPath}"`);
console.log(`Generated non-minified CSS: ${cssDistCdnPath}`);
await execPromise(`npx tailwindcss -i "${cdnCssSrc}" -o "${cssDistCdnMinPath}" --minify`);
console.log(`Generated minified CSS: ${cssDistCdnMinPath}`);
}
console.log('Build process finished successfully!');
}
+109
View File
@@ -0,0 +1,109 @@
import fs from 'fs/promises';
import path from 'path';
const styles = ['vega', 'nova', 'maia', 'lyra', 'mira', 'luma', 'sera', 'rhea'];
const excludedComponents = new Set(['form']);
// Keep cascade stable for components whose selectors intentionally compose.
const componentOrder = [
'accordion',
'alert',
'avatar',
'badge',
'breadcrumb',
'button',
'button-group',
'card',
'chart',
'collapsible',
'command',
'combobox',
'dialog',
'drawer',
'dropdown-menu',
'empty',
'field',
'checkbox',
'input',
'item',
'kbd',
'label',
'native-select',
'popover',
'progress',
'radio',
'range',
'select',
'sidebar',
'scrollbar',
'skeleton',
'switch',
'table',
'tabs',
'textarea',
'input-group',
'toast',
'tooltip',
];
async function existingComponentNames(cssDir) {
const componentDir = path.join(cssDir, 'components');
const files = await fs.readdir(componentDir);
const names = files
.filter((file) => file.endsWith('.css'))
.map((file) => path.basename(file, '.css'))
.filter((name) => !excludedComponents.has(name));
const known = componentOrder.filter((name) => names.includes(name));
const extra = names.filter((name) => !componentOrder.includes(name)).sort();
return [...known, ...extra];
}
function componentImports(names) {
return [
'/* Components */',
...names.map((name) => `@import "./components/${name}.css";`),
].join('\n');
}
async function writeIfChanged(filePath, content) {
let current = null;
try {
current = await fs.readFile(filePath, 'utf8');
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}
if (current !== content) await fs.writeFile(filePath, content);
}
export async function generateCssEntrypoints({ cssDir = path.resolve('src/css') } = {}) {
const components = await existingComponentNames(cssDir);
const componentsCss = `${componentImports(components)}\n`;
const baseCss = `@import "./base/base.css";\n@import "./basecoat-components.css";\n`;
await writeIfChanged(path.join(cssDir, 'basecoat-components.css'), componentsCss);
await writeIfChanged(path.join(cssDir, 'basecoat-base.css'), baseCss);
await writeIfChanged(path.join(cssDir, 'basecoat-base.cdn.css'), '@import "tailwindcss" source(none);\n@import "./basecoat-base.css";\n');
for (const style of styles) {
await writeIfChanged(
path.join(cssDir, `basecoat-${style}.css`),
`@import "./basecoat-base.css";\n@import "./styles/${style}.css";\n`,
);
await writeIfChanged(
path.join(cssDir, `basecoat-${style}.cdn.css`),
`@import "tailwindcss" source(none);\n@import "./basecoat-${style}.css";\n`,
);
}
await writeIfChanged(path.join(cssDir, 'basecoat.css'), '@import "./basecoat-vega.css";\n');
await writeIfChanged(path.join(cssDir, 'basecoat.all.css'), '@import "./basecoat.css";\n');
await writeIfChanged(path.join(cssDir, 'basecoat.cdn.css'), '@import "tailwindcss" source(none);\n@import "./basecoat.css";\n');
await writeIfChanged(path.join(cssDir, 'basecoat-compat.css'), '@import "./compat/legacy.css";\n');
await writeIfChanged(path.join(cssDir, 'basecoat-compat.cdn.css'), '@import "tailwindcss" source(none);\n@reference "./basecoat.css";\n@import "./basecoat-compat.css";\n');
return { components, styles };
}
if (import.meta.url === `file://${process.argv[1]}`) {
await generateCssEntrypoints();
}
+68
View File
@@ -0,0 +1,68 @@
import { defineConfig } from 'astro/config';
import { readFileSync } from 'node:fs';
import sitemap from '@astrojs/sitemap';
import reallySimpleDocs from 'reallysimpledocs/astro';
const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
const includeInSitemap = (page) => {
const { pathname } = new URL(page);
return pathname === '/' || (
pathname.endsWith('/') &&
!pathname.startsWith('/fragments/')
);
};
export default defineConfig({
site: process.env.SITE_URL || 'https://basecoatui.com',
vite: {
build: {
assetsInlineLimit: 0,
},
resolve: {
dedupe: ['basecoat-css'],
},
},
integrations: [
reallySimpleDocs({
docsDir: './src/docs',
routeBase: '/',
style: 'vega',
css: false,
bodyAttrs: {
class: 'antialiased',
'hx-boost': 'true',
'hx-target': '#content',
'hx-select': '#content',
'hx-swap': 'outerHTML',
'hx-push-url': 'true',
},
components: {
Head: './src/components/StyleHead.astro',
SidebarHeader: './src/components/SidebarHeader.astro',
ContentHeader: './src/components/ContentHeader.astro',
},
site: {
title: 'Basecoat',
subtitle: `v${packageJson.version}`,
description: 'A components library built with Tailwind CSS that works with any web stack.',
url: process.env.SITE_URL || 'https://basecoatui.com',
keywords: ['components', 'component library', 'component system', 'UI', 'UI kit', 'shadcn', 'shadcn/ui', 'Tailwind CSS', 'Tailwind', 'CSS', 'HTML', 'Jinja', 'Nunjucks', 'JS', 'JavaScript', 'vanilla JS', 'vanilla JavaScript'],
author: {
name: 'Ronan Berder',
x: '@hunvreus',
},
favicon: 'favicon.svg',
appleTouchIcon: 'apple-touch-icon.png',
socialImage: 'social.png',
logo: {
svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" class="h-4 w-4"><rect width="256" height="256" fill="none"></rect><line x1="208" y1="128" x2="128" y2="208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"></line><line x1="192" y1="40" x2="40" y2="192" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"></line></svg>',
},
},
}),
sitemap({
filter: includeInSitemap,
}),
],
});
+7398
View File
File diff suppressed because it is too large Load Diff

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