@archetypeai/ds-lib-tokens
v0.17.0
Published
Archetype AI Design System Tokens
Downloads
5,256
Readme
@archetypeai/ds-lib-tokens
Tailwind v4 theme for the Archetype AI Design System. Provides semantic design tokens, color palettes, and base styles for use with the design system component flavors.
Install
npm install @archetypeai/ds-lib-tokens tailwindcssUsage
Import the theme before Tailwind in your global CSS:
@import '@archetypeai/ds-lib-tokens/fonts.css'; /* optional → PP Neue Montreal (requires font files in static/fonts/) */
@import '@archetypeai/ds-lib-tokens/theme.css';
@import 'tailwindcss';<div class="bg-background text-foreground p-md border-border rounded-lg">
Hello world
</div>What's Included
| Feature | Details |
|---------|---------|
| Semantic Tokens | OKLCH CSS variables (shadcn-style): background, foreground, card, popover, primary, secondary, muted, accent, destructive, border, separator, scrollbar, scrollbar-track, input, ring, chart-1–5, sidebar, atai-neutral, atai-good, atai-warning, atai-critical, atai-always-black, atai-always-white |
| Brand colors | 4 flat OKLCH values, no scale: brand-babyblue, brand-yellow, brand-black, brand-white |
| Color palettes | 26 OKLCH ramps: Tailwind's 22 stock scales (red…rose, slate, gray, zinc, neutral, stone) plus 4 design-system neutrals (mauve, olive, mist, taupe). Each runs 50–950 plus an 850 step — see below |
| Spacing scale | --spacing-xs (0.25rem) → --spacing-xl (1.5rem), used as p-md, gap-lg, … |
| Radius scale | --radius-xs → --radius-xl, --radius-full, plus --atai-radius-interactive (radius for interactive elements, rounded-interactive) |
| Icon stroke widths | --stroke-width-icon-{default,interactive,status,emphasis} (1 / 1.25 / 1.5 / 2) for Lucide icons |
| Width scale guard | --max-width-*/--min-width-* alias Tailwind's --container-* scale, so max-w-md resolves to container sizes instead of the t-shirt spacing tokens |
| Dark mode | .dark class via @custom-variant dark (&:where(.dark, .dark *)) |
| Typography | Font-family stacks with metric-matched fallbacks, a paired type scale (--text-*--line-height / --letter-spacing on every step, plus --text-2xs), and a semantic HTML base layer |
| Scrollbars | --scrollbar/--scrollbar-track (thumb + track colour, bg-scrollbar) and --scrollbar-width. Applied globally in the base layer, so every scroller follows the theme with no app code |
Brand colors and palettes are defined in colors.css and loaded automatically via theme.css.
The 850 step
Every ramp carries an 850 shade that stock Tailwind does not have. Tailwind's scale is coarse at the dark end — zinc-800 → zinc-900 drops 0.064 in OKLCH lightness — which left dark mode with a single surface stop that --card and --popover both had to share, so a Card inside a Dialog was distinguishable only by its ring.
Each 850 is the per-component midpoint of its 800 and 900 neighbours in OKLCH, gamut-mapped back into sRGB where a straight midpoint fell outside it (emerald, sky, rose — chroma reduced, lightness and hue held). --card uses zinc-850 in dark mode, giving surfaces four steps instead of three:
| L | token | surface |
|---|---|---|
| 0.141 | --background | page |
| 0.210 | --popover / --sidebar / --secondary | dialog, dropdown menu, select, side panel, sidebar |
| 0.242 | --card | card, alert, item, charts |
| 0.274 | --input / --muted / --border | fields, wells, edges |
Light mode is unaffected: --card stays zinc-50 against a white --popover.
Type scale
Every stock text-* step carries its own line-height and optical letter-spacing, so text-sm alone is on-brand — never pair a text-* utility with leading-* or tracking-*; the ramp already supplies both. Tracking tightens as size grows; leading loosens as size shrinks.
| step | size | line-height | letter-spacing |
|---|---|---|---|
| text-2xs | 10px | 14px | 0.02em |
| text-xs | 12px | 16px | 0.01em |
| text-sm | 14px | 20px | 0 |
| text-base | 16px | 24px | 0 |
| text-lg | 18px | 26px | -0.005em |
| text-xl | 20px | 28px | -0.01em |
| text-2xl | 24px | 30px | -0.015em |
| text-3xl | 30px | 36px | -0.02em |
| text-4xl | 36px | 40px | -0.025em |
| text-5xl | 48px | 52px | -0.03em |
| text-6xl–text-9xl | 60–128px | 1 | -0.035 / -0.04em |
Line-heights are whole pixels at the root size so adjacent rows and cells share a pixel grid.
text-2xs (10px) is the floor for dense metadata — use it instead of arbitrary text-[10px]/text-[11px]. Letter-spacing is keyed by size, not family, so font-mono at lg and above inherits the negative tracking; at the sizes mono is used in the design system (xs/sm) it is 0 or positive.
The primitives follow the same rule: no leading-* or tracking-* sits beside a text-* anywhere in ds-ui-svelte. The one deliberate off-scale construct is Counter's digit roller (text-[1em] + leading-(--counter-row) in counter.svelte), which needs em-relative glyphs and a fixed row height to animate a rolling digit; it is the only place arbitrary sizing is accepted. At size="inherit" that row is 1lh, so a bare counter set in running text rolls on the line height it inherits rather than on a pill's fixed row.
Semantic HTML base layer
theme.css styles a small set of authored-content elements so plain markup is on-brand without utility classes. code and pre need no rule: Tailwind preflight already gives them --font-mono, and PP Neue Montreal Sans and Mono share identical x-height and cap-height, so inline code sits at the same size as the text around it.
| element | styles |
|---|---|
| body | bg-background text-foreground font-sans text-sm — the base size is set once here and inherited |
| h1 / h2 / h3 | text-primary, text-3xl / text-2xl / text-xl, font-normal, text-balance; h1 also capitalize |
| h4 / h5 / h6 | text-muted-foreground, text-lg / text-base / text-sm, font-normal |
| p | text-primary text-pretty (inherits size) |
| small | text-primary text-xs |
| strong | font-medium (inherits size) |
| blockquote | border-s-2 ps-6 italic |
| a | cursor-pointer — affordance only, no colour and no size |
Only h1–h6 and small carry a font-size; everything else inherits, so a size on a parent always wins.
Deliberately unstyled: span, label, em, code, pre. They are structural — they appear inside third-party widgets, form libraries and highlighted code — and an element rule on them beats the parent's size in every consumer, which forced apps to restate text-* on every inline child. They inherit from their parent instead. a is in the same family and carries no typography for the same reason, but it does get cursor-pointer: the UA sheet gives the pointer only to a[href], so an anchor wired to a click handler — or handed to a router through a child snippet before the href lands — would otherwise read as plain text. Consequences: bare links are still visually flat (Tailwind preflight gives a color: inherit; text-decoration: inherit) until the app styles them, and a purely decorative anchor now shows a pointer it has not earned; <label> typography is owned by the Label primitive. h1 is title-cased (capitalize, override with normal-case); no other heading carries a text-transform — uppercase is opt-in via Label variants and the typography="mono" axis. All elements default to border-border and outline-ring/50.
Scrollbars are governed by the base layer too: the universal * rule sets scrollbar-color / scrollbar-width from --scrollbar / --scrollbar-track / --scrollbar-width, so every overflow box — including ones in your own app — gets a themed bar with no classes. The declaration sits on * rather than :root on purpose: both properties are inherited, but var() substitutes where the declaration lives, so * is what lets a .dark on any subtree flip that subtree's scrollbars.
Only the standard scrollbar properties are used. In Chromium 121+ / WebKit 18.2+ a non-auto scrollbar-color or scrollbar-width opts an element out of the legacy ::-webkit-scrollbar pseudo-elements, so the two are mutually exclusive rather than layered — and the standard pair is the only one Chromium, WebKit and Gecko all implement. Consequences: thumb radius and an explicit hover colour are not expressible (engines derive hover from the thumb colour), --scrollbar-width takes only auto | thin | none, and macOS renders overlay scrollbars that take no layout space and ignore thickness. Retheme by redefining the tokens; hide a single scroller's bar with [scrollbar-width:none].
Fonts
fonts.css is included in this package with two kinds of @font-face declarations: 13 WOFF2 faces of PP Neue Montreal Sans + Mono (font-display: swap) pointing to /fonts/*.woff2, plus 20 metric-matched fallback faces (per-OS base × per weight). The font binaries themselves are not part of the npm package - the CLI (ds create / ds init / ds add --fonts <path>) copies them into your project's static/fonts/. The 13 files total ~820 KB (the foundry's TTF/OTF were 2.1 MB); regenerate them from the originals with fontTools if the brand font is ever updated:
python3 -c "from fontTools.ttLib import TTFont; from pathlib import Path
for src in sorted(Path('.').glob('PPNeueMontreal*.[to]tf')): f = TTFont(src); f.flavor = 'woff2'; f.save(src.with_suffix('.woff2'))"When the binaries are absent, the fallback faces take over: each re-shapes a local system font to PP Neue Montreal's own metrics — x-height, average character width, and line box — via size-adjust + ascent/descent/line-gap-override, so an un-provisioned project still renders at the brand font's proportions and lays out identically. When the binaries are present, the same faces make the fallback→brand-font swap shift-free (no layout shift / CLS).
The fallback bases are chosen per operating system (the stacks in theme.css order them so each platform resolves its closest native font, all confirmed local()-reachable):
| | Apple | Windows | Android | |---|---|---|---| | Sans | Helvetica Neue | Arial | Roboto | | Mono | Menlo | Consolas / Cascadia Mono | Roboto Mono |
SF Mono is intentionally not used — it is not reachable via local(). Override numbers are generated with @capsizecss from the Regular weights (regenerate if the brand font's metrics change).
Preloading
font-display: swap plus the metric-matched fallbacks make the swap shift-free, but the first paint still happens in the fallback face unless the browser is told to fetch the brand font before it discovers the @font-face rule. The CLI therefore inserts three preloads above %sveltekit.head% in src/app.html whenever it installs fonts - the faces the first paint uses: Book (400 sans, body), Medium (500 sans, strong and emphasis) and Mono Book (400 mono, labels, badges, counts). Every other face is fetched lazily when rendered text first needs it.
<link rel="preload" as="font" type="font/woff2" crossorigin href="/fonts/PPNeueMontreal-Book.woff2" />
<link rel="preload" as="font" type="font/woff2" crossorigin href="/fonts/PPNeueMontreal-Medium.woff2" />
<link rel="preload" as="font" type="font/woff2" crossorigin href="/fonts/PPNeueMontrealMono-Book.woff2" />crossorigin is mandatory even for same-origin fonts - font requests are always CORS-mode, and a preload without it does not match, so the browser downloads the file twice. If your host has no src/app.html (or a custom one without the placeholder) the CLI prints these tags for you to paste into <head>.
Weights are declared per instance: local('Helvetica Neue') always resolves to the Regular cut regardless of the font-weight descriptor, so a single face would silently render Medium as Regular and faux-bold 700. Each weight therefore names its real cut (local('HelveticaNeue-Medium') / -Bold, etc.). Bases without a true Medium (Arial, Menlo, Cascadia) omit 500, so it matches down to Regular.
