@assure-one/design-system 1.31.0 → 1.33.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 (42) hide show
  1. package/README.md +61 -6
  2. package/codemods/0.2.0-radix-migration.mjs +315 -0
  3. package/codemods/README.md +365 -0
  4. package/codemods/lib/css-selectors.mjs +33 -0
  5. package/codemods/lib/css-values.mjs +223 -0
  6. package/codemods/lib/ds-stylesheet.mjs +168 -0
  7. package/codemods/lib/environment.mjs +72 -0
  8. package/codemods/lib/files.mjs +100 -0
  9. package/codemods/lib/forms.mjs +253 -0
  10. package/codemods/lib/jsx.mjs +0 -0
  11. package/codemods/lib/ledger.mjs +84 -0
  12. package/codemods/lib/registry.mjs +32 -0
  13. package/codemods/lib/report.mjs +119 -0
  14. package/codemods/lib/runner.mjs +164 -0
  15. package/codemods/run.mjs +161 -0
  16. package/codemods/transforms/cm-14-hidden-mirrors.mjs +275 -0
  17. package/codemods/transforms/cm-15-dom-selectors.mjs +573 -0
  18. package/codemods/transforms/cm-16-globals-css.mjs +487 -0
  19. package/codemods/transforms/cm-20-select-sentinels.mjs +442 -0
  20. package/dist/css/base.css +60 -0
  21. package/dist/css/components.css +7 -0
  22. package/dist/css/legacy-aliases.css +665 -0
  23. package/dist/css/shadcn.css +155 -0
  24. package/dist/css/tailwind.css +296 -0
  25. package/dist/css/tokens.css +630 -0
  26. package/dist/index.d.ts +807 -27
  27. package/dist/index.js +2038 -683
  28. package/dist/index.js.map +1 -1
  29. package/dist/styles.css +1 -1
  30. package/dist/system-BDU18fVg.d.ts +559 -0
  31. package/dist/testing/index.cjs +458 -0
  32. package/dist/testing/index.d.cts +253 -0
  33. package/dist/testing/index.d.ts +253 -0
  34. package/dist/testing/index.js +452 -0
  35. package/dist/testing/setup.cjs +123 -0
  36. package/dist/testing/setup.js +121 -0
  37. package/dist/testing/style-stub.cjs +7 -0
  38. package/dist/testing/style-stub.js +5 -0
  39. package/dist/tokens/index.d.ts +50 -439
  40. package/dist/tokens/index.js +557 -50
  41. package/dist/tokens/index.js.map +1 -1
  42. package/package.json +74 -5
package/README.md CHANGED
@@ -11,6 +11,8 @@ The single source of truth for visual design across all Assure products.
11
11
  - [**`CHANGELOG.md`**](./CHANGELOG.md) — release-by-release log of what shipped.
12
12
  - [**`CONTRIBUTING.md`**](./CONTRIBUTING.md) — dev loop, release flow, versioning, pitfalls.
13
13
  - [**`CLAUDE.md`**](./CLAUDE.md) — invariants and conventions for AI assistants working in this repo.
14
+ - [**`docs/integration/css.md`**](./docs/integration/css.md) — how an application loads our CSS: layer order, import sequence, the compatibility preset, removing `@source`, runtime brand theming.
15
+ - [**`docs/testing.md`**](./docs/testing.md) — rendering the real package in your app's Jest tests (`@assure-one/design-system/testing`, experimental).
14
16
  - [**`claude-skills/`**](./claude-skills) — drop-in Claude Code skill for consuming projects.
15
17
 
16
18
  ## Install
@@ -38,6 +40,49 @@ export default function Page() {
38
40
  }
39
41
  ```
40
42
 
43
+ ### The split CSS entries (experimental)
44
+
45
+ `styles.css` stays the supported way to consume the design system, unchanged.
46
+ Alongside it, the package now also publishes the individual stylesheets the new
47
+ CSS mode is built from. They are **experimental**: nothing requires them yet,
48
+ no component reads them, and importing them changes nothing on its own.
49
+
50
+ | Subpath | What it is |
51
+ | --- | --- |
52
+ | `./css/tokens.css` | The `--ds-*` design tokens: `:root`, the colour scheme scope and the brand scopes. Usable without any component. |
53
+ | `./css/legacy-aliases.css` | Read-aliases from today's token names to the namespaced ones (`--color-surface: var(--ds-color-surface)`), for as long as your own CSS reads design-system token names. |
54
+ | `./css/tailwind.css` | A Tailwind `@theme` bridge, so `bg-surface/40`, `hover:text-fg-3` and `rounded-control` compile in *your* build instead of silently producing nothing. |
55
+ | `./css/shadcn.css` | The app-vocabulary bridge: `background`, `foreground`, `primary`, `muted`, `destructive`, the radius scale and the rest, each reading one `--app-*` input you can override. |
56
+ | `./css/base.css` | Optional document defaults: body background, ink, font and `color-scheme`. Ships no preflight, no element rules and no font import. |
57
+ | `./css/components.css` | The component utilities compiled with Tailwind's `ds` prefix (`ds:flex`, `ds:bg-surface`), inside `@layer ds` and bound to the `--ds-*` tokens. **Unused until the class-vocabulary flip (W1-11):** no component emits a prefixed class yet, so importing it styles nothing. Published so the file, its size and its hygiene can be reviewed before the flip. |
58
+
59
+ Import order is `tokens.css` → `legacy-aliases.css` (optional) → `tailwind.css`
60
+ → `shadcn.css` (optional) → `base.css` (optional) → `components.css`, after your own
61
+ `@import "tailwindcss"` and under the layer statement
62
+ `@layer theme, base, ds, components, utilities;`.
63
+ [**`docs/integration/css.md`**](./docs/integration/css.md) has the full
64
+ sequence, where the per-app preset goes, when the `@source` into our package may
65
+ be removed, and the fixture run behind each of those claims. The reasoning lives
66
+ in [ADR-004](./docs/adr/004-css-delivery-cascade.md) and
67
+ [ADR-005](./docs/adr/005-app-vocabulary-bridge.md).
68
+
69
+ ### Runtime brand theming (experimental)
70
+
71
+ For a tenant colour known only at runtime, `@assure-one/design-system/tokens`
72
+ exports `createBrandTheme()`: a validated seed becomes the custom properties a
73
+ built-in brand scope declares, ready to spread into a `style` attribute.
74
+
75
+ ```tsx
76
+ import { createBrandTheme } from "@assure-one/design-system/tokens";
77
+
78
+ <div style={{ ...createBrandTheme({ brand: "#2258d8", accent: "#f59e0b" }) }}>…</div>;
79
+ ```
80
+
81
+ It is pure and SSR-deterministic, clamps a fill too pale to read against the
82
+ canvas, and guarantees WCAG AA for every foreground it emits. The seed shape is
83
+ gated by decision D7 and may change while that decision is open — see
84
+ [`docs/integration/css.md`](./docs/integration/css.md#5-runtime-brand-theming).
85
+
41
86
  ## Develop
42
87
 
43
88
  ```bash
@@ -52,14 +97,24 @@ See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the full dev loop, including iter
52
97
 
53
98
  ## Architecture
54
99
 
55
- 3-tier token system (W3C Design Tokens spec compatible):
100
+ 3-tier token system, authored as DTCG JSON in [`tokens/src`](./tokens/README.md)
101
+ and generated into `src/tokens/` and the `./css/*` entries (ADR-003):
56
102
 
57
- ```
58
- reference → system (semantic) → component
59
- color.navy.700 → color.action.primary.bg → button.primary.bg
60
- ```
103
+ | Tier | Example (namespaced name → value) | Reads |
104
+ | --- | --- | --- |
105
+ | reference | `--ds-ref-brand-pro-fg: #6c42f8` | a literal; the raw ramps, one per brand |
106
+ | system (semantic) | `--ds-color-action-brand-bg: var(--ds-ref-brand-pro-fg)` | the reference tier; **this is the public contract** |
107
+ | component | `--ds-menu-item-hover-bg: var(--ds-color-canvas-sunken)` | the system tier; only where a component must be themable on its own |
108
+
109
+ Components consume **system tokens**, never reference values directly. Themes
110
+ (colour scheme, per-product brand, a runtime brand from `createBrandTheme()`)
111
+ re-map system → reference without touching component code.
61
112
 
62
- Components consume **system tokens**, never reference values directly. Themes (light, dark, per-product accent) re-map system → reference without touching component code.
113
+ Every token has two published spellings: the namespaced `--ds-*` name above and
114
+ the legacy name it ships as today (`--color-brand`, `--color-pro-fg`,
115
+ `--color-menu-item-hover-bg`). `css/legacy-aliases.css` declares
116
+ `<legacy>: var(<--ds-* name>)` in the same scopes, so both resolve to the same
117
+ value — proved by `pnpm tokens:ds-check`.
63
118
 
64
119
  ## Releases
65
120
 
@@ -0,0 +1,315 @@
1
+ /**
2
+ * @assure-one/design-system 0.1.x → 0.2.0 migration codemod
3
+ *
4
+ * Run from the consumer repo (firm or portal):
5
+ *
6
+ * npx jscodeshift -t node_modules/@assure-one/design-system/codemods/0.2.0-radix-migration.mjs \
7
+ * --extensions=tsx,ts,jsx,js \
8
+ * --parser=tsx \
9
+ * 'src/**\/*.{ts,tsx,js,jsx}'
10
+ *
11
+ * Or, if you've copied the file locally:
12
+ *
13
+ * npx jscodeshift -t ./codemods/0.2.0-radix-migration.mjs \
14
+ * --extensions=tsx,ts,jsx,js --parser=tsx 'src/**\/*.{ts,tsx}'
15
+ *
16
+ * VERBOSE per-file logs:
17
+ * VERBOSE=1 npx jscodeshift -t ... 'src/**\/*.tsx'
18
+ *
19
+ * What it does
20
+ * ------------
21
+ * 1. <Slider value={n} /> → <Slider value={[n]} />
22
+ * <Slider defaultValue={n} /> → <Slider defaultValue={[n]} />
23
+ * Adds a `// TODO 0.2.0: onValueChange now receives number[]` line above
24
+ * any <Slider> with `onValueChange` so the consumer can review the body.
25
+ *
26
+ * 2. <Avatar><Image src=... alt=... /></Avatar> (next/image inner)
27
+ * → <Avatar src=... alt=... />
28
+ * Drops the inner <Image>. If <Avatar> already has `src`, leaves it and
29
+ * emits a TODO. If the <Image> isn't `next/image`, leaves it untouched.
30
+ *
31
+ * 3. useToast — no-op. `useToast()` already returned `{ toast }`; 0.2.0
32
+ * adds `dismiss` (additive). Existing call sites keep working.
33
+ *
34
+ * 4. TeamMemberSelect — no-op. The `value: ""` (unassigned) contract is
35
+ * preserved on the consumer-facing API; the `__unassigned__` sentinel
36
+ * is internal only.
37
+ *
38
+ * Idempotent: running twice changes nothing the second time. Already-array
39
+ * Slider values and already-flattened Avatars are skipped.
40
+ */
41
+
42
+ const SLIDER_NUMERIC_PROPS = ["value", "defaultValue"];
43
+ const TODO_SLIDER = " TODO 0.2.0: onValueChange now receives number[] (was number) ";
44
+ const TODO_SLIDER_IDENT =
45
+ " TODO 0.2.0: Slider value/defaultValue is now number[] — wrap in [] if this expression is still number ";
46
+ const TODO_AVATAR_CONFLICT =
47
+ " TODO 0.2.0: <Avatar> already has src — could not auto-merge with inner next/image child ";
48
+ const TODO_AVATAR_MERGED =
49
+ " TODO 0.2.0: review hoisted Avatar src/alt (was inner next/image child) ";
50
+
51
+ export default function transformer(file, api) {
52
+ const j = api.jscodeshift;
53
+ const root = j(file.source);
54
+ let changed = false;
55
+ const log = [];
56
+
57
+ /* -------------------------------------------------------------------- */
58
+ /* Helpers */
59
+ /* -------------------------------------------------------------------- */
60
+
61
+ function jsxOpeningName(opening) {
62
+ const name = opening.name;
63
+ if (!name) return null;
64
+ if (name.type === "JSXIdentifier") return name.name;
65
+ return null;
66
+ }
67
+
68
+ function findAttr(opening, attrName) {
69
+ return opening.attributes?.find(
70
+ (a) =>
71
+ a.type === "JSXAttribute" &&
72
+ a.name?.type === "JSXIdentifier" &&
73
+ a.name.name === attrName,
74
+ );
75
+ }
76
+
77
+ function isAlreadyArrayExpression(attr) {
78
+ if (!attr || attr.type !== "JSXAttribute") return false;
79
+ const v = attr.value;
80
+ if (!v || v.type !== "JSXExpressionContainer") return false;
81
+ const expr = v.expression;
82
+ if (!expr) return false;
83
+ // already array literal
84
+ if (expr.type === "ArrayExpression") return true;
85
+ // identifier / member / call — assume the consumer is already passing an
86
+ // array; do not double-wrap. (Idempotency over a brittle type check.)
87
+ return (
88
+ expr.type === "Identifier" ||
89
+ expr.type === "MemberExpression" ||
90
+ expr.type === "CallExpression" ||
91
+ expr.type === "ConditionalExpression" ||
92
+ expr.type === "LogicalExpression"
93
+ );
94
+ }
95
+
96
+ function isScalarNumericLiteralExpression(attr) {
97
+ if (!attr || attr.type !== "JSXAttribute") return false;
98
+ const v = attr.value;
99
+ // Bare `value=42` is not legal JSX numeric, so we only care about expr
100
+ // containers wrapping a NumericLiteral or unary minus numeric.
101
+ if (!v || v.type !== "JSXExpressionContainer") return false;
102
+ const e = v.expression;
103
+ if (!e) return false;
104
+ if (e.type === "NumericLiteral" || e.type === "Literal") {
105
+ return typeof e.value === "number";
106
+ }
107
+ if (
108
+ e.type === "UnaryExpression" &&
109
+ e.operator === "-" &&
110
+ e.argument &&
111
+ (e.argument.type === "NumericLiteral" ||
112
+ (e.argument.type === "Literal" && typeof e.argument.value === "number"))
113
+ ) {
114
+ return true;
115
+ }
116
+ return false;
117
+ }
118
+
119
+ function wrapAttrInArray(attr) {
120
+ const expr = attr.value.expression;
121
+ attr.value = j.jsxExpressionContainer(j.arrayExpression([expr]));
122
+ }
123
+
124
+ function hasLeadingComment(node, text) {
125
+ const list = node.leadingComments || [];
126
+ return list.some((c) => c.value === text);
127
+ }
128
+
129
+ function addLeadingComment(path, text) {
130
+ const node = path.node;
131
+ if (hasLeadingComment(node, text)) return false;
132
+ node.comments = node.comments || [];
133
+ const comment = { type: "CommentLine", value: text, leading: true, trailing: false };
134
+ node.comments.unshift(comment);
135
+ // jscodeshift / recast also reads `leadingComments`
136
+ node.leadingComments = node.leadingComments || [];
137
+ node.leadingComments.unshift(comment);
138
+ return true;
139
+ }
140
+
141
+ /* -------------------------------------------------------------------- */
142
+ /* Detect imported local names */
143
+ /* */
144
+ /* We only want to touch <Slider> and <Avatar> when they come from */
145
+ /* @assure-one/design-system (or are unambiguous globally). To stay */
146
+ /* safe across the firm + portal codebases, we scope by import source. */
147
+ /* -------------------------------------------------------------------- */
148
+
149
+ const dsLocalNames = new Set();
150
+ const nextImageLocalNames = new Set();
151
+
152
+ root.find(j.ImportDeclaration).forEach((path) => {
153
+ const src = path.node.source?.value;
154
+ if (!src) return;
155
+ if (src === "@assure-one/design-system") {
156
+ for (const spec of path.node.specifiers || []) {
157
+ if (spec.type === "ImportSpecifier" && spec.local?.name) {
158
+ dsLocalNames.add(spec.local.name);
159
+ }
160
+ }
161
+ }
162
+ if (src === "next/image") {
163
+ for (const spec of path.node.specifiers || []) {
164
+ if (
165
+ (spec.type === "ImportDefaultSpecifier" ||
166
+ spec.type === "ImportSpecifier") &&
167
+ spec.local?.name
168
+ ) {
169
+ nextImageLocalNames.add(spec.local.name);
170
+ }
171
+ }
172
+ }
173
+ });
174
+
175
+ // The DS exports `Slider` and `Avatar` under those exact names. If the
176
+ // consumer didn't import them from DS we leave their JSX alone — that
177
+ // avoids touching a local component that happens to share a name.
178
+ const sliderLocal = dsLocalNames.has("Slider") ? "Slider" : null;
179
+ const avatarLocal = dsLocalNames.has("Avatar") ? "Avatar" : null;
180
+
181
+ /* -------------------------------------------------------------------- */
182
+ /* Change 1 — Slider value/defaultValue scalar → array */
183
+ /* -------------------------------------------------------------------- */
184
+
185
+ if (sliderLocal) {
186
+ root
187
+ .find(j.JSXElement, {
188
+ openingElement: { name: { type: "JSXIdentifier", name: sliderLocal } },
189
+ })
190
+ .forEach((path) => {
191
+ const opening = path.node.openingElement;
192
+
193
+ let wrappedAny = false;
194
+ let needsIdentTodo = false;
195
+ for (const propName of SLIDER_NUMERIC_PROPS) {
196
+ const attr = findAttr(opening, propName);
197
+ if (!attr) continue;
198
+ if (isScalarNumericLiteralExpression(attr)) {
199
+ wrapAttrInArray(attr);
200
+ changed = true;
201
+ wrappedAny = true;
202
+ log.push(`Slider: ${propName}={n} → ${propName}={[n]}`);
203
+ continue;
204
+ }
205
+ if (isAlreadyArrayExpression(attr)) {
206
+ // Could be Identifier / MemberExpression / CallExpression / etc.
207
+ // We can't tell from AST whether it's number[] (already migrated)
208
+ // or number (needs migration). Leave it and emit a TODO so the
209
+ // consumer reviews. Idempotent on the array-literal case.
210
+ const v = attr.value?.expression;
211
+ if (v && v.type !== "ArrayExpression") {
212
+ needsIdentTodo = true;
213
+ }
214
+ }
215
+ }
216
+ if (needsIdentTodo) {
217
+ if (addLeadingComment(path, TODO_SLIDER_IDENT)) {
218
+ changed = true;
219
+ log.push("Slider: added value/defaultValue identifier TODO comment");
220
+ }
221
+ }
222
+
223
+ // TODO comment for onValueChange
224
+ const onValueChange = findAttr(opening, "onValueChange");
225
+ if (onValueChange) {
226
+ if (addLeadingComment(path, TODO_SLIDER)) {
227
+ changed = true;
228
+ log.push("Slider: added onValueChange TODO comment");
229
+ }
230
+ }
231
+ });
232
+ }
233
+
234
+ /* -------------------------------------------------------------------- */
235
+ /* Change 2 — Avatar with next/image child → flat src/alt props */
236
+ /* -------------------------------------------------------------------- */
237
+
238
+ if (avatarLocal && nextImageLocalNames.size > 0) {
239
+ root
240
+ .find(j.JSXElement, {
241
+ openingElement: { name: { type: "JSXIdentifier", name: avatarLocal } },
242
+ })
243
+ .forEach((path) => {
244
+ const el = path.node;
245
+ const opening = el.openingElement;
246
+ const children = el.children || [];
247
+
248
+ // Find a single next/image child (ignoring whitespace text nodes)
249
+ const meaningful = children.filter(
250
+ (c) =>
251
+ !(
252
+ c.type === "JSXText" &&
253
+ (c.value === "" || /^\s*$/.test(c.value))
254
+ ),
255
+ );
256
+ if (meaningful.length !== 1) return;
257
+ const child = meaningful[0];
258
+ if (child.type !== "JSXElement") return;
259
+ const childName = jsxOpeningName(child.openingElement);
260
+ if (!childName || !nextImageLocalNames.has(childName)) return;
261
+
262
+ // Don't merge if Avatar already has src — emit TODO and skip
263
+ const existingSrc = findAttr(opening, "src");
264
+ if (existingSrc) {
265
+ if (addLeadingComment(path, TODO_AVATAR_CONFLICT)) {
266
+ changed = true;
267
+ log.push("Avatar: src conflict — TODO comment added");
268
+ }
269
+ return;
270
+ }
271
+
272
+ // Hoist src + alt from <Image>
273
+ const childOpening = child.openingElement;
274
+ const srcAttr = findAttr(childOpening, "src");
275
+ const altAttr = findAttr(childOpening, "alt");
276
+ if (!srcAttr) return; // nothing to hoist
277
+
278
+ const newAttrs = [...opening.attributes];
279
+ newAttrs.push(j.jsxAttribute(j.jsxIdentifier("src"), srcAttr.value));
280
+ if (altAttr && !findAttr(opening, "alt")) {
281
+ newAttrs.push(j.jsxAttribute(j.jsxIdentifier("alt"), altAttr.value));
282
+ }
283
+ opening.attributes = newAttrs;
284
+
285
+ // Self-close the Avatar (no remaining children)
286
+ opening.selfClosing = true;
287
+ el.closingElement = null;
288
+ el.children = [];
289
+
290
+ addLeadingComment(path, TODO_AVATAR_MERGED);
291
+ changed = true;
292
+ log.push("Avatar: hoisted next/image src/alt and removed inner element");
293
+ });
294
+ }
295
+
296
+ /* -------------------------------------------------------------------- */
297
+ /* Done */
298
+ /* -------------------------------------------------------------------- */
299
+
300
+ if (!changed) return null;
301
+
302
+ if (process.env.VERBOSE) {
303
+ // eslint-disable-next-line no-console
304
+ console.log(`[0.2.0-radix-migration] ${file.path}`);
305
+ for (const line of log) {
306
+ // eslint-disable-next-line no-console
307
+ console.log(` - ${line}`);
308
+ }
309
+ }
310
+
311
+ return root.toSource({ quote: "double" });
312
+ }
313
+
314
+ // jscodeshift parser hint
315
+ export const parser = "tsx";