@wastedtokens/ui
v0.1.1
Published
The wastedtokens house design system — exchange-room tokens + a small primitive set (Eyebrow, Card, Button, LampDot, SiteFooter), shipped as TypeScript source and consumed via transpilePackages. Extracted from Switchboard's brand.
Downloads
287
Readme
@wastedtokens/ui
The wastedtokens house design system — the "exchange" language extracted from
Switchboard: exchange-room tokens (bakelite / paper / ink / brass, one lamp-amber accent)
plus a small primitive set. Shipped as TypeScript source, consumed via
transpilePackages (no build step, no prebuilt bundle).
- Private, all-rights-reserved (
UNLICENSED), published to the public@wastedtokensnpm scope so the apps can install it. - RSC-compatible: no client hooks; drop the components straight into a server component.
- Not a deployed app — a library. No Docker, no manifest, no onboard.
Install
npm install @wastedtokens/uiPeer dependency: React >= 19.
Consumer wiring (do all three — copy verbatim)
1. In the app's globals.css (the example assumes src/app/globals.css), after
@import "tailwindcss";:
@import "tailwindcss";
@import "@wastedtokens/ui/tokens.css";
@source "../../node_modules/@wastedtokens/ui";@import "@wastedtokens/ui/tokens.css"brings in the palette, the@themecolor tokens, and thelog-line/lamp-pulsekeyframes.@source "../../node_modules/@wastedtokens/ui"is required: Tailwind v4 must scan the package's source to see the utility class names its components use, or they get purged. The path is relative to the CSS file — fromsrc/app/globals.cssthe project-rootnode_modulesis two levels up (../../). Count the hops for your own layout: aglobals.cssatsrc/globals.cssuses../node_modules/@wastedtokens/ui; one at the project root uses./node_modules/@wastedtokens/ui.
2. In next.config.ts:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
transpilePackages: ["@wastedtokens/ui"],
};
export default nextConfig;3. Load the fonts app-side (the font contract — see below).
Font contract (loading stays app-side — the package NEVER loads fonts)
next/font cannot run inside a package, so each app loads the fonts itself and exposes
them on the exact CSS variable names below. tokens.css only consumes these variables
(via its @theme --font-* mappings and the components' font-family references):
| CSS variable | Font | next/font source |
|----------------------|-------------|------------------|
| --font-geist-sans | Geist | next/font/google → Geist |
| --font-geist-mono | Geist Mono | next/font/google → Geist_Mono |
| --font-archivo | Archivo | next/font/google → Archivo (weights ["500","700"]) — display face for eyebrows/wordmark |
In src/app/layout.tsx:
import { Archivo, Geist, Geist_Mono } from "next/font/google";
const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] });
const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] });
const archivo = Archivo({ variable: "--font-archivo", subsets: ["latin"], weight: ["500", "700"] });
// then on <html>:
// className={`${geistSans.variable} ${geistMono.variable} ${archivo.variable} …`}If an app omits a variable, the corresponding text falls back to the browser default — the package will render, just off-brand. Keep the three variable names byte-exact.
Tokens
tokens.css exposes both the raw custom properties (the source-of-truth names, matching
Switchboard's existing brand exactly) and Tailwind v4 @theme mappings:
| Custom property | @theme utility token | Value | Role |
|-------------------|------------------------|------------|------|
| --bakelite | --color-bakelite | #1a1917 | exchange-room near-black (hero/panels) |
| --line-lamp | --color-lamp | #e8a33d | operator lamp amber — the one accent |
| --patch-brass | --color-brass | #b08d57 | engraved-plate rules and eyebrows |
| --paper | --color-paper | #fafaf8 | page body below the hero |
| --ink | --color-ink | #1c1c1a | body text |
| --accent | --color-accent | var(--line-lamp) | the one-accent slot |
The one-accent rule: --accent defaults to the lamp amber. An app tints its surface by
overriding it once: :root { --accent: #3d7de8; }. Button and LampDot reference
--accent, so a single override recolors them. Don't scatter multiple accents on one
surface — one accent per app is the house rule.
Components
All are server-component-safe.
Eyebrow— the brass, letter-spaced Archivo section label. Props: standardh2attributes +as?: "h2" | "h3" | "p" | "span"+tracking?: string(letter-spacing override, e.g."0.22em"; applied as inline style so the consumer's Tailwind scanner is never in the loop — the default stays thetracking-[0.28em]utility). CallerclassNameis prepended to the base classes, so an adopting app's class attribute keeps its original byte order.Card— a flat panel. Props:tone?: "paper" | "bakelite"(default"paper") + standarddivattributes.Button— the CTA. Renders<a>whenhrefis set, else<button>. Props:variant?: "solid" | "quiet"(default"solid"),href?,asChild?, plus the matching element's attributes.solidfills with--accent;quietis styled for dark (bakelite) surfaces. WithasChild, pass a single element child (e.g. Next's<Link>) — Button contributes only its classes onto that child and renders no element of its own, so client-side navigation works. The child must be a real element — a Fragment throws (cloning a class onto a Fragment silently drops the styling):<Button asChild><Link href="/docs">Read the docs</Link></Button>LampDot— the operator line lamp. Props:{ live?: boolean; accent?: string; className?: string; size?: string }. Pulses whenlive;accentgives one lamp its own color (defaults to--accent);sizeis a Tailwind size utility (defaulth-2 w-2). A raw<span className="lamp-live …" aria-hidden />is equally first-class —.lamp-liveships intokens.cssas public API; prefer the raw span inside ternaries or where per-site layout classes make the component boundary noise.SiteFooter— the "Part of wastedtokens" affordance + link to wastedtokens.io. Props:{ appName?: string; note?: string; children?: ReactNode; className?: string }.noteis a plain-text suffix after the home link (… wastedtokens · closed preview, no SLA) — a string on purpose, since it renders inside a<span>where block content would be invalid HTML;childrenis the right-hand link slot (e.g. Privacy / Terms). The home link itself is not suppressible — that affordance is the component's point.
import { Eyebrow, Card, Button, LampDot, SiteFooter } from "@wastedtokens/ui";Development
npm run typecheck # tsc --noEmit
npm test # render smoke (react-dom/server) via tsxPublishing is manual (npm publish, publishConfig.access: public) and gated on Moshe's
2FA key. Package facts and shared conventions live in AGENTS.md.
