PCBJam's shared UI components: shadcn/ui on Radix, styled only through design tokens. Both the PCBJam web app and the editor use it.
The rule: this package is the only place that styles raw <button>, <select>,
<input> or <textarea> elements, or builds a badge, tab strip or popup from plain
elements. Apps compose these components and never restyle the primitives.
src/components/*: the components. Import them from@pcbjam/ui.src/theme.css: neutral defaults for every token (shadcn's slate theme), for:rootand.dark. Import it first; a brand stylesheet loaded after it overrides tokens.--font-sansis the typeface (Tailwind's stack by default; the preset'sfont-sansand the page font read it), so a brand sets its type with one token.tailwind-preset.js: maps the tokens to Tailwind colours and radii.Buttonis shadcn's, plus two text variants forsize="text", which has no box and takes the surrounding font and colour:variant="link"is a text action on its own ("Or use SSH instead"), underlined on hover;variant="inline"is an action inside a sentence ("… or let PCBJam generate one"), always underlined.size="row"is a full-width list row (variant="ghost"for a hover fill);variant="plain"has no colour, fill or underline of its own, for a region whose content is the affordance (a disclosure header, a tree row's name) or a control on a surface that styles it (the editor's overlays).size="xs"is a 28px button (row actions, admin chips);size="icon-xs"is a bare icon in a dense header or row (close, collapse), as on Toggle.Selecttakesvalue=""on an item (Radix alone refuses it), so a native<select>'s "All" or "None" option ports over as is.Comboboxis the Select with a search box, for long lists (branches, repositories, symbols).Badgetakes atone(neutral, primary, success, warning, danger, info, preview) and avariant(soft, outline, solid). The tones are tokens intheme.csswith their own dark values; each clears 4.5:1 on its 15% tint in both themes.size="count"is the round number on an icon or row (unread, changed files).Codeis inline code or an id, sha, path or key: monospace on a muted chip. With layout classes it is a copyable value box.Kbdis a key or shortcut (⌘, Esc), shadcn's Kbd: a hint that takes no clicks.Tabsswitch panels (TabsListvariantsegmented,underline, orrail: a vertical column of icon tabs).ToggleGrouppicks one (type="single", items are radios) or several values;variant="segmented"is the segmented control. A single group ignores a click that would clear its value unlessdeselectableis set.Toggleis one pressed/unpressed button;variant="ghost"has no pressed look of its own (style it withdata-[state=on]:),size="row"makes a full-width menu row andsize="icon-xs"a bare icon.RadioCardis a whole card as one radio of aRadioGroup(templates, licences, project sources): no dot, the checked card wears the primary border and ring, arrow keys move the choice.Slideris shadcn's on Radix: the same track in every browser, arrow keys, Shift+arrow for ten steps, Home and End.thumbLabelnames the thumb, which carries the slider role.SearchInputis an Input with a search icon and, givenonClear, a clear button;size="sm"is 32px.classNamelays out the field,inputClassNamestyles the input, and every other prop goes to the input.Togglealso takessize="xs"(28px), matching Button's.Checkbox,RadioGroupandSwitchare buttons withrole="checkbox","radio"and"switch", so Playwright'scheck()andtoBeChecked()work as on native inputs.onCheckedChangegetstrue,falseor"indeterminate". Use a Switch for a setting that applies at once, a Checkbox for a choice that is submitted or a list selection.Tipreplaces atitleattribute:<Tip content="Close"><button>…</button></Tip>. Liketitle, it names an icon-only trigger (its text becomes the aria-label when the trigger has no name of its own; only on elements that take a name, never a plain span or div), and it still shows on a disabled trigger. Like a title, it closes when the pointer leaves and lets clicks through. Tips nest like titles: over an inner trigger only the inner tooltip opens, and a trigger opens on focus only for its own focus. It needs oneTooltipProviderabove it, at the app root (a test harness that mounts a component alone needs one too).Toaster(Sonner) goes once at the app root;toast(),toast.error(),toast.warning(),toast.info()andtoast.success()raise a toast from anywhere, in the tone colours. It follows thedarkclass on the root element.testIdputs adata-testidon the toast.- Tests: a Select or Combobox trigger carries the chosen value in
data-value, and each option its value indata-option-value. Pick one with a click on the trigger, then on[role=option][data-option-value="…"]. components.json: the target fornpx shadcn add. Generated files import@/lib/utils; change that to a relative import (../lib/utils), or the import guardrail test fails.
No brand: no PCBJam colours, fonts or logos live here. See TRADEMARKS.md.
// tailwind.config.js
import uiPreset from "@pcbjam/ui/tailwind-preset";
export default {
presets: [uiPreset],
content: ["./index.html", "./src/**/*.{ts,tsx}", "<path to this package>/src/**/*.{ts,tsx}"],
};/* index.css, before the @tailwind directives */
@import "@pcbjam/ui/theme.css";The package ships TypeScript source; the app's bundler compiles it. Two pnpm workspaces
install it (the root app and pcbjam/web), so its own node_modules can point at the
other workspace's React. That is safe only while the app dedupes React, which
@vitejs/plugin-react does by default. An app built without that plugin must set
resolve.dedupe: ["react", "react-dom"] itself.
The editor pauses its guide overlay while a dialog is open. It does that through
<DialogEventsProvider onContentMount={...}>; this package never imports app code.
- Source files import only React, Radix,
class-variance-authority,clsx,tailwind-merge,lucide-react,cmdkand each other. Nothing from an app, nothing GPL. - Every installed dependency, direct or transitive, is under MIT, ISC, BSD, 0BSD or Apache-2.0.
Apps style controls only through this package: a raw <button>, <select>, <input> or
<textarea> with a className (or a spread, which can carry one) outside it fails CI. File,
colour and hidden inputs are exempt. Each app runs it as pnpm lint:ui:
node <path to this package>/tools/ui-rule.mjs src ui-rule-baseline.json [--update]Code from before the rule is listed per file in the app's ui-rule-baseline.json, and the
counts may only go down. A file that gains one fails, and so does a file that loses one until
--update lowers the baseline, so the next change cannot quietly take the slot back.
MIT, see LICENSE. The components are derived from shadcn/ui (MIT); its notice is in
THIRD_PARTY_NOTICES.md.