synthesisui 0.16.46 → 0.16.47

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.
@@ -3,6 +3,7 @@ import { basename, join, relative } from "node:path";
3
3
  import { readToken, resolveRegistry } from "../config.js";
4
4
  import { isNearDuplicate } from "../doctor/color-distance.js";
5
5
  import { emptyTally, scanComponentsInto, tallyToInventory, } from "../doctor/components-scan.js";
6
+ import { crosswalk } from "../doctor/crosswalk.js";
6
7
  import { diagnose, scanSource } from "../doctor/scan.js";
7
8
  import { parseSchemeBlocks } from "../doctor/scheme-blocks.js";
8
9
  import { buildTable } from "../doctor/tokens.js";
@@ -440,6 +441,43 @@ function printComponents(c) {
440
441
  if (mine.length > 12) {
441
442
  console.log(body(paint.faint(`(${mine.length - 12} more of yours)`)));
442
443
  }
444
+ printCrosswalk(mine);
445
+ }
446
+ /**
447
+ * The crosswalk, printed and nothing else.
448
+ *
449
+ * Three buckets, each line carrying the evidence that put it there - a name
450
+ * the catalogue knows, or a measured overlap with something they already wrote.
451
+ * Nothing is mapped, adopted or sent: the thresholds below are a first guess
452
+ * and the only way to tune them is to read this against a real codebase, which
453
+ * is exactly how the colour work found its own.
454
+ */
455
+ function printCrosswalk(mine) {
456
+ if (mine.length === 0)
457
+ return;
458
+ const rows = crosswalk(mine);
459
+ const of = (b) => rows.filter((r) => r.bucket === b);
460
+ const exists = of("exists");
461
+ const nearly = of("nearly");
462
+ const exclusive = of("exclusive");
463
+ console.log("");
464
+ console.log(section("Which of these already exist"));
465
+ const show = (title, list, limit) => {
466
+ if (list.length === 0)
467
+ return;
468
+ console.log(body(paint.strong(title)));
469
+ for (const r of list.slice(0, limit)) {
470
+ console.log(body(` ${r.component.name.padEnd(18)} ${paint.faint(`${r.component.files} files`)} ${paint.dim(r.because)}`));
471
+ }
472
+ if (list.length > limit) {
473
+ console.log(body(paint.faint(` (${list.length - limit} more)`)));
474
+ }
475
+ console.log("");
476
+ };
477
+ show(`Already in the catalogue (${exists.length})`, exists, 6);
478
+ show(`The same decision, twice (${nearly.length})`, nearly, 6);
479
+ show(`Only yours (${exclusive.length})`, exclusive, 8);
480
+ console.log(body(paint.dim("Nothing was mapped. This is a reading - the adopting is a decision you make.")));
443
481
  }
444
482
  function printAgentContract() {
445
483
  console.log("");
@@ -0,0 +1,307 @@
1
+ /**
2
+ * THE CROSSWALK - does this component already exist, here or in the catalogue?
3
+ *
4
+ * Phase one counted what a project composes. This asks the question that
5
+ * follows and is the whole reason to ask it: which of those are things the
6
+ * design system already has, which are the same thing twice, and which are
7
+ * genuinely theirs.
8
+ *
9
+ * Three signals, strongest first, and only the third needs a person:
10
+ *
11
+ * 1. THE CANONICAL NAME. `Pill`, `Chip` and `Tag` are a badge. Cheap, and it
12
+ * settles most of the list. The table below is curated knowledge, the same
13
+ * kind as the hue lanes the colour report argues with.
14
+ * 2. THE SHAPE OF THE AXES. Two components carrying a closed set on the same
15
+ * kind of axis, with values that overlap, are the same kind of thing -
16
+ * and overlap is measurable rather than felt.
17
+ * 3. JUDGEMENT, when the first two disagree. Reported, never applied.
18
+ *
19
+ * It runs in BOTH directions on purpose. Comparing only against our catalogue
20
+ * would have missed the finding that mattered most on a real dashboard (dono,
21
+ * 30/07): `Loader` (59 uses), `SimpleLoading` (21) and `ShimmerLoader` (158)
22
+ * are three loading components in one app. Redundancy is redundancy whoever
23
+ * owns it.
24
+ */
25
+ /** Their word → the canonical thing it is. */
26
+ const SYNONYM = {
27
+ button: "button",
28
+ btn: "button",
29
+ iconbutton: "button",
30
+ toolbarbutton: "button",
31
+ badge: "badge",
32
+ pill: "badge",
33
+ chip: "badge",
34
+ tag: "badge",
35
+ label: "badge",
36
+ card: "card",
37
+ panel: "card",
38
+ tile: "card",
39
+ input: "input",
40
+ textfield: "input",
41
+ textinput: "input",
42
+ field: "input",
43
+ modal: "modal",
44
+ dialog: "modal",
45
+ modalbox: "modal",
46
+ drawer: "drawer",
47
+ sheet: "drawer",
48
+ spinner: "spinner",
49
+ loader: "spinner",
50
+ loading: "spinner",
51
+ simpleloading: "spinner",
52
+ shimmerloader: "skeleton",
53
+ skeleton: "skeleton",
54
+ shimmer: "skeleton",
55
+ tooltip: "tooltip",
56
+ popover: "popover",
57
+ avatar: "avatar",
58
+ alert: "alert",
59
+ banner: "alert",
60
+ toast: "toast",
61
+ snackbar: "toast",
62
+ tabs: "tabs",
63
+ table: "table",
64
+ datatable: "table",
65
+ select: "select",
66
+ dropdown: "select",
67
+ checkbox: "checkbox",
68
+ radio: "radio",
69
+ toggle: "switch",
70
+ switch: "switch",
71
+ link: "link",
72
+ anchor: "link",
73
+ text: "text",
74
+ typography: "text",
75
+ heading: "heading",
76
+ title: "heading",
77
+ divider: "divider",
78
+ separator: "divider",
79
+ breadcrumb: "breadcrumb",
80
+ progress: "progress",
81
+ slider: "slider",
82
+ };
83
+ /** What the toolkit ships, with the axes each one carries. Curated because the
84
+ * CLI has no catalogue to read offline; kept short and canonical. */
85
+ const CATALOGUE = {
86
+ button: ["intent", "size"],
87
+ badge: ["intent"],
88
+ card: ["intent"],
89
+ input: ["size"],
90
+ modal: [],
91
+ drawer: ["side"],
92
+ spinner: ["size"],
93
+ skeleton: [],
94
+ tooltip: ["side"],
95
+ popover: ["side"],
96
+ avatar: ["size"],
97
+ alert: ["intent"],
98
+ toast: ["intent"],
99
+ tabs: [],
100
+ table: ["density"],
101
+ select: ["size"],
102
+ checkbox: [],
103
+ radio: [],
104
+ switch: ["size"],
105
+ link: ["intent"],
106
+ text: ["size"],
107
+ heading: ["size"],
108
+ divider: [],
109
+ breadcrumb: [],
110
+ progress: [],
111
+ slider: [],
112
+ };
113
+ /** Values that mean the same decision under different spellings. */
114
+ const VALUE_SYNONYM = {
115
+ warn: "warning",
116
+ destructive: "danger",
117
+ error: "danger",
118
+ negative: "danger",
119
+ positive: "success",
120
+ ok: "success",
121
+ info: "info",
122
+ informational: "info",
123
+ primary: "primary",
124
+ main: "primary",
125
+ secondary: "accent",
126
+ accent: "accent",
127
+ neutral: "neutral",
128
+ default: "neutral",
129
+ xs: "xs",
130
+ xsm: "xs",
131
+ small: "sm",
132
+ sm: "sm",
133
+ medium: "md",
134
+ md: "md",
135
+ large: "lg",
136
+ lg: "lg",
137
+ };
138
+ /** Axis names that ask the same question. */
139
+ const AXIS_SYNONYM = {
140
+ variant: "intent",
141
+ intent: "intent",
142
+ tone: "intent",
143
+ color: "intent",
144
+ colour: "intent",
145
+ kind: "intent",
146
+ type: "intent",
147
+ status: "intent",
148
+ severity: "intent",
149
+ size: "size",
150
+ scale: "size",
151
+ density: "density",
152
+ dense: "density",
153
+ compact: "density",
154
+ side: "side",
155
+ placement: "side",
156
+ position: "side",
157
+ };
158
+ export function canonicalName(name) {
159
+ const head = name
160
+ .split(".")[0]
161
+ .toLowerCase()
162
+ .replace(/[^a-z]/g, "");
163
+ if (SYNONYM[head])
164
+ return SYNONYM[head];
165
+ // A compound like `WidgetCard` or `ArrowDownIcon` is only canonical when the
166
+ // WHOLE word is - a widget card is not a card, it is their own thing.
167
+ return null;
168
+ }
169
+ export function canonicalAxes(props) {
170
+ const out = new Set();
171
+ for (const p of Object.keys(props)) {
172
+ const axis = AXIS_SYNONYM[p.toLowerCase()];
173
+ if (axis)
174
+ out.add(axis);
175
+ }
176
+ return out;
177
+ }
178
+ function canonicalValues(values) {
179
+ return new Set(values
180
+ .map((v) => v.trim().toLowerCase())
181
+ .map((v) => VALUE_SYNONYM[v] ?? v)
182
+ // A raw colour or a css length is a value passed through, not a named
183
+ // option somebody designed - it says nothing about what this component IS.
184
+ .filter((v) => v && !/^(#|var\(|rgb|\d)/.test(v)));
185
+ }
186
+ /** How much two components' axis VALUES overlap, 0-1 (Jaccard). */
187
+ export function axisOverlap(a, b) {
188
+ const va = new Set();
189
+ const vb = new Set();
190
+ for (const [p, values] of Object.entries(a)) {
191
+ if (AXIS_SYNONYM[p.toLowerCase()]) {
192
+ for (const v of canonicalValues(values))
193
+ va.add(v);
194
+ }
195
+ }
196
+ for (const [p, values] of Object.entries(b)) {
197
+ if (AXIS_SYNONYM[p.toLowerCase()]) {
198
+ for (const v of canonicalValues(values))
199
+ vb.add(v);
200
+ }
201
+ }
202
+ if (va.size === 0 || vb.size === 0)
203
+ return 0;
204
+ let shared = 0;
205
+ for (const v of va)
206
+ if (vb.has(v))
207
+ shared += 1;
208
+ return shared / new Set([...va, ...vb]).size;
209
+ }
210
+ /** Below this, two components share a value or two by accident. */
211
+ export const TWIN_OVERLAP = 0.5;
212
+ /**
213
+ * And a ratio needs a denominator worth trusting.
214
+ *
215
+ * The first real run called an `InformationIcon` the same decision as a
216
+ * spinner, at 100% (dono, 30/07) - both carried exactly one readable option,
217
+ * `sm`, so one shared value out of one union was a perfect score. Same lesson
218
+ * the colour work learned twice: a ratio over a tiny set says nothing.
219
+ */
220
+ export const TWIN_MIN_SHARED = 2;
221
+ function sharedCount(a, b) {
222
+ const va = new Set();
223
+ const vb = new Set();
224
+ for (const [p, values] of Object.entries(a)) {
225
+ if (AXIS_SYNONYM[p.toLowerCase()]) {
226
+ for (const v of canonicalValues(values))
227
+ va.add(v);
228
+ }
229
+ }
230
+ for (const [p, values] of Object.entries(b)) {
231
+ if (AXIS_SYNONYM[p.toLowerCase()]) {
232
+ for (const v of canonicalValues(values))
233
+ vb.add(v);
234
+ }
235
+ }
236
+ let n = 0;
237
+ for (const v of va)
238
+ if (vb.has(v))
239
+ n += 1;
240
+ return n;
241
+ }
242
+ export function crosswalk(components) {
243
+ const mine = components.filter((c) => !c.from);
244
+ // Two of THEIR components reading as the same catalogue entry is the
245
+ // strongest redundancy signal there is, and it needs no threshold: it is not
246
+ // "these look alike", it is "you wrote this twice". The first run filed both
247
+ // of a project's spinners as `exists` and said nothing about them being two.
248
+ const claims = new Map();
249
+ for (const c of mine) {
250
+ const k = canonicalName(c.name);
251
+ if (!k)
252
+ continue;
253
+ claims.set(k, [...(claims.get(k) ?? []), c.name]);
254
+ }
255
+ return mine
256
+ .map((component) => {
257
+ const canonical = canonicalName(component.name);
258
+ const twins = mine
259
+ .filter((o) => o.name !== component.name)
260
+ .map((o) => ({
261
+ name: o.name,
262
+ overlap: axisOverlap(component.props, o.props),
263
+ }))
264
+ .filter((t) => t.overlap >= TWIN_OVERLAP &&
265
+ sharedCount(component.props, mine.find((m) => m.name === t.name)?.props ?? {}) >= TWIN_MIN_SHARED)
266
+ .sort((a, b) => b.overlap - a.overlap);
267
+ if (canonical && CATALOGUE[canonical]) {
268
+ const alsoClaiming = (claims.get(canonical) ?? []).filter((n) => n !== component.name);
269
+ if (alsoClaiming.length > 0) {
270
+ return {
271
+ component,
272
+ canonical,
273
+ twins,
274
+ bucket: "nearly",
275
+ because: `you have ${alsoClaiming.length + 1} components that all read as ds-${canonical} (${[component.name, ...alsoClaiming].join(", ")})`,
276
+ };
277
+ }
278
+ const axes = [...canonicalAxes(component.props)];
279
+ return {
280
+ component,
281
+ canonical,
282
+ twins,
283
+ bucket: "exists",
284
+ because: axes.length
285
+ ? `reads as ds-${canonical}; its ${axes.join(" and ")} ${axes.length === 1 ? "maps" : "map"} onto ours`
286
+ : `reads as ds-${canonical}`,
287
+ };
288
+ }
289
+ if (twins.length > 0) {
290
+ return {
291
+ component,
292
+ canonical,
293
+ twins,
294
+ bucket: "nearly",
295
+ because: `shares ${Math.round(twins[0].overlap * 100)}% of its options with ${twins[0].name} - one decision, two components`,
296
+ };
297
+ }
298
+ return {
299
+ component,
300
+ canonical,
301
+ twins,
302
+ bucket: "exclusive",
303
+ because: "nothing in the catalogue does this - it is yours",
304
+ };
305
+ })
306
+ .sort((a, b) => b.component.files - a.component.files);
307
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.46",
3
+ "version": "0.16.47",
4
4
  "description": "Bring SynthesisUI design systems into any project - tokens, typed components, whole pages and an agent-ready CLAUDE.md manifest.",
5
5
  "type": "module",
6
6
  "bin": {