@astryxdesign/cli 0.7.2-canary.c68f5e8 → 0.7.2-canary.ccf1a1c

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 (23) hide show
  1. package/api/component/component.test.mjs +357 -184
  2. package/assets/templates/blocks/components/Drawer/DrawerForm.doc.mjs +24 -0
  3. package/assets/templates/blocks/components/Drawer/DrawerForm.tsx +62 -0
  4. package/assets/templates/blocks/components/Drawer/DrawerScrim.doc.mjs +22 -0
  5. package/assets/templates/blocks/components/Drawer/DrawerScrim.tsx +68 -0
  6. package/assets/templates/blocks/components/Drawer/DrawerShowcase.doc.mjs +22 -0
  7. package/assets/templates/blocks/components/Drawer/DrawerShowcase.tsx +41 -0
  8. package/assets/templates/blocks/components/Drawer/DrawerStart.doc.mjs +21 -0
  9. package/assets/templates/blocks/components/Drawer/DrawerStart.tsx +41 -0
  10. package/assets/templates/blocks/components/Drawer/DrawerWidths.doc.mjs +22 -0
  11. package/assets/templates/blocks/components/Drawer/DrawerWidths.tsx +54 -0
  12. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.doc.mjs +15 -0
  13. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.tsx +73 -0
  14. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.doc.mjs +22 -0
  15. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.tsx +49 -0
  16. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.doc.mjs +21 -0
  17. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.tsx +42 -0
  18. package/assets/templates/pages/detail-page/page.tsx +3 -3
  19. package/assets/templates/pages/table-inbox/page.tsx +10 -8
  20. package/authoring/doctypes/base/type.ts +6 -0
  21. package/authoring/doctypes/component/component.doc.mjs +2 -2
  22. package/clients/cli/lib/component-format.mjs +5 -1
  23. 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(path.dirname(fileURLToPath(import.meta.url)), '../../../..');
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('routes a bare name to component.detail', async () => {
26
- const r = await component('Button', {cwd});
27
- expect(r.type).toBe('component.detail');
28
- expect(r.data.name).toBe('Button');
29
- }, SLOW);
30
-
31
- it('routes no-name to the list leaf (component.list, detail=names)', async () => {
32
- const r = await component(undefined, {cwd});
33
- expect(r.type).toBe('component.list');
34
- expect(r.data.detail).toBe('names');
35
- expect(Object.keys(r.data.components).length).toBeGreaterThan(0);
36
- }, SLOW);
37
-
38
- it('routes --category to the list leaf, filtered to that group', async () => {
39
- const r = await component(undefined, {cwd, category: 'Layout'});
40
- expect(r.type).toBe('component.list');
41
- expect(Object.keys(r.data.components)).toContain('Layout');
42
- }, SLOW);
43
-
44
- it('treats an empty-string name as a list (not a not-found error)', async () => {
45
- const r = await component('', {cwd});
46
- expect(r.type).toBe('component.list');
47
- }, SLOW);
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('--detail compact lists with detail=compact', async () => {
52
- const r = await component(undefined, {cwd, list: true, detail: 'compact'});
53
- expect(r.type).toBe('component.list');
54
- expect(r.data.detail).toBe('compact');
55
- }, SLOW);
56
-
57
- it('--detail full lists with detail=full', async () => {
58
- const r = await component(undefined, {cwd, list: true, detail: 'full'});
59
- expect(r.type).toBe('component.list');
60
- expect(r.data.detail).toBe('full');
61
- }, SLOW);
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('--props routes to component.detail.props', async () => {
66
- const r = await component('Button', {cwd, props: true});
67
- expect(r.type).toBe('component.detail.props');
68
- }, SLOW);
69
-
70
- it('--source routes to component.detail.source', async () => {
71
- const r = await component('Button', {cwd, source: true});
72
- expect(r.type).toBe('component.detail.source');
73
- }, SLOW);
74
-
75
- it('--source WINS over --props when both are set (precedence lock)', async () => {
76
- const r = await component('Button', {cwd, props: true, source: true});
77
- expect(r.type).toBe('component.detail.source');
78
- }, SLOW);
79
-
80
- it('--list WINS over a given name (list dispatch precedence lock)', async () => {
81
- const r = await component('Button', {cwd, list: true});
82
- expect(r.type).toBe('component.list');
83
- }, SLOW);
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('strips a legacy XDS prefix (XDSButton -> Button)', async () => {
88
- const r = await component('XDSButton', {cwd});
89
- expect(r.type).toBe('component.detail');
90
- expect(r.data.name).toBe('Button');
91
- }, SLOW);
92
-
93
- it('is case-SENSITIVE: a lowercased name is unknown', async () => {
94
- let err;
95
- try {
96
- await component('button', {cwd});
97
- } catch (e) {
98
- err = e;
99
- }
100
- expect(err).toBeDefined();
101
- expect(err.code).toBe('ERR_UNKNOWN_COMPONENT');
102
- }, SLOW);
103
-
104
- it('throws ERR_UNKNOWN_COMPONENT for a genuinely unknown name', async () => {
105
- let err;
106
- try {
107
- await component('ZzzNope99', {cwd});
108
- } catch (e) {
109
- err = e;
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('--package (core) routes --showcase like the no-scope path', async () => {
131
- const ns = await component('Button', {cwd, showcase: true});
132
- const sc = await component('Button', {cwd, package: CORE, showcase: true});
133
- expect(sc.type).toBe(ns.type); // was silently component.detail before the fix
134
- }, SLOW);
135
-
136
- it('--package (core) routes --blocks like the no-scope path', async () => {
137
- const ns = await component('Button', {cwd, blocks: true});
138
- const sc = await component('Button', {cwd, package: CORE, blocks: true});
139
- expect(sc.type).toBe(ns.type);
140
- expect(sc.type).toBe('component.detail.blocks');
141
- }, SLOW);
142
-
143
- it('--package (core) still routes --source and --props', async () => {
144
- expect((await component('Button', {cwd, package: CORE, source: true})).type).toBe(
145
- 'component.detail.source',
146
- );
147
- expect((await component('Button', {cwd, package: CORE, props: true})).type).toBe(
148
- 'component.detail.props',
149
- );
150
- }, SLOW);
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('a non-string category throws a coded error (not a raw TypeError)', async () => {
155
- for (const bad of [123, {}, [1]]) {
156
- const err = await component(undefined, {
157
- cwd,
158
- category: /** @type {any} */ (bad),
159
- }).catch(e => e);
160
- expect(err).toBeInstanceOf(AstryxError);
161
- expect(err.code).toBe('ERR_UNKNOWN_CATEGORY');
162
- }
163
- }, SLOW);
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('rejects a detail level the CLI rejects, with the same code', async () => {
170
- const cliRun = await runCli(['--json', 'component', '--list', '--detail', 'bogus'], cwd);
171
- expect(cliRun.code).toBe(1);
172
- expect(JSON.parse(cliRun.stdout).code).toBe('ERR_INVALID_DETAIL');
173
-
174
- const bogus = /** @type {any} */ ('bogus');
175
- for (const call of [
176
- () => component(undefined, {cwd, list: true, detail: bogus}),
177
- () => component('Button', {cwd, detail: bogus}),
178
- ]) {
179
- const err = await call().catch(e => e);
180
- expect(err).toBeInstanceOf(AstryxError);
181
- expect(err.code).toBe('ERR_INVALID_DETAIL');
182
- }
183
- }, SLOW);
184
-
185
- it('rejects a lang the CLI rejects, with the same code', async () => {
186
- const cliRun = await runCli(['--json', 'component', 'Button', '--lang', 'fr'], cwd);
187
- expect(cliRun.code).toBe(1);
188
- expect(JSON.parse(cliRun.stdout).code).toBe('ERR_INVALID_LANG');
189
-
190
- for (const call of [
191
- () => component('Button', {cwd, lang: 'fr'}),
192
- () => component(undefined, {cwd, list: true, detail: 'compact', lang: 'fr'}),
193
- ]) {
194
- const err = await call().catch(e => e);
195
- expect(err).toBeInstanceOf(AstryxError);
196
- expect(err.code).toBe('ERR_INVALID_LANG');
197
- }
198
- const zh = await component('Button', {cwd, lang: 'zh'});
199
- expect(zh.type).toBe('component.detail');
200
- }, SLOW);
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('--detail compact returns less text than full', async () => {
205
- const full = await runCli(['component', 'Button'], cwd);
206
- const compact = await runCli(['component', 'Button', '--detail', 'compact'], cwd);
207
- expect(full.code).toBe(0);
208
- expect(compact.code).toBe(0);
209
- expect(compact.stdout.length).toBeLessThan(full.stdout.length);
210
- // Compact still shows the name and import
211
- expect(compact.stdout).toContain('Button');
212
- expect(compact.stdout).toContain('@astryxdesign/core');
213
- }, SLOW);
214
-
215
- it('--detail brief returns less text than compact', async () => {
216
- const compact = await runCli(['component', 'Button', '--detail', 'compact'], cwd);
217
- const brief = await runCli(['component', 'Button', '--detail', 'brief'], cwd);
218
- expect(brief.code).toBe(0);
219
- expect(brief.stdout.length).toBeLessThan(compact.stdout.length);
220
- expect(brief.stdout).toContain('Button');
221
- }, SLOW);
222
-
223
- it('--json --detail compact returns the same full JSON as --json (contract)', async () => {
224
- const full = await runCli(['--json', 'component', 'Button'], cwd);
225
- const compact = await runCli(['--json', 'component', 'Button', '--detail', 'compact'], cwd);
226
- expect(full.code).toBe(0);
227
- expect(compact.code).toBe(0);
228
- const fullData = JSON.parse(full.stdout);
229
- const compactData = JSON.parse(compact.stdout);
230
- // JSON shape is identical — detail is a text-only projection
231
- expect(Object.keys(compactData.data).sort()).toEqual(Object.keys(fullData.data).sort());
232
- }, SLOW);
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
+ }