@fragments-sdk/classifier 0.2.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 (51) hide show
  1. package/LICENSE +84 -0
  2. package/dist/index.d.ts +184 -0
  3. package/dist/index.js +1856 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +45 -0
  6. package/src/__tests__/combiner.test.ts +222 -0
  7. package/src/__tests__/fixtures.ts +96 -0
  8. package/src/ai/__tests__/cache-key.test.ts +50 -0
  9. package/src/ai/__tests__/prompt.test.ts +95 -0
  10. package/src/ai/__tests__/schema.test.ts +145 -0
  11. package/src/ai/__tests__/secret-scrub.test.ts +70 -0
  12. package/src/ai/__tests__/signal.test.ts +94 -0
  13. package/src/ai/cache-key.ts +46 -0
  14. package/src/ai/index.ts +42 -0
  15. package/src/ai/prompt.ts +154 -0
  16. package/src/ai/schema.ts +148 -0
  17. package/src/ai/secret-scrub.ts +116 -0
  18. package/src/ai/signal.ts +81 -0
  19. package/src/ai/version.ts +15 -0
  20. package/src/canonical-vocab/resolve-by-html-element.ts +72 -0
  21. package/src/combiner/__tests__/band.test.ts +155 -0
  22. package/src/combiner/__tests__/group.test.ts +85 -0
  23. package/src/combiner/__tests__/rank.test.ts +54 -0
  24. package/src/combiner/band.ts +85 -0
  25. package/src/combiner/group.ts +62 -0
  26. package/src/combiner/rank.ts +57 -0
  27. package/src/combiner.ts +124 -0
  28. package/src/index.ts +76 -0
  29. package/src/signals/__tests__/aria-role.test.ts +53 -0
  30. package/src/signals/__tests__/barrel-export.test.ts +29 -0
  31. package/src/signals/__tests__/html-root.test.ts +55 -0
  32. package/src/signals/__tests__/input-type.test.ts +58 -0
  33. package/src/signals/__tests__/library-reexport.test.ts +68 -0
  34. package/src/signals/__tests__/name-match.test.ts +43 -0
  35. package/src/signals/__tests__/path-hint.test.ts +55 -0
  36. package/src/signals/__tests__/prop-fingerprint.test.ts +105 -0
  37. package/src/signals/__tests__/registry.test.ts +27 -0
  38. package/src/signals/aria-role.ts +94 -0
  39. package/src/signals/barrel-export.ts +28 -0
  40. package/src/signals/html-root.ts +85 -0
  41. package/src/signals/index.ts +39 -0
  42. package/src/signals/input-type.ts +63 -0
  43. package/src/signals/library-reexport.ts +70 -0
  44. package/src/signals/name-match.ts +92 -0
  45. package/src/signals/path-hint.ts +94 -0
  46. package/src/signals/prop-fingerprint.ts +121 -0
  47. package/src/types.ts +58 -0
  48. package/src/vocabulary/canonicals.ts +106 -0
  49. package/src/vocabulary/library-map.ts +301 -0
  50. package/src/vocabulary/prop-fingerprints.ts +433 -0
  51. package/src/vocabulary/synonyms.ts +130 -0
@@ -0,0 +1,433 @@
1
+ // PROP_FINGERPRINT data — `01-architecture.md` §9.5.
2
+ //
3
+ // Each canonical has a fingerprint of required + optional + anti props.
4
+ // The signal scores: 0.3 if all required props are present; +0.05 per optional
5
+ // match (capped at 0.3 total per spec); −0.15 per anti-fingerprint hit.
6
+
7
+ import { canonicalId, type CanonicalId } from '../types.js';
8
+
9
+ export type TypeMatcher = (typeText: string) => boolean;
10
+
11
+ export interface RequiredPropTest {
12
+ // Names in `oneOf` are alternatives — any of them matching counts as the
13
+ // single required prop. Used for canonicals where libraries name the same
14
+ // prop differently (e.g. Checkbox: `onChange` OR `onCheckedChange`).
15
+ oneOf: ReadonlyArray<{ name: string; typeMatcher?: TypeMatcher }>;
16
+ }
17
+
18
+ export interface OptionalPropTest {
19
+ name: string;
20
+ typeMatcher?: TypeMatcher;
21
+ }
22
+
23
+ export interface AntiPropTest {
24
+ name: string;
25
+ typeMatcher?: TypeMatcher;
26
+ }
27
+
28
+ export interface PropFingerprint {
29
+ required: ReadonlyArray<RequiredPropTest>;
30
+ optional: ReadonlyArray<OptionalPropTest>;
31
+ anti: ReadonlyArray<AntiPropTest>;
32
+ }
33
+
34
+ const c = canonicalId;
35
+
36
+ // Primitive type matchers — string predicates, no full type parsing per
37
+ // brief §Open questions(2). Patterns are intentionally permissive.
38
+ const isFunction: TypeMatcher = (text) => /=>/.test(text) || /^Function$/.test(text.trim());
39
+ const isReactNode: TypeMatcher = (text) =>
40
+ /ReactNode|ReactElement|JSX\.Element|React\.(ReactNode|Children|ReactElement)/.test(text) ||
41
+ /string/.test(text);
42
+ const isBoolean: TypeMatcher = (text) => /\bboolean\b/.test(text);
43
+ const isString: TypeMatcher = (text) => /\bstring\b/.test(text);
44
+ const isNumber: TypeMatcher = (text) => /\bnumber\b/.test(text);
45
+ const isOpenChange: TypeMatcher = (text) => /\bopen\b.*=>|=>.*\bvoid\b|=>/.test(text);
46
+ const isOptions: TypeMatcher = (text) => /\b(option|item)s?\b/i.test(text) || /\[\]/.test(text);
47
+ const isDate: TypeMatcher = (text) => /\bDate\b/.test(text);
48
+
49
+ export const PROP_FINGERPRINTS: ReadonlyMap<CanonicalId, PropFingerprint> = new Map([
50
+ [c('Button'), {
51
+ required: [
52
+ { oneOf: [{ name: 'onClick', typeMatcher: isFunction }] },
53
+ { oneOf: [{ name: 'children', typeMatcher: isReactNode }] },
54
+ ],
55
+ optional: [
56
+ { name: 'variant' },
57
+ { name: 'size' },
58
+ { name: 'disabled', typeMatcher: isBoolean },
59
+ { name: 'type' },
60
+ { name: 'loading', typeMatcher: isBoolean },
61
+ { name: 'startIcon' },
62
+ { name: 'endIcon' },
63
+ { name: 'leftIcon' },
64
+ { name: 'rightIcon' },
65
+ ],
66
+ anti: [
67
+ { name: 'value', typeMatcher: isString },
68
+ { name: 'onChange', typeMatcher: isFunction },
69
+ { name: 'checked', typeMatcher: isBoolean },
70
+ ],
71
+ }],
72
+
73
+ [c('IconButton'), {
74
+ required: [
75
+ { oneOf: [{ name: 'onClick', typeMatcher: isFunction }] },
76
+ { oneOf: [{ name: 'icon' }, { name: 'aria-label' }, { name: 'ariaLabel' }] },
77
+ ],
78
+ optional: [
79
+ { name: 'variant' },
80
+ { name: 'size' },
81
+ { name: 'disabled', typeMatcher: isBoolean },
82
+ ],
83
+ anti: [
84
+ { name: 'children' },
85
+ { name: 'value' },
86
+ ],
87
+ }],
88
+
89
+ [c('Checkbox'), {
90
+ required: [
91
+ { oneOf: [{ name: 'checked', typeMatcher: isBoolean }] },
92
+ { oneOf: [
93
+ { name: 'onChange', typeMatcher: isFunction },
94
+ { name: 'onCheckedChange', typeMatcher: isFunction },
95
+ ] },
96
+ ],
97
+ optional: [
98
+ { name: 'defaultChecked', typeMatcher: isBoolean },
99
+ { name: 'indeterminate', typeMatcher: isBoolean },
100
+ { name: 'disabled', typeMatcher: isBoolean },
101
+ ],
102
+ anti: [
103
+ { name: 'children', typeMatcher: isReactNode },
104
+ ],
105
+ }],
106
+
107
+ [c('Switch'), {
108
+ required: [
109
+ { oneOf: [{ name: 'checked', typeMatcher: isBoolean }] },
110
+ { oneOf: [
111
+ { name: 'onChange', typeMatcher: isFunction },
112
+ { name: 'onCheckedChange', typeMatcher: isFunction },
113
+ ] },
114
+ ],
115
+ optional: [
116
+ { name: 'defaultChecked', typeMatcher: isBoolean },
117
+ { name: 'disabled', typeMatcher: isBoolean },
118
+ ],
119
+ anti: [
120
+ { name: 'indeterminate' },
121
+ ],
122
+ }],
123
+
124
+ [c('Dialog'), {
125
+ required: [
126
+ { oneOf: [{ name: 'open', typeMatcher: isBoolean }] },
127
+ { oneOf: [
128
+ { name: 'onOpenChange', typeMatcher: isOpenChange },
129
+ { name: 'onClose', typeMatcher: isFunction },
130
+ ] },
131
+ ],
132
+ optional: [
133
+ { name: 'title' },
134
+ { name: 'description' },
135
+ { name: 'modal', typeMatcher: isBoolean },
136
+ ],
137
+ anti: [
138
+ { name: 'value' },
139
+ { name: 'onChange' },
140
+ ],
141
+ }],
142
+
143
+ [c('Tooltip'), {
144
+ required: [
145
+ { oneOf: [{ name: 'content' }, { name: 'title' }] },
146
+ ],
147
+ optional: [
148
+ { name: 'delayDuration', typeMatcher: isNumber },
149
+ { name: 'side' },
150
+ { name: 'placement' },
151
+ ],
152
+ anti: [
153
+ { name: 'open', typeMatcher: isBoolean },
154
+ { name: 'value' },
155
+ ],
156
+ }],
157
+
158
+ [c('Tabs'), {
159
+ required: [
160
+ { oneOf: [{ name: 'value', typeMatcher: isString }] },
161
+ { oneOf: [
162
+ { name: 'onValueChange', typeMatcher: isFunction },
163
+ { name: 'onChange', typeMatcher: isFunction },
164
+ ] },
165
+ ],
166
+ optional: [
167
+ { name: 'defaultValue', typeMatcher: isString },
168
+ { name: 'orientation' },
169
+ ],
170
+ anti: [
171
+ { name: 'checked' },
172
+ ],
173
+ }],
174
+
175
+ [c('Combobox'), {
176
+ required: [
177
+ { oneOf: [
178
+ { name: 'value', typeMatcher: isString },
179
+ { name: 'inputValue', typeMatcher: isString },
180
+ ] },
181
+ { oneOf: [
182
+ { name: 'onValueChange', typeMatcher: isFunction },
183
+ { name: 'onInputChange', typeMatcher: isFunction },
184
+ { name: 'onChange', typeMatcher: isFunction },
185
+ ] },
186
+ { oneOf: [
187
+ { name: 'options', typeMatcher: isOptions },
188
+ { name: 'items', typeMatcher: isOptions },
189
+ ] },
190
+ ],
191
+ optional: [
192
+ { name: 'placeholder', typeMatcher: isString },
193
+ { name: 'disabled', typeMatcher: isBoolean },
194
+ ],
195
+ anti: [],
196
+ }],
197
+
198
+ [c('Select'), {
199
+ required: [
200
+ { oneOf: [{ name: 'value', typeMatcher: isString }] },
201
+ { oneOf: [
202
+ { name: 'onValueChange', typeMatcher: isFunction },
203
+ { name: 'onChange', typeMatcher: isFunction },
204
+ ] },
205
+ ],
206
+ optional: [
207
+ { name: 'options', typeMatcher: isOptions },
208
+ { name: 'placeholder', typeMatcher: isString },
209
+ { name: 'disabled', typeMatcher: isBoolean },
210
+ ],
211
+ anti: [
212
+ { name: 'inputValue' },
213
+ { name: 'multiple', typeMatcher: isBoolean },
214
+ ],
215
+ }],
216
+
217
+ [c('Slider'), {
218
+ required: [
219
+ { oneOf: [{ name: 'value', typeMatcher: isNumber }] },
220
+ { oneOf: [
221
+ { name: 'onValueChange', typeMatcher: isFunction },
222
+ { name: 'onChange', typeMatcher: isFunction },
223
+ ] },
224
+ ],
225
+ optional: [
226
+ { name: 'min', typeMatcher: isNumber },
227
+ { name: 'max', typeMatcher: isNumber },
228
+ { name: 'step', typeMatcher: isNumber },
229
+ ],
230
+ anti: [
231
+ { name: 'checked' },
232
+ ],
233
+ }],
234
+
235
+ [c('Input'), {
236
+ required: [
237
+ { oneOf: [{ name: 'value', typeMatcher: isString }] },
238
+ { oneOf: [{ name: 'onChange', typeMatcher: isFunction }] },
239
+ ],
240
+ optional: [
241
+ { name: 'placeholder', typeMatcher: isString },
242
+ { name: 'type', typeMatcher: isString },
243
+ { name: 'disabled', typeMatcher: isBoolean },
244
+ ],
245
+ anti: [
246
+ { name: 'options' },
247
+ { name: 'open', typeMatcher: isBoolean },
248
+ ],
249
+ }],
250
+
251
+ [c('Textarea'), {
252
+ required: [
253
+ { oneOf: [{ name: 'value', typeMatcher: isString }] },
254
+ { oneOf: [{ name: 'onChange', typeMatcher: isFunction }] },
255
+ { oneOf: [{ name: 'rows', typeMatcher: isNumber }, { name: 'minRows', typeMatcher: isNumber }] },
256
+ ],
257
+ optional: [
258
+ { name: 'placeholder', typeMatcher: isString },
259
+ { name: 'disabled', typeMatcher: isBoolean },
260
+ ],
261
+ anti: [],
262
+ }],
263
+
264
+ [c('NumberInput'), {
265
+ required: [
266
+ { oneOf: [{ name: 'value', typeMatcher: isNumber }] },
267
+ { oneOf: [{ name: 'onChange', typeMatcher: isFunction }] },
268
+ ],
269
+ optional: [
270
+ { name: 'min', typeMatcher: isNumber },
271
+ { name: 'max', typeMatcher: isNumber },
272
+ { name: 'step', typeMatcher: isNumber },
273
+ ],
274
+ anti: [],
275
+ }],
276
+
277
+ [c('DatePicker'), {
278
+ required: [
279
+ { oneOf: [{ name: 'value', typeMatcher: isDate }, { name: 'date', typeMatcher: isDate }, { name: 'selected', typeMatcher: isDate }] },
280
+ { oneOf: [{ name: 'onChange', typeMatcher: isFunction }, { name: 'onDateChange', typeMatcher: isFunction }, { name: 'onSelect', typeMatcher: isFunction }] },
281
+ ],
282
+ optional: [
283
+ { name: 'minDate' },
284
+ { name: 'maxDate' },
285
+ { name: 'format', typeMatcher: isString },
286
+ ],
287
+ anti: [],
288
+ }],
289
+
290
+ [c('Card'), {
291
+ required: [
292
+ { oneOf: [{ name: 'children', typeMatcher: isReactNode }] },
293
+ ],
294
+ optional: [
295
+ { name: 'title' },
296
+ { name: 'footer' },
297
+ ],
298
+ anti: [
299
+ { name: 'value' },
300
+ { name: 'onChange' },
301
+ { name: 'onClick' },
302
+ { name: 'open' },
303
+ { name: 'checked' },
304
+ ],
305
+ }],
306
+
307
+ [c('Badge'), {
308
+ required: [
309
+ { oneOf: [{ name: 'children', typeMatcher: isReactNode }] },
310
+ ],
311
+ optional: [
312
+ { name: 'variant' },
313
+ { name: 'count', typeMatcher: isNumber },
314
+ ],
315
+ anti: [
316
+ { name: 'onClick' },
317
+ { name: 'value' },
318
+ ],
319
+ }],
320
+
321
+ [c('Avatar'), {
322
+ required: [
323
+ { oneOf: [{ name: 'src', typeMatcher: isString }, { name: 'name', typeMatcher: isString }, { name: 'alt', typeMatcher: isString }] },
324
+ ],
325
+ optional: [
326
+ { name: 'size' },
327
+ { name: 'fallback' },
328
+ ],
329
+ anti: [
330
+ { name: 'value' },
331
+ { name: 'onChange' },
332
+ ],
333
+ }],
334
+
335
+ [c('Progress'), {
336
+ required: [
337
+ { oneOf: [{ name: 'value', typeMatcher: isNumber }] },
338
+ ],
339
+ optional: [
340
+ { name: 'max', typeMatcher: isNumber },
341
+ { name: 'indeterminate', typeMatcher: isBoolean },
342
+ ],
343
+ anti: [
344
+ { name: 'onChange' },
345
+ ],
346
+ }],
347
+
348
+ [c('Alert'), {
349
+ required: [
350
+ { oneOf: [{ name: 'children', typeMatcher: isReactNode }, { name: 'message' }, { name: 'title' }] },
351
+ ],
352
+ optional: [
353
+ { name: 'severity' },
354
+ { name: 'variant' },
355
+ { name: 'icon' },
356
+ ],
357
+ anti: [
358
+ { name: 'value' },
359
+ { name: 'onChange' },
360
+ { name: 'onClick' },
361
+ ],
362
+ }],
363
+
364
+ [c('Toast'), {
365
+ required: [
366
+ { oneOf: [{ name: 'message' }, { name: 'title' }, { name: 'children' }] },
367
+ { oneOf: [{ name: 'duration', typeMatcher: isNumber }, { name: 'autoHideDuration', typeMatcher: isNumber }, { name: 'onClose', typeMatcher: isFunction }] },
368
+ ],
369
+ optional: [
370
+ { name: 'severity' },
371
+ { name: 'position' },
372
+ ],
373
+ anti: [],
374
+ }],
375
+
376
+ [c('Accordion'), {
377
+ required: [
378
+ { oneOf: [{ name: 'value' }, { name: 'expandedItems' }, { name: 'defaultValue' }] },
379
+ ],
380
+ optional: [
381
+ { name: 'type' },
382
+ { name: 'collapsible', typeMatcher: isBoolean },
383
+ { name: 'multiple', typeMatcher: isBoolean },
384
+ ],
385
+ anti: [
386
+ { name: 'open', typeMatcher: isBoolean },
387
+ ],
388
+ }],
389
+
390
+ [c('Drawer'), {
391
+ required: [
392
+ { oneOf: [{ name: 'open', typeMatcher: isBoolean }] },
393
+ { oneOf: [
394
+ { name: 'onOpenChange', typeMatcher: isOpenChange },
395
+ { name: 'onClose', typeMatcher: isFunction },
396
+ ] },
397
+ ],
398
+ optional: [
399
+ { name: 'side' },
400
+ { name: 'anchor' },
401
+ { name: 'placement' },
402
+ ],
403
+ anti: [],
404
+ }],
405
+
406
+ [c('Popover'), {
407
+ required: [
408
+ { oneOf: [{ name: 'open', typeMatcher: isBoolean }] },
409
+ { oneOf: [
410
+ { name: 'onOpenChange', typeMatcher: isOpenChange },
411
+ { name: 'anchorEl' },
412
+ ] },
413
+ ],
414
+ optional: [
415
+ { name: 'side' },
416
+ { name: 'placement' },
417
+ ],
418
+ anti: [
419
+ { name: 'modal', typeMatcher: isBoolean },
420
+ ],
421
+ }],
422
+ ]);
423
+
424
+ // §9.5 polymorphic-prop penalty: when `as`/`component`/`polymorphicAs` is on
425
+ // the component, every hypothesis loses 0.1.
426
+ export const POLYMORPHIC_PROP_NAMES: ReadonlySet<string> = new Set([
427
+ 'as',
428
+ 'component',
429
+ 'polymorphicAs',
430
+ 'asChild',
431
+ // `asChild` is borderline (Radix uses it on real primitives), but combined
432
+ // with the heavy anti-correlation we still apply the small penalty.
433
+ ]);
@@ -0,0 +1,130 @@
1
+ // NAME_MATCH synonym table — `01-architecture.md` §5 (final paragraph) + §9.6.
2
+ //
3
+ // Each canonical entry has a synonym list of names that should match after
4
+ // strip rules are applied. Matching is case-insensitive against the stripped
5
+ // name. Synonyms are weight-bearing only when at least one other signal agrees
6
+ // (enforced by the combiner via the §10.3 strict signal-count rule).
7
+
8
+ import { canonicalId, type CanonicalId } from '../types.js';
9
+
10
+ const c = canonicalId;
11
+
12
+ export const NAME_STRIP_PREFIXES: ReadonlyArray<string> = [
13
+ 'Mui',
14
+ 'Chakra',
15
+ 'Ant',
16
+ 'Mantine',
17
+ 'Radix',
18
+ 'Base',
19
+ ];
20
+
21
+ export const NAME_STRIP_SUFFIXES: ReadonlyArray<string> = [
22
+ 'Base',
23
+ 'Root',
24
+ 'Primitive',
25
+ 'Inner',
26
+ 'Component',
27
+ 'Impl',
28
+ ];
29
+
30
+ // Variant suffixes are only stripped if the remainder is a canonical name
31
+ // (handled in the signal logic, not unconditionally).
32
+ export const NAME_VARIANT_SUFFIXES: ReadonlyArray<string> = [
33
+ 'Primary',
34
+ 'Secondary',
35
+ 'Outline',
36
+ 'Ghost',
37
+ 'Solid',
38
+ ];
39
+
40
+ // Synonyms map — keyed by canonical id, values are aliases (case-folded by
41
+ // the signal at lookup time). The canonical id itself does NOT need to appear
42
+ // in its own synonym list; the signal includes it implicitly.
43
+ export const SYNONYMS: ReadonlyMap<CanonicalId, ReadonlyArray<string>> = new Map([
44
+ [c('Button'), ['Btn', 'BaseButton', 'PrimaryButton', 'ActionButton', 'CTAButton']],
45
+ [c('IconButton'), ['IconBtn', 'IconAction']],
46
+ [c('ToggleButton'), ['Toggle', 'ToggleBtn']],
47
+ [c('Input'), ['TextInput', 'TextField', 'TextBox', 'StringInput']],
48
+ [c('Textarea'), ['TextArea', 'MultilineInput', 'TextAreaInput']],
49
+ [c('NumberInput'), ['NumberField', 'NumericInput']],
50
+ [c('PasswordInput'), ['PasswordField']],
51
+ [c('Checkbox'), ['CheckBox', 'Check']],
52
+ [c('Radio'), ['RadioInput', 'RadioButton']],
53
+ [c('RadioGroup'), ['RadioButtonGroup', 'RadioList']],
54
+ [c('Switch'), ['Toggle', 'OnOff', 'SwitchInput']],
55
+ [c('Slider'), ['Range', 'RangeInput']],
56
+ [c('Select'), ['Dropdown', 'DropDown', 'SelectInput']],
57
+ [c('Combobox'), ['Autocomplete', 'AutoComplete', 'ComboBox', 'TypeAhead']],
58
+ [c('MultiSelect'), ['MultiSelectInput', 'TagsInput']],
59
+ [c('DatePicker'), ['DateInput', 'DateField']],
60
+ [c('TimePicker'), ['TimeInput']],
61
+ [c('Calendar'), ['DatePickerCalendar', 'CalendarGrid']],
62
+ [c('Form'), []],
63
+ [c('Field'), ['FormField', 'FieldGroup']],
64
+ [c('Label'), ['FormLabel']],
65
+ [c('FieldError'), ['ErrorMessage', 'FormError']],
66
+ [c('FieldHint'), ['HelperText', 'FormHelperText', 'Hint']],
67
+
68
+ [c('Dialog'), ['Modal', 'DialogModal']],
69
+ [c('AlertDialog'), ['ConfirmDialog', 'ConfirmModal']],
70
+ [c('Drawer'), ['SidePanel', 'SideDrawer']],
71
+ [c('Sheet'), ['BottomSheet']],
72
+ [c('Popover'), ['Pop', 'PopoverPanel']],
73
+ [c('Tooltip'), ['Tip', 'ToolTip']],
74
+ [c('HoverCard'), ['HoverPreview']],
75
+ [c('Toast'), ['Notification', 'Snackbar']],
76
+
77
+ [c('Tabs'), ['TabGroup', 'TabBar']],
78
+ [c('Accordion'), ['Collapse', 'Collapsible']],
79
+ [c('Disclosure'), ['Details']],
80
+ [c('Menu'), ['DropdownMenu', 'DropDownMenu', 'ActionMenu']],
81
+ [c('ContextMenu'), ['RightClickMenu']],
82
+ [c('MenuBar'), ['MenuStrip']],
83
+ [c('Breadcrumb'), ['Breadcrumbs', 'BreadCrumb']],
84
+ [c('Pagination'), ['Paginator', 'PageNav']],
85
+ [c('Stepper'), ['Steps', 'Wizard', 'StepIndicator']],
86
+ [c('NavigationMenu'), ['NavMenu', 'NavBar', 'Navigation']],
87
+
88
+ [c('Card'), ['Panel', 'Tile']],
89
+ [c('Badge'), ['Pill']],
90
+ [c('Chip'), ['Tag']],
91
+ [c('Tag'), ['Chip']],
92
+ [c('Avatar'), ['Profile', 'ProfilePicture', 'UserAvatar']],
93
+ [c('Alert'), ['Notice', 'Message']],
94
+ [c('Banner'), ['Notification', 'Notice']],
95
+ [c('Skeleton'), ['Placeholder', 'Loading']],
96
+ [c('Spinner'), ['Loader', 'LoadingSpinner']],
97
+ [c('Progress'), ['ProgressBar']],
98
+ [c('Separator'), ['Divider', 'Hr']],
99
+
100
+ [c('Table'), []],
101
+ [c('DataTable'), ['DataGrid']],
102
+ [c('List'), ['ItemList']],
103
+ [c('Tree'), ['TreeView']],
104
+ [c('ScrollArea'), ['ScrollBox', 'ScrollContainer']],
105
+
106
+ [c('Stack'), ['HStack', 'VStack', 'Flex']],
107
+ [c('Grid'), ['GridLayout']],
108
+ [c('Box'), ['View']],
109
+ [c('Container'), ['Wrapper']],
110
+ ]);
111
+
112
+ // Reverse index: lowercased synonym → canonical(s) it points to. Built once.
113
+ const REVERSE_SYNONYMS: ReadonlyMap<string, ReadonlyArray<CanonicalId>> = (() => {
114
+ const map = new Map<string, CanonicalId[]>();
115
+ for (const [canon, aliases] of SYNONYMS.entries()) {
116
+ const canonLower = (canon as string).toLowerCase();
117
+ if (!map.has(canonLower)) map.set(canonLower, []);
118
+ map.get(canonLower)!.push(canon);
119
+ for (const alias of aliases) {
120
+ const key = alias.toLowerCase();
121
+ if (!map.has(key)) map.set(key, []);
122
+ map.get(key)!.push(canon);
123
+ }
124
+ }
125
+ return map;
126
+ })();
127
+
128
+ export function lookupSynonym(name: string): ReadonlyArray<CanonicalId> {
129
+ return REVERSE_SYNONYMS.get(name.toLowerCase()) ?? [];
130
+ }