@robomous/ui-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +38 -0
  3. package/components.json +13 -0
  4. package/dist/index.d.ts +44 -0
  5. package/dist/index.js +47 -0
  6. package/dist/lib/button.d.ts +1 -0
  7. package/dist/lib/button.js +4 -0
  8. package/dist/lib/cn.d.ts +18 -0
  9. package/dist/lib/cn.js +21 -0
  10. package/dist/lib/menu.d.ts +1 -0
  11. package/dist/lib/menu.js +16 -0
  12. package/dist/lib/progress.d.ts +3 -0
  13. package/dist/lib/progress.js +6 -0
  14. package/dist/lib/select.d.ts +1 -0
  15. package/dist/lib/select.js +3 -0
  16. package/dist/primitives/alert.d.ts +10 -0
  17. package/dist/primitives/alert.js +27 -0
  18. package/dist/primitives/badge.d.ts +9 -0
  19. package/dist/primitives/badge.js +28 -0
  20. package/dist/primitives/button.d.ts +10 -0
  21. package/dist/primitives/button.js +35 -0
  22. package/dist/primitives/card.d.ts +11 -0
  23. package/dist/primitives/card.js +24 -0
  24. package/dist/primitives/combobox.d.ts +24 -0
  25. package/dist/primitives/combobox.js +58 -0
  26. package/dist/primitives/dialog.d.ts +17 -0
  27. package/dist/primitives/dialog.js +36 -0
  28. package/dist/primitives/dropdown-menu.d.ts +29 -0
  29. package/dist/primitives/dropdown-menu.js +50 -0
  30. package/dist/primitives/field.d.ts +24 -0
  31. package/dist/primitives/field.js +67 -0
  32. package/dist/primitives/input-group.d.ts +16 -0
  33. package/dist/primitives/input-group.js +56 -0
  34. package/dist/primitives/input.d.ts +3 -0
  35. package/dist/primitives/input.js +6 -0
  36. package/dist/primitives/label.d.ts +4 -0
  37. package/dist/primitives/label.js +7 -0
  38. package/dist/primitives/progress.d.ts +4 -0
  39. package/dist/primitives/progress.js +7 -0
  40. package/dist/primitives/select.d.ts +15 -0
  41. package/dist/primitives/select.js +35 -0
  42. package/dist/primitives/separator.d.ts +4 -0
  43. package/dist/primitives/separator.js +8 -0
  44. package/dist/primitives/sheet.d.ts +14 -0
  45. package/dist/primitives/sheet.js +36 -0
  46. package/dist/primitives/skeleton.d.ts +2 -0
  47. package/dist/primitives/skeleton.js +6 -0
  48. package/dist/primitives/sonner.d.ts +4 -0
  49. package/dist/primitives/sonner.js +40 -0
  50. package/dist/primitives/table.d.ts +10 -0
  51. package/dist/primitives/table.js +27 -0
  52. package/dist/primitives/tabs.d.ts +11 -0
  53. package/dist/primitives/tabs.js +28 -0
  54. package/dist/primitives/textarea.d.ts +3 -0
  55. package/dist/primitives/textarea.js +6 -0
  56. package/dist/primitives/tooltip.d.ts +7 -0
  57. package/dist/primitives/tooltip.js +17 -0
  58. package/dist/statusTone.d.ts +37 -0
  59. package/dist/statusTone.js +38 -0
  60. package/dist/tokens.d.ts +26 -0
  61. package/dist/tokens.js +96 -0
  62. package/gates/canonical.test.mjs +71 -0
  63. package/gates/extensions.test.mjs +373 -0
  64. package/gates/index.d.mts +51 -0
  65. package/gates/index.mjs +514 -0
  66. package/gates/tokens.test.mjs +276 -0
  67. package/package.json +60 -0
  68. package/shadcn/README.md +13 -0
  69. package/shadcn/alert.tsx +76 -0
  70. package/shadcn/badge.tsx +49 -0
  71. package/shadcn/button.tsx +67 -0
  72. package/shadcn/card.tsx +103 -0
  73. package/shadcn/combobox.tsx +299 -0
  74. package/shadcn/dialog.tsx +166 -0
  75. package/shadcn/dropdown-menu.tsx +267 -0
  76. package/shadcn/field.tsx +236 -0
  77. package/shadcn/input-group.tsx +154 -0
  78. package/shadcn/input.tsx +19 -0
  79. package/shadcn/label.tsx +22 -0
  80. package/shadcn/progress.tsx +29 -0
  81. package/shadcn/select.tsx +190 -0
  82. package/shadcn/separator.tsx +28 -0
  83. package/shadcn/sheet.tsx +145 -0
  84. package/shadcn/skeleton.tsx +13 -0
  85. package/shadcn/sonner.tsx +49 -0
  86. package/shadcn/table.tsx +114 -0
  87. package/shadcn/tabs.tsx +88 -0
  88. package/shadcn/textarea.tsx +18 -0
  89. package/shadcn/tooltip.tsx +57 -0
  90. package/src/harness.test.tsx +37 -0
  91. package/src/index.ts +119 -0
  92. package/src/lib/button.ts +4 -0
  93. package/src/lib/cn.test.ts +33 -0
  94. package/src/lib/cn.ts +23 -0
  95. package/src/lib/menu.ts +16 -0
  96. package/src/lib/progress.ts +6 -0
  97. package/src/lib/select.ts +4 -0
  98. package/src/primitives/alert.tsx +76 -0
  99. package/src/primitives/badge.tsx +57 -0
  100. package/src/primitives/button.tsx +67 -0
  101. package/src/primitives/card.tsx +103 -0
  102. package/src/primitives/combobox.test.tsx +38 -0
  103. package/src/primitives/combobox.tsx +299 -0
  104. package/src/primitives/dialog.tsx +166 -0
  105. package/src/primitives/dropdown-menu.tsx +267 -0
  106. package/src/primitives/field.tsx +236 -0
  107. package/src/primitives/input-group.tsx +154 -0
  108. package/src/primitives/input.tsx +19 -0
  109. package/src/primitives/label.tsx +22 -0
  110. package/src/primitives/primitives.test.tsx +440 -0
  111. package/src/primitives/progress.tsx +29 -0
  112. package/src/primitives/select.tsx +190 -0
  113. package/src/primitives/separator.tsx +28 -0
  114. package/src/primitives/sheet.tsx +145 -0
  115. package/src/primitives/skeleton.tsx +13 -0
  116. package/src/primitives/sonner.test.tsx +41 -0
  117. package/src/primitives/sonner.tsx +65 -0
  118. package/src/primitives/table.tsx +114 -0
  119. package/src/primitives/tabs.tsx +88 -0
  120. package/src/primitives/textarea.tsx +18 -0
  121. package/src/primitives/tooltip.tsx +57 -0
  122. package/src/statusTone.test.ts +20 -0
  123. package/src/statusTone.ts +52 -0
  124. package/src/styles.css +204 -0
  125. package/src/tokens.test.ts +285 -0
  126. package/src/tokens.ts +102 -0
package/src/styles.css ADDED
@@ -0,0 +1,204 @@
1
+ /*
2
+ * @robomous/ui-core — the design system's stylesheet.
3
+ *
4
+ * This is the shadcn preset `b2iH` (style `nova` on the Radix base, base colour
5
+ * `neutral`, chart palette `neutral`, icons `lucide`, Geist throughout with the
6
+ * heading face inheriting the body's, radius medium, menu inverted/subtle, and
7
+ * the pointer cursor on pressable controls) — the CLI's exact generated output,
8
+ * transcribed verbatim from a Docker scratch project built with `shadcn` 4.19.0. The preset is the source of truth for every
9
+ * name below except one: `brand` is Robomous's own, justified in
10
+ * `DESIGN.md` and following shadcn's own extension convention — a value
11
+ * in `:root`, a dark counterpart in `.dark`, exposure through `@theme
12
+ * inline`. Consumers add their extensions in their own stylesheet after
13
+ * importing this one. `src/tokens.ts` is the TypeScript mirror of this file
14
+ * and `tokens.test.ts` asserts the two agree, declaration for declaration.
15
+ *
16
+ * A consuming app imports exactly this:
17
+ *
18
+ * import "@robomous/ui-core/styles.css";
19
+ *
20
+ * and adds an `@source` for its own sources. Tailwind v4 is CSS-first: there
21
+ * is no `tailwind.config.js` anywhere in this repository and there must not
22
+ * be one, or the tokens acquire a second home.
23
+ */
24
+
25
+ @import "tailwindcss";
26
+ @import "tw-animate-css";
27
+ @import "shadcn/tailwind.css";
28
+ @import "@fontsource-variable/geist";
29
+
30
+ /*
31
+ * The package's own components, so a class string that only ever appears
32
+ * inside `ui-core` still produces CSS in the consumer's build. Relative to
33
+ * this file, which is what `@source` takes.
34
+ */
35
+ @source ".";
36
+
37
+ @custom-variant dark (&:is(.dark *));
38
+
39
+ :root {
40
+ --background: oklch(1 0 0);
41
+ --foreground: oklch(0.145 0 0);
42
+ --card: oklch(1 0 0);
43
+ --card-foreground: oklch(0.145 0 0);
44
+ --popover: oklch(1 0 0);
45
+ --popover-foreground: oklch(0.145 0 0);
46
+ --primary: oklch(0.205 0 0);
47
+ --primary-foreground: oklch(0.985 0 0);
48
+ --secondary: oklch(0.97 0 0);
49
+ --secondary-foreground: oklch(0.205 0 0);
50
+ --muted: oklch(0.97 0 0);
51
+ --muted-foreground: oklch(0.556 0 0);
52
+ --accent: oklch(0.97 0 0);
53
+ --accent-foreground: oklch(0.205 0 0);
54
+ --destructive: oklch(0.577 0.245 27.325);
55
+ --border: oklch(0.922 0 0);
56
+ --input: oklch(0.922 0 0);
57
+ --ring: oklch(0.708 0 0);
58
+ --chart-1: oklch(0.87 0 0);
59
+ --chart-2: oklch(0.556 0 0);
60
+ --chart-3: oklch(0.439 0 0);
61
+ --chart-4: oklch(0.371 0 0);
62
+ --chart-5: oklch(0.269 0 0);
63
+ --radius: 0.625rem;
64
+ --sidebar: oklch(0.985 0 0);
65
+ --sidebar-foreground: oklch(0.145 0 0);
66
+ --sidebar-primary: oklch(0.205 0 0);
67
+ --sidebar-primary-foreground: oklch(0.985 0 0);
68
+ --sidebar-accent: oklch(0.97 0 0);
69
+ --sidebar-accent-foreground: oklch(0.205 0 0);
70
+ --sidebar-border: oklch(0.922 0 0);
71
+ --sidebar-ring: oklch(0.708 0 0);
72
+
73
+ /*
74
+ * Robomous coral. Identity only — the wordmark and its styleguide swatch,
75
+ * named in `DESIGN.md` — never a functional-UI colour. A third site
76
+ * is a design decision, not a styling one.
77
+ */
78
+ --brand: oklch(0.653 0.178 32.3);
79
+ }
80
+
81
+ .dark {
82
+ --background: oklch(0.145 0 0);
83
+ --foreground: oklch(0.985 0 0);
84
+ --card: oklch(0.205 0 0);
85
+ --card-foreground: oklch(0.985 0 0);
86
+ --popover: oklch(0.205 0 0);
87
+ --popover-foreground: oklch(0.985 0 0);
88
+ --primary: oklch(0.922 0 0);
89
+ --primary-foreground: oklch(0.205 0 0);
90
+ --secondary: oklch(0.269 0 0);
91
+ --secondary-foreground: oklch(0.985 0 0);
92
+ --muted: oklch(0.269 0 0);
93
+ --muted-foreground: oklch(0.708 0 0);
94
+ --accent: oklch(0.269 0 0);
95
+ --accent-foreground: oklch(0.985 0 0);
96
+ --destructive: oklch(0.704 0.191 22.216);
97
+ --border: oklch(1 0 0 / 10%);
98
+ --input: oklch(1 0 0 / 15%);
99
+ --ring: oklch(0.556 0 0);
100
+ --chart-1: oklch(0.87 0 0);
101
+ --chart-2: oklch(0.556 0 0);
102
+ --chart-3: oklch(0.439 0 0);
103
+ --chart-4: oklch(0.371 0 0);
104
+ --chart-5: oklch(0.269 0 0);
105
+ --sidebar: oklch(0.205 0 0);
106
+ --sidebar-foreground: oklch(0.985 0 0);
107
+ --sidebar-primary: oklch(0.488 0.243 264.376);
108
+ --sidebar-primary-foreground: oklch(0.985 0 0);
109
+ --sidebar-accent: oklch(0.269 0 0);
110
+ --sidebar-accent-foreground: oklch(0.985 0 0);
111
+ --sidebar-border: oklch(1 0 0 / 10%);
112
+ --sidebar-ring: oklch(0.556 0 0);
113
+
114
+ --brand: oklch(0.653 0.178 32.3);
115
+ }
116
+
117
+ @theme inline {
118
+ --font-sans: 'Geist Variable', sans-serif;
119
+ --font-heading: var(--font-sans);
120
+ --color-sidebar-ring: var(--sidebar-ring);
121
+ --color-sidebar-border: var(--sidebar-border);
122
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
123
+ --color-sidebar-accent: var(--sidebar-accent);
124
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
125
+ --color-sidebar-primary: var(--sidebar-primary);
126
+ --color-sidebar-foreground: var(--sidebar-foreground);
127
+ --color-sidebar: var(--sidebar);
128
+ --color-chart-5: var(--chart-5);
129
+ --color-chart-4: var(--chart-4);
130
+ --color-chart-3: var(--chart-3);
131
+ --color-chart-2: var(--chart-2);
132
+ --color-chart-1: var(--chart-1);
133
+ --color-ring: var(--ring);
134
+ --color-input: var(--input);
135
+ --color-border: var(--border);
136
+ --color-destructive: var(--destructive);
137
+ --color-accent-foreground: var(--accent-foreground);
138
+ --color-accent: var(--accent);
139
+ --color-muted-foreground: var(--muted-foreground);
140
+ --color-muted: var(--muted);
141
+ --color-secondary-foreground: var(--secondary-foreground);
142
+ --color-secondary: var(--secondary);
143
+ --color-primary-foreground: var(--primary-foreground);
144
+ --color-primary: var(--primary);
145
+ --color-popover-foreground: var(--popover-foreground);
146
+ --color-popover: var(--popover);
147
+ --color-card-foreground: var(--card-foreground);
148
+ --color-card: var(--card);
149
+ --color-foreground: var(--foreground);
150
+ --color-background: var(--background);
151
+ --radius-sm: calc(var(--radius) * 0.6);
152
+ --radius-md: calc(var(--radius) * 0.8);
153
+ --radius-lg: var(--radius);
154
+ --radius-xl: calc(var(--radius) * 1.4);
155
+ --radius-2xl: calc(var(--radius) * 1.8);
156
+ --radius-3xl: calc(var(--radius) * 2.2);
157
+ --radius-4xl: calc(var(--radius) * 2.6);
158
+
159
+ /* Robomous's one identity token, exposed the same way as every preset colour. */
160
+ --color-brand: var(--brand);
161
+ }
162
+
163
+ @layer base {
164
+ /* The hairline and the outline *colour* — the two things every element
165
+ inherits. The focus treatment itself belongs to the primitives: each one
166
+ carries Nova's own `ring-3` ring in `ring/50` (DESIGN.md, Borders and
167
+ focus), so the base layer names the colour and stays out of the geometry.
168
+ Nothing here may declare focus geometry again: one blanket declaration in
169
+ this layer is how thirteen components' rings got overridden at once. */
170
+ * {
171
+ @apply border-border outline-ring/50;
172
+ }
173
+ /* Motion is opt-out for vestibular safety: under prefers-reduced-motion every
174
+ animation and transition collapses to a frame. Information is never
175
+ motion-only (DESIGN.md §Accessibility), so nothing is lost. */
176
+ @media (prefers-reduced-motion: reduce) {
177
+ *,
178
+ ::before,
179
+ ::after {
180
+ animation-duration: 0.01ms !important;
181
+ animation-iteration-count: 1 !important;
182
+ transition-duration: 0.01ms !important;
183
+ scroll-behavior: auto !important;
184
+ }
185
+ }
186
+ body {
187
+ @apply bg-background text-foreground;
188
+ }
189
+ /* The preset's pointer option. Scoped to what is actually pressable and away
190
+ from what is not: a disabled control keeps the arrow, because a hand over
191
+ something that will not respond is the cursor telling a lie. */
192
+ button:not(:disabled), [role="button"]:not(:disabled) {
193
+ cursor: pointer;
194
+ }
195
+ html {
196
+ @apply font-sans;
197
+ }
198
+ /* One family now: the preset sets `--font-heading` to `--font-sans`, so this
199
+ rule keeps the semantic hook without changing the face. A later preset that
200
+ splits them again lands here without a diff at every heading. */
201
+ h1, h2, h3, h4 {
202
+ @apply font-heading;
203
+ }
204
+ }
@@ -0,0 +1,285 @@
1
+ /**
2
+ * @vitest-environment node
3
+ *
4
+ * The foundation gate: `styles.css` is the shadcn preset's exact generated
5
+ * output (`:root`, `.dark`, `@theme inline`, the base layer) plus `brand`,
6
+ * the one Robomous extension. `tokens.ts` is the TypeScript mirror a
7
+ * `<canvas>`/`<svg>` or a test reads a colour off of; this suite parses the
8
+ * stylesheet structurally and asserts the two agree, declaration for
9
+ * declaration, and that none of the tokens this rewrite retired have crept
10
+ * back in.
11
+ *
12
+ * The CSS is parsed rather than imported — same reason as before: vitest's
13
+ * jsdom does not evaluate `@theme`, and `import.meta.url` under jsdom is an
14
+ * `http://localhost/` URL that `fileURLToPath` rejects. Hence `node` above.
15
+ */
16
+
17
+ import { readFileSync } from "node:fs";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ import { describe, expect, it } from "vitest";
21
+
22
+ import { cssVar, DARK_THEME, LIGHT_THEME, THEME } from "./tokens";
23
+
24
+ const STYLESHEET = readFileSync(fileURLToPath(new URL("./styles.css", import.meta.url)), "utf8");
25
+
26
+ /** Whitespace is presentation; a value that wraps is the same value. */
27
+ function normalize(value: string): string {
28
+ return value.replace(/\s+/g, " ").trim();
29
+ }
30
+
31
+ /**
32
+ * The `{ … }` body belonging to the block whose header (e.g. `:root {` or
33
+ * `.dark {`) appears first in the file, matched by brace depth rather than by
34
+ * the next `\n}` so a nested `calc(...)` or `color-mix(...)` cannot fool it.
35
+ */
36
+ function blockBody(css: string, header: string): string {
37
+ const headerAt = css.indexOf(header);
38
+ expect(headerAt, `styles.css has no ${JSON.stringify(header)} block`).toBeGreaterThan(-1);
39
+ const braceAt = css.indexOf("{", headerAt);
40
+ let depth = 1;
41
+ let i = braceAt + 1;
42
+ while (depth > 0 && i < css.length) {
43
+ if (css[i] === "{") depth++;
44
+ else if (css[i] === "}") depth--;
45
+ i++;
46
+ }
47
+ return css.slice(braceAt + 1, i - 1);
48
+ }
49
+
50
+ /** Every `--name: value;` declaration in a block, keyed WITH the leading `--`. */
51
+ function rawDeclarations(block: string): Map<string, string> {
52
+ const map = new Map<string, string>();
53
+ for (const match of block.matchAll(/(--[\w-]+):\s*([^;]+);/g)) {
54
+ map.set(match[1], normalize(match[2]));
55
+ }
56
+ return map;
57
+ }
58
+
59
+ /** The same, keyed WITHOUT the leading `--` — the shape `LIGHT_THEME` uses. */
60
+ function declarations(block: string): Map<string, string> {
61
+ const map = new Map<string, string>();
62
+ for (const [name, value] of rawDeclarations(block)) {
63
+ map.set(name.slice(2), value);
64
+ }
65
+ return map;
66
+ }
67
+
68
+ // The shadcn standard vocabulary, exactly as the CLI 4.18.0 scratch emitted
69
+ // it — not derived from `tokens.ts`, so a mistake in the mirror cannot also
70
+ // erase the thing it was supposed to mirror.
71
+ const CHART_NAMES = ["chart-1", "chart-2", "chart-3", "chart-4", "chart-5"];
72
+ const SIDEBAR_NAMES = [
73
+ "sidebar",
74
+ "sidebar-foreground",
75
+ "sidebar-primary",
76
+ "sidebar-primary-foreground",
77
+ "sidebar-accent",
78
+ "sidebar-accent-foreground",
79
+ "sidebar-border",
80
+ "sidebar-ring",
81
+ ];
82
+ const BASE_SEMANTIC_NAMES = [
83
+ "background",
84
+ "foreground",
85
+ "card",
86
+ "card-foreground",
87
+ "popover",
88
+ "popover-foreground",
89
+ "primary",
90
+ "primary-foreground",
91
+ "secondary",
92
+ "secondary-foreground",
93
+ "muted",
94
+ "muted-foreground",
95
+ "accent",
96
+ "accent-foreground",
97
+ "destructive",
98
+ "border",
99
+ "input",
100
+ "ring",
101
+ ];
102
+ const SEMANTIC_NAMES = [...BASE_SEMANTIC_NAMES, ...CHART_NAMES, ...SIDEBAR_NAMES];
103
+
104
+ // The one extension this package keeps beyond shadcn's vocabulary.
105
+ const EXTENSION_NAMES = ["brand"];
106
+
107
+ const ORANGE_CHART = {
108
+ "chart-1": "oklch(0.87 0 0)",
109
+ "chart-2": "oklch(0.556 0 0)",
110
+ "chart-3": "oklch(0.439 0 0)",
111
+ "chart-4": "oklch(0.371 0 0)",
112
+ "chart-5": "oklch(0.269 0 0)",
113
+ } as const;
114
+
115
+ describe(":root", () => {
116
+ const root = declarations(blockBody(STYLESHEET, ":root {"));
117
+
118
+ it("declares every standard shadcn semantic variable, the five extensions, and --radius, and nothing else", () => {
119
+ expect([...root.keys()].sort()).toEqual(
120
+ [...SEMANTIC_NAMES, ...EXTENSION_NAMES, "radius"].sort(),
121
+ );
122
+ });
123
+
124
+ it("matches LIGHT_THEME's value for every name it declares", () => {
125
+ for (const [name, value] of root) {
126
+ if (name === "radius") continue;
127
+ expect(LIGHT_THEME[name], `LIGHT_THEME is missing ${name}`).toBe(value);
128
+ }
129
+ });
130
+
131
+ it("names no variable LIGHT_THEME does not also carry", () => {
132
+ const lightKeys = Object.keys(LIGHT_THEME).sort();
133
+ const rootKeys = [...root.keys()].filter((name) => name !== "radius").sort();
134
+ expect(lightKeys).toEqual(rootKeys);
135
+ });
136
+
137
+ it("pins --radius to the preset's medium step, in both the CSS and THEME", () => {
138
+ expect(root.get("radius")).toBe("0.625rem");
139
+ expect(THEME.radius).toBe("0.625rem");
140
+ });
141
+
142
+ it("pins the neutral chart palette exactly", () => {
143
+ for (const [name, value] of Object.entries(ORANGE_CHART)) {
144
+ expect(root.get(name)).toBe(value);
145
+ }
146
+ });
147
+ });
148
+
149
+ describe(".dark", () => {
150
+ const dark = declarations(blockBody(STYLESHEET, ".dark {"));
151
+
152
+ it("declares every standard shadcn semantic variable and the five extensions, and nothing else (no --radius)", () => {
153
+ expect([...dark.keys()].sort()).toEqual([...SEMANTIC_NAMES, ...EXTENSION_NAMES].sort());
154
+ });
155
+
156
+ it("matches DARK_THEME's value for every name it declares", () => {
157
+ for (const [name, value] of dark) {
158
+ expect(DARK_THEME[name], `DARK_THEME is missing ${name}`).toBe(value);
159
+ }
160
+ });
161
+
162
+ it("names no variable DARK_THEME does not also carry", () => {
163
+ expect(Object.keys(DARK_THEME).sort()).toEqual([...dark.keys()].sort());
164
+ });
165
+
166
+ it("pins the neutral chart palette exactly, unchanged from light", () => {
167
+ for (const [name, value] of Object.entries(ORANGE_CHART)) {
168
+ expect(dark.get(name)).toBe(value);
169
+ }
170
+ });
171
+ });
172
+
173
+ describe("@theme inline", () => {
174
+ const inline = rawDeclarations(blockBody(STYLESHEET, "@theme inline {"));
175
+
176
+ it("exposes --color-<name>: var(--<name>) for every semantic and extension name", () => {
177
+ for (const name of [...SEMANTIC_NAMES, ...EXTENSION_NAMES]) {
178
+ expect(inline.get(`--color-${name}`), `@theme inline is missing --color-${name}`).toBe(
179
+ `var(--${name})`,
180
+ );
181
+ }
182
+ });
183
+
184
+ it("declares the two font variables from THEME", () => {
185
+ expect(inline.get("--font-sans")).toBe(normalize(THEME.fontSans));
186
+ expect(inline.get("--font-heading")).toBe(normalize(THEME.fontHeading));
187
+ });
188
+
189
+ it("derives all seven radius steps from --radius, verbatim", () => {
190
+ expect(inline.get("--radius-sm")).toBe("calc(var(--radius) * 0.6)");
191
+ expect(inline.get("--radius-md")).toBe("calc(var(--radius) * 0.8)");
192
+ expect(inline.get("--radius-lg")).toBe("var(--radius)");
193
+ expect(inline.get("--radius-xl")).toBe("calc(var(--radius) * 1.4)");
194
+ expect(inline.get("--radius-2xl")).toBe("calc(var(--radius) * 1.8)");
195
+ expect(inline.get("--radius-3xl")).toBe("calc(var(--radius) * 2.2)");
196
+ expect(inline.get("--radius-4xl")).toBe("calc(var(--radius) * 2.6)");
197
+ });
198
+ });
199
+
200
+ /**
201
+ * The four `--text-*` custom properties this rewrite retired (Task 2 moved
202
+ * their consumers onto Tailwind's standard scale: xs/sm/base/2xl). Assembled
203
+ * from fragments — the same trick `tests/scripts/design_tokens.test.mjs`'s
204
+ * `HEX` uses — so this guard's own source never spells any retired name as a
205
+ * contiguous string and cannot trip the repo-wide sweep that proves the
206
+ * migration complete everywhere else.
207
+ */
208
+ const RETIRED_TEXT_SCALE_SUFFIXES = ["meta", "body", "section", "page"];
209
+ const RETIRED_TEXT_SCALE = RETIRED_TEXT_SCALE_SUFFIXES.map((name) => `--text-${name}`);
210
+
211
+ describe("legacy tokens", () => {
212
+ it("removes every v1 name this rewrite retired from the whole stylesheet", () => {
213
+ const retired = [
214
+ "--color-primary-hover",
215
+ "--color-disabled",
216
+ "--color-disabled-foreground",
217
+ "--color-success-hover",
218
+ "--color-destructive-foreground",
219
+ "--color-sidebar-strong",
220
+ "--color-sidebar-muted",
221
+ ...RETIRED_TEXT_SCALE,
222
+ "--spacing-sidebar-mobile",
223
+ ];
224
+ const present = retired.filter((name) => STYLESHEET.includes(name));
225
+ expect(present, `styles.css still names: ${present.join(", ")}`).toEqual([]);
226
+ });
227
+ });
228
+
229
+ describe("structure", () => {
230
+ it("declares the dark variant and the four imports, in order", () => {
231
+ const anchors = [
232
+ '@import "tailwindcss";',
233
+ '@import "tw-animate-css";',
234
+ '@import "shadcn/tailwind.css";',
235
+ // One family, so one font import: `b2iH` sets the heading face to the body's
236
+ // rather than naming a second one.
237
+ '@import "@fontsource-variable/geist";',
238
+ "@custom-variant dark (&:is(.dark *));",
239
+ ];
240
+ let cursor = -1;
241
+ for (const anchor of anchors) {
242
+ const at = STYLESHEET.indexOf(anchor);
243
+ expect(at, `styles.css is missing ${JSON.stringify(anchor)}`).toBeGreaterThan(-1);
244
+ expect(at, `${JSON.stringify(anchor)} is out of order`).toBeGreaterThan(cursor);
245
+ cursor = at;
246
+ }
247
+ });
248
+
249
+ it("reaches its own package's classes: @source after the imports", () => {
250
+ const sourceAt = STYLESHEET.indexOf('@source ".";');
251
+ const lastImportAt = STYLESHEET.lastIndexOf("@import");
252
+ expect(sourceAt, "styles.css has no @source \".\";").toBeGreaterThan(-1);
253
+ expect(sourceAt).toBeGreaterThan(lastImportAt);
254
+ });
255
+
256
+ it("applies the base-layer ring/border rule shadcn ships", () => {
257
+ expect(/\*\s*\{\s*@apply border-border outline-ring\/50;\s*\}/.test(STYLESHEET)).toBe(true);
258
+ });
259
+
260
+ /**
261
+ * The `*` rule above names the outline *colour* and nothing else about focus.
262
+ * The geometry is the primitives': every focusable one carries Nova's
263
+ * `focus-visible:ring-3 focus-visible:ring-ring/50` (the tab bar adds a 1px
264
+ * `outline-ring` on top of it), so a stylesheet-level `:focus-visible`
265
+ * override would now fight the components instead of backing them up.
266
+ *
267
+ * Asserted as an absence, because the absence is what the migration bought:
268
+ * re-adding a blanket rule here is how a single global declaration would
269
+ * quietly start overriding thirteen components again.
270
+ */
271
+ it("leaves focus geometry to the primitives: no stylesheet-level :focus-visible rule", () => {
272
+ expect(STYLESHEET).not.toContain(":focus-visible");
273
+ });
274
+
275
+ it("applies the heading font at the semantic-HTML level", () => {
276
+ expect(/h1,\s*h2,\s*h3,\s*h4\s*\{\s*@apply font-heading;\s*\}/.test(STYLESHEET)).toBe(true);
277
+ });
278
+ });
279
+
280
+ describe("cssVar", () => {
281
+ it("wraps a bare token name as a CSS var() reference", () => {
282
+ expect(cssVar("popover")).toBe("var(--popover)");
283
+ expect(cssVar("brand")).toBe("var(--brand)");
284
+ });
285
+ });
package/src/tokens.ts ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The design tokens, as TypeScript.
3
+ *
4
+ * `styles.css` is the one that *runs* — Tailwind reads its `:root`, `.dark`
5
+ * and `@theme inline` blocks and every utility in the package comes out of
6
+ * them. This module exists for the two kinds of caller that cannot read CSS:
7
+ * a `<canvas>`/`<svg>` that needs a colour as a string, and `tokens.test.ts`,
8
+ * which parses the stylesheet and asserts the two agree, declaration for
9
+ * declaration.
10
+ *
11
+ * `LIGHT_THEME`/`DARK_THEME` are the shadcn preset (`b2iH` — style
12
+ * `nova`, base colour `neutral`, chart palette `neutral`) exactly as the CLI
13
+ * 4.19.0 scratch generated it, plus `brand` (Robomous coral — identity only).
14
+ * Consumer extensions such as VisionSet's `stage` and `origin-*` live in the
15
+ * consumer's own token module. Everything else is shadcn's own vocabulary.
16
+ */
17
+
18
+ export const LIGHT_THEME: Readonly<Record<string, string>> = Object.freeze({
19
+ background: "oklch(1 0 0)",
20
+ foreground: "oklch(0.145 0 0)",
21
+ card: "oklch(1 0 0)",
22
+ "card-foreground": "oklch(0.145 0 0)",
23
+ popover: "oklch(1 0 0)",
24
+ "popover-foreground": "oklch(0.145 0 0)",
25
+ primary: "oklch(0.205 0 0)",
26
+ "primary-foreground": "oklch(0.985 0 0)",
27
+ secondary: "oklch(0.97 0 0)",
28
+ "secondary-foreground": "oklch(0.205 0 0)",
29
+ muted: "oklch(0.97 0 0)",
30
+ "muted-foreground": "oklch(0.556 0 0)",
31
+ accent: "oklch(0.97 0 0)",
32
+ "accent-foreground": "oklch(0.205 0 0)",
33
+ destructive: "oklch(0.577 0.245 27.325)",
34
+ border: "oklch(0.922 0 0)",
35
+ input: "oklch(0.922 0 0)",
36
+ ring: "oklch(0.708 0 0)",
37
+ "chart-1": "oklch(0.87 0 0)",
38
+ "chart-2": "oklch(0.556 0 0)",
39
+ "chart-3": "oklch(0.439 0 0)",
40
+ "chart-4": "oklch(0.371 0 0)",
41
+ "chart-5": "oklch(0.269 0 0)",
42
+ sidebar: "oklch(0.985 0 0)",
43
+ "sidebar-foreground": "oklch(0.145 0 0)",
44
+ "sidebar-primary": "oklch(0.205 0 0)",
45
+ "sidebar-primary-foreground": "oklch(0.985 0 0)",
46
+ "sidebar-accent": "oklch(0.97 0 0)",
47
+ "sidebar-accent-foreground": "oklch(0.205 0 0)",
48
+ "sidebar-border": "oklch(0.922 0 0)",
49
+ "sidebar-ring": "oklch(0.708 0 0)",
50
+
51
+ // Robomous coral. Identity only — the wordmark and its styleguide swatch
52
+ // — never a functional-UI colour.
53
+ brand: "oklch(0.653 0.178 32.3)",
54
+ });
55
+
56
+ export const DARK_THEME: Readonly<Record<string, string>> = Object.freeze({
57
+ background: "oklch(0.145 0 0)",
58
+ foreground: "oklch(0.985 0 0)",
59
+ card: "oklch(0.205 0 0)",
60
+ "card-foreground": "oklch(0.985 0 0)",
61
+ popover: "oklch(0.205 0 0)",
62
+ "popover-foreground": "oklch(0.985 0 0)",
63
+ primary: "oklch(0.922 0 0)",
64
+ "primary-foreground": "oklch(0.205 0 0)",
65
+ secondary: "oklch(0.269 0 0)",
66
+ "secondary-foreground": "oklch(0.985 0 0)",
67
+ muted: "oklch(0.269 0 0)",
68
+ "muted-foreground": "oklch(0.708 0 0)",
69
+ accent: "oklch(0.269 0 0)",
70
+ "accent-foreground": "oklch(0.985 0 0)",
71
+ destructive: "oklch(0.704 0.191 22.216)",
72
+ border: "oklch(1 0 0 / 10%)",
73
+ input: "oklch(1 0 0 / 15%)",
74
+ ring: "oklch(0.556 0 0)",
75
+ "chart-1": "oklch(0.87 0 0)",
76
+ "chart-2": "oklch(0.556 0 0)",
77
+ "chart-3": "oklch(0.439 0 0)",
78
+ "chart-4": "oklch(0.371 0 0)",
79
+ "chart-5": "oklch(0.269 0 0)",
80
+ sidebar: "oklch(0.205 0 0)",
81
+ "sidebar-foreground": "oklch(0.985 0 0)",
82
+ "sidebar-primary": "oklch(0.488 0.243 264.376)",
83
+ "sidebar-primary-foreground": "oklch(0.985 0 0)",
84
+ "sidebar-accent": "oklch(0.269 0 0)",
85
+ "sidebar-accent-foreground": "oklch(0.985 0 0)",
86
+ "sidebar-border": "oklch(1 0 0 / 10%)",
87
+ "sidebar-ring": "oklch(0.556 0 0)",
88
+
89
+ brand: "oklch(0.653 0.178 32.3)",
90
+ });
91
+
92
+ /** The two provenance facts a colour string alone cannot carry. */
93
+ export const THEME = {
94
+ radius: "0.625rem",
95
+ fontSans: "'Geist Variable', sans-serif",
96
+ fontHeading: "var(--font-sans)",
97
+ } as const;
98
+
99
+ /** `cssVar("popover")` → `"var(--popover)"` — for a runtime caller that needs a string. */
100
+ export function cssVar(name: string): string {
101
+ return `var(--${name})`;
102
+ }