@astryxdesign/cli 0.7.2-canary.f16dc9f → 0.7.2-canary.fe2c655
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/api/component/component.test.mjs +357 -184
- package/assets/templates/blocks/components/Drawer/DrawerForm.doc.mjs +24 -0
- package/assets/templates/blocks/components/Drawer/DrawerForm.tsx +62 -0
- package/assets/templates/blocks/components/Drawer/DrawerScrim.doc.mjs +22 -0
- package/assets/templates/blocks/components/Drawer/DrawerScrim.tsx +68 -0
- package/assets/templates/blocks/components/Drawer/DrawerShowcase.doc.mjs +22 -0
- package/assets/templates/blocks/components/Drawer/DrawerShowcase.tsx +41 -0
- package/assets/templates/blocks/components/Drawer/DrawerStart.doc.mjs +21 -0
- package/assets/templates/blocks/components/Drawer/DrawerStart.tsx +41 -0
- package/assets/templates/blocks/components/Drawer/DrawerWidths.doc.mjs +22 -0
- package/assets/templates/blocks/components/Drawer/DrawerWidths.tsx +54 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.doc.mjs +15 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.tsx +73 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.doc.mjs +22 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.tsx +49 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.doc.mjs +21 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.tsx +42 -0
- package/assets/templates/pages/detail-page/page.tsx +3 -3
- package/assets/templates/pages/table-inbox/page.tsx +10 -8
- package/authoring/doctypes/base/type.ts +6 -0
- package/authoring/doctypes/component/component.doc.mjs +2 -2
- package/clients/cli/lib/component-format.mjs +5 -1
- package/package.json +10 -10
|
@@ -17,217 +17,390 @@ import {AstryxError} from '../error.mjs';
|
|
|
17
17
|
import {runCli} from '../../test-utils/run-cli.mjs';
|
|
18
18
|
|
|
19
19
|
// api/component/ -> up 4 = repo root (has packages/core, which findCoreDir walks to).
|
|
20
|
-
const REPO = path.resolve(
|
|
20
|
+
const REPO = path.resolve(
|
|
21
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
22
|
+
'../../../..',
|
|
23
|
+
);
|
|
21
24
|
const cwd = REPO;
|
|
22
25
|
const SLOW = 30_000;
|
|
23
26
|
|
|
24
27
|
describe('component dispatcher — routing', () => {
|
|
25
|
-
it(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
28
|
+
it(
|
|
29
|
+
'routes a bare name to component.detail',
|
|
30
|
+
async () => {
|
|
31
|
+
const r = await component('Button', {cwd});
|
|
32
|
+
expect(r.type).toBe('component.detail');
|
|
33
|
+
expect(r.data.name).toBe('Button');
|
|
34
|
+
},
|
|
35
|
+
SLOW,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
it(
|
|
39
|
+
'routes no-name to the list leaf (component.list, detail=names)',
|
|
40
|
+
async () => {
|
|
41
|
+
const r = await component(undefined, {cwd});
|
|
42
|
+
expect(r.type).toBe('component.list');
|
|
43
|
+
expect(r.data.detail).toBe('names');
|
|
44
|
+
expect(Object.keys(r.data.components).length).toBeGreaterThan(0);
|
|
45
|
+
},
|
|
46
|
+
SLOW,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
it(
|
|
50
|
+
'routes --category to the list leaf, filtered to that group',
|
|
51
|
+
async () => {
|
|
52
|
+
const r = await component(undefined, {cwd, category: 'Layout'});
|
|
53
|
+
expect(r.type).toBe('component.list');
|
|
54
|
+
expect(Object.keys(r.data.components)).toContain('Layout');
|
|
55
|
+
},
|
|
56
|
+
SLOW,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
it(
|
|
60
|
+
'treats an empty-string name as a list (not a not-found error)',
|
|
61
|
+
async () => {
|
|
62
|
+
const r = await component('', {cwd});
|
|
63
|
+
expect(r.type).toBe('component.list');
|
|
64
|
+
},
|
|
65
|
+
SLOW,
|
|
66
|
+
);
|
|
48
67
|
});
|
|
49
68
|
|
|
50
69
|
describe('component dispatcher — detail levels on list', () => {
|
|
51
|
-
it(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
it(
|
|
71
|
+
'--detail compact lists with detail=compact',
|
|
72
|
+
async () => {
|
|
73
|
+
const r = await component(undefined, {
|
|
74
|
+
cwd,
|
|
75
|
+
list: true,
|
|
76
|
+
detail: 'compact',
|
|
77
|
+
});
|
|
78
|
+
expect(r.type).toBe('component.list');
|
|
79
|
+
expect(r.data.detail).toBe('compact');
|
|
80
|
+
},
|
|
81
|
+
SLOW,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
it(
|
|
85
|
+
'--detail full lists with detail=full',
|
|
86
|
+
async () => {
|
|
87
|
+
const r = await component(undefined, {cwd, list: true, detail: 'full'});
|
|
88
|
+
expect(r.type).toBe('component.list');
|
|
89
|
+
expect(r.data.detail).toBe('full');
|
|
90
|
+
},
|
|
91
|
+
SLOW,
|
|
92
|
+
);
|
|
62
93
|
});
|
|
63
94
|
|
|
64
95
|
describe('component dispatcher — projection precedence (locked)', () => {
|
|
65
|
-
it(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
96
|
+
it(
|
|
97
|
+
'--props routes to component.detail.props',
|
|
98
|
+
async () => {
|
|
99
|
+
const r = await component('Button', {cwd, props: true});
|
|
100
|
+
expect(r.type).toBe('component.detail.props');
|
|
101
|
+
},
|
|
102
|
+
SLOW,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
it(
|
|
106
|
+
'--source routes to component.detail.source',
|
|
107
|
+
async () => {
|
|
108
|
+
const r = await component('Button', {cwd, source: true});
|
|
109
|
+
expect(r.type).toBe('component.detail.source');
|
|
110
|
+
},
|
|
111
|
+
SLOW,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
it(
|
|
115
|
+
'--source WINS over --props when both are set (precedence lock)',
|
|
116
|
+
async () => {
|
|
117
|
+
const r = await component('Button', {cwd, props: true, source: true});
|
|
118
|
+
expect(r.type).toBe('component.detail.source');
|
|
119
|
+
},
|
|
120
|
+
SLOW,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
it(
|
|
124
|
+
'--list WINS over a given name (list dispatch precedence lock)',
|
|
125
|
+
async () => {
|
|
126
|
+
const r = await component('Button', {cwd, list: true});
|
|
127
|
+
expect(r.type).toBe('component.list');
|
|
128
|
+
},
|
|
129
|
+
SLOW,
|
|
130
|
+
);
|
|
84
131
|
});
|
|
85
132
|
|
|
86
133
|
describe('component dispatcher — name resolution', () => {
|
|
87
|
-
it(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
expect(err.code).toBe('ERR_UNKNOWN_COMPONENT');
|
|
112
|
-
}, SLOW);
|
|
113
|
-
|
|
114
|
-
it('throws ERR_UNKNOWN_PACKAGE when --package names a missing package', async () => {
|
|
115
|
-
let err;
|
|
116
|
-
try {
|
|
117
|
-
await component('Button', {cwd, package: '@nonexistent/pkg-xyz'});
|
|
118
|
-
} catch (e) {
|
|
119
|
-
err = e;
|
|
120
|
-
}
|
|
121
|
-
expect(err).toBeDefined();
|
|
122
|
-
expect(err.code).toBe('ERR_UNKNOWN_PACKAGE');
|
|
123
|
-
}, SLOW);
|
|
124
|
-
});
|
|
134
|
+
it(
|
|
135
|
+
'strips a legacy XDS prefix (XDSButton -> Button)',
|
|
136
|
+
async () => {
|
|
137
|
+
const r = await component('XDSButton', {cwd});
|
|
138
|
+
expect(r.type).toBe('component.detail');
|
|
139
|
+
expect(r.data.name).toBe('Button');
|
|
140
|
+
},
|
|
141
|
+
SLOW,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
it(
|
|
145
|
+
'is case-SENSITIVE: a lowercased name is unknown',
|
|
146
|
+
async () => {
|
|
147
|
+
let err;
|
|
148
|
+
try {
|
|
149
|
+
await component('button', {cwd});
|
|
150
|
+
} catch (e) {
|
|
151
|
+
err = e;
|
|
152
|
+
}
|
|
153
|
+
expect(err).toBeDefined();
|
|
154
|
+
expect(err.code).toBe('ERR_UNKNOWN_COMPONENT');
|
|
155
|
+
},
|
|
156
|
+
SLOW,
|
|
157
|
+
);
|
|
125
158
|
|
|
159
|
+
it(
|
|
160
|
+
'throws ERR_UNKNOWN_COMPONENT for a genuinely unknown name',
|
|
161
|
+
async () => {
|
|
162
|
+
let err;
|
|
163
|
+
try {
|
|
164
|
+
await component('ZzzNope99', {cwd});
|
|
165
|
+
} catch (e) {
|
|
166
|
+
err = e;
|
|
167
|
+
}
|
|
168
|
+
expect(err.code).toBe('ERR_UNKNOWN_COMPONENT');
|
|
169
|
+
},
|
|
170
|
+
SLOW,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
it(
|
|
174
|
+
'throws ERR_UNKNOWN_PACKAGE when --package names a missing package',
|
|
175
|
+
async () => {
|
|
176
|
+
let err;
|
|
177
|
+
try {
|
|
178
|
+
await component('Button', {cwd, package: '@nonexistent/pkg-xyz'});
|
|
179
|
+
} catch (e) {
|
|
180
|
+
err = e;
|
|
181
|
+
}
|
|
182
|
+
expect(err).toBeDefined();
|
|
183
|
+
expect(err.code).toBe('ERR_UNKNOWN_PACKAGE');
|
|
184
|
+
},
|
|
185
|
+
SLOW,
|
|
186
|
+
);
|
|
187
|
+
});
|
|
126
188
|
|
|
127
189
|
describe('component dispatcher — scoped --package routes all projections', () => {
|
|
128
190
|
const CORE = '@astryxdesign/core';
|
|
129
191
|
|
|
130
|
-
it(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
192
|
+
it(
|
|
193
|
+
'--package (core) routes --showcase like the no-scope path',
|
|
194
|
+
async () => {
|
|
195
|
+
const ns = await component('Button', {cwd, showcase: true});
|
|
196
|
+
const sc = await component('Button', {
|
|
197
|
+
cwd,
|
|
198
|
+
package: CORE,
|
|
199
|
+
showcase: true,
|
|
200
|
+
});
|
|
201
|
+
expect(sc.type).toBe(ns.type); // was silently component.detail before the fix
|
|
202
|
+
},
|
|
203
|
+
SLOW,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
it(
|
|
207
|
+
'--package (core) routes --blocks like the no-scope path',
|
|
208
|
+
async () => {
|
|
209
|
+
const ns = await component('Button', {cwd, blocks: true});
|
|
210
|
+
const sc = await component('Button', {cwd, package: CORE, blocks: true});
|
|
211
|
+
expect(sc.type).toBe(ns.type);
|
|
212
|
+
expect(sc.type).toBe('component.detail.blocks');
|
|
213
|
+
},
|
|
214
|
+
SLOW,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
it(
|
|
218
|
+
"--props carries a prop doc's stability marker (Drawer's experimental prop)",
|
|
219
|
+
async () => {
|
|
220
|
+
const r = await component('Drawer', {cwd, props: true});
|
|
221
|
+
expect(r.type).toBe('component.detail.props');
|
|
222
|
+
const experimental = r.data.find(p => p.name === 'experimental');
|
|
223
|
+
expect(experimental?.stability).toBe('experimental');
|
|
224
|
+
// Only the experimental prop object is marked; stable props carry no field.
|
|
225
|
+
expect(
|
|
226
|
+
r.data.filter(p => p.stability !== undefined).map(p => p.name),
|
|
227
|
+
).toEqual(['experimental']);
|
|
228
|
+
},
|
|
229
|
+
SLOW,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
it(
|
|
233
|
+
'--props --json and the text props table both show the stability marker',
|
|
234
|
+
async () => {
|
|
235
|
+
const json = await runCli(
|
|
236
|
+
['component', 'Drawer', '--props', '--json'],
|
|
237
|
+
cwd,
|
|
238
|
+
);
|
|
239
|
+
expect(json.status).toBe(0);
|
|
240
|
+
const envelope = JSON.parse(json.stdout);
|
|
241
|
+
expect(envelope.type).toBe('component.detail.props');
|
|
242
|
+
expect(
|
|
243
|
+
envelope.data.find(p => p.name === 'experimental')?.stability,
|
|
244
|
+
).toBe('experimental');
|
|
245
|
+
|
|
246
|
+
const text = await runCli(['component', 'Drawer', '--props'], cwd);
|
|
247
|
+
expect(text.status).toBe(0);
|
|
248
|
+
const rows = text.stdout
|
|
249
|
+
.split('\n')
|
|
250
|
+
.filter(line => line.startsWith('| `'));
|
|
251
|
+
const experimentalRow = rows.find(line =>
|
|
252
|
+
line.startsWith('| `experimental` |'),
|
|
253
|
+
);
|
|
254
|
+
expect(experimentalRow).toContain('**(experimental)**');
|
|
255
|
+
expect(
|
|
256
|
+
rows.filter(line => line.includes('**(experimental)**')),
|
|
257
|
+
).toHaveLength(1);
|
|
258
|
+
},
|
|
259
|
+
SLOW,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
it(
|
|
263
|
+
'--package (core) still routes --source and --props',
|
|
264
|
+
async () => {
|
|
265
|
+
expect(
|
|
266
|
+
(await component('Button', {cwd, package: CORE, source: true})).type,
|
|
267
|
+
).toBe('component.detail.source');
|
|
268
|
+
expect(
|
|
269
|
+
(await component('Button', {cwd, package: CORE, props: true})).type,
|
|
270
|
+
).toBe('component.detail.props');
|
|
271
|
+
},
|
|
272
|
+
SLOW,
|
|
273
|
+
);
|
|
151
274
|
});
|
|
152
275
|
|
|
153
276
|
describe('component dispatcher — category guard', () => {
|
|
154
|
-
it(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
277
|
+
it(
|
|
278
|
+
'a non-string category throws a coded error (not a raw TypeError)',
|
|
279
|
+
async () => {
|
|
280
|
+
for (const bad of [123, {}, [1]]) {
|
|
281
|
+
const err = await component(undefined, {
|
|
282
|
+
cwd,
|
|
283
|
+
category: /** @type {any} */ (bad),
|
|
284
|
+
}).catch(e => e);
|
|
285
|
+
expect(err).toBeInstanceOf(AstryxError);
|
|
286
|
+
expect(err.code).toBe('ERR_UNKNOWN_CATEGORY');
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
SLOW,
|
|
290
|
+
);
|
|
164
291
|
});
|
|
165
292
|
|
|
166
293
|
describe('component dispatcher — detail and lang guards', () => {
|
|
167
294
|
// `detail` and `lang` are the same controls as the CLI's --detail and --lang:
|
|
168
295
|
// same accepted values, same error codes.
|
|
169
|
-
it(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
()
|
|
177
|
-
()
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
296
|
+
it(
|
|
297
|
+
'rejects a detail level the CLI rejects, with the same code',
|
|
298
|
+
async () => {
|
|
299
|
+
const cliRun = await runCli(
|
|
300
|
+
['--json', 'component', '--list', '--detail', 'bogus'],
|
|
301
|
+
cwd,
|
|
302
|
+
);
|
|
303
|
+
expect(cliRun.code).toBe(1);
|
|
304
|
+
expect(JSON.parse(cliRun.stdout).code).toBe('ERR_INVALID_DETAIL');
|
|
305
|
+
|
|
306
|
+
const bogus = /** @type {any} */ ('bogus');
|
|
307
|
+
for (const call of [
|
|
308
|
+
() => component(undefined, {cwd, list: true, detail: bogus}),
|
|
309
|
+
() => component('Button', {cwd, detail: bogus}),
|
|
310
|
+
]) {
|
|
311
|
+
const err = await call().catch(e => e);
|
|
312
|
+
expect(err).toBeInstanceOf(AstryxError);
|
|
313
|
+
expect(err.code).toBe('ERR_INVALID_DETAIL');
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
SLOW,
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
it(
|
|
320
|
+
'rejects a lang the CLI rejects, with the same code',
|
|
321
|
+
async () => {
|
|
322
|
+
const cliRun = await runCli(
|
|
323
|
+
['--json', 'component', 'Button', '--lang', 'fr'],
|
|
324
|
+
cwd,
|
|
325
|
+
);
|
|
326
|
+
expect(cliRun.code).toBe(1);
|
|
327
|
+
expect(JSON.parse(cliRun.stdout).code).toBe('ERR_INVALID_LANG');
|
|
328
|
+
|
|
329
|
+
for (const call of [
|
|
330
|
+
() => component('Button', {cwd, lang: 'fr'}),
|
|
331
|
+
() =>
|
|
332
|
+
component(undefined, {
|
|
333
|
+
cwd,
|
|
334
|
+
list: true,
|
|
335
|
+
detail: 'compact',
|
|
336
|
+
lang: 'fr',
|
|
337
|
+
}),
|
|
338
|
+
]) {
|
|
339
|
+
const err = await call().catch(e => e);
|
|
340
|
+
expect(err).toBeInstanceOf(AstryxError);
|
|
341
|
+
expect(err.code).toBe('ERR_INVALID_LANG');
|
|
342
|
+
}
|
|
343
|
+
const zh = await component('Button', {cwd, lang: 'zh'});
|
|
344
|
+
expect(zh.type).toBe('component.detail');
|
|
345
|
+
},
|
|
346
|
+
SLOW,
|
|
347
|
+
);
|
|
201
348
|
});
|
|
202
349
|
|
|
203
350
|
describe('component dispatcher — detail levels on single component (text only)', () => {
|
|
204
|
-
it(
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
351
|
+
it(
|
|
352
|
+
'--detail compact returns less text than full',
|
|
353
|
+
async () => {
|
|
354
|
+
const full = await runCli(['component', 'Button'], cwd);
|
|
355
|
+
const compact = await runCli(
|
|
356
|
+
['component', 'Button', '--detail', 'compact'],
|
|
357
|
+
cwd,
|
|
358
|
+
);
|
|
359
|
+
expect(full.code).toBe(0);
|
|
360
|
+
expect(compact.code).toBe(0);
|
|
361
|
+
expect(compact.stdout.length).toBeLessThan(full.stdout.length);
|
|
362
|
+
// Compact still shows the name and import
|
|
363
|
+
expect(compact.stdout).toContain('Button');
|
|
364
|
+
expect(compact.stdout).toContain('@astryxdesign/core');
|
|
365
|
+
},
|
|
366
|
+
SLOW,
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
it(
|
|
370
|
+
'--detail brief returns less text than compact',
|
|
371
|
+
async () => {
|
|
372
|
+
const compact = await runCli(
|
|
373
|
+
['component', 'Button', '--detail', 'compact'],
|
|
374
|
+
cwd,
|
|
375
|
+
);
|
|
376
|
+
const brief = await runCli(
|
|
377
|
+
['component', 'Button', '--detail', 'brief'],
|
|
378
|
+
cwd,
|
|
379
|
+
);
|
|
380
|
+
expect(brief.code).toBe(0);
|
|
381
|
+
expect(brief.stdout.length).toBeLessThan(compact.stdout.length);
|
|
382
|
+
expect(brief.stdout).toContain('Button');
|
|
383
|
+
},
|
|
384
|
+
SLOW,
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
it(
|
|
388
|
+
'--json --detail compact returns the same full JSON as --json (contract)',
|
|
389
|
+
async () => {
|
|
390
|
+
const full = await runCli(['--json', 'component', 'Button'], cwd);
|
|
391
|
+
const compact = await runCli(
|
|
392
|
+
['--json', 'component', 'Button', '--detail', 'compact'],
|
|
393
|
+
cwd,
|
|
394
|
+
);
|
|
395
|
+
expect(full.code).toBe(0);
|
|
396
|
+
expect(compact.code).toBe(0);
|
|
397
|
+
const fullData = JSON.parse(full.stdout);
|
|
398
|
+
const compactData = JSON.parse(compact.stdout);
|
|
399
|
+
// JSON shape is identical — detail is a text-only projection
|
|
400
|
+
expect(Object.keys(compactData.data).sort()).toEqual(
|
|
401
|
+
Object.keys(fullData.data).sort(),
|
|
402
|
+
);
|
|
403
|
+
},
|
|
404
|
+
SLOW,
|
|
405
|
+
);
|
|
233
406
|
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
|
|
4
|
+
export const doc = {
|
|
5
|
+
type: 'block',
|
|
6
|
+
name: 'Edit form with footer actions',
|
|
7
|
+
displayName: 'Edit form with footer actions',
|
|
8
|
+
description:
|
|
9
|
+
'Compose a Layout with DrawerHeader, the fields in LayoutContent, and Cancel and Save in a pinned LayoutFooter. purpose="form" keeps Escape but ignores scrim clicks, so a stray click outside does not discard edits.',
|
|
10
|
+
exampleFor: 'Drawer',
|
|
11
|
+
isReady: true,
|
|
12
|
+
aspectRatio: 16 / 9,
|
|
13
|
+
componentsUsed: [
|
|
14
|
+
'Drawer',
|
|
15
|
+
'DrawerHeader',
|
|
16
|
+
'Button',
|
|
17
|
+
'Layout',
|
|
18
|
+
'LayoutContent',
|
|
19
|
+
'LayoutFooter',
|
|
20
|
+
'HStack',
|
|
21
|
+
'VStack',
|
|
22
|
+
'TextInput',
|
|
23
|
+
],
|
|
24
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @input Controlled open state and two editable fields
|
|
7
|
+
* @output A form drawer with a header, scrolling fields, and pinned footer
|
|
8
|
+
* actions; purpose="form" keeps Escape but ignores scrim clicks
|
|
9
|
+
* @position Copyable Drawer example
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {useState} from 'react';
|
|
13
|
+
import {Drawer, DrawerHeader} from '@astryxdesign/core/Drawer';
|
|
14
|
+
import {Button} from '@astryxdesign/core/Button';
|
|
15
|
+
import {Layout, LayoutContent, LayoutFooter} from '@astryxdesign/core/Layout';
|
|
16
|
+
import {HStack, VStack} from '@astryxdesign/core/Stack';
|
|
17
|
+
import {TextInput} from '@astryxdesign/core/TextInput';
|
|
18
|
+
|
|
19
|
+
export default function DrawerForm() {
|
|
20
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
21
|
+
const [name, setName] = useState('web-prod-04');
|
|
22
|
+
const [owner, setOwner] = useState('Platform team');
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<Button label="Edit host" onClick={() => setIsOpen(true)} />
|
|
27
|
+
<Drawer
|
|
28
|
+
isOpen={isOpen}
|
|
29
|
+
onOpenChange={setIsOpen}
|
|
30
|
+
label="Edit host"
|
|
31
|
+
purpose="form">
|
|
32
|
+
<Layout
|
|
33
|
+
header={<DrawerHeader title="Edit host" onOpenChange={setIsOpen} />}
|
|
34
|
+
content={
|
|
35
|
+
<LayoutContent>
|
|
36
|
+
<VStack gap={4}>
|
|
37
|
+
<TextInput label="Name" value={name} onChange={setName} />
|
|
38
|
+
<TextInput label="Owner" value={owner} onChange={setOwner} />
|
|
39
|
+
</VStack>
|
|
40
|
+
</LayoutContent>
|
|
41
|
+
}
|
|
42
|
+
footer={
|
|
43
|
+
<LayoutFooter>
|
|
44
|
+
<HStack gap={2} hAlign="end">
|
|
45
|
+
<Button
|
|
46
|
+
label="Cancel"
|
|
47
|
+
variant="secondary"
|
|
48
|
+
onClick={() => setIsOpen(false)}
|
|
49
|
+
/>
|
|
50
|
+
<Button
|
|
51
|
+
label="Save"
|
|
52
|
+
variant="primary"
|
|
53
|
+
onClick={() => setIsOpen(false)}
|
|
54
|
+
/>
|
|
55
|
+
</HStack>
|
|
56
|
+
</LayoutFooter>
|
|
57
|
+
}
|
|
58
|
+
/>
|
|
59
|
+
</Drawer>
|
|
60
|
+
</>
|
|
61
|
+
);
|
|
62
|
+
}
|