npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dowel-ui

v0.20.3

Published

The lacodda line design system: theme tokens and React primitives, distributed as a shadcn-compatible registry.

Readme

dowel

The lacodda line design system: theme tokens and React primitives, distributed as a shadcn-compatible registry.

A dowel is the hidden peg that joins two boards so the seam does not show. That is what this does for the products of the line: they look made by one hand, and nobody sees the joint.

Documentation — what everything is and why it is that way. The stand — every component, live, in either theme and in the accent of any product of the line.

Status: v0.20.3 - the theme, the scales, an accent per product, forty-five components - overlays, menus, the command palette and the three ways of saying something happened - and the gates each one passes: axe, the keyboard, a dependency budget and a picture in both themes. Every component has a page of its own on the stand, which remembers the theme and the accent you left it in. Components install from a versioned registry, the docs are served in the form an agent reads, and dowel check tells a project what stands between it and the vocabulary. Two products of the line live on it. See the roadmap.

The theme

One import, and a product has the vocabulary of the line:

npm install dowel-ui

The package is dowel-ui: npm declines the bare name as too close to del and bower. The system is dowel everywhere else.

@import 'dowel-ui/theme.css';

:root {
  /* The product's own colour. Everything else follows from it. */
  --accent-base: #d9569e;
}

That single line moves the accent and both of its partners, the accent's soft fill, the focus ring, the scrollbars, and the tint in the greys. Set nothing at all and you get dowel's own amber.

Dark is the default; light arrives with the reader's system preference, or pinned with a class:

<html>                <!-- follows the operating system -->
<html class="light">  <!-- pinned light -->
<html class="dark">   <!-- pinned dark -->

With Tailwind 4 the tokens are utilities, because the theme declares them in a @theme block:

<div class="bg-raise text-text border border-line">
  <button class="bg-accent text-on-accent">Save</button>
</div>

The stock palette is dropped deliberately, so a stray bg-zinc-800 does not compile. If a colour is worth using, it is worth a name in the vocabulary.

What makes it different

  • The accent is derived, not configured. A product states one hue and the theme works out the rest, including what colour text has to be to sit on top of it. A light accent takes dark glyphs, a dark one white - checked against every colour in the line rather than left to each product to get right.
  • The greys belong to the product. They carry a trace of its hue, so the chrome of one product is not the chrome of another with a different button colour.
  • Contrast is a test, not an intention. Every accent of the line is measured against WCAG AA in CI, in both themes, as a fill and as text.
  • The scales were read, not invented. Radius, type, elevation and stacking order come from what the line's products already draw, so existing code fits them - and the places where those products disagreed with themselves are settled rather than preserved.

The same vocabulary also ships as DTCG JSON at dowel-ui/tokens.json, generated from the stylesheet so the two cannot drift.

A product of the line states one thing about its appearance - which product it is:

@import 'dowel-ui/theme.css';
@import 'dowel-ui/accents/kilna.css';

Or copy the files in instead of depending on the package:

npx shadcn@latest add https://lacodda.github.io/dowel/r/theme.json

Primitives

Components are copied into your project rather than imported, so they become your code:

npx shadcn@latest add https://lacodda.github.io/dowel/r/button.json

Or the set a product usually starts from, in one command - app, forms and feedback are three sets read off what the line's products converged on:

npx shadcn@latest add https://lacodda.github.io/dowel/r/app.json

A set carries no files of its own: it resolves into the same per-component installs you could have typed, so nothing of it survives in your project and there is no membership to leave. Each minor of the registry is also served frozen at r/v0.14/…, for an install that has to be repeatable - inside a snapshot the cross-references point into the same snapshot, so a component and the sibling it reuses are the pair that shipped together. See installing from the registry.

Forty-five of them so far. The everyday ones - Button, Input, Textarea, Panel, Badge, Chip, Kbd, Spinner, Truncate and Copyable; Field and the three controls that answer a question - Checkbox, RadioGroup and Switch; five for a number or a judgement - NumberField, Slider, RatingScale, DurationField and PasswordField; five for a date or a time - Calendar, DatePicker, DateRangePicker, TimeField and the calendar-math under them; the six that float above the page - Dialog, ConfirmDialog, Drawer, Popover, PreviewCard and Tooltip; four for choosing something - Menu, ContextMenu, Select and Combobox; three for finding it - SearchField, CommandPalette and the useShortcut behind them; and three for saying that something happened: Toast, Alert and Banner.

Those last three are the ones products keep confusing, so each page names all four options: a toast goes away, an alert is still true after a reload, a banner is true on every screen, and anything that needs an answer is a dialog.

The palette is a Combobox rather than a Dialog with a field in it, which is Base UI's own arrangement: put the input inside the popup and the popup becomes a dialog on its own, with the input still the combobox that owns the list. So the filtering, the highlight and the arrow keys are the ones Combobox already has - there is no second implementation of any of it.

Select is the one the line's oldest rule is about. It renders <button role="combobox"> and no native <select> at all - the browser draws that popup in the operating system's own chrome, where no stylesheet reaches it, and on a screen of the product's own controls it reads as a foreign object. multiple is a prop on Select and on Combobox rather than a component of its own.

The overlays are built on Base UI, which supplies the part that is genuinely hard and invisible when it works: the focus trap, the return of focus to whatever opened the thing, Escape, the scroll lock, and the aria-labelledby that names a popup by its own title. Choosing between them is the harder question, and a guide covers it.

Each is written in the vocabulary - no raw colours, no dark: utilities - so the same component is correct in both themes and in every product's accent. Every one of them is on the stand, live - change the theme and the accent and watch what follows.

That convention is a lint rule, and it ships with the package:

// eslint.config.js
import dowel from 'dowel-ui/eslint'

export default [...dowel.configs.recommended]

Two rules come with it. One reports a hex, an rgb(), a stock Tailwind colour, bg-white and any dark: utility, in the file that wrote one. The other forbids the native <select>, whose popup the browser draws in the operating system's own chrome where no CSS reaches it - see the guide.

Every primitive also runs a gate of its own before it ships: axe over the rendered DOM in every variant, the keyboard over every interactive one, a declared budget for what it may import, a picture of it in both themes compared against a baseline, and a check that it carries no word of its own to translate

Full vocabulary, shown rather than tabulated: colours in both themes, the scales - radius, type, motion, elevation and stacking order - and the accents, where the same screen is drawn in every colour of the line.

Moving an existing project over

A product arriving at dowel almost never arrives from nothing - it arrives from stock shadcn/ui, whose theme names colours by their role in a page (--background, --muted-foreground) where dowel names them by what they are on a screen. The package ships the tools for that crossing:

$ npx dowel check              # what is not on the dowel vocabulary yet
$ npx dowel codemod --write    # rewrite the names that can be rewritten
$ npx dowel doctor             # whether the installation itself is wired right
$ npx dowel diff dialog        # what you changed since you copied it in

check, doctor and diff only read; codemod writes, and only when asked twice. Everything reads the catalogue inside the installed package, so none of it needs the network.

Two things are deliberately never rewritten, and the tools say so rather than guessing: a colour, because which token it was reaching for is a decision; and --accent, because it is the one name both vocabularies use for opposite things - stock's hover fill and dowel's product hue. The first run of check against dowel's own stand reported twenty-four violations that were all correct code, which is exactly how that rule was learned - see the migration guide.

A day in the life

The shortest complete path: a new project, the theme, a component, a screen.

Install the theme and say which product this is. A product of the line imports its accent; anything else sets --accent-base directly.

$ pnpm add dowel-ui
/* src/styles.css */
@import 'tailwindcss';
@import 'dowel-ui/theme.css';
@import 'dowel-ui/accents/kasl-server.css';

Tell shadcn where components go. One file, and the ui alias is the one every dowel component targets:

{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "default",
  "rsc": false,
  "tsx": true,
  "tailwind": { "config": "", "css": "src/styles.css", "baseColor": "neutral", "cssVariables": true },
  "aliases": { "components": "@/components", "ui": "@/components/ui", "lib": "@/lib", "utils": "@/lib/utils" }
}

Copy in what the screen needs.

$ npx shadcn@latest add https://lacodda.github.io/dowel/r/app.json

Write the screen in the vocabulary. No colour is written down, so this is correct in both themes and in whatever accent the product turns out to have:

import { Panel } from '@/components/ui/panel'
import { Button } from '@/components/ui/button'

export function SignIn() {
  return (
    <Panel className="w-80 p-5">
      <h1 className="text-lg font-semibold text-text">Sign in</h1>
      <p className="text-xs text-dim">Your team's server</p>
      <Button variant="primary" className="mt-4 w-full">Sign in</Button>
    </Panel>
  )
}

Check the wiring before debugging the screen.

$ npx dowel doctor
The installation is coherent.

That is the whole loop, and it is the one the line's second product actually went through. kasl-server had written its own theme first, in the same words - bg, raise, line, dim, accent - so moving over deleted about a hundred and thirty lines and changed nothing on screen except one thing it silently fixed: the hand-written theme pinned dark ink on the accent for both themes, which was right on gold and wrong on the darkened gold the light theme uses, where it measured 3.49:1. Derived, it is white there, at 6.01:1.

Roadmap

Development goes in versions; each one is a single coherent theme, and ends in a release.

| | Delivers | | --- | --- | | 0.1 - 0.3 | The vocabulary: colours and modes, scales and motion, the accents of the line | | 0.4 - 0.7 | The primitive pipeline, the base components, the first consumer, the quality gates | | 0.8 - 0.11 | Overlays, menus and selection, the command palette, feedback | | 0.12 - 0.15 | The registry as a product, AI-readiness, migration tooling, the second consumer | | 0.16 - 0.24 | Forms, data and charts | | 0.25 - 0.33 | Frame and navigation, blocks, resilience and docs | | 1.0 | Four web products of the line on dowel; the token vocabulary and the registry format frozen |

Documentation

For a machine

The documentation is also served in the form an agent reads, generated from the same sources the site is built from:

  • llms.txt - the index, and llms-full.txt for all of it in one request
  • any page as plain Markdown, by appending .md to its URL
  • a JSON Schema of a registry item, validated against every item the registry serves
  • an AGENTS.md a consumer installs like any other item, so a product on dowel can tell its own agents what its code cannot

License

MIT