{"_id":"@5voltfx/design-system","_rev":"8-82cdab0c9baef35375237bf89124dd31","name":"@5voltfx/design-system","dist-tags":{"latest":"0.6.1"},"versions":{"0.1.0":{"name":"@5voltfx/design-system","version":"0.1.0","_id":"@5voltfx/design-system@0.1.0","maintainers":[{"name":"5voltfx","email":"matt@5voltfx.com"}],"dist":{"shasum":"45b7de75c506c98bb07a8d8d10dc55476982f236","tarball":"https://registry.npmjs.org/@5voltfx/design-system/-/design-system-0.1.0.tgz","fileCount":43,"integrity":"sha512-dXbY6r+Pz4f80bTwrSVwkeUBDQ160nxS6CluRRU3XfNm6UZJiuw6QnmV/mNIv+hz9FPcRCozD4rFOIzttUvaWA==","signatures":[{"sig":"MEQCIBtJ+jG/chMQSf6umrRyNQ+Mjsd33hq0cb/2BMUWREVGAiAeg09MPwCEAZaIaWPYkpQFxWC0oflP4Oa5Isj0tB7vsA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":26193},"main":"./src/index.js","type":"module","exports":{".":"./src/index.js","./styles.css":"./src/tokens/index.css","./vite-plugin":"./vite-plugin.js","./components/*":"./src/components/*/index.js"},"gitHead":"61c537b469e70cd286cdb2b8f8f4c60971cc4661","_npmUser":{"name":"5voltfx","email":"matt@5voltfx.com"},"_npmVersion":"10.9.0","directories":{},"_nodeVersion":"23.3.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"peerDependencies":{"react":"^19.0.0","react-dom":"^19.0.0","react-router-dom":"^7.0.0"},"_npmOperationalInternal":{"tmp":"tmp/design-system_0.1.0_1785530318911_0.6443208099816173","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@5voltfx/design-system","version":"0.1.1","_id":"@5voltfx/design-system@0.1.1","maintainers":[{"name":"5voltfx","email":"matt@5voltfx.com"}],"dist":{"shasum":"fe1a18eb2bd1cafc3bbb6becc26120e8bbc4997f","tarball":"https://registry.npmjs.org/@5voltfx/design-system/-/design-system-0.1.1.tgz","fileCount":58,"integrity":"sha512-JwDIbpqgWjk8eXWQaZVNbsaWFf+k17EKMdOM5VsBwr/lYnmO1QBuoJ4LSbAaDf5xHmQWXEEllcZBKu44RlCHsA==","signatures":[{"sig":"MEQCIGRcJyPngN6nOtHfhrD4DPMXnfdrUfqYpgp/8M290UmRAiAIQ077I3R5JwlK1CMZNy3EvtsfwRQV7HW/bAkw237FAw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":91163},"main":"./dist/index.js","type":"module","exports":{".":"./dist/index.js","./styles.css":"./dist/index.css","./vite-plugin":"./vite-plugin.js","./components/*":"./dist/components/*/index.js"},"gitHead":"6e466e865c40ebcb26cab23c3dc8c6ccdcfc0dcd","scripts":{"dev":"tsup --watch","build":"tsup","prepare":"npm run build","prepublishOnly":"npm run build"},"_npmUser":{"name":"5voltfx","email":"matt@5voltfx.com"},"_npmVersion":"10.9.0","directories":{},"sideEffects":["**/*.css"],"_nodeVersion":"23.3.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","typescript":"^5.0.0"},"peerDependencies":{"react":"^19.0.0","react-dom":"^19.0.0","react-router-dom":"^7.0.0"},"_npmOperationalInternal":{"tmp":"tmp/design-system_0.1.1_1785531680040_0.8197887451139267","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@5voltfx/design-system","version":"0.2.0","_id":"@5voltfx/design-system@0.2.0","maintainers":[{"name":"5voltfx","email":"matt@5voltfx.com"}],"dist":{"shasum":"86c96749c042465324b84dc2567dbe262045e032","tarball":"https://registry.npmjs.org/@5voltfx/design-system/-/design-system-0.2.0.tgz","fileCount":258,"integrity":"sha512-08SDQoWUhK41p43gYPocxW1Ym9bnYLo/6D5AGaCQPQT06Ny0xUKIplP4b6Fz68uC4+XPb3gwZ4IMlIEq3Phobg==","signatures":[{"sig":"MEYCIQDK9l+vVKzZwBr6CV46EHHJtMryVJrXnCcQ7njO3kd0NAIhAOZXPU9typmrrVyD+r8BpQ6TF/CR/oPr9bmeVrBpRkef","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":433746},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./styles.css":"./dist/index.css","./vite-plugin":"./vite-plugin.js","./components/*":{"types":"./dist/components/*/index.d.ts","import":"./dist/components/*/index.js"}},"gitHead":"9db943288ba7ee61288214854ba7ef3892dd466e","scripts":{"dev":"tsup --watch","test":"vitest","build":"tsup && tsc","prepare":"npm run build","prepublishOnly":"npm run build"},"_npmUser":{"name":"5voltfx","email":"matt@5voltfx.com"},"_npmVersion":"10.9.0","directories":{},"sideEffects":["**/*.css"],"_nodeVersion":"23.3.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","jsdom":"^27.0.1","react":"^19.2.8","vitest":"^3.2.7","react-dom":"^19.2.8","typescript":"^5.0.0","@types/react":"^19.2.18","@testing-library/react":"^16.3.2","@testing-library/jest-dom":"^7.0.0","@testing-library/user-event":"^14.6.1"},"peerDependencies":{"react":"^19.0.0","react-dom":"^19.0.0","react-router-dom":"^7.0.0"},"_npmOperationalInternal":{"tmp":"tmp/design-system_0.2.0_1787866480232_0.39746510868864027","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@5voltfx/design-system","version":"0.3.0","_id":"@5voltfx/design-system@0.3.0","maintainers":[{"name":"5voltfx","email":"matt@5voltfx.com"}],"dist":{"shasum":"dd17df4fdb9b88cfb3e25a79288f31377ee27210","tarball":"https://registry.npmjs.org/@5voltfx/design-system/-/design-system-0.3.0.tgz","fileCount":278,"integrity":"sha512-YX0rQeK+5Yn0rYr2TKGD+DORj1G1iBQRmQFYA81dfBKjRHLstbMfRw1XQUgqPsa9WWXy02TkeDJlZA2qb/qXjQ==","signatures":[{"sig":"MEQCIBuF1zcW7tr2cxfq8G2rMkG4G2+J3k1x9C1tmyH9FonwAiAeWoNsH2ltUzsD+3vZze9X3iYdUYaoubXBc+7MfDyUvQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":600662},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./styles.css":"./dist/index.css","./vite-plugin":"./vite-plugin.js","./components/*":{"types":"./dist/components/*/index.d.ts","import":"./dist/components/*/index.js"}},"gitHead":"8288fd5d3c65622efafc527e93616945b9960c1b","scripts":{"dev":"tsup --watch","test":"vitest","build":"tsup && tsc","prepare":"npm run build","prepublishOnly":"npm run build"},"_npmUser":{"name":"5voltfx","email":"matt@5voltfx.com"},"_npmVersion":"10.9.0","directories":{},"sideEffects":["**/*.css"],"_nodeVersion":"23.3.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","jsdom":"^27.0.1","react":"^19.2.8","vitest":"^3.2.7","react-dom":"^19.2.8","typescript":"^5.0.0","@types/react":"^19.2.18","@testing-library/react":"^16.3.2","@testing-library/jest-dom":"^7.0.0","@testing-library/user-event":"^14.6.1"},"peerDependencies":{"react":"^19.0.0","react-dom":"^19.0.0","react-router-dom":"^7.0.0"},"_npmOperationalInternal":{"tmp":"tmp/design-system_0.3.0_1787891425946_0.39657099033933774","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@5voltfx/design-system","version":"0.4.0","_id":"@5voltfx/design-system@0.4.0","maintainers":[{"name":"5voltfx","email":"matt@5voltfx.com"}],"dist":{"shasum":"8c28e12e4c0c4cb6a8839b7feb7ca506119db51b","tarball":"https://registry.npmjs.org/@5voltfx/design-system/-/design-system-0.4.0.tgz","fileCount":278,"integrity":"sha512-dYzttb+j0JE59Re4aXVNcIcZgBgcv2wyTgGf/MAd3Tns8kiVH0zrSOPojls602UdJqZpE/pzyqPhfne7E6QJ6w==","signatures":[{"sig":"MEQCICNAb2vWzc7JUjwpQY+EmkaPDsgr2ZIdY4dv/oq4aRtAAiA04GRAoPMA2WAMedznx8t8/itgcHmAAVj8Q9Nwyozs3w==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":602788},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./styles.css":"./dist/index.css","./vite-plugin":"./vite-plugin.js","./components/*":{"types":"./dist/components/*/index.d.ts","import":"./dist/components/*/index.js"}},"gitHead":"95f286b45f407436e01e3f5597935d73969eadeb","scripts":{"dev":"tsup --watch","test":"vitest","build":"tsup && tsc","prepare":"npm run build","prepublishOnly":"npm run build"},"_npmUser":{"name":"5voltfx","email":"matt@5voltfx.com"},"_npmVersion":"10.9.0","directories":{},"sideEffects":["**/*.css"],"_nodeVersion":"23.3.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","jsdom":"^27.0.1","react":"^19.2.8","vitest":"^3.2.7","react-dom":"^19.2.8","typescript":"^5.0.0","@types/react":"^19.2.18","@testing-library/react":"^16.3.2","@testing-library/jest-dom":"^7.0.0","@testing-library/user-event":"^14.6.1"},"peerDependencies":{"react":"^19.0.0","react-dom":"^19.0.0","react-router-dom":"^7.0.0"},"_npmOperationalInternal":{"tmp":"tmp/design-system_0.4.0_1787892328948_0.37542879806854934","host":"s3://npm-registry-packages-npm-production"}},"0.5.0":{"name":"@5voltfx/design-system","version":"0.5.0","_id":"@5voltfx/design-system@0.5.0","maintainers":[{"name":"5voltfx","email":"matt@5voltfx.com"}],"dist":{"shasum":"83f976fa91fd1a91bffeb87236330edc1d197e05","tarball":"https://registry.npmjs.org/@5voltfx/design-system/-/design-system-0.5.0.tgz","fileCount":278,"integrity":"sha512-xaJRuLrUiu0Llt0EtY/PBmxqwYfg6yNQq/B+pZtZ870g82qKePqTJVP5YSzfStzKwu7bp1sKJec21mnThN2tcw==","signatures":[{"sig":"MEUCIF9qZx1c+foIUVj4j2prYzVTqH8GkJh/8DVE0eZDPBCcAiEAhY9vd7iUPvO+85elgqG1mX0JXL58pDKyqicQNUoHF1c=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":611201},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./styles.css":"./dist/index.css","./vite-plugin":"./vite-plugin.js","./components/*":{"types":"./dist/components/*/index.d.ts","import":"./dist/components/*/index.js"}},"gitHead":"773c8c37c0b6b5dc2696462c63ccedb40163ba09","scripts":{"dev":"tsup --watch","test":"vitest","build":"tsup && tsc","prepare":"npm run build","prepublishOnly":"npm run build"},"_npmUser":{"name":"5voltfx","email":"matt@5voltfx.com"},"_npmVersion":"10.9.0","directories":{},"sideEffects":["**/*.css"],"_nodeVersion":"23.3.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","jsdom":"^27.0.1","react":"^19.2.8","vitest":"^3.2.7","react-dom":"^19.2.8","typescript":"^5.0.0","@types/react":"^19.2.18","@testing-library/react":"^16.3.2","@testing-library/jest-dom":"^7.0.0","@testing-library/user-event":"^14.6.1"},"peerDependencies":{"react":"^19.0.0","react-dom":"^19.0.0","react-router-dom":"^7.0.0"},"_npmOperationalInternal":{"tmp":"tmp/design-system_0.5.0_1788056210238_0.4302346247542599","host":"s3://npm-registry-packages-npm-production"}},"0.6.0":{"name":"@5voltfx/design-system","version":"0.6.0","_id":"@5voltfx/design-system@0.6.0","maintainers":[{"name":"5voltfx","email":"matt@5voltfx.com"}],"dist":{"shasum":"bef2f37cfb8e06fedd07c7e81e7062a0d421ff2e","tarball":"https://registry.npmjs.org/@5voltfx/design-system/-/design-system-0.6.0.tgz","fileCount":324,"integrity":"sha512-U9oxBJ90O5rGiHYwHmI+1wMI7kTEVxUxc+8EBtXEyNVgpXSOADm20afYNupK45J10+XuyJQ71VZyFCYe59JEgQ==","signatures":[{"sig":"MEUCIQChhntogr9JCAD+U1vRA4Bq58RSImvbyHakriCjTyQU0QIgS3weURirKn1fAZsSJI1yafOI1JolY8xSO3cZmyb2Wb0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":701180},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./styles.css":"./dist/index.css","./vite-plugin":"./vite-plugin.js","./components/*":{"types":"./dist/components/*/index.d.ts","import":"./dist/components/*/index.js"}},"gitHead":"7540c5daebb8526b33662f66ad4ed6a3b5ff9870","scripts":{"dev":"tsup --watch","test":"vitest","build":"tsup && tsc","prepare":"npm run build","prepublishOnly":"npm run build"},"_npmUser":{"name":"5voltfx","email":"matt@5voltfx.com"},"_npmVersion":"10.9.0","directories":{},"sideEffects":["**/*.css"],"_nodeVersion":"23.3.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","jsdom":"^27.0.1","react":"^19.2.8","vitest":"^3.2.7","react-dom":"^19.2.8","typescript":"^5.0.0","@types/react":"^19.2.18","@testing-library/react":"^16.3.2","@testing-library/jest-dom":"^7.0.0","@testing-library/user-event":"^14.6.1"},"peerDependencies":{"react":"^19.0.0","react-dom":"^19.0.0","react-router-dom":"^7.0.0"},"_npmOperationalInternal":{"tmp":"tmp/design-system_0.6.0_1788120141998_0.5691257919661912","host":"s3://npm-registry-packages-npm-production"}},"0.6.1":{"name":"@5voltfx/design-system","version":"0.6.1","type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./styles.css":"./dist/index.css","./components/*":{"types":"./dist/components/*/index.d.ts","import":"./dist/components/*/index.js"},"./vite-plugin":{"types":"./vite-plugin.d.ts","import":"./vite-plugin.js"}},"sideEffects":["**/*.css"],"scripts":{"build":"tsup && tsc","typecheck":"tsc -p tsconfig.typecheck.json","dev":"tsup --watch","test":"vitest","prepublishOnly":"npm run build","prepare":"npm run build"},"publishConfig":{"access":"public"},"peerDependencies":{"react":"^19.0.0","react-dom":"^19.0.0","react-router-dom":"^7.0.0"},"devDependencies":{"@testing-library/jest-dom":"^7.0.0","@testing-library/react":"^16.3.2","@testing-library/user-event":"^14.6.1","@types/react":"^19.2.18","jsdom":"^27.0.1","react":"^19.2.8","react-dom":"^19.2.8","tsup":"^8.5.0","typescript":"^5.0.0","vitest":"^3.2.7"},"_id":"@5voltfx/design-system@0.6.1","gitHead":"3f2a0f5d43a6624f4744a06fd2fd281167fe0e3e","description":"A shared React component library and token system for the three 5VoltFX/Matt Welker sites — `song-chart`, `mattwelker.com`, and `5voltfx.com`. Plain React 19 and plain CSS custom properties, no UI framework.","_nodeVersion":"23.3.0","_npmVersion":"10.9.0","dist":{"integrity":"sha512-55wis3BItLMZiSqAtEeGk5+31h24AoxqiyUNtYHZqVJytMIAz3ZRffzkHV6vVh57g1XTh56AOaQdas5EELJ/9w==","shasum":"f0f07bb6d9908ae5fb29d83665c91f7071ad1d98","tarball":"https://registry.npmjs.org/@5voltfx/design-system/-/design-system-0.6.1.tgz","fileCount":442,"unpackedSize":939012,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCff79cT4opGSXGnXNxcRz5UP7PSY/hvU4AgfSXGDNdVQIhAIDKb9TVypO1MEPQgeORuLSDXbYHF4PbM0a21UqgQ3y4"}]},"_npmUser":{"name":"5voltfx","email":"matt@5voltfx.com"},"directories":{},"maintainers":[{"name":"5voltfx","email":"matt@5voltfx.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/design-system_0.6.1_1788288485543_0.7652258196822919"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-31T20:38:38.724Z","modified":"2026-09-01T18:48:05.878Z","0.1.0":"2026-07-31T20:38:39.064Z","0.1.1":"2026-07-31T21:01:20.179Z","0.2.0":"2026-08-27T21:34:40.391Z","0.3.0":"2026-08-28T04:30:26.124Z","0.4.0":"2026-08-28T04:45:29.129Z","0.5.0":"2026-08-30T02:16:50.412Z","0.6.0":"2026-08-30T20:02:22.155Z","0.6.1":"2026-09-01T18:48:05.709Z"},"maintainers":[{"name":"5voltfx","email":"matt@5voltfx.com"}],"readme":"# @5voltfx/design-system\n\nA shared React component library and token system for the three 5VoltFX/Matt Welker\nsites — `song-chart`, `mattwelker.com`, and `5voltfx.com`. Plain React 19 and plain\nCSS custom properties, no UI framework.\n\nThe system has a point of view: it is drawn as a **drafting document**. Components\nare specimens on a ruled ground, named and specified. Two of the eight themes\n(`instrument`, `instrument-day`) lean all the way into that; the other six are\npainted themes that inherit the same structure with a softer finish.\n\n---\n\n## Seeing it\n\nThe `playground/` app is the live style guide. It consumes the package exactly the\nway the three sites do, so what you see there is what you get.\n\n```bash\ncd playground\nnpm install        # first time only\nnpm run dev        # http://localhost:5173/\n```\n\nTheme switcher is in the footer — all eight themes, live.\n\n| Route | What it shows |\n|---|---|\n| `/` | Tokens — color families, type scale, spacing, elevation, radius, motion |\n| `/primitives` | Button, Card, Badge, Heading, Text, Container/Stack/Grid |\n| `/plates` | Plate anatomy — Plate, Specs, Caption, Readout, Chapter |\n| `/forms` | FormField, Label, Input, Textarea, Select, Checkbox, Radio, Switch |\n| `/overlays` | Modal, Menu, Tabs, Accordion |\n| `/navigation` | SlideRuleNav and DialNav, all six variants |\n\n### Working on the library and the playground at once\n\nThe playground depends on the package via `file:..` and resolves to `dist/`, not\n`src/`. So run the library's watch build in a second terminal:\n\n```bash\nnpm run dev        # in the repo root — tsup --watch, rebuilds dist/\n```\n\nThe playground's Vite config uses the `watchDesignSystem()` plugin (exported from\n`@5voltfx/design-system/vite-plugin`) to watch `dist/` and force a full reload.\nWithout both halves running, source edits will not appear.\n\nOther scripts:\n\n```bash\nnpm run build      # tsup + tsc — bundles dist/ and emits the .d.ts files\nnpm run typecheck  # tsc, no emit\nnpm test           # vitest\n```\n\n**`npm run dev` does not regenerate type declarations.** It runs `tsup` only;\nthe `.d.ts` files come from the `tsc` half of `npm run build`. Watch mode leaves\nthe existing declarations in place rather than deleting them, so editor\nautocomplete keeps working — but it goes stale the moment you change a\ncomponent's props. Run `npm run build` before publishing, or whenever you want\ntypes that match the source.\n\n### Blank page? Clear the Vite cache\n\n```bash\nrm -rf playground/node_modules/.vite && cd playground && npm run dev\n```\n\nVite pre-bundles dependencies into `node_modules/.vite/deps` and stamps every\ndep URL with a `browserHash` recorded in `deps/_metadata.json`. That cache is\nkeyed off the lockfile and config — **not** off the contents of a `file:`-linked\npackage. So when `dist/` gains a new export, the cache doesn't notice, the\nbrowser links against the old module, and you get a blank page with one console\nerror naming the missing export:\n\n```\nSyntaxError: The requested module '/node_modules/@5voltfx/design-system/dist/index.js?v=<hash>'\ndoes not provide an export named 'Badge'\n```\n\nMisleading, because the export is there — `grep 'Badge' dist/index.js` finds it,\nand so does curling the URL. Only the browser's module graph is behind. A hard\nreload does not clear it; deleting `.vite` does.\n\nExpect this in the consuming sites too, for the same reason. Suspect it whenever\na component that exists everywhere on disk is \"not exported.\"\n\n---\n\n## Installing in a site\n\n```bash\nnpm install @5voltfx/design-system\n```\n\n```jsx\nimport \"@5voltfx/design-system/styles.css\";   // tokens + all component CSS\nimport { ThemeProvider, Header, Button } from \"@5voltfx/design-system\";\n```\n\n`ThemeProvider` must wrap the app — it sets `data-theme` on `<html>`, which is\nwhat every token resolves against. Components that link (`Header`, `Nav`,\n`SlideRuleNav`, `DialNav` with `to`) need a `react-router-dom` Router above them.\n\n**Fonts are not injected.** The package deliberately ships no font-loading side\neffects. Add the `<link>` yourself:\n\n```html\n<!-- Space Grotesk — the body face for the six painted themes -->\n<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap\">\n\n<!-- Only if you use the instrument themes -->\n<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=Archivo+Narrow:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Source+Serif+4:ital,wght@0,400;0,600;1,400;1,600&display=swap\">\n```\n\nPeer dependencies: `react` ^19, `react-dom` ^19, `react-router-dom` ^7.\n\n---\n\n## Vocabulary\n\nWords this system uses in a specific way. Using them precisely is the fastest way\nto be understood — by a person or by Claude.\n\n**Plate** — a documented specimen. A `Card` with a head (number, name, kind), a\nstage (the thing itself), and a foot (specs, caption). Rendered as a `<figure>` so\nthe specification is bound to the specimen, not floating after it.\n\n**Ground** — the ruled desk a plate sits on. Painted from `--ds-color-ground-grid`,\nwhich is zero-alpha in every theme that doesn't want one. Lands on `html`/`body`\nautomatically, or on any `.ds-ground` container.\n\n**Finish** — the shape and shadow a theme is machined to: corner radius, shadow,\nregistration marks. The instrument themes override it to hard 3px corners and a\nhard offset shadow. Palette alone can't get there — a 16px-rounded card under a\nblurred shadow reads as soft whatever colors are in it.\n\n**Rule** — the component tokens for the two slide-rule navigations (`--ds-rule-*`).\n\n**The three voices** — a drafting document doesn't set everything in one face:\n\n| Token | Role |\n|---|---|\n| `--ds-font-display` | **names** things — headings, control labels |\n| `--ds-font-mono` | **reads** values — specs, readouts, numerals |\n| `--ds-font-note` | **explains** — body prose, captions |\n\nAll three default to `--ds-font-body`, so the six painted themes pay nothing for\nthem. The instrument themes opt in: Archivo Narrow / JetBrains Mono / Source Serif 4.\n\n**The accent's two jobs** — `--ds-color-bg-2` is the accent as a **fill** (something\nmust be readable *on* it, via `--ds-color-on-accent`); `--ds-color-font-2` is the\naccent as **text** (it must be readable *on* the surface behind it). These pull in\nopposite directions. Never substitute one for the other — say which you mean.\n\n**Station** — one selectable position on `SlideRuleNav` or `DialNav`. Between 2 and\n8; past 8 the extras aren't rendered and the nav warns in dev.\n\n**Detent** — the notched travel between stations, at quarter-tick intervals.\n\n---\n\n## Tokens\n\nAll tokens are CSS custom properties prefixed `--ds-`, defined in `src/tokens/`\nand imported in that order by `src/tokens/index.css`.\n\n### Color\n\nEvery theme overrides the full set inside its own `[data-theme=\"…\"]` block. The\n`:root` values are the default (coral) theme, so components render sensibly before\n`ThemeProvider` runs.\n\n| Family | Meaning |\n|---|---|\n| `--ds-color-bg-0 … bg-4` | Surfaces. `bg-0` is the page ground, `bg-2` is the **accent as fill** |\n| `--ds-color-font-0 … font-4` | Text. `font-0` is primary copy, `font-1` muted, `font-2` the **accent as text** |\n| `--ds-color-bw-0 … bw-4` | Neutral ramp — borders, rules, dividers |\n| `--ds-color-on-accent` | Text that sits on `bg-2`. Chosen per theme, not pinned to `font-0` |\n| `--ds-color-url`, `--ds-color-visited` | Link states |\n| `--ds-color-highlight` | Selection/emphasis |\n| `--ds-color-ground-grid` | The ruled grid. Zero-alpha unless a theme opts in |\n\nStatus colors (`semantic.css`) are theme-agnostic: `--ds-color-danger`, `-success`,\n`-warning`, `-info`, each with a matching `--ds-color-on-*`. All four fills carry\nwhite. `instrument-day` darkens all four, because it's the one genuinely light\nsurface and they landed between 2.0:1 and 3.3:1 on it.\n\nForm chrome is single-sourced so every control matches: `--ds-color-field-bg`,\n`--ds-color-field-border`, `--ds-color-focus-ring`.\n\n### Typography\n\nModular scale, ~1.25 ratio, base 1rem:\n`--ds-font-size-100` (0.8rem) → `--ds-font-size-700` (3.052rem).\n\nWeights `--ds-font-weight-regular|medium|semibold|bold`. Line heights\n`--ds-line-height-tight|normal|relaxed`. Display tracking via\n`--ds-tracking-display` (`normal` by default; `0.06em` on the instrument themes,\nbecause condensed faces want air).\n\n### Spacing\n\n`--ds-space-1` (0.25rem) through `--ds-space-8` (4rem):\n0.25 / 0.5 / 0.75 / 1 / 1.5 / 2 / 3 / 4 rem.\n\n`Stack` and `Grid` take the bare number as their `gap` prop — `gap=\"5\"` resolves to\n`var(--ds-space-5)`.\n\n### Radius, elevation, motion, z-index\n\n| Group | Tokens |\n|---|---|\n| Radius | `--ds-radius-sm` 6px, `-md` 10px, `-lg` 16px, `-pill` 999px |\n| Elevation | `--ds-shadow-sm|md|lg` — tinted with the theme accent at low opacity, not flat black |\n| Motion | `--ds-motion-duration-fast` 120ms, `-base` 200ms, `-slow` 320ms; `--ds-motion-ease-standard` |\n| Z-index | `--ds-z-base` 0, `-dropdown` 1000, `-sticky` 1100, `-overlay` 1200, `-modal` 1300, `-popover` 1400, `-toast` 1500 |\n\nThe instrument themes reduce radius to 2–3px and swap the diffuse shadow for a hard\noffset one. `--ds-radius-pill` is deliberately left alone — a caller asking for a\npill is asking for a specific geometry, not the theme's default corner.\n\n---\n\n## Themes\n\nEight, applied by `ThemeProvider` setting `data-theme` on `<html>`.\n\n| id | Character |\n|---|---|\n| `coral` | Creative warm — the default |\n| `ink` | Dark indigo |\n| `sage` | Muted olive |\n| `midnight` | High contrast |\n| `amber` | Golden |\n| `harvest` | Earthy warm |\n| `instrument` | Drafting, dark stock |\n| `instrument-day` | Drafting, celluloid — the one genuinely light theme |\n\nThe first six are painted themes: palette only, soft finish. The two instrument\nthemes additionally take the three type voices, hard corners, an offset shadow,\ncorner registration marks, and the ruled ground.\n\nAll eight are measured at **zero WCAG AA failures** across tokens, primitives,\nplates, forms, overlays, and navigation. `src/tokens/themes.test.js` asserts this —\nif you add or move a color, that suite is the gate.\n\n```jsx\n<ThemeProvider storageKey=\"myapp.theme\" defaultTheme=\"instrument\">\n```\n\n| Prop | Default | Notes |\n|---|---|---|\n| `storageKey` | `\"ds.theme\"` | localStorage key; syncs across tabs |\n| `defaultTheme` | `\"coral\"` | |\n| `onThemeChange` | — | Called with the new theme id |\n\n`useTheme()` returns `{ theme, setTheme, themes }` and throws outside a provider.\n`THEMES`, `THEME_IDS`, and `DEFAULT_THEME` are exported for building your own picker.\n\n---\n\n## Components\n\nEverything imports from the package root. All components take `className` and\nforward unknown props to their root element. Many take `as` to change the tag.\n\n### Primitives\n\n| Component | Key props |\n|---|---|\n| `Button` | `variant` `primary`\\|`secondary`\\|`ghost` · `size` `sm`\\|`md`\\|`lg` · `shape` `sharp`\\|`soft`\\|`rounded`\\|`pill` · `icon` · `as` |\n| `Card` | `as` |\n| `Badge` | `variant` `solid`\\|`outline`\\|`accent`\\|`brass`\\|`good`\\|`warn`\\|`quiet` · `as` |\n| `Heading` | `level` 1–6 · `size` 200–700 (defaults from level) · `decorative` |\n| `Text` | `as` · `size` `100`\\|`200`\\|`300` (default `200`) · `tone` `default`\\|`muted` |\n\n`Heading` is set in the display voice and `Text` in the note voice, so the\ninstrument themes get the type split for free without any component change.\n\n`Button` is flat and 2D on purpose — no elevation, no hover lift. Hover and press\nare communicated with color only. Its `icon` wrapper is `aria-hidden`, so the label\ntext must carry the meaning.\n\n### Layout\n\n| Component | Key props |\n|---|---|\n| `Container` | `as` — centered 1200px max-width, responsive gutters |\n| `Stack` | `gap` (space step, default `\"4\"`) · `direction` (default `column`) · `align` · `as` |\n| `Grid` | `columns` (default 2) · `gap` (default `\"4\"`) · `as` |\n\n### Plate anatomy\n\nThe system's distinctive layer — a documented specimen and the parts that describe it.\n\n```jsx\n<Plate\n  number=\"L-I\"\n  name=\"Cursor rule\"\n  kind=\"Top nav\"\n  specs={[[\"Stations\", \"5\"], [\"Travel\", \"Detented\"]]}\n  caption=\"A fixed scale with a cursor that travels over it.\"\n>\n  <SlideRuleNav variant=\"cursor\" items={items} />\n</Plate>\n```\n\n| Component | Key props |\n|---|---|\n| `Plate` | `number` · `name` · `kind` · `specs` (array of `[key, value]`) · `caption` · `as` (default `figure`) |\n| `Specs` | `items` — array of `[key, value]`; renders a `<dl>` |\n| `Caption` | `as` (default `p`) — the explaining voice, in prose |\n| `Readout` | `fields` `[{label, value, kind}]` · `status` `{text, tone: \"lock\"\\|\"drift\"}` |\n| `Chapter` | `as` (default `h2`) — a section rule with a label set into it |\n\nEvery part of `Plate` is optional. With none of them it *is* a `Card`, which is what\nit's built on — so it inherits the theme's finish, registration marks included.\n\n`Readout` is presentational and announces nothing. A component whose value changes\nduring a drag owns its own live region, so only committed changes are announced.\n\n### Chrome\n\n| Component | Key props |\n|---|---|\n| `Header` | `brand` · `brandHref` (default `/`) · `links` · `actions` |\n| `Nav` | `links` — `[{ to, label, end }]`, active state via `NavLink` |\n| `Footer` | `brand` · `showThemeSwitcher` (default true) · `left` · `right` |\n| `ThemeSwitcher` | — swatch row, one per theme |\n\n`Header` and `Footer` are shells with slots. Each site's bespoke chrome —\nsong-chart's auth-aware menu, mattwelker.com's audio toggle and Venmo link,\n5voltfx.com's logo fallback — gets passed in rather than encoded here.\n\n### Forms\n\n| Component | Key props |\n|---|---|\n| `FormField` | `label` · `help` · `error` · `required` · `htmlFor` · single control as child |\n| `Label` | `required` |\n| `Input` | `size` `sm`\\|`md`\\|`lg` · `invalid` · all native `<input>` props |\n| `Textarea` | `rows` (default 4) · `invalid` |\n| `Select` | `size` · `invalid` · `<option>` children |\n| `Checkbox` | `label` |\n| `Radio` | `label` · `value` |\n| `RadioGroup` | `name` · `value` · `onChange` · `options` `[{value, label}]` or `Radio` children |\n| `Switch` | `label` |\n\n`FormField` does the accessibility wiring: generates an id, links the label via\n`htmlFor`, connects `help`/`error` through `aria-describedby`, and sets\n`aria-invalid` plus the control's `invalid` styling when `error` is present. Prefer\nit over hand-wiring.\n\n```jsx\n<FormField label=\"Email\" help=\"We never share it.\" error={err} required>\n  <Input type=\"email\" value={v} onChange={onChange} />\n</FormField>\n```\n\nEvery control is a styled native element — `Checkbox`, `Radio`, and `Switch` put a\nstyled indicator over a real `<input>`, and `Select` stays a real `<select>` — so\nkeyboard, focus, and toggle behavior are native and free.\n\n### Overlays\n\n| Component | Key props |\n|---|---|\n| `Modal` | `open` · `onClose` · `title` · `size` `sm`\\|`md`\\|`lg` |\n| `Menu` | `trigger` · `items` `[{label, onSelect, disabled}]` · `align` `start`\\|`end` |\n| `Tabs` | `items` `[{id, label, content, disabled}]` · `value`/`defaultValue` · `onChange` |\n| `Accordion` | `items` `[{id, title, content}]` · `multiple` · `defaultOpen` |\n\n`Modal` portals to `document.body` and handles focus trap, Escape, backdrop click,\nscroll lock, and focus restore. `Menu` uses roving focus with arrow keys and closes\non outside click or Escape. `Tabs` is uncontrolled unless you pass `value`; arrows\nmove selection, Home/End jump to the ends.\n\n### Slide-rule navigation\n\nThe signature pieces — navigation drawn as instrument mechanisms, with detented\ntravel between stations.\n\n```jsx\n<SlideRuleNav variant=\"vernier\" items={items} readout label=\"Main\" />\n<DialNav variant=\"volvelle\" items={items} label=\"Sections\" />\n```\n\n| Component | Variants | Key props |\n|---|---|---|\n| `SlideRuleNav` | `cursor` · `slide` · `vernier` (default) | `items` · `value`/`defaultValue` · `onChange` · `readout` · `label` |\n| `DialNav` | `volvelle` (default) · `concentric` · `fan` | `items` · `value`/`defaultValue` · `onChange` · `expanded`/`defaultExpanded` · `onExpandedChange` · `label` |\n\nItems are `{ id, label, to?, end?, onSelect?, value? }`. With `to` they render\n`NavLink`s and need a Router; without, they call `onSelect`.\n\n`SlideRuleNav` is the linear form, meant for a top nav. `cursor` and `vernier` hold\na fixed scale and travel a cursor over it; `slide` inverts that — the index is\nfixed and the slide itself travels, carrying its stations with it.\n\n`DialNav` is the circular form, meant for a sidebar that extends on hover. `volvelle`\nspins its rail under a fixed index; `concentric` is two-level; `fan` holds its\nspokes still and sweeps a pawl, righting only the indexed label.\n\nBoth clamp to **2–8 stations** and respect `prefers-reduced-motion`. An unknown\n`variant` falls back to the default and warns in dev rather than throwing.\n\n---\n\n## Conventions\n\n**Class names** are BEM-ish and `ds-`-prefixed: `ds-button`, `ds-button--primary`,\n`ds-button__icon`. Block, `--modifier`, `__element`.\n\n**Every component takes `className`** and merges it after its own classes, so a\nconsuming site can always override.\n\n**Polymorphism via `as`** where the tag is a legitimate choice (`Button as=\"a\"`,\n`Chapter as=\"div\"`). Not everywhere — `Plate`'s foot switches between `figcaption`\nand `div` based on `as`, because `figcaption` is only valid inside a `figure`.\n\n**Structure over decoration.** `Specs` is a `<dl>` because each key names a property\nand each value gives its measurement. `Plate` is a `<figure>`. `Chapter` is a\nheading by default, because a document divider that isn't in the outline is a\ndecoration.\n\n**No side effects beyond CSS.** No font loading, no global listeners at import time.\n`sideEffects` is declared as `**/*.css` only, so tree-shaking works.\n\n**Contrast is a gate, not a preference.** All eight themes are at zero AA failures\nand `themes.test.js` enforces it.\n\n---\n\n## Layout of the repo\n\n```\nsrc/\n  index.js              # the public surface — every export, grouped\n  tokens/               # CSS custom properties, imported in order by index.css\n    colors themes base ground typography spacing radius\n    elevation motion zindex semantic rule finish\n  theme/                # ThemeProvider, useTheme, themeConstants\n  components/<Name>/    # Name.jsx, Name.css, index.js, Name.test.jsx\n    _shared/            # useDetentTravel, useReducedMotion, validateStationCount\nplayground/             # the live style guide (see \"Seeing it\")\nvite-plugin.js          # watchDesignSystem() for local cross-repo HMR\n```\n\nComponent tokens live with their components' concern — the slide-rule navs read\n`--ds-rule-*` from `tokens/rule.css`, whose defaults are expressions over the\nbg/font/bw families, so both navs re-skin under every theme with no per-theme work.\n\n---\n\n## Releasing\n\n`npm publish` requires a manual version bump in `package.json` first — there is no\nversion script. `prepublishOnly` runs the build.\n","readmeFilename":"README.md","description":"A shared React component library and token system for the three 5VoltFX/Matt Welker sites — `song-chart`, `mattwelker.com`, and `5voltfx.com`. Plain React 19 and plain CSS custom properties, no UI framework."}