@l4st/driftwood (0.24.0)

Published 2026-10-09 20:05:20 +00:00 by L4stIdi0t in L4stIdi0t/driftwood

Installation

@l4st:registry=
npm install @l4st/driftwood@0.24.0
"@l4st/driftwood": "0.24.0"

About this package

driftwood

A dashboard built from driftwood components, shown in light and dark at once: stat cards, a select, a multi-select, a slider, a switch, a tree, a progress bar, badges, an accordion and keyboard shortcuts

One screen, one DOM, split down the middle: the right half is the same markup inside color-scheme: dark. That is all the kit's dark mode is — no class, no script. The image is generated by pnpm shots from a real page, so it cannot drift from the components.

A hand-written Svelte 5 + Tailwind v4 component kit. Zero UI dependencies — no shadcn, no Melt, no Bits, no Flowbite. Extracted from two apps that had both grown the same set of primitives and were copying fixes between each other.

Personal library: no npm registry, no semver theatre. It exists so one component gets fixed once.

What's in it

Presentational Button ButtonGroup Badge Card StatCard Alert Avatar Image Separator Table Input Textarea Label Kbd Skeleton Spinner Progress
Interactive Select MultiSelect Combobox TagsInput RadioGroup Switch Checkbox Toggle Slider Tabs Accordion AccordionItem Collapsible Menu Tree Pagination Tooltip Popover Dialog CommandPalette Toaster + toast()
Forms Field — label, control, hint and error as one unit, with the ids wired. Every control takes name and submits like a native one
Numeric NumberInput — unit suffix, clamps on commit, arrow stepping
In place InlineEdit — a value shown as text that becomes an Input or NumberInput when clicked
Editing CodeEditor — highlighted layer behind a real textarea, folding, its own undo stack. Bring a grammar
Helpers cn() · thumbHashToDataURL · portal · place / track · the reveal / popIn / fadeUp transitions · four stylesheets

Browse the components → — one page each, with a screenshot in both themes, the props read straight out of the source, and the example that produced the image. The same pages are on the wiki, rendered, with a sidebar and a "which one do I want" table — that is the one to send a person to. Both are generated: pnpm images writes the pages, pnpm wiki publishes them.

Every interactive component implements its own ARIA roles, states and keyboard handling — that is the whole reason the kit is hand-written rather than wrapping a headless library.

Install

The target registry is this Forgejo instance, not npmjs.com — publishConfig points at https://code.letsnot.run/api/packages/L4stIdi0t/npm/, and pnpm is only the client doing the upload. Publishing needs credentials; reading does not, because the repository is public.

npm version patch      # the tag is what consumers pin
git push --follow-tags # CI publishes on a v* tag

Publishing uses the instance's existing secrets, FORGEJO_USER and FORGEJO_API — a username and a user access token carrying write:package. Forgejo's automatic per-job token is scoped to the repository and the package API rejects it for writes, which is why the workflow does not use it. The auth job checks both secrets exist and that the token identifies a user before anything is uploaded.

To publish from a laptop instead, the same credential goes in ~/.npmrc — never in the repo. The registry answers a Bearer token with [E401] … need: Basic realm="Gitea Package API", so use _auth, which is the base64 of user:token and is sent as Basic:

printf 'L4stIdi0t:<token>' | base64
# ~/.npmrc
//code.letsnot.run/api/packages/L4stIdi0t/npm/:_auth=<that base64>
//code.letsnot.run/api/packages/L4stIdi0t/npm/:always-auth=true

The consuming app maps the scope to the registry in its own .npmrc:

@l4st:registry=https://code.letsnot.run/api/packages/L4stIdi0t/npm/
// package.json
"dependencies": { "@l4st/driftwood": "^0.1.5" }

0.1.5 is published and serves anonymously — verified by installing it into an empty project with pnpm: the package arrives with dist/ already built, every export resolves, and no build script runs.

Do not install v0.1.0. It predates the prepare fix, so it installs a package with no dist/ — every export points at a file that is not there. 0.1.1 is the first version that installs correctly by any route.

The other two paths, and what they cost

Path Works?
Registry tarball (above) Yes, everywhere. Verified by packing and pnpm add-ing the tarball into a scratch project: 63 files, dist/ present, no build step
git+https://…#v0.1.0 npm only. npm prepares a git dependency itself, so svelte-package runs and dist/ appears (verified). pnpm refuses: ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED, and the allowBuilds key it demands contains the commit SHA — so every kit update means editing pnpm-workspace.yaml in every app
link:../../WebstormProjects/driftwood Local development only, for editing the kit and an app together. Run pnpm package in the kit after a change; the app reads dist/

A git install also depends on prepare running svelte-package. Until that was fixed, a git install produced a package containing package.json and README.md and nothing else, with every export pointing at a file that did not exist.

Peer dependencies

The app provides these; the kit bundles none of them:

Peer Why If it is missing
svelte ^5.56.1 runes, $props.id() nothing works
tailwindcss ^4.3.0 every component is Tailwind classes nothing is styled
@lucide/svelte ^1.23.0 icons inside Select, MultiSelect, Menu, Dialog, Tree, Accordion, Collapsible the build cannot resolve the import
tw-animate-css ^1.4.0 animate-in / fade-in-0 / zoom-in-95 in Dialog and Tooltip the build passes and the entrance animation silently disappears — those classes simply do not exist

Install the peers explicitly — pnpm add @lucide/svelte tw-animate-css. Both package managers auto-install a missing peer, so the build succeeds either way, but vite dev then fails to render with

Unknown file extension ".svelte" for …/@lucide/svelte/dist/icons/a-arrow-down.svelte

because vite-plugin-svelte only adds Svelte packages it can see in your dependencies to ssr.noExternal; an auto-installed peer is invisible to it and Node's ESM loader is handed a .svelte file. tw-animate-css has to be in your CSS regardless.

Wire up the CSS

In your app's app.css — verified against a clean SvelteKit app that installed the packed tarball and built:

@import 'tailwindcss';
@import 'tw-animate-css';             /* required: Dialog and Tooltip entrances */

@import '@l4st/driftwood/base.css';   /* .indeterminate-bar, reduced-motion */

/* Your palette. Copy the token list from @l4st/driftwood/tokens.css and change
   the values — that is where your app gets its identity. Or import
   '@l4st/driftwood/tokens.css' for the neutral default. */
:root { --background: …; --primary: …; --radius: 0.625rem; /* … */ }

@import '@l4st/driftwood/theme.css';  /* tokens -> utilities, and the @source */
@import '@l4st/driftwood/code.css';   /* only if you use CodeEditor */

Order matters in one direction: theme.css last. Its @theme inline block and its @layer base defaults have to see the token names, and it is what maps them onto utilities. Your palette can sit anywhere before it.

theme.css carries @source "../components", which is what keeps the kit's own classes in your build — Tailwind skips node_modules otherwise. Verified twice: a Tailwind CLI run with source(none) and no consumer markup at all still emits bg-primary, border-border and .indeterminate-bar; and the clean consumer app's built stylesheet contains those plus animate-in and zoom-in-95.

Dark mode follows the system

The default palette needs no wiring: both themes live in one token block via light-dark(), and :root declares color-scheme: light dark. No class on <html>, no script, no flash-of-wrong-theme. That color-scheme declaration is the part a media query alone never gave you — it is what makes the browser's own furniture (scrollbars, form controls, autofill, the caret) match the theme.

tokens.css also ships a single @media (prefers-color-scheme: dark) rule. It only restates that declaration, but it is the query some dark-mode extensions (Noir, for one) look for. A site without it looks light-only to them, and they repaint a dark theme that is already there. Keep it if you write your own palette.

Add this to app.html so the first paint is right too:

<meta name="color-scheme" content="light dark" />

To pin a theme, flip color-scheme; every token follows on its own:

:root.dark,  :root[data-theme='dark']  { color-scheme: dark; }
:root.light, :root[data-theme='light'] { color-scheme: light; }

Both selectors ship in tokens.css, so a class-based switcher like mode-watcher works alongside the system default with nothing added. For a three-way switcher, remove the attribute for "system" rather than writing data-theme="system" — absence is what hands the decision back to the OS.

No component uses a dark: variant — they read tokens only — so an app with its own palette can keep using two blocks (.dark, or a media query) instead; nothing in the kit cares how the tokens are produced. light-dark() needs Chrome 123 / Safari 17.5 / Firefox 120.

Verified in a real browser (pnpm probe): surfaces, text and body background all change with the emulated OS theme, opacity modifiers like bg-primary/90 resolve per theme through Tailwind's color-mix(), and both pinning strategies override the system.

Full detail — how light-dark() and color-scheme combine, writing your own palette, switchers, older browsers: docs/theming.md.

Wire up your coding agent

The kit ships an agent bundle — the same instructions that keep an agent from writing bg-slate-800, re-implementing dark mode, or vendoring a copy of a component:

sh node_modules/@l4st/driftwood/agents/install.sh

That drops the driftwood-ui skill and a /driftwood-upgrade command into the app's .claude/. One manual step is left: paste agents/snippets/agents-md.md into the app's own AGENTS.md or CLAUDE.md.

The package also carries docs/ — a page per component with props, keyboard contract and a working example, plus docs/theming.md — so an agent can read the real API out of node_modules instead of inferring one. See agents/README.md.

Use it

<script lang="ts">
	import { Button, Card, Dialog, toast } from '@l4st/driftwood';
	import { cn } from '@l4st/driftwood/utils';
</script>

Mount <Toaster /> once, in your root +layout.svelte.

Develop

pnpm install
pnpm dev        # the gallery — every component, live, at :5173
pnpm check      # svelte-check; 0 errors expected
pnpm verify     # kit invariants + doc freshness
pnpm probe      # behaviour probes in a real browser
pnpm images     # regenerate every image (components + hero) and the docs
pnpm shots      # just the images (light + dark + hero, headless Chromium)
pnpm docs:gen   # regenerate docs/components/ from the source
pnpm package    # build dist/ + publint

pnpm verify is the one that catches what a component change forgot — the barrel export, the gallery entry, the screenshot, the doc page. See AGENTS.md for the full definition of done.

src/routes/ is the gallery, a reference page for the kit; it is not published. src/lib/ is the package.

WebStorm

The .idea/ project is committed: Open the folder and the four scripts above are already there as run configurations (dev, check, build, package), the generated folders (.svelte-kit/, dist/, build/, node_modules/) are excluded from indexing, and the code style is set to tabs and single quotes so Reformat Code agrees with the rest of the repo. .editorconfig says the same thing for any other editor. Per-developer files (workspace.xml, shelves) are ignored by .idea/.gitignore.

Install the Svelte plugin if it is not already enabled — that is what gives .svelte files their component and rune awareness.

See AGENTS.md for the conventions any change here has to hold to.

Dependencies

Dependencies

ID Version
clsx ^2.1.1
tailwind-merge ^3.6.0

Development dependencies

ID Version
@axe-core/playwright ^4.13.0
@lucide/svelte ^1.28.0
@sveltejs/adapter-auto ^7.0.1
@sveltejs/kit ^2.69.1
@sveltejs/package ^2.5.4
@sveltejs/vite-plugin-svelte ^7.1.2
@tailwindcss/vite ^4.3.3
axe-core ^4.13.0
playwright ^1.62.1
publint ^0.3.16
svelte ^5.56.1
svelte-check ^4.6.0
tailwindcss ^4.3.3
tw-animate-css ^1.4.0
typescript ^6.0.3
vite ^8.0.16

Peer dependencies

ID Version
@lucide/svelte ^1.23.0
svelte ^5.56.1
tailwindcss ^4.3.0
tw-animate-css ^1.4.0

Keywords

svelte sveltekit tailwindcss components ui
Details
npm
2026-10-09 20:05:20 +00:00
2
MIT
latest
587 KiB
Assets (1)
Versions (29) View all
0.24.0 2026-10-09
0.23.1 2026-10-06
0.23.0 2026-10-06
0.22.0 2026-10-06
0.21.0 2026-10-05