@astryxdesign/cli 0.1.1 → 0.1.2-canary.2149fa3

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 (113) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/docs/theme.doc.mjs +2 -2
  3. package/docs/working-with-ai.doc.mjs +6 -6
  4. package/package.json +19 -8
  5. package/src/api/discover.mjs +78 -26
  6. package/src/api/index.mjs +1 -0
  7. package/src/api/layout.mjs +301 -0
  8. package/src/api/layout.test.mjs +238 -0
  9. package/src/api/template.mjs +191 -50
  10. package/src/api/template.test.mjs +2 -0
  11. package/src/api/theme-add.mjs +182 -0
  12. package/src/codemods/__tests__/registry.test.mjs +1 -0
  13. package/src/codemods/registry.mjs +1 -0
  14. package/src/codemods/transforms/v0.1.2/__tests__/rename-text-color-active-to-accent.test.mjs +120 -0
  15. package/src/codemods/transforms/v0.1.2/index.mjs +19 -0
  16. package/src/codemods/transforms/v0.1.2/rename-text-color-active-to-accent.mjs +136 -0
  17. package/src/commands/build-theme.import-path.test.mjs +22 -2
  18. package/src/commands/build-theme.mjs +185 -16
  19. package/src/commands/gap-report.mjs +17 -9
  20. package/src/commands/gap-report.test.mjs +21 -16
  21. package/src/commands/init.mjs +34 -8
  22. package/src/commands/init.next-steps.test.mjs +46 -0
  23. package/src/commands/layout.mjs +139 -0
  24. package/src/commands/swizzle.mjs +51 -23
  25. package/src/commands/upgrade.mjs +2 -71
  26. package/src/config.mjs +31 -0
  27. package/src/config.test.mjs +24 -0
  28. package/src/index.mjs +6 -0
  29. package/src/lib/config-schema.mjs +119 -0
  30. package/src/lib/config.mjs +34 -7
  31. package/src/lib/config.test.mjs +51 -2
  32. package/src/lib/error-codes.mjs +11 -0
  33. package/src/lib/integrations.mjs +155 -0
  34. package/src/lib/integrations.test.mjs +154 -0
  35. package/src/lib/levenshtein.mjs +29 -0
  36. package/src/lib/manifest.mjs +10 -0
  37. package/src/lib/package-scanner.mjs +31 -7
  38. package/src/lib/string-utils.mjs +5 -14
  39. package/src/lib/xle/browser.d.ts +100 -0
  40. package/src/lib/xle/browser.mjs +120 -0
  41. package/src/lib/xle/expand.mjs +622 -0
  42. package/src/lib/xle/parse.mjs +581 -0
  43. package/src/lib/xle/print.mjs +174 -0
  44. package/src/lib/xle/registry-core.mjs +170 -0
  45. package/src/lib/xle/registry.mjs +237 -0
  46. package/src/lib/xle/splice.mjs +137 -0
  47. package/src/lib/xle/validate.mjs +356 -0
  48. package/src/lib/xle/xle.test.mjs +333 -0
  49. package/src/types/config.d.ts +99 -0
  50. package/src/utils/github.mjs +12 -27
  51. package/templates/blocks/components/Card/ClickableCardWithNestedButton.doc.mjs +1 -1
  52. package/templates/blocks/components/Card/SelectableCardMulti.doc.mjs +1 -1
  53. package/templates/blocks/components/ChatComposerDrawer/ChatComposerDrawerWithProgress.tsx +1 -1
  54. package/templates/blocks/components/ChatDictationButton/ChatDictationButtonShowcase.tsx +3 -3
  55. package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.doc.mjs +15 -0
  56. package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.tsx +26 -0
  57. package/templates/blocks/components/DateInput/DateInputDateRange.doc.mjs +2 -2
  58. package/templates/blocks/components/FileInput/FileInputShowcase.tsx +4 -2
  59. package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.doc.mjs +14 -0
  60. package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.tsx +43 -0
  61. package/templates/blocks/components/MultiSelector/MultiSelectorShowcase.tsx +6 -2
  62. package/templates/blocks/components/NumberInput/NumberInputShowcase.tsx +6 -2
  63. package/templates/blocks/components/Outline/OutlineShowcase.doc.mjs +15 -0
  64. package/templates/blocks/components/Outline/OutlineShowcase.tsx +22 -0
  65. package/templates/blocks/components/RadioList/RadioListShowcase.tsx +8 -1
  66. package/templates/blocks/components/SegmentedControl/SegmentedControlShowcase.tsx +3 -1
  67. package/templates/blocks/components/Selector/SelectorShowcase.tsx +4 -1
  68. package/templates/blocks/components/Slider/SliderShowcase.tsx +10 -1
  69. package/templates/blocks/components/Tab/TabShowcase.tsx +3 -1
  70. package/templates/blocks/components/TabList/TabListShowcase.tsx +3 -1
  71. package/templates/blocks/components/TabMenu/TabMenuShowcase.tsx +3 -1
  72. package/templates/blocks/components/Table/ColumnResizeHookUsage.doc.mjs +14 -0
  73. package/templates/blocks/components/Table/ColumnResizeHookUsage.tsx +59 -0
  74. package/templates/blocks/components/Table/StickyColumnsHookUsage.doc.mjs +14 -0
  75. package/templates/blocks/components/Table/StickyColumnsHookUsage.tsx +104 -0
  76. package/templates/blocks/components/Text/TextColors.tsx +20 -5
  77. package/templates/blocks/components/TextArea/TextAreaShowcase.tsx +4 -2
  78. package/templates/blocks/components/TextInput/TextInputShowcase.tsx +4 -2
  79. package/templates/blocks/components/Thumbnail/ThumbnailDisabled.tsx +11 -6
  80. package/templates/blocks/components/Thumbnail/ThumbnailGallery.tsx +43 -7
  81. package/templates/blocks/components/Thumbnail/ThumbnailRemovable.tsx +40 -3
  82. package/templates/blocks/components/Thumbnail/ThumbnailShowcase.tsx +6 -5
  83. package/templates/blocks/components/Thumbnail/ThumbnailStates.tsx +11 -6
  84. package/templates/blocks/components/Timestamp/TimestampColors.tsx +2 -2
  85. package/templates/blocks/components/ToggleButton/ToggleButtonGroup.doc.mjs +1 -1
  86. package/templates/blocks/components/Tokenizer/TokenizerShowcase.tsx +8 -6
  87. package/templates/blocks/components/Toolbar/ToolbarTableFilter.doc.mjs +2 -2
  88. package/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +23 -14
  89. package/templates/blocks/components/Typeahead/TypeaheadShowcase.tsx +6 -2
  90. package/templates/pages/shell-nav/page.tsx +321 -0
  91. package/templates/pages/shell-nav/template.doc.mjs +12 -0
  92. package/templates/pages/shell-side-nav/page.tsx +242 -0
  93. package/templates/pages/shell-side-nav/template.doc.mjs +12 -0
  94. package/templates/pages/shell-top-nav/page.tsx +224 -0
  95. package/templates/pages/shell-top-nav/template.doc.mjs +12 -0
  96. package/templates/pages/theme-showcase/page.tsx +8 -19
  97. package/templates/themes/butter/butterTheme.ts +916 -0
  98. package/templates/themes/butter/icons.tsx +77 -0
  99. package/templates/themes/chocolate/chocolateTheme.ts +230 -0
  100. package/templates/themes/chocolate/icons.tsx +77 -0
  101. package/templates/themes/gothic/gothicTheme.ts +657 -0
  102. package/templates/themes/gothic/icons.tsx +77 -0
  103. package/templates/themes/manifest.json +90 -0
  104. package/templates/themes/matcha/icons.tsx +67 -0
  105. package/templates/themes/matcha/matchaTheme.ts +247 -0
  106. package/templates/themes/neutral/icons.tsx +77 -0
  107. package/templates/themes/neutral/neutralTheme.ts +603 -0
  108. package/templates/themes/stone/icons.tsx +77 -0
  109. package/templates/themes/stone/stoneTheme.ts +652 -0
  110. package/templates/themes/y2k/icons.tsx +67 -0
  111. package/templates/themes/y2k/y2kTheme.ts +617 -0
  112. package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.doc.mjs +0 -14
  113. package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.tsx +0 -57
@@ -0,0 +1,333 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Unit tests for the XLE/XLO parser, printers, and validator.
5
+ * Parser/printer tests are registry-free; validator tests use a small
6
+ * synthetic registry so they don't depend on @xds/core docs.
7
+ */
8
+
9
+ import {describe, it, expect} from 'vitest';
10
+ import {parse, parseCompact, parseOutline, detectForm, XLEParseError} from './parse.mjs';
11
+ import {toCompact, toOutline} from './print.mjs';
12
+ import {validate} from './validate.mjs';
13
+ import {parseEnumValues, SPACING_STEPS} from './registry.mjs';
14
+
15
+ // ─── parser: compact ───────────────────────────────────────────────────────
16
+
17
+ describe('parseCompact', () => {
18
+ it('parses child and sibling operators', () => {
19
+ const doc = parseCompact('A > B + C');
20
+ expect(doc.roots).toHaveLength(1);
21
+ expect(doc.roots[0].name).toBe('A');
22
+ expect(doc.roots[0].children.map(c => c.name)).toEqual(['B', 'C']);
23
+ });
24
+
25
+ it('parses groups with repeat', () => {
26
+ const doc = parseCompact('V > (C + D)*3');
27
+ const group = doc.roots[0].children[0];
28
+ expect(group.kind).toBe('group');
29
+ expect(group.repeat).toBe(3);
30
+ expect(group.children.map(c => c.name)).toEqual(['C', 'D']);
31
+ });
32
+
33
+ it('parses node anatomy: id, enum mods, payload, attrs, hint, repeat, selected', () => {
34
+ const doc = parseCompact('Tab#main.lg"Overview"[divider sz=md]{card-callout}!*2');
35
+ const node = doc.roots[0];
36
+ expect(node.id).toBe('main');
37
+ expect(node.enumMods).toEqual(['lg']);
38
+ expect(node.payload).toBe('Overview');
39
+ expect(node.attrs).toEqual([
40
+ expect.objectContaining({kind: 'flag', key: 'divider'}),
41
+ expect.objectContaining({kind: 'kv', key: 'sz', value: 'md'}),
42
+ ]);
43
+ expect(node.hint.name).toBe('card-callout');
44
+ expect(node.selected).toBe(true);
45
+ expect(node.repeat).toBe(2);
46
+ });
47
+
48
+ it('parses fused shorthands, objects, lists, and negation', () => {
49
+ const doc = parseCompact('G[c{min:340,fit} g4 dv=[top,bottom] !scroll]');
50
+ const attrs = doc.roots[0].attrs;
51
+ expect(attrs[0]).toEqual(expect.objectContaining({key: 'c', value: {min: 340, fit: true}}));
52
+ expect(attrs[1]).toEqual(expect.objectContaining({key: 'g', value: 4}));
53
+ expect(attrs[2]).toEqual(expect.objectContaining({key: 'dv', value: ['top', 'bottom']}));
54
+ expect(attrs[3]).toEqual(expect.objectContaining({kind: 'neg', key: 'scroll'}));
55
+ });
56
+
57
+ it('parses slot attrs with nested sub-expressions', () => {
58
+ const doc = parseCompact('A[@topNav=(TN > TNI"Home" + TNI"Docs") @banner=\'hello\']');
59
+ const [topNav, banner] = doc.roots[0].slots;
60
+ expect(topNav.key).toBe('topNav');
61
+ expect(topNav.value.subexpr[0].name).toBe('TN');
62
+ expect(topNav.value.subexpr[0].children).toHaveLength(2);
63
+ expect(banner.value).toBe('hello');
64
+ });
65
+
66
+ it('parses detached overlays after ;;', () => {
67
+ const doc = parseCompact('V > B"Delete"[opens=#confirm] ;; AD#confirm"Sure?"');
68
+ expect(doc.roots).toHaveLength(1);
69
+ expect(doc.overlays).toHaveLength(1);
70
+ expect(doc.overlays[0].id).toBe('confirm');
71
+ });
72
+
73
+ it('rejects ^ with a correction toward groups', () => {
74
+ expect(() => parseCompact('A > B ^ C')).toThrow(/group siblings with \(\.\.\.\)/);
75
+ });
76
+
77
+ it('rejects brace content that is not a block name, teaching the payload form', () => {
78
+ try {
79
+ parseCompact('C{Total Revenue}');
80
+ expect.unreachable();
81
+ } catch (e) {
82
+ expect(e).toBeInstanceOf(XLEParseError);
83
+ expect(e.message).toMatch(/not a block reference/);
84
+ expect(e.message).toMatch(/quoted payload/);
85
+ }
86
+ });
87
+
88
+ it('reports unterminated constructs with positions', () => {
89
+ expect(() => parseCompact('V[g6 > C')).toThrow(XLEParseError);
90
+ expect(() => parseCompact('Tx"unclosed')).toThrow(/Unterminated/);
91
+ });
92
+ });
93
+
94
+ // ─── parser: outline ───────────────────────────────────────────────────────
95
+
96
+ const OUTLINE = `
97
+ AppShell
98
+ topNav: TN "Acme"
99
+ Layout > LC !scroll
100
+ V g=6
101
+ Tx.lg "Hello"
102
+ repeat 3:
103
+ C {card-callout}
104
+ Tbar "Actions"
105
+ B "Delete" opens=#confirm
106
+
107
+ overlays:
108
+ AD#confirm "Sure?"
109
+ `;
110
+
111
+ describe('parseOutline', () => {
112
+ it('parses indentation, slot lines, inline chains, repeats, overlays', () => {
113
+ const doc = parseOutline(OUTLINE);
114
+ const shell = doc.roots[0];
115
+ expect(shell.name).toBe('AppShell');
116
+ expect(shell.slots[0].key).toBe('topNav');
117
+ expect(shell.slots[0].value.subexpr[0].payload).toBe('Acme');
118
+
119
+ const layout = shell.children[0];
120
+ expect(layout.name).toBe('Layout');
121
+ const lc = layout.children[0];
122
+ expect(lc.name).toBe('LC');
123
+ expect(lc.attrs[0]).toEqual(expect.objectContaining({kind: 'neg', key: 'scroll'}));
124
+
125
+ const v = lc.children[0];
126
+ expect(v.children[0].enumMods).toEqual(['lg']);
127
+ const repeat = v.children[1];
128
+ expect(repeat.kind).toBe('group');
129
+ expect(repeat.repeat).toBe(3);
130
+ expect(repeat.children[0].hint.name).toBe('card-callout');
131
+
132
+ // Tbar is a sibling of V (child of LC), not swallowed by the repeat block
133
+ expect(lc.children.map(c => c.name ?? 'group')).toEqual(['V', 'Tbar']);
134
+ expect(doc.overlays).toHaveLength(1);
135
+ expect(doc.overlays[0].id).toBe('confirm');
136
+ });
137
+
138
+ it('parses block-form slots', () => {
139
+ const doc = parseOutline('ChL\n composer:\n ChC\n ChML');
140
+ const chl = doc.roots[0];
141
+ expect(chl.slots[0].key).toBe('composer');
142
+ expect(chl.slots[0].value.subexpr[0].name).toBe('ChC');
143
+ expect(chl.children.map(c => c.name)).toEqual(['ChML']);
144
+ });
145
+
146
+ it('errors on a slot line with no parent component', () => {
147
+ expect(() => parseOutline('topNav: TN')).toThrow(/no parent component/);
148
+ });
149
+ });
150
+
151
+ // ─── form detection ────────────────────────────────────────────────────────
152
+
153
+ describe('detectForm', () => {
154
+ it('single line → compact', () => {
155
+ expect(detectForm('A > B + C')).toBe('compact');
156
+ });
157
+ it('multi-line ending in operators → compact', () => {
158
+ expect(detectForm('A > B +\n C > D')).toBe('compact');
159
+ });
160
+ it('indented multi-line → outline', () => {
161
+ expect(detectForm('A\n B\n C')).toBe('outline');
162
+ });
163
+ it('repeat/overlays markers → outline', () => {
164
+ expect(detectForm('V\nrepeat 2:\n C')).toBe('outline');
165
+ });
166
+ });
167
+
168
+ // ─── printers: round-trips ─────────────────────────────────────────────────
169
+
170
+ /** Strip positions for structural comparison. */
171
+ function shape(doc) {
172
+ return JSON.parse(JSON.stringify(doc, (key, value) =>
173
+ key === 'line' || key === 'col' || key === 'form' || key === 'raw' ? undefined : value));
174
+ }
175
+
176
+ const ROUND_TRIP_CASES = [
177
+ 'A > B + C',
178
+ 'V[g6] > (C{card-callout}*4) + T[striped]',
179
+ 'Ctr[h=100dvh] > C[w=400 p8] > B.primary"Sign in"',
180
+ 'A[@topNav=(TN > TNI"Home")] > L > LC[!scroll] > Tx"hi"',
181
+ 'V > B"Del"[opens=#confirm] ;; AD#confirm"Sure?"',
182
+ 'TL > Tab"Overview"! + Tab"Activity"',
183
+ ];
184
+
185
+ describe('print round-trips', () => {
186
+ for (const source of ROUND_TRIP_CASES) {
187
+ it(`compact → compact: ${source}`, () => {
188
+ const doc = parse(source, {form: 'compact'});
189
+ const reparsed = parse(toCompact(doc), {form: 'compact'});
190
+ expect(shape(reparsed)).toEqual(shape(doc));
191
+ });
192
+
193
+ it(`compact → outline → parse: ${source}`, () => {
194
+ const doc = parse(source, {form: 'compact'});
195
+ const outline = toOutline(doc);
196
+ const reparsed = parse(outline, {form: 'outline'});
197
+ // Outline flattens repeat-less disambiguation groups — compare the
198
+ // outline printed from both, which is canonical.
199
+ expect(toOutline(reparsed)).toEqual(outline);
200
+ });
201
+ }
202
+
203
+ it('outline → compact → parse → outline is stable', () => {
204
+ const doc = parseOutline(OUTLINE);
205
+ const compact = toCompact(doc);
206
+ const reparsed = parseCompact(compact);
207
+ expect(toOutline(reparsed)).toEqual(toOutline(doc));
208
+ });
209
+ });
210
+
211
+ // ─── validator (synthetic registry) ────────────────────────────────────────
212
+
213
+ function prop(name, type, required = false) {
214
+ return {
215
+ name, type, required,
216
+ enumValues: parseEnumValues(type),
217
+ isBoolean: type === 'boolean',
218
+ isFunction: /^\(/.test(type),
219
+ isNode: /ReactNode/.test(type),
220
+ };
221
+ }
222
+
223
+ function makeTestRegistry() {
224
+ const make = (name, props) => ({
225
+ name, exportName: `XDS${name}`, dirName: name, importPath: `@xds/core/${name}`,
226
+ props: new Map(props.map(p => [p.name, p])),
227
+ });
228
+ const components = new Map([
229
+ ['AppShell', make('AppShell', [prop('contentPadding', 'SpacingStep'), prop('children', 'ReactNode'), prop('topNav', 'ReactNode')])],
230
+ ['Stack', make('Stack', [prop('gap', 'SpacingStep'), prop('hAlign', "'start' | 'center' | 'end' | 'between'"), prop('vAlign', "'start' | 'center' | 'end'")])],
231
+ ['Chip', make('Chip', [prop('variant', "'neutral' | 'success' | 'error'"), prop('label', 'string', true)])],
232
+ ['Box', make('Box', [prop('isScrollable', 'boolean'), prop('children', 'ReactNode')])],
233
+ ]);
234
+ const aliases = new Map([['Sh', 'AppShell'], ['St', 'Stack'], ['Ch', 'Chip'], ['Bx', 'Box']]);
235
+ return {components, aliases, componentNames: [...components.keys()]};
236
+ }
237
+
238
+ const BLOCKS = [{dirName: 'CardCallout', description: '', category: 'components/Card'}];
239
+
240
+ describe('validate', () => {
241
+ const registry = makeTestRegistry();
242
+
243
+ it('binds aliases, full names, and XDS-prefixed names identically', () => {
244
+ for (const name of ['Sh', 'AppShell', 'XDSAppShell']) {
245
+ const doc = parse(`${name}[cp6]`);
246
+ const {errors} = validate(doc, registry, BLOCKS);
247
+ expect(errors).toEqual([]);
248
+ expect(doc.roots[0].bound.props.get('contentPadding')).toBe(6);
249
+ }
250
+ });
251
+
252
+ it('rejects unknown components with ranked suggestions', () => {
253
+ const doc = parse('AppShel');
254
+ const {errors} = validate(doc, registry, BLOCKS);
255
+ expect(errors[0].message).toMatch(/Unknown component/);
256
+ expect(errors[0].suggestions).toContain('AppShell');
257
+ });
258
+
259
+ it('rejects per-component shorthand misuse and suggests the right prop', () => {
260
+ const doc = parse('Sh[p6]');
261
+ const {errors} = validate(doc, registry, BLOCKS);
262
+ expect(errors[0].message).toMatch(/AppShell has no prop 'padding'/);
263
+ expect(errors[0].suggestions).toContain('contentPadding');
264
+ });
265
+
266
+ it('rejects out-of-enum values, listing the legal set', () => {
267
+ const doc = parse('St[g7]');
268
+ const {errors} = validate(doc, registry, BLOCKS);
269
+ expect(errors[0].message).toMatch(/must be one of 0 \| 0\.5/);
270
+ });
271
+
272
+ it('resolves axis-neutral j= per component axis', () => {
273
+ const doc = parse('St[j=between]');
274
+ const {errors} = validate(doc, registry, BLOCKS);
275
+ expect(errors).toEqual([]);
276
+ // synthetic Stack has no `justify`, so j= falls back to hAlign
277
+ expect(doc.roots[0].bound.props.get('hAlign')).toBe('between');
278
+ });
279
+
280
+ it('resolves dot-modifiers against unique enum values, with typo help', () => {
281
+ const ok = parse('Ch.success"Active"');
282
+ expect(validate(ok, registry, BLOCKS).errors).toEqual([]);
283
+ expect(ok.roots[0].bound.props.get('variant')).toBe('success');
284
+
285
+ const bad = parse('Ch.sucess"Active"');
286
+ const {errors} = validate(bad, registry, BLOCKS);
287
+ expect(errors[0].suggestions).toContain('success');
288
+ });
289
+
290
+ it('resolves negation flags to boolean props', () => {
291
+ const doc = parse('Bx[!scroll]');
292
+ expect(validate(doc, registry, BLOCKS).errors).toEqual([]);
293
+ expect(doc.roots[0].bound.props.get('isScrollable')).toBe(false);
294
+ });
295
+
296
+ it('rejects unknown blocks unless loose', () => {
297
+ const doc = parse('Bx{nope-not-real}');
298
+ expect(validate(doc, registry, BLOCKS).errors[0].message).toMatch(/Unknown block/);
299
+
300
+ const loose = parse('Bx{nope-not-real}');
301
+ const result = validate(loose, registry, BLOCKS, {loose: true});
302
+ expect(result.errors).toEqual([]);
303
+ expect(result.warnings[0].message).toMatch(/TODO placeholder/);
304
+ });
305
+
306
+ it('resolves block hints case/kebab-insensitively', () => {
307
+ const doc = parse('Bx{card-callout}');
308
+ expect(validate(doc, registry, BLOCKS).errors).toEqual([]);
309
+ expect(doc.roots[0].hint.block.name).toBe('CardCallout');
310
+ });
311
+
312
+ it('enforces structural pairings', () => {
313
+ const doc = parse('Bx > Sh');
314
+ const {errors} = validate(doc, registry, BLOCKS);
315
+ expect(errors[0].message).toMatch(/outermost/);
316
+ });
317
+ });
318
+
319
+ // ─── registry helpers ──────────────────────────────────────────────────────
320
+
321
+ describe('parseEnumValues', () => {
322
+ it('parses quoted string unions', () => {
323
+ expect(parseEnumValues("'a' | 'b' | 'c'")).toEqual(['a', 'b', 'c']);
324
+ });
325
+ it('parses numeric unions and SpacingStep', () => {
326
+ expect(parseEnumValues('1|2|3')).toEqual([1, 2, 3]);
327
+ expect(parseEnumValues('SpacingStep')).toEqual(SPACING_STEPS);
328
+ });
329
+ it('returns null for non-enums', () => {
330
+ expect(parseEnumValues('number | string')).toBeNull();
331
+ expect(parseEnumValues('boolean')).toBeNull();
332
+ });
333
+ });
@@ -0,0 +1,99 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** User config exported from astryx.config.mjs. */
4
+ export interface AstryxConfig {
5
+ /** External package directories to scan for package.json astryx.docs metadata. */
6
+ packages?: string | string[];
7
+ /** Integration package names or manifest paths. */
8
+ integrations?: string | string[];
9
+ /** Gap report delivery override. Omit for default GitHub issue filing. */
10
+ gapReport?: false | {command: string};
11
+ /** Template hooks. */
12
+ template?: {
13
+ get?: (id: string) => string | Promise<string>;
14
+ };
15
+ [key: string]: unknown;
16
+ }
17
+
18
+ export interface AstryxIntegrationCodemod {
19
+ name: string;
20
+ from?: string;
21
+ to?: string;
22
+ title?: string;
23
+ description?: string;
24
+ pr?: string;
25
+ optional?: boolean;
26
+ fileExtensions?: string[];
27
+ transform:
28
+ | string
29
+ | ((file: unknown, api: unknown) => string | undefined | null);
30
+ }
31
+
32
+ export interface AstryxPostCodemodContext {
33
+ packageDir: string;
34
+ codemodDir: string;
35
+ changedFiles: string[];
36
+ absoluteChangedFiles: string[];
37
+ packageChangedFiles: string[];
38
+ apply: boolean;
39
+ run: (
40
+ command: string,
41
+ args?: string[],
42
+ options?: {
43
+ cwd?: string;
44
+ timeoutMs?: number;
45
+ env?: Record<string, string>;
46
+ },
47
+ ) => Promise<void>;
48
+ }
49
+
50
+ export interface AstryxPostCodemodHook {
51
+ name?: string;
52
+ run?: (context: AstryxPostCodemodContext) => void | Promise<void>;
53
+ command?: (context: AstryxPostCodemodContext) =>
54
+ | {
55
+ command: string;
56
+ args?: string[];
57
+ cwd?: string;
58
+ timeoutMs?: number;
59
+ env?: Record<string, string>;
60
+ }
61
+ | null
62
+ | undefined
63
+ | Promise<
64
+ | {
65
+ command: string;
66
+ args?: string[];
67
+ cwd?: string;
68
+ timeoutMs?: number;
69
+ env?: Record<string, string>;
70
+ }
71
+ | null
72
+ | undefined
73
+ >;
74
+ }
75
+
76
+ /** Integration manifest exported from an astryx.integration.mjs file. */
77
+ export interface AstryxIntegration {
78
+ name: string;
79
+ version?: string;
80
+ displayName?: string;
81
+ description?: string;
82
+ /** Relative docs root containing *.doc.mjs files. */
83
+ docs?: string;
84
+ category?: string;
85
+ /** Relative block-template root. */
86
+ blocks?: string;
87
+ gapReport?: false | {command: string};
88
+ template?: {
89
+ get?: string | ((id: string) => string | Promise<string>);
90
+ };
91
+ codemods?: AstryxIntegrationCodemod[];
92
+ postCodemod?: AstryxPostCodemodHook[];
93
+ [key: string]: unknown;
94
+ }
95
+
96
+ export declare function createConfig<T extends AstryxConfig>(config: T): T;
97
+ export declare function createIntegration<T extends AstryxIntegration>(
98
+ integration: T,
99
+ ): T;
@@ -16,8 +16,8 @@
16
16
  */
17
17
 
18
18
  import {execFileSync} from 'node:child_process';
19
- import * as fs from 'node:fs';
20
19
  import * as path from 'node:path';
20
+ import {loadConfig} from '../lib/config.mjs';
21
21
 
22
22
  /** Default target repository for gap report issues. */
23
23
  const DEFAULT_REPO = 'facebookexperimental/xds';
@@ -44,7 +44,7 @@ export const GAP_CATEGORIES = [
44
44
  * - command: string → run this script instead of creating a GitHub issue
45
45
  * - neither → default behavior (GitHub issue to facebookexperimental/xds)
46
46
  */
47
- export function loadGapReportConfig() {
47
+ export async function loadGapReportConfig() {
48
48
  // 1. Check environment variable
49
49
  const envVar = process.env.ASTRYX_GAP_REPORT;
50
50
  if (envVar) {
@@ -55,27 +55,12 @@ export function loadGapReportConfig() {
55
55
  return {enabled: true, command: envVar};
56
56
  }
57
57
 
58
- // 2. Check astryx.config.mjs
59
- const configPath = path.join(process.cwd(), 'astryx.config.mjs');
60
- if (fs.existsSync(configPath)) {
61
- try {
62
- const content = fs.readFileSync(configPath, 'utf-8');
63
- const match = content.match(/gapReport\s*:\s*(.+?)[\s,}]/s);
64
- if (match) {
65
- const value = match[1].trim();
66
- if (value === 'false') {
67
- return {enabled: false};
68
- }
69
- // Look for command property in object literal
70
- const cmdMatch = content.match(
71
- /gapReport\s*:\s*\{[^}]*command\s*:\s*['"](.+?)['"]/s,
72
- );
73
- if (cmdMatch) {
74
- return {enabled: true, command: cmdMatch[1]};
75
- }
76
- }
77
- } catch {
78
- // Config parse error — fall through to defaults
58
+ // 2. Check astryx.config.mjs + integration manifests
59
+ const config = await loadConfig(process.cwd());
60
+ if (config.gapReport !== undefined) {
61
+ if (config.gapReport === false) return {enabled: false};
62
+ if (config.gapReport?.command) {
63
+ return {enabled: true, command: config.gapReport.command};
79
64
  }
80
65
  }
81
66
 
@@ -172,14 +157,14 @@ function runCustomCommand(command, report) {
172
157
  * repo: string,
173
158
  * }}
174
159
  */
175
- export function buildGapReportPreview({
160
+ export async function buildGapReportPreview({
176
161
  component,
177
162
  category,
178
163
  intention,
179
164
  detail,
180
165
  source = 'cli',
181
166
  }) {
182
- const config = loadGapReportConfig();
167
+ const config = await loadGapReportConfig();
183
168
  const categoryLabel =
184
169
  GAP_CATEGORIES.find(c => c.value === category)?.label ?? category;
185
170
 
@@ -238,8 +223,8 @@ export function buildGapReportPreview({
238
223
  * @param {string} [opts.source] Who filed it: "interactive", "llm-auto", "cli"
239
224
  * @returns {string|null} URL of the created issue (or custom command output), null if disabled
240
225
  */
241
- export function createGapReport(opts) {
242
- const preview = buildGapReportPreview(opts);
226
+ export async function createGapReport(opts) {
227
+ const preview = await buildGapReportPreview(opts);
243
228
 
244
229
  if (!preview.enabled) {
245
230
  return null;
@@ -5,7 +5,7 @@ export const doc = {
5
5
  type: 'block',
6
6
  exampleFor: 'ClickableCard',
7
7
  name: 'ClickableCardWithNestedButton',
8
- displayName: 'Clickable Card With Nested Button',
8
+ displayName: 'Clickable Card Nested Button',
9
9
  description:
10
10
  'A product card that navigates on click but has an independent "Add to cart" button inside.',
11
11
  isReady: true,
@@ -5,7 +5,7 @@ export const doc = {
5
5
  type: 'block',
6
6
  exampleFor: 'SelectableCard',
7
7
  name: 'SelectableCardMulti',
8
- displayName: 'Selectable Card Multi',
8
+ displayName: 'Selectable Card Multi-select',
9
9
  description:
10
10
  'Multi-select tag picker using color variant selectable cards with color-matched selection borders.',
11
11
  isReady: true,
@@ -44,7 +44,7 @@ export default function ChatComposerDrawerWithProgress() {
44
44
  }
45
45
  headerContext={
46
46
  <Stack direction="horizontal" gap={2} vAlign="center">
47
- <ProgressBar value={42} label="Context usage" isLabelHidden hasValueLabel />
47
+ <ProgressBar value={42} label="Context usage" isLabelHidden />
48
48
  </Stack>
49
49
  }
50
50
  />
@@ -19,7 +19,7 @@ export default function ChatDictationButtonShowcase() {
19
19
  const dictation = useChatDictation({
20
20
  inputRef,
21
21
  hasSounds: true,
22
- onResult: (text) => {
22
+ onResult: text => {
23
23
  console.log('Dictation result:', text);
24
24
  },
25
25
  });
@@ -31,7 +31,7 @@ export default function ChatDictationButtonShowcase() {
31
31
  input.
32
32
  </Text>
33
33
  <ChatComposer
34
- onSubmit={(v) => console.log('Submit:', v)}
34
+ onSubmit={v => console.log('Submit:', v)}
35
35
  input={<ChatComposerInput handleRef={inputRef} />}
36
36
  sendActions={<ChatDictationButton dictation={dictation} />}
37
37
  />
@@ -63,7 +63,7 @@ export default function ChatDictationButtonShowcase() {
63
63
  </HStack>
64
64
  )}
65
65
  {!dictation.isSupported && (
66
- <Text type="supporting" color="active">
66
+ <Text type="supporting" color="accent">
67
67
  SpeechRecognition is not supported in this browser.
68
68
  </Text>
69
69
  )}
@@ -0,0 +1,15 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('../../../../../core/src/docs-types').TemplateDoc} */
4
+ export const doc = {
5
+ type: 'block',
6
+ exampleFor: 'CommandPaletteEmpty',
7
+ name: 'CommandPaletteEmpty',
8
+ displayName: 'Command Palette Empty',
9
+ description:
10
+ 'Command palette empty state shown when no commands are available.',
11
+ isReady: true,
12
+ isShowcase: true,
13
+ aspectRatio: 4 / 3,
14
+ componentsUsed: ['CommandPalette', 'CommandPaletteEmpty', 'Text'],
15
+ };
@@ -0,0 +1,26 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ import {useMemo} from 'react';
6
+ import {CommandPalette} from '@astryxdesign/core/CommandPalette';
7
+ import {Text} from '@astryxdesign/core/Text';
8
+ import {createStaticSource} from '@astryxdesign/core/Typeahead';
9
+
10
+ export default function CommandPaletteEmptyShowcase() {
11
+ const emptySource = useMemo(() => createStaticSource([]), []);
12
+
13
+ return (
14
+ <CommandPalette
15
+ isOpen
16
+ isInline
17
+ onOpenChange={() => {}}
18
+ searchSource={emptySource}
19
+ emptyBootstrapText={
20
+ <Text type="supporting" color="secondary">
21
+ No commands available yet
22
+ </Text>
23
+ }
24
+ />
25
+ );
26
+ }
@@ -4,8 +4,8 @@
4
4
  export const doc = {
5
5
  type: 'block',
6
6
  exampleFor: 'DateInput',
7
- name: 'DateInput — Date Range',
8
- displayName: 'DateInput — Date Range',
7
+ name: 'DateInput — Min/Max Constraints',
8
+ displayName: 'DateInput — Min/Max Constraints',
9
9
  description:
10
10
  'Date input constrained to a min/max window. Use when only certain dates are valid, like booking availability or a fiscal quarter.',
11
11
  isReady: true,
@@ -2,15 +2,17 @@
2
2
 
3
3
  'use client';
4
4
 
5
+ import {useState} from 'react';
5
6
  import {FileInput} from '@astryxdesign/core/FileInput';
6
7
 
7
8
  export default function FileInputShowcase() {
9
+ const [value, setValue] = useState<File | File[] | null>(null);
8
10
  return (
9
11
  <div style={{width: 350}}>
10
12
  <FileInput
11
13
  label="Upload file"
12
- value={null}
13
- onChange={() => {}}
14
+ value={value}
15
+ onChange={setValue}
14
16
  placeholder="Drag files here or click to browse"
15
17
  />
16
18
  </div>
@@ -0,0 +1,14 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('../../../../../core/src/docs-types').TemplateDoc} */
4
+ export const doc = {
5
+ type: 'block',
6
+ exampleFor: 'LinkProvider',
7
+ name: 'Link Provider — Custom Link Component',
8
+ displayName: 'Link Provider — Custom Link Component',
9
+ description:
10
+ 'Routes every Astryx link through a custom component that intercepts the click, the hook frameworks like Next.js use for client-side navigation. Click the link to see the custom handler fire instead of a full-page load.',
11
+ isReady: true,
12
+ aspectRatio: 4 / 3,
13
+ componentsUsed: ['LinkProvider', 'Link'],
14
+ };