@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.
- package/LICENSE +202 -0
- package/README.md +38 -0
- package/components.json +13 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +47 -0
- package/dist/lib/button.d.ts +1 -0
- package/dist/lib/button.js +4 -0
- package/dist/lib/cn.d.ts +18 -0
- package/dist/lib/cn.js +21 -0
- package/dist/lib/menu.d.ts +1 -0
- package/dist/lib/menu.js +16 -0
- package/dist/lib/progress.d.ts +3 -0
- package/dist/lib/progress.js +6 -0
- package/dist/lib/select.d.ts +1 -0
- package/dist/lib/select.js +3 -0
- package/dist/primitives/alert.d.ts +10 -0
- package/dist/primitives/alert.js +27 -0
- package/dist/primitives/badge.d.ts +9 -0
- package/dist/primitives/badge.js +28 -0
- package/dist/primitives/button.d.ts +10 -0
- package/dist/primitives/button.js +35 -0
- package/dist/primitives/card.d.ts +11 -0
- package/dist/primitives/card.js +24 -0
- package/dist/primitives/combobox.d.ts +24 -0
- package/dist/primitives/combobox.js +58 -0
- package/dist/primitives/dialog.d.ts +17 -0
- package/dist/primitives/dialog.js +36 -0
- package/dist/primitives/dropdown-menu.d.ts +29 -0
- package/dist/primitives/dropdown-menu.js +50 -0
- package/dist/primitives/field.d.ts +24 -0
- package/dist/primitives/field.js +67 -0
- package/dist/primitives/input-group.d.ts +16 -0
- package/dist/primitives/input-group.js +56 -0
- package/dist/primitives/input.d.ts +3 -0
- package/dist/primitives/input.js +6 -0
- package/dist/primitives/label.d.ts +4 -0
- package/dist/primitives/label.js +7 -0
- package/dist/primitives/progress.d.ts +4 -0
- package/dist/primitives/progress.js +7 -0
- package/dist/primitives/select.d.ts +15 -0
- package/dist/primitives/select.js +35 -0
- package/dist/primitives/separator.d.ts +4 -0
- package/dist/primitives/separator.js +8 -0
- package/dist/primitives/sheet.d.ts +14 -0
- package/dist/primitives/sheet.js +36 -0
- package/dist/primitives/skeleton.d.ts +2 -0
- package/dist/primitives/skeleton.js +6 -0
- package/dist/primitives/sonner.d.ts +4 -0
- package/dist/primitives/sonner.js +40 -0
- package/dist/primitives/table.d.ts +10 -0
- package/dist/primitives/table.js +27 -0
- package/dist/primitives/tabs.d.ts +11 -0
- package/dist/primitives/tabs.js +28 -0
- package/dist/primitives/textarea.d.ts +3 -0
- package/dist/primitives/textarea.js +6 -0
- package/dist/primitives/tooltip.d.ts +7 -0
- package/dist/primitives/tooltip.js +17 -0
- package/dist/statusTone.d.ts +37 -0
- package/dist/statusTone.js +38 -0
- package/dist/tokens.d.ts +26 -0
- package/dist/tokens.js +96 -0
- package/gates/canonical.test.mjs +71 -0
- package/gates/extensions.test.mjs +373 -0
- package/gates/index.d.mts +51 -0
- package/gates/index.mjs +514 -0
- package/gates/tokens.test.mjs +276 -0
- package/package.json +60 -0
- package/shadcn/README.md +13 -0
- package/shadcn/alert.tsx +76 -0
- package/shadcn/badge.tsx +49 -0
- package/shadcn/button.tsx +67 -0
- package/shadcn/card.tsx +103 -0
- package/shadcn/combobox.tsx +299 -0
- package/shadcn/dialog.tsx +166 -0
- package/shadcn/dropdown-menu.tsx +267 -0
- package/shadcn/field.tsx +236 -0
- package/shadcn/input-group.tsx +154 -0
- package/shadcn/input.tsx +19 -0
- package/shadcn/label.tsx +22 -0
- package/shadcn/progress.tsx +29 -0
- package/shadcn/select.tsx +190 -0
- package/shadcn/separator.tsx +28 -0
- package/shadcn/sheet.tsx +145 -0
- package/shadcn/skeleton.tsx +13 -0
- package/shadcn/sonner.tsx +49 -0
- package/shadcn/table.tsx +114 -0
- package/shadcn/tabs.tsx +88 -0
- package/shadcn/textarea.tsx +18 -0
- package/shadcn/tooltip.tsx +57 -0
- package/src/harness.test.tsx +37 -0
- package/src/index.ts +119 -0
- package/src/lib/button.ts +4 -0
- package/src/lib/cn.test.ts +33 -0
- package/src/lib/cn.ts +23 -0
- package/src/lib/menu.ts +16 -0
- package/src/lib/progress.ts +6 -0
- package/src/lib/select.ts +4 -0
- package/src/primitives/alert.tsx +76 -0
- package/src/primitives/badge.tsx +57 -0
- package/src/primitives/button.tsx +67 -0
- package/src/primitives/card.tsx +103 -0
- package/src/primitives/combobox.test.tsx +38 -0
- package/src/primitives/combobox.tsx +299 -0
- package/src/primitives/dialog.tsx +166 -0
- package/src/primitives/dropdown-menu.tsx +267 -0
- package/src/primitives/field.tsx +236 -0
- package/src/primitives/input-group.tsx +154 -0
- package/src/primitives/input.tsx +19 -0
- package/src/primitives/label.tsx +22 -0
- package/src/primitives/primitives.test.tsx +440 -0
- package/src/primitives/progress.tsx +29 -0
- package/src/primitives/select.tsx +190 -0
- package/src/primitives/separator.tsx +28 -0
- package/src/primitives/sheet.tsx +145 -0
- package/src/primitives/skeleton.tsx +13 -0
- package/src/primitives/sonner.test.tsx +41 -0
- package/src/primitives/sonner.tsx +65 -0
- package/src/primitives/table.tsx +114 -0
- package/src/primitives/tabs.tsx +88 -0
- package/src/primitives/textarea.tsx +18 -0
- package/src/primitives/tooltip.tsx +57 -0
- package/src/statusTone.test.ts +20 -0
- package/src/statusTone.ts +52 -0
- package/src/styles.css +204 -0
- package/src/tokens.test.ts +285 -0
- 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
|
+
}
|