@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.
- package/CHANGELOG.md +50 -0
- package/docs/theme.doc.mjs +2 -2
- package/docs/working-with-ai.doc.mjs +6 -6
- package/package.json +19 -8
- package/src/api/discover.mjs +78 -26
- package/src/api/index.mjs +1 -0
- package/src/api/layout.mjs +301 -0
- package/src/api/layout.test.mjs +238 -0
- package/src/api/template.mjs +191 -50
- package/src/api/template.test.mjs +2 -0
- package/src/api/theme-add.mjs +182 -0
- package/src/codemods/__tests__/registry.test.mjs +1 -0
- package/src/codemods/registry.mjs +1 -0
- package/src/codemods/transforms/v0.1.2/__tests__/rename-text-color-active-to-accent.test.mjs +120 -0
- package/src/codemods/transforms/v0.1.2/index.mjs +19 -0
- package/src/codemods/transforms/v0.1.2/rename-text-color-active-to-accent.mjs +136 -0
- package/src/commands/build-theme.import-path.test.mjs +22 -2
- package/src/commands/build-theme.mjs +185 -16
- package/src/commands/gap-report.mjs +17 -9
- package/src/commands/gap-report.test.mjs +21 -16
- package/src/commands/init.mjs +34 -8
- package/src/commands/init.next-steps.test.mjs +46 -0
- package/src/commands/layout.mjs +139 -0
- package/src/commands/swizzle.mjs +51 -23
- package/src/commands/upgrade.mjs +2 -71
- package/src/config.mjs +31 -0
- package/src/config.test.mjs +24 -0
- package/src/index.mjs +6 -0
- package/src/lib/config-schema.mjs +119 -0
- package/src/lib/config.mjs +34 -7
- package/src/lib/config.test.mjs +51 -2
- package/src/lib/error-codes.mjs +11 -0
- package/src/lib/integrations.mjs +155 -0
- package/src/lib/integrations.test.mjs +154 -0
- package/src/lib/levenshtein.mjs +29 -0
- package/src/lib/manifest.mjs +10 -0
- package/src/lib/package-scanner.mjs +31 -7
- package/src/lib/string-utils.mjs +5 -14
- package/src/lib/xle/browser.d.ts +100 -0
- package/src/lib/xle/browser.mjs +120 -0
- package/src/lib/xle/expand.mjs +622 -0
- package/src/lib/xle/parse.mjs +581 -0
- package/src/lib/xle/print.mjs +174 -0
- package/src/lib/xle/registry-core.mjs +170 -0
- package/src/lib/xle/registry.mjs +237 -0
- package/src/lib/xle/splice.mjs +137 -0
- package/src/lib/xle/validate.mjs +356 -0
- package/src/lib/xle/xle.test.mjs +333 -0
- package/src/types/config.d.ts +99 -0
- package/src/utils/github.mjs +12 -27
- package/templates/blocks/components/Card/ClickableCardWithNestedButton.doc.mjs +1 -1
- package/templates/blocks/components/Card/SelectableCardMulti.doc.mjs +1 -1
- package/templates/blocks/components/ChatComposerDrawer/ChatComposerDrawerWithProgress.tsx +1 -1
- package/templates/blocks/components/ChatDictationButton/ChatDictationButtonShowcase.tsx +3 -3
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.doc.mjs +15 -0
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.tsx +26 -0
- package/templates/blocks/components/DateInput/DateInputDateRange.doc.mjs +2 -2
- package/templates/blocks/components/FileInput/FileInputShowcase.tsx +4 -2
- package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.doc.mjs +14 -0
- package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.tsx +43 -0
- package/templates/blocks/components/MultiSelector/MultiSelectorShowcase.tsx +6 -2
- package/templates/blocks/components/NumberInput/NumberInputShowcase.tsx +6 -2
- package/templates/blocks/components/Outline/OutlineShowcase.doc.mjs +15 -0
- package/templates/blocks/components/Outline/OutlineShowcase.tsx +22 -0
- package/templates/blocks/components/RadioList/RadioListShowcase.tsx +8 -1
- package/templates/blocks/components/SegmentedControl/SegmentedControlShowcase.tsx +3 -1
- package/templates/blocks/components/Selector/SelectorShowcase.tsx +4 -1
- package/templates/blocks/components/Slider/SliderShowcase.tsx +10 -1
- package/templates/blocks/components/Tab/TabShowcase.tsx +3 -1
- package/templates/blocks/components/TabList/TabListShowcase.tsx +3 -1
- package/templates/blocks/components/TabMenu/TabMenuShowcase.tsx +3 -1
- package/templates/blocks/components/Table/ColumnResizeHookUsage.doc.mjs +14 -0
- package/templates/blocks/components/Table/ColumnResizeHookUsage.tsx +59 -0
- package/templates/blocks/components/Table/StickyColumnsHookUsage.doc.mjs +14 -0
- package/templates/blocks/components/Table/StickyColumnsHookUsage.tsx +104 -0
- package/templates/blocks/components/Text/TextColors.tsx +20 -5
- package/templates/blocks/components/TextArea/TextAreaShowcase.tsx +4 -2
- package/templates/blocks/components/TextInput/TextInputShowcase.tsx +4 -2
- package/templates/blocks/components/Thumbnail/ThumbnailDisabled.tsx +11 -6
- package/templates/blocks/components/Thumbnail/ThumbnailGallery.tsx +43 -7
- package/templates/blocks/components/Thumbnail/ThumbnailRemovable.tsx +40 -3
- package/templates/blocks/components/Thumbnail/ThumbnailShowcase.tsx +6 -5
- package/templates/blocks/components/Thumbnail/ThumbnailStates.tsx +11 -6
- package/templates/blocks/components/Timestamp/TimestampColors.tsx +2 -2
- package/templates/blocks/components/ToggleButton/ToggleButtonGroup.doc.mjs +1 -1
- package/templates/blocks/components/Tokenizer/TokenizerShowcase.tsx +8 -6
- package/templates/blocks/components/Toolbar/ToolbarTableFilter.doc.mjs +2 -2
- package/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +23 -14
- package/templates/blocks/components/Typeahead/TypeaheadShowcase.tsx +6 -2
- package/templates/pages/shell-nav/page.tsx +321 -0
- package/templates/pages/shell-nav/template.doc.mjs +12 -0
- package/templates/pages/shell-side-nav/page.tsx +242 -0
- package/templates/pages/shell-side-nav/template.doc.mjs +12 -0
- package/templates/pages/shell-top-nav/page.tsx +224 -0
- package/templates/pages/shell-top-nav/template.doc.mjs +12 -0
- package/templates/pages/theme-showcase/page.tsx +8 -19
- package/templates/themes/butter/butterTheme.ts +916 -0
- package/templates/themes/butter/icons.tsx +77 -0
- package/templates/themes/chocolate/chocolateTheme.ts +230 -0
- package/templates/themes/chocolate/icons.tsx +77 -0
- package/templates/themes/gothic/gothicTheme.ts +657 -0
- package/templates/themes/gothic/icons.tsx +77 -0
- package/templates/themes/manifest.json +90 -0
- package/templates/themes/matcha/icons.tsx +67 -0
- package/templates/themes/matcha/matchaTheme.ts +247 -0
- package/templates/themes/neutral/icons.tsx +77 -0
- package/templates/themes/neutral/neutralTheme.ts +603 -0
- package/templates/themes/stone/icons.tsx +77 -0
- package/templates/themes/stone/stoneTheme.ts +652 -0
- package/templates/themes/y2k/icons.tsx +67 -0
- package/templates/themes/y2k/y2kTheme.ts +617 -0
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.doc.mjs +0 -14
- 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;
|
package/src/utils/github.mjs
CHANGED
|
@@ -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
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
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
|
|
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:
|
|
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={
|
|
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="
|
|
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 —
|
|
8
|
-
displayName: 'DateInput —
|
|
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={
|
|
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
|
+
};
|