@astryxdesign/cli 0.7.2-canary.c9f2659 → 0.7.2-canary.cbb7fa3

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 (72) hide show
  1. package/api/component/_adapter.d.mts +41 -1
  2. package/api/component/_adapter.mjs +93 -7
  3. package/api/component/component.mjs +22 -5
  4. package/api/component/component.test.mjs +420 -167
  5. package/api/component/integration-export-name.test.mjs +644 -0
  6. package/api/component/list/list.mjs +42 -4
  7. package/api/gap-report/gap-report.mjs +67 -4
  8. package/api/integration/add-contribution.component-names.test.mjs +66 -0
  9. package/api/integration/add-contribution.mjs +7 -3
  10. package/api/integration/pack-check.mjs +61 -107
  11. package/api/integration/pack-check.test.mjs +359 -0
  12. package/api/search/search.d.mts +9 -0
  13. package/api/search/search.mjs +108 -16
  14. package/api/search/search.test.mjs +36 -0
  15. package/api/swizzle/copy/copy.mjs +28 -14
  16. package/api/template/template.test.mjs +120 -3
  17. package/api/theme/build/build.family.test.mjs +16 -0
  18. package/api/theme/build/build.mjs +77 -18
  19. package/api/theme/themeBuild.doc.mjs +3 -3
  20. package/assets/codemods/transforms/next/__tests__/migrate-list-edge-compensation-to-edge.test.mjs +120 -0
  21. package/assets/codemods/transforms/next/index.mjs +11 -1
  22. package/assets/codemods/transforms/next/migrate-list-edge-compensation-to-edge.mjs +198 -0
  23. package/assets/docs/tree/add-a-component.doc.mjs +10 -0
  24. package/assets/docs/tree/checks.doc.mjs +1 -1
  25. package/assets/docs/tree/component-family.doc.mjs +5 -1
  26. package/assets/docs/tree/component-imports.doc.mjs +8 -0
  27. package/assets/docs/tree/single-component.doc.mjs +1 -1
  28. package/assets/docs/tree/troubleshooting.doc.mjs +9 -1
  29. package/assets/docs/tree/versioning.doc.mjs +1 -0
  30. package/assets/docs/working-with-ai.doc.mjs +43 -4
  31. package/assets/templates/blocks/components/Drawer/DrawerForm.doc.mjs +24 -0
  32. package/assets/templates/blocks/components/Drawer/DrawerForm.tsx +62 -0
  33. package/assets/templates/blocks/components/Drawer/DrawerScrim.doc.mjs +22 -0
  34. package/assets/templates/blocks/components/Drawer/DrawerScrim.tsx +68 -0
  35. package/assets/templates/blocks/components/Drawer/DrawerShowcase.doc.mjs +22 -0
  36. package/assets/templates/blocks/components/Drawer/DrawerShowcase.tsx +41 -0
  37. package/assets/templates/blocks/components/Drawer/DrawerStart.doc.mjs +21 -0
  38. package/assets/templates/blocks/components/Drawer/DrawerStart.tsx +41 -0
  39. package/assets/templates/blocks/components/Drawer/DrawerWidths.doc.mjs +22 -0
  40. package/assets/templates/blocks/components/Drawer/DrawerWidths.tsx +54 -0
  41. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.doc.mjs +15 -0
  42. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.tsx +73 -0
  43. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.doc.mjs +22 -0
  44. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.tsx +49 -0
  45. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.doc.mjs +21 -0
  46. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.tsx +42 -0
  47. package/assets/templates/pages/detail-page/page.tsx +3 -3
  48. package/assets/templates/pages/table-inbox/page.tsx +10 -8
  49. package/authoring/doctypes/base/type.ts +6 -0
  50. package/authoring/doctypes/component/component.doc.mjs +4 -4
  51. package/authoring/doctypes/component/type.ts +6 -3
  52. package/clients/cli/commands/build-theme.path-safety.test.mjs +84 -0
  53. package/clients/cli/commands/build-theme.registry.test.mjs +5 -2
  54. package/clients/cli/commands/theme-build.doc.mjs +2 -2
  55. package/clients/cli/lib/component-format.mjs +5 -1
  56. package/foundation/discovery/component-discovery.d.mts +172 -10
  57. package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
  58. package/foundation/discovery/component-discovery.mjs +863 -55
  59. package/foundation/discovery/component-discovery.test.mjs +96 -0
  60. package/foundation/discovery/component-replacement.mjs +18 -0
  61. package/foundation/discovery/component-replacement.test.mjs +40 -0
  62. package/foundation/discovery/module-exports.d.mts +27 -0
  63. package/foundation/discovery/module-exports.mjs +356 -31
  64. package/foundation/discovery/module-exports.test.mjs +172 -0
  65. package/foundation/discovery/template-adapter.mjs +74 -5
  66. package/foundation/doc-compiler/doc-loads.test.mjs +5 -1
  67. package/foundation/doc-compiler/inputs.mjs +23 -0
  68. package/foundation/doc-compiler/inputs.test.mjs +0 -1
  69. package/foundation/integrations/contribution-inventory.mjs +6 -1
  70. package/foundation/xle/registry.mjs +4 -0
  71. package/foundation/xle/registry.test.mjs +28 -0
  72. package/package.json +10 -10
@@ -0,0 +1,198 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Codemod: migrate List edgeCompensation to edge
5
+ *
6
+ * `edgeCompensation="inline"` on an imported `List` becomes `edge="outset"`,
7
+ * the value it already resolves to. List resolves
8
+ * `edge ?? (edgeCompensation === 'inline' ? 'outset' : 'padded')`, so `edge`
9
+ * wins only when it has a value. The deprecated prop is removed beside an
10
+ * `edge` string that no later spread can override; beside a computed `edge`,
11
+ * which may be undefined at runtime, it gets a TODO instead.
12
+ *
13
+ * Built on the shared `transformProp` helper, so it does not follow
14
+ * variables, helpers, wrappers, or types. Where a List writes
15
+ * `edgeCompensation` but the codemod cannot prove the result, it keeps the
16
+ * old prop and leaves a TODO instead of guessing: on a dynamic value, and
17
+ * wherever the List also spreads props. A spread before the old prop may
18
+ * carry an `edge` that wins today and that a new `edge` would override; one
19
+ * after it may override either prop. A spread onto a List that never writes
20
+ * `edgeCompensation` is not evidence of use, so it stays unchanged
21
+ * (`spec:AST-040` FR1): the deprecated prop keeps working through it, and
22
+ * its `@deprecated` type reports a typed use where the props are written.
23
+ * Comments are the codemod's only report channel (`api.report` is a stub),
24
+ * and each is added once, so a second run changes nothing.
25
+ */
26
+
27
+ import {transformProp} from '../../transform-prop.mjs';
28
+
29
+ export const meta = {
30
+ title: 'Migrate List edgeCompensation to edge',
31
+ description:
32
+ 'Renames `edgeCompensation="inline"` to `edge="outset"` on List, and ' +
33
+ 'drops `edgeCompensation` beside an `edge` string. Keeps the old prop ' +
34
+ 'with a TODO where `edge` is computed or a later spread may override ' +
35
+ 'it, on a dynamic value, and where the List also spreads props; the ' +
36
+ 'deprecated prop still works there.',
37
+ };
38
+
39
+ const OLD_NAME = 'edgeCompensation';
40
+ const NEW_NAME = 'edge';
41
+ const CORE_IMPORT_RE = /^@(astryxdesign|xds)\/core(?:\/|$)/;
42
+
43
+ const COMPONENT_PROPS = new Map([['List', new Set([OLD_NAME])]]);
44
+
45
+ const DYNAMIC_COMMENT =
46
+ ' TODO(astryx upgrade): edgeCompensation is deprecated. Replace ' +
47
+ 'edgeCompensation="inline" with edge="outset" by hand; this value is ' +
48
+ 'computed. ';
49
+ const BOTH_SET_COMMENT =
50
+ ' TODO(astryx upgrade): edgeCompensation is deprecated. edge is also set ' +
51
+ 'here, but it may be undefined, and then edgeCompensation still applies. ' +
52
+ 'Make edge default to "outset" where edgeCompensation is "inline", then ' +
53
+ 'remove edgeCompensation. ';
54
+ const SPREAD_COMMENT =
55
+ ' TODO(astryx upgrade): edgeCompensation is deprecated, but this List ' +
56
+ 'also spreads props that may set edge, so it was left unchanged. Replace ' +
57
+ 'edgeCompensation="inline" with edge="outset" by hand, after checking ' +
58
+ 'which edge the spread passes. ';
59
+
60
+ const TYPE_WRAPPERS = new Set([
61
+ 'TSAsExpression',
62
+ 'TSSatisfiesExpression',
63
+ 'TSTypeAssertion',
64
+ 'TSNonNullExpression',
65
+ 'TypeCastExpression',
66
+ 'ParenthesizedExpression',
67
+ ]);
68
+
69
+ /** @param {any} node */
70
+ function unwrap(node) {
71
+ let current = node;
72
+ while (current && TYPE_WRAPPERS.has(current.type)) {
73
+ current = current.expression;
74
+ }
75
+ return current;
76
+ }
77
+
78
+ /** @param {any} node */
79
+ function isStringLiteral(node) {
80
+ const value = unwrap(node);
81
+ return (
82
+ value != null &&
83
+ (value.type === 'StringLiteral' || value.type === 'Literal') &&
84
+ typeof value.value === 'string'
85
+ );
86
+ }
87
+
88
+ /**
89
+ * The value of a JSX attribute: the string of `edge="flush"`, or the
90
+ * expression inside `edge={...}`.
91
+ * @param {any} attribute
92
+ */
93
+ function attributeValue(attribute) {
94
+ return attribute.value?.type === 'JSXExpressionContainer'
95
+ ? attribute.value.expression
96
+ : attribute.value;
97
+ }
98
+
99
+ /** @param {any} node */
100
+ function isInlineLiteral(node) {
101
+ const value = unwrap(node);
102
+ return (
103
+ value != null &&
104
+ (value.type === 'StringLiteral' || value.type === 'Literal') &&
105
+ value.value === 'inline'
106
+ );
107
+ }
108
+
109
+ /**
110
+ * Attach a comment to a node once, so re-running the codemod adds nothing.
111
+ * @param {any} j @param {any} node @param {string} text
112
+ */
113
+ function addComment(j, node, text) {
114
+ node.comments ??= [];
115
+ if (node.comments.some((/** @type {any} */ c) => c.value === text)) {
116
+ return false;
117
+ }
118
+ node.comments.push(j.commentBlock(text, true, false));
119
+ return true;
120
+ }
121
+
122
+ /** @param {any} attributes */
123
+ function firstSpread(attributes) {
124
+ return attributes.find(
125
+ (/** @type {any} */ attribute) => attribute.type === 'JSXSpreadAttribute',
126
+ );
127
+ }
128
+
129
+ /**
130
+ * @param {import('../../../../authoring/codemod/type').AstryxCodemodFile} file
131
+ * @param {import('../../../../authoring/codemod/type').CodemodTransformApi} api
132
+ * @returns {string | null | undefined}
133
+ */
134
+ export default function transformer(file, api) {
135
+ const j = api.jscodeshift;
136
+ const root = j(file.source);
137
+ let hasChanges = false;
138
+
139
+ transformProp(
140
+ root,
141
+ j,
142
+ {
143
+ matchesImport: source => CORE_IMPORT_RE.test(source),
144
+ components: COMPONENT_PROPS,
145
+ },
146
+ propPath => {
147
+ const opening = propPath.parent?.node;
148
+ const attributes = opening?.attributes ?? [];
149
+ const prop = propPath.node;
150
+ const edgeIndex = attributes.findIndex(
151
+ (/** @type {any} */ attribute) =>
152
+ attribute.type === 'JSXAttribute' &&
153
+ attribute.name?.name === NEW_NAME,
154
+ );
155
+ if (edgeIndex !== -1) {
156
+ // `edge` wins only when it has a value. A string that no later
157
+ // spread can override always does, so the old prop is inert and
158
+ // removing it changes nothing. A computed `edge` may be undefined,
159
+ // and then the old prop still decides.
160
+ const isProven =
161
+ isStringLiteral(attributeValue(attributes[edgeIndex])) &&
162
+ !attributes
163
+ .slice(edgeIndex + 1)
164
+ .some(
165
+ (/** @type {any} */ attribute) =>
166
+ attribute.type === 'JSXSpreadAttribute',
167
+ );
168
+ if (isProven) {
169
+ opening.attributes = attributes.filter(
170
+ (/** @type {any} */ attribute) => attribute !== prop,
171
+ );
172
+ hasChanges = true;
173
+ } else {
174
+ hasChanges = addComment(j, prop, BOTH_SET_COMMENT) || hasChanges;
175
+ }
176
+ return;
177
+ }
178
+ // With a spread on the element the rename cannot be proven: a spread
179
+ // before the old prop may carry an `edge` that wins today and that a
180
+ // new `edge` would override, and one after it may override either
181
+ // prop. The old prop keeps working, so it stays, with a TODO.
182
+ if (firstSpread(attributes)) {
183
+ hasChanges = addComment(j, prop, SPREAD_COMMENT) || hasChanges;
184
+ return;
185
+ }
186
+ if (isInlineLiteral(attributeValue(prop))) {
187
+ prop.name.name = NEW_NAME;
188
+ prop.value = j.stringLiteral('outset');
189
+ hasChanges = true;
190
+ } else {
191
+ hasChanges = addComment(j, prop, DYNAMIC_COMMENT) || hasChanges;
192
+ }
193
+ },
194
+ );
195
+
196
+ if (!hasChanges) return undefined;
197
+ return root.toSource({quote: 'single'});
198
+ }
@@ -71,5 +71,15 @@ Declare component root ./components in astryx.integration.mjs.
71
71
  },
72
72
  ],
73
73
  },
74
+ {
75
+ id: 'document-an-existing-component',
76
+ title: 'Document a component you already have',
77
+ content: [
78
+ {
79
+ type: 'prose',
80
+ text: "To document a component your package already has, write its doc beside the source file and name the doc after that file, such as `acme-gallery.doc.mjs` beside `acme-gallery.tsx`. Set `name` to the export apps import, such as `name: 'AcmeGallery'`, and keep both files inside the components root that `astryx.integration.mjs` declares. The file does not have to be named after the component; {@link generic:component-imports} shows how apps then find it.",
81
+ },
82
+ ],
83
+ },
74
84
  ],
75
85
  };
@@ -105,7 +105,7 @@ export const docs = {
105
105
  'It runs `npm pack` the way `npm publish` would, including your `prepack` script.',
106
106
  'It checks that every contribution file is in the `.tgz` file. A root missing from `files` fails with `Add "templates" to "files" in package.json.`',
107
107
  'It lists the components, templates, themes, docs, and codemods the temporary app sees, and compares them with your source.',
108
- "It resolves each component's `import`, and each template's public import, the way Node does, and checks that the module exports the component, or a default export for a template.",
108
+ "It resolves each component's `import`, and each template's public import, the way Node does, and checks that the module exports the component under the name its doc documents, or a default export for a template. A type-only export does not count.",
109
109
  'For each theme, it rebuilds the source, compares the built module and production CSS with the authored outputs, and proves the module, CSS, and optional font CSS exports resolve from the packed package.',
110
110
  'It fails a package that needs an `@astryxdesign/cli` peer and lacks it: `>=0.6.6` for a template that sets `keywords`, and `>=0.6.4` for a template that sets `replaces`, a docs section, a placed guide, a doc section with an `id`, or a theme.',
111
111
  ],
@@ -95,7 +95,11 @@ export default {
95
95
  },
96
96
  {
97
97
  type: 'prose',
98
- text: 'The public module named by `import` must export every component or hook named by a full entry.',
98
+ text: "The public module named by `import` must export every component or hook named by a full entry. Apps then list, search and open each component in the family by its own name. A full entry the module does not export is not listed, and `integration verify` does not report it: check the family with `astryx component --list`. When `import` names output your pack lifecycle builds (such as `dist`), the members are listed once that output exists, and `integration verify` compares them as they are listed after the pack builds it.",
99
+ },
100
+ {
101
+ type: 'prose',
102
+ text: "A member's source is the `.tsx` file in the family doc's folder, or below it, that declares it: the one the family's own file or the module `import` names reaches, else the one in that folder that declares the name, as when `import` names built output. A member that only a bundle or a file outside that folder declares is still listed, with no source for `--source` or `swizzle`. A member is not listed when a doc's file or a Core name your package replaces already has its name. A member that shares a name with Core opens with `--package`.",
99
103
  },
100
104
  ],
101
105
  },
@@ -35,6 +35,14 @@ export const docs = {
35
35
  type: 'prose',
36
36
  text: 'Add writes the entry only when package.json already has an `exports` map, so start every package with `"exports": {}`.',
37
37
  },
38
+ {
39
+ type: 'prose',
40
+ text: 'A file does not have to be named after its component. When `Slides.tsx` exports `AcmeCarousel` and nothing named `Slides`, keep the doc beside it as `Slides.doc.mjs` and set its `name` to `AcmeCarousel`: apps list, search and open the component by that name, and `integration verify` checks that its import exports `AcmeCarousel`. A file that exports its own name keeps that name. A package that names its files in kebab case works the same way: `acme-gallery.doc.mjs` beside `acme-gallery.tsx` sets `name: \'AcmeGallery\'`.',
41
+ },
42
+ {
43
+ type: 'prose',
44
+ text: "The file name still opens the doc. A name that Core or another doc's file already has stays theirs, and so does a Core name your package replaces: with `Badge.doc.mjs` naming `AcmeBadge`, `astryx component Badge` opens Core's `Badge`, and `--package` with your package name reaches yours.",
45
+ },
38
46
  ],
39
47
  },
40
48
  {
@@ -56,7 +56,7 @@ export default {
56
56
  style: 'unordered',
57
57
  items: [
58
58
  'Keep `props` on the top-level doc. Private implementation helpers do not need entries.',
59
- 'Keep the doc beside the source and change both in the same pull request.',
59
+ 'Name the doc after the source file beside it, such as `AcmeCarousel.doc.mjs` beside `AcmeCarousel.tsx`, and change both in the same pull request.',
60
60
  'If the module later exposes several related public exports, adapt this doc with {@link generic:component-family}.',
61
61
  ],
62
62
  },
@@ -67,7 +67,7 @@ export const docs = {
67
67
  rows: [
68
68
  [
69
69
  '`component_export_missing`: `Component "AcmeCarousel" advertises import "@acme/astryx-widgets/components/AcmeCarousel", but that packed module does not export "AcmeCarousel".`',
70
- "package.json has no `exports` map, or the module does not export the name. Keep `export function AcmeCarousel`, and add the entry: `npm pkg set 'exports[./components/AcmeCarousel]=./components/AcmeCarousel.tsx'`.",
70
+ "package.json has no `exports` map, or the module does not export the name. Keep `export function AcmeCarousel`, and add the entry: `npm pkg set 'exports[./components/AcmeCarousel]=./components/AcmeCarousel.tsx'`. When the quoted name is a file name, such as `acme-carousel`, set the doc's `name` to the component the file exports. A type-only export does not count.",
71
71
  ],
72
72
  [
73
73
  '`template_export_missing`: `Template "acme-dashboard" public import "@acme/astryx-widgets/templates/acme-dashboard" does not have a default export.`',
@@ -125,6 +125,14 @@ export const docs = {
125
125
  type: 'table',
126
126
  headers: ['Symptom', 'Fix'],
127
127
  rows: [
128
+ [
129
+ '`Error: No component named "AcmeCarousel"` for a component your package documents.',
130
+ "Its doc still names its file. Set `name` to the export, such as `name: 'AcmeCarousel'` in `acme-carousel.doc.mjs`. A family member is listed only when the module its family doc imports exports it ({@link generic:component-family}).",
131
+ ],
132
+ [
133
+ '`npx astryx component Dialog` opens Core\'s `Dialog`, not yours.',
134
+ "A name that Core, a legacy docs package, or another package's file already has stays theirs. Run `npx astryx component Dialog --package @acme/astryx-widgets`, or give your component a name of its own.",
135
+ ],
128
136
  [
129
137
  '`Error: Template "dashboard" is ambiguous — narrow it with --type and/or --package.`',
130
138
  "Your template id matches a Core id. Rename it, such as `acme-dashboard`, or set `replaces: 'dashboard'` to take its place. `npx astryx doctor integration templates` warns about this before you publish.",
@@ -140,6 +140,7 @@ export const docs = {
140
140
  'Published 0.6.3 and earlier reject a template that sets `replaces` or `keywords`, drop that template, and hide every doc topic your package ships. Published 0.6.4 and 0.6.5 still drop a template that sets `keywords`.',
141
141
  'A stable CLI before 0.6.4 cannot read a docs section, a section `id`, or a theme folder that `integration add theme` writes. It can then hide every doc topic your package ships.',
142
142
  'Stable 0.6.3 still loads your components, but 0.6.0 cannot read the component docs that `integration add component` writes: `component AcmeCarousel` fails there.',
143
+ 'An app on an older CLI lists a component whose doc names another export of its file under the file name, and lists no family members, so `component AcmeCarousel` fails there.',
143
144
  ],
144
145
  },
145
146
  {
@@ -44,7 +44,7 @@ export const docs = {
44
44
  },
45
45
  {
46
46
  type: 'prose',
47
- text: 'By default this creates `AGENTS.md` (the tool-agnostic standard most agents read). To target a specific tool\'s file instead:',
47
+ text: "By default this creates `AGENTS.md` (the tool-agnostic standard most agents read). To target a specific tool's file instead:",
48
48
  },
49
49
  {
50
50
  type: 'code',
@@ -182,21 +182,60 @@ astryx docs principles --dense`,
182
182
  },
183
183
  {
184
184
  type: 'prose',
185
- text: 'Add the server to your MCP config file. This works with any MCP-compatible tool: Claude Desktop (claude_desktop_config.json), Cursor (.cursor/mcp.json), Windsurf (.windsurf/mcp.json), Cline, and others.',
185
+ text: 'Add the server to your MCP config file. The entry is a remote HTTP server, and the exact shape differs per client, so use the snippet for the tool you run. An entry with an unrecognized transport is skipped with a warning rather than an error, which reads as a server that connects but exposes no tools.',
186
186
  },
187
187
  {
188
188
  type: 'code',
189
189
  lang: 'json',
190
- label: 'MCP config (same for all tools)',
190
+ label: 'Claude Code (.mcp.json)',
191
191
  code: `{
192
192
  "mcpServers": {
193
193
  "astryx": {
194
- "type": "url",
194
+ "type": "http",
195
195
  "url": "https://astryx.atmeta.com/mcp"
196
196
  }
197
197
  }
198
198
  }`,
199
199
  },
200
+ {
201
+ type: 'prose',
202
+ text: 'Claude Code can also write that entry from the terminal:',
203
+ },
204
+ {
205
+ type: 'code',
206
+ lang: 'bash',
207
+ code: `claude mcp add --transport http astryx https://astryx.atmeta.com/mcp`,
208
+ },
209
+ {
210
+ type: 'code',
211
+ lang: 'json',
212
+ label: 'Cline (cline_mcp_settings.json) - type is streamableHttp',
213
+ code: `{
214
+ "mcpServers": {
215
+ "astryx": {
216
+ "type": "streamableHttp",
217
+ "url": "https://astryx.atmeta.com/mcp"
218
+ }
219
+ }
220
+ }`,
221
+ },
222
+ {
223
+ type: 'code',
224
+ lang: 'json',
225
+ label:
226
+ 'Cursor (.cursor/mcp.json) - remote servers take no type field',
227
+ code: `{
228
+ "mcpServers": {
229
+ "astryx": {
230
+ "url": "https://astryx.atmeta.com/mcp"
231
+ }
232
+ }
233
+ }`,
234
+ },
235
+ {
236
+ type: 'prose',
237
+ text: 'Claude Desktop is the exception: claude_desktop_config.json holds local stdio servers, so a remote server is added through Settings, then Connectors, then Add custom connector, pasting the URL above.',
238
+ },
200
239
  {
201
240
  type: 'prose',
202
241
  text: 'Once connected, your AI tool can search for components by natural language (e.g. "dropdown menu", "success message") and retrieve full documentation without any manual CLI invocation. The server uses the same keyword index from component docs, so search quality improves automatically as component documentation is updated.',
@@ -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
+ }
@@ -0,0 +1,22 @@
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: 'Modal or non-modal (hasScrim)',
7
+ displayName: 'Modal or non-modal (hasScrim)',
8
+ description:
9
+ 'The default modal drawer dims the page, traps focus, and closes on a scrim click. With hasScrim={false}, the drawer has no scrim or focus trap and leaves the page interactive.',
10
+ exampleFor: 'Drawer',
11
+ isReady: true,
12
+ aspectRatio: 16 / 9,
13
+ componentsUsed: [
14
+ 'Drawer',
15
+ 'Button',
16
+ 'Layout',
17
+ 'LayoutContent',
18
+ 'HStack',
19
+ 'DrawerHeader',
20
+ 'Text',
21
+ ],
22
+ };
@@ -0,0 +1,68 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @input Separate controlled states for modal and non-modal drawers
7
+ * @output A comparison of hasScrim's two presentations
8
+ * @position Copyable Drawer example
9
+ */
10
+
11
+ import {useState} from 'react';
12
+ import {Drawer, DrawerHeader} from '@astryxdesign/core/Drawer';
13
+ import {Button} from '@astryxdesign/core/Button';
14
+ import {Layout, LayoutContent} from '@astryxdesign/core/Layout';
15
+ import {HStack} from '@astryxdesign/core/Stack';
16
+ import {Text} from '@astryxdesign/core/Text';
17
+
18
+ export default function DrawerScrim() {
19
+ const [openModal, setOpenModal] = useState(false);
20
+ const [openPanel, setOpenPanel] = useState(false);
21
+
22
+ return (
23
+ <>
24
+ <HStack gap={2}>
25
+ <Button label="Open modal drawer" onClick={() => setOpenModal(true)} />
26
+ <Button
27
+ label="Open non-modal drawer"
28
+ onClick={() => setOpenPanel(true)}
29
+ />
30
+ </HStack>
31
+ <Drawer
32
+ isOpen={openModal}
33
+ onOpenChange={setOpenModal}
34
+ label="Edit details">
35
+ <Layout
36
+ header={
37
+ <DrawerHeader title="Edit details" onOpenChange={setOpenModal} />
38
+ }
39
+ content={
40
+ <LayoutContent>
41
+ <Text type="body">
42
+ The scrim blocks the page behind this modal drawer.
43
+ </Text>
44
+ </LayoutContent>
45
+ }
46
+ />
47
+ </Drawer>
48
+ <Drawer
49
+ isOpen={openPanel}
50
+ onOpenChange={setOpenPanel}
51
+ label="Inspector"
52
+ hasScrim={false}>
53
+ <Layout
54
+ header={
55
+ <DrawerHeader title="Inspector" onOpenChange={setOpenPanel} />
56
+ }
57
+ content={
58
+ <LayoutContent>
59
+ <Text type="body">
60
+ The page behind this non-modal drawer stays interactive.
61
+ </Text>
62
+ </LayoutContent>
63
+ }
64
+ />
65
+ </Drawer>
66
+ </>
67
+ );
68
+ }
@@ -0,0 +1,22 @@
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: 'Basic',
7
+ displayName: 'Basic',
8
+ description:
9
+ 'Open a modal drawer from a single trigger. The scrim dims the page; Escape, a scrim click, or the header close button closes it, and focus returns to the trigger.',
10
+ exampleFor: 'Drawer',
11
+ isReady: true,
12
+ isShowcase: true,
13
+ aspectRatio: 16 / 9,
14
+ componentsUsed: [
15
+ 'Drawer',
16
+ 'Button',
17
+ 'Layout',
18
+ 'LayoutContent',
19
+ 'DrawerHeader',
20
+ 'Text',
21
+ ],
22
+ };
@@ -0,0 +1,41 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @input Controlled open state for a modal Drawer
7
+ * @output A single-trigger drawer with a scrim and a DrawerHeader close button
8
+ * @position Drawer's docsite showcase and copyable CLI block
9
+ */
10
+
11
+ import {useState} from 'react';
12
+ import {Drawer, DrawerHeader} from '@astryxdesign/core/Drawer';
13
+ import {Button} from '@astryxdesign/core/Button';
14
+ import {Layout, LayoutContent} from '@astryxdesign/core/Layout';
15
+ import {Text} from '@astryxdesign/core/Text';
16
+
17
+ export default function DrawerShowcase() {
18
+ const [isOpen, setIsOpen] = useState(false);
19
+
20
+ return (
21
+ <>
22
+ <Button label="Open drawer" onClick={() => setIsOpen(true)} />
23
+ <Drawer
24
+ isOpen={isOpen}
25
+ onOpenChange={setIsOpen}
26
+ label="Details"
27
+ width={360}>
28
+ <Layout
29
+ header={<DrawerHeader title="Details" onOpenChange={setIsOpen} />}
30
+ content={
31
+ <LayoutContent>
32
+ <Text type="body">
33
+ Close with Escape, the scrim, or the close button.
34
+ </Text>
35
+ </LayoutContent>
36
+ }
37
+ />
38
+ </Drawer>
39
+ </>
40
+ );
41
+ }
@@ -0,0 +1,21 @@
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: 'Slide in from the start edge',
7
+ displayName: 'Slide in from the start edge',
8
+ description:
9
+ 'Set side="start" to open from the inline start edge: left in LTR and right in RTL. The default end edge follows the inspector convention.',
10
+ exampleFor: 'Drawer',
11
+ isReady: true,
12
+ aspectRatio: 16 / 9,
13
+ componentsUsed: [
14
+ 'Drawer',
15
+ 'Button',
16
+ 'Layout',
17
+ 'LayoutContent',
18
+ 'DrawerHeader',
19
+ 'Text',
20
+ ],
21
+ };