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

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 (26) hide show
  1. package/api/component/component.test.mjs +357 -184
  2. package/api/template/template.test.mjs +120 -3
  3. package/assets/templates/blocks/components/Drawer/DrawerForm.doc.mjs +24 -0
  4. package/assets/templates/blocks/components/Drawer/DrawerForm.tsx +62 -0
  5. package/assets/templates/blocks/components/Drawer/DrawerScrim.doc.mjs +22 -0
  6. package/assets/templates/blocks/components/Drawer/DrawerScrim.tsx +68 -0
  7. package/assets/templates/blocks/components/Drawer/DrawerShowcase.doc.mjs +22 -0
  8. package/assets/templates/blocks/components/Drawer/DrawerShowcase.tsx +41 -0
  9. package/assets/templates/blocks/components/Drawer/DrawerStart.doc.mjs +21 -0
  10. package/assets/templates/blocks/components/Drawer/DrawerStart.tsx +41 -0
  11. package/assets/templates/blocks/components/Drawer/DrawerWidths.doc.mjs +22 -0
  12. package/assets/templates/blocks/components/Drawer/DrawerWidths.tsx +54 -0
  13. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.doc.mjs +15 -0
  14. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.tsx +73 -0
  15. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.doc.mjs +22 -0
  16. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.tsx +49 -0
  17. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.doc.mjs +21 -0
  18. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.tsx +42 -0
  19. package/assets/templates/pages/detail-page/page.tsx +3 -3
  20. package/assets/templates/pages/table-inbox/page.tsx +10 -8
  21. package/authoring/doctypes/base/type.ts +6 -0
  22. package/authoring/doctypes/component/component.doc.mjs +2 -2
  23. package/clients/cli/lib/component-format.mjs +5 -1
  24. package/foundation/discovery/template-adapter.mjs +74 -5
  25. package/foundation/doc-compiler/doc-loads.test.mjs +1 -1
  26. package/package.json +10 -10
@@ -0,0 +1,49 @@
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 DrawerHeader with a subtitle, a status badge in its end slot, and a close button
8
+ * @position Copyable DrawerHeader example
9
+ */
10
+
11
+ import {useState} from 'react';
12
+ import {Drawer, DrawerHeader} from '@astryxdesign/core/Drawer';
13
+ import {Badge} from '@astryxdesign/core/Badge';
14
+ import {Button} from '@astryxdesign/core/Button';
15
+ import {Layout, LayoutContent} from '@astryxdesign/core/Layout';
16
+ import {Text} from '@astryxdesign/core/Text';
17
+
18
+ export default function DrawerHeaderSubtitleEndContent() {
19
+ const [isOpen, setIsOpen] = useState(false);
20
+
21
+ return (
22
+ <>
23
+ <Button label="Open host details" onClick={() => setIsOpen(true)} />
24
+ <Drawer
25
+ isOpen={isOpen}
26
+ onOpenChange={setIsOpen}
27
+ label="web-prod-04"
28
+ width={360}>
29
+ <Layout
30
+ header={
31
+ <DrawerHeader
32
+ title="web-prod-04"
33
+ subtitle="us-east-1"
34
+ endContent={<Badge label="Healthy" variant="success" />}
35
+ onOpenChange={setIsOpen}
36
+ />
37
+ }
38
+ content={
39
+ <LayoutContent>
40
+ <Text type="body">
41
+ All 6 instances are passing readiness checks.
42
+ </Text>
43
+ </LayoutContent>
44
+ }
45
+ />
46
+ </Drawer>
47
+ </>
48
+ );
49
+ }
@@ -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: 'With close button',
7
+ displayName: 'With close button',
8
+ description:
9
+ 'Pass onOpenChange to render a close button that calls it with false. Drawer renders no close button of its own, so this is the usual visible way to close.',
10
+ exampleFor: 'DrawerHeader',
11
+ isReady: true,
12
+ aspectRatio: 16 / 9,
13
+ componentsUsed: [
14
+ 'Drawer',
15
+ 'DrawerHeader',
16
+ 'Button',
17
+ 'Layout',
18
+ 'LayoutContent',
19
+ 'Text',
20
+ ],
21
+ };
@@ -0,0 +1,42 @@
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 Drawer whose DrawerHeader renders a close button because it receives onOpenChange
8
+ * @position Copyable DrawerHeader 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 {Text} from '@astryxdesign/core/Text';
16
+
17
+ export default function DrawerHeaderWithCloseButton() {
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
+ Passing onOpenChange renders the close button, which calls it
34
+ with false.
35
+ </Text>
36
+ </LayoutContent>
37
+ }
38
+ />
39
+ </Drawer>
40
+ </>
41
+ );
42
+ }
@@ -600,9 +600,9 @@ export default function DetailPage2Template() {
600
600
  }
601
601
  end={!isNarrow && showSidePanel ? <RightPanel /> : undefined}
602
602
  />
603
- {/* Mobile: the side panel content opens as a full-screen dialog. (A
604
- side drawer/sheet would be more idiomatic, but Astryx has no Drawer
605
- component yet — #2575 — so we use the fullscreen Dialog variant.) */}
603
+ {/* Mobile: the side-panel content uses the full viewport so order details
604
+ are not squeezed beside narrow page chrome. Drawer remains available
605
+ for side-overlay flows that intentionally preserve page context. */}
606
606
  <Dialog
607
607
  variant="fullscreen"
608
608
  isOpen={isNarrow && isPanelDialogOpen}
@@ -1652,8 +1652,8 @@ const COMPOSER_TOOLS: {
1652
1652
  /**
1653
1653
  * The compose sheet.
1654
1654
  *
1655
- * **`BottomSheet hasScrim={false}`, which is the only non-modal surface in
1656
- * core.** With the scrim off the sheet opens with `show()` rather than
1655
+ * **`BottomSheet hasScrim={false}` keeps this bottom-edge composer non-modal.**
1656
+ * With the scrim off the sheet opens with `show()` rather than
1657
1657
  * `showModal()`, so it never enters the top layer, never makes the page inert
1658
1658
  * and never locks body scroll: the inbox behind it stays live. That matters
1659
1659
  * more here than anywhere else in this template, because writing a message is
@@ -1661,12 +1661,14 @@ const COMPOSER_TOOLS: {
1661
1661
  * actually said, copy an order number out of a thread. A modal composer
1662
1662
  * answers that with "close me first".
1663
1663
  *
1664
- * The trade is position, and it is worth naming. A sheet rises from the bottom
1665
- * edge, centred: its positioner is `inset-inline: 0` with
1666
- * `justify-content: center`, and there is no anchor, side or width prop. So
1667
- * this cannot be docked to the inline-end corner the way a `Dialog` can be.
1668
- * `Dialog` takes a static `position` and would put the window in that corner —
1669
- * but `Dialog` is always modal, so buying the corner costs the live inbox.
1664
+ * The trade is bottom-edge geometry, and it is worth naming. A non-modal
1665
+ * `Drawer` keeps page context live from an inline edge, but it is a full-height
1666
+ * side panel; this composer intentionally rises from the bottom. A sheet's
1667
+ * positioner is `inset-inline: 0` with `justify-content: center`, and there is
1668
+ * no anchor, side or width prop. So this cannot be docked to the inline-end
1669
+ * corner the way a `Dialog` can be. `Dialog` takes a static `position` and would
1670
+ * put the window in that corner — but `Dialog` is always modal, so buying the
1671
+ * corner costs the live inbox.
1670
1672
  * `ChatComposerDrawer` is a false friend: despite the name it is an in-flow
1671
1673
  * disclosure tray for a ChatComposer's attachments, with no positioning of its
1672
1674
  * own.
@@ -383,6 +383,12 @@ export interface ComponentPropDoc {
383
383
  * ```
384
384
  */
385
385
  slotElements?: ComponentSlotElement[];
386
+ /** Lifecycle marker for a prop outside the stable compatibility promise
387
+ * (spec:AST-017 FR21). Set `'experimental'` on a stable component's
388
+ * optional `experimental` prop object, and say in `description` that its
389
+ * contents may change or be removed in a patch release. Omit for stable
390
+ * props. */
391
+ stability?: 'experimental';
386
392
  }
387
393
 
388
394
  /**
@@ -56,7 +56,7 @@ export const doc = {
56
56
  name: 'replaces',
57
57
  type: 'string',
58
58
  description:
59
- "Integration components only: the exact `name` of the Core ComponentDoc this component takes over for unqualified lookup, so every app that loads the integration gets it from component detail, component lists, search, `swizzle <Name>`, and issue routing; `swizzle --list` keeps listing Core names. The Core original stays reachable with `--package @astryxdesign/core`. It takes effect only when the package's peer range starts at the release that applies it, `\"@astryxdesign/cli\": \">=0.6.7\"` or later; without such a range the component keeps its own name and `doctor integration components` warns. Set it only to intentionally own a Core identity; give an alternative or variant its own name instead.",
59
+ 'Integration components only: the exact `name` of the Core ComponentDoc this component takes over for unqualified lookup, so every app that loads the integration gets it from component detail, component lists, search, `swizzle <Name>`, and issue routing; `swizzle --list` keeps listing Core names. The Core original stays reachable with `--package @astryxdesign/core`. It takes effect only when the package\'s peer range starts at the release that applies it, `"@astryxdesign/cli": ">=0.6.7"` or later; without such a range the component keeps its own name and `doctor integration components` warns. Set it only to intentionally own a Core identity; give an alternative or variant its own name instead.',
60
60
  },
61
61
  {
62
62
  name: 'keywords',
@@ -181,7 +181,7 @@ export const doc = {
181
181
  name: 'props',
182
182
  type: 'ComponentPropDoc[]',
183
183
  description:
184
- 'SingleComponentDoc variant (required there): all public props for the one primary component. Each prop is {name, type, description, default?, required?, slotElements?}. Skip styling props like xstyle/className/style. Also present on SubComponentDoc.',
184
+ 'SingleComponentDoc variant (required there): all public props for the one primary component. Each prop is {name, type, description, default?, required?, slotElements?, stability?}. Skip styling props like xstyle/className/style. Also present on SubComponentDoc.',
185
185
  },
186
186
  {
187
187
  name: 'components',
@@ -68,8 +68,12 @@ function formatPropsTable(props) {
68
68
  for (const p of props) {
69
69
  const def = p.default ? `\`${mdCell(p.default)}\`` : '-';
70
70
  const req = p.required ? ' **(required)**' : '';
71
+ // The text projection of the prop doc's `stability` field: an
72
+ // experimental prop is outside the stable compatibility promise.
73
+ const stability =
74
+ p.stability === 'experimental' ? ' **(experimental)**' : '';
71
75
  lines.push(
72
- `| \`${mdCell(p.name)}\` | \`${mdCell(p.type)}\` | ${def} | ${mdCell(p.description)}${req} |`,
76
+ `| \`${mdCell(p.name)}\` | \`${mdCell(p.type)}\` | ${def} | ${mdCell(p.description)}${req}${stability} |`,
73
77
  );
74
78
  }
75
79
  return lines.join('\n');
@@ -15,7 +15,8 @@
15
15
  * needs it: `Project` assembles templates from it. While this sat in api/, that
16
16
  * made a cycle across the layer boundary — Project reached up for discovery and
17
17
  * this module imported Project back. Nothing here depends on api/; every import
18
- * is node:*, jiti, foundation/*, or authoring/*.
18
+ * is node:*, jiti, foundation/*, or authoring/*, plus a lazily required
19
+ * @babel/parser for extractComponents' local-declaration scan.
19
20
  *
20
21
  * @position packages/cli/foundation/discovery — shared template discovery/IO;
21
22
  * consumed by Project and by the api/template leaves.
@@ -512,6 +513,19 @@ function expressionUse(source, open, close) {
512
513
  return null;
513
514
  }
514
515
 
516
+ /** @type {typeof import('@babel/parser') | undefined} */
517
+ let babelParser;
518
+
519
+ /**
520
+ * The template source parser, loaded on first use so listing templates
521
+ * never pays for it.
522
+ * @returns {typeof import('@babel/parser')}
523
+ */
524
+ function templateSourceParser() {
525
+ babelParser ??= require('@babel/parser');
526
+ return /** @type {typeof import('@babel/parser')} */ (babelParser);
527
+ }
528
+
515
529
  /**
516
530
  * The [start, end) range of every comment in `source`, or none when it does
517
531
  * not parse as TSX.
@@ -520,8 +534,7 @@ function expressionUse(source, open, close) {
520
534
  */
521
535
  function commentRanges(source) {
522
536
  try {
523
- const {parse} = require('@babel/parser');
524
- const {comments} = parse(source, {
537
+ const {comments} = templateSourceParser().parse(source, {
525
538
  sourceType: 'module',
526
539
  plugins: ['jsx', 'typescript'],
527
540
  });
@@ -1267,7 +1280,8 @@ export async function discoverIntegrationTemplatesForOne(integration) {
1267
1280
  template: id,
1268
1281
  message: `Template "${id}" is missing a "type" of "page" or "block". Stamp the default export with type: 'page' or type: 'block'.`,
1269
1282
  });
1270
- const declared = doc?.replaces ?? (await declaredReplacement(docPath, id));
1283
+ const declared =
1284
+ doc?.replaces ?? (await declaredReplacement(docPath, id));
1271
1285
  if (declared != null) {
1272
1286
  errors.push({
1273
1287
  code: 'invalid_template_replacement',
@@ -1423,6 +1437,51 @@ const UBIQUITOUS = new Set([
1423
1437
  'Icon',
1424
1438
  ]);
1425
1439
 
1440
+ /**
1441
+ * Names bound by top-level `function`, `class`, `const`, `let` or `var`
1442
+ * declarations (exported or not). Returns an empty set when the source does
1443
+ * not parse, so a parse failure never drops a real component.
1444
+ *
1445
+ * @param {string} src
1446
+ * @returns {Set<string>}
1447
+ */
1448
+ function topLevelBindingNames(src) {
1449
+ /** @type {Set<string>} */
1450
+ const names = new Set();
1451
+ let program;
1452
+ try {
1453
+ program = templateSourceParser().parse(src, {
1454
+ sourceType: 'module',
1455
+ plugins: ['jsx', 'typescript'],
1456
+ errorRecovery: true,
1457
+ }).program;
1458
+ } catch {
1459
+ return names;
1460
+ }
1461
+ for (const statement of program.body) {
1462
+ const node =
1463
+ statement.type === 'ExportNamedDeclaration' ||
1464
+ statement.type === 'ExportDefaultDeclaration'
1465
+ ? statement.declaration
1466
+ : statement;
1467
+ if (node == null) continue;
1468
+ if (
1469
+ (node.type === 'FunctionDeclaration' ||
1470
+ node.type === 'ClassDeclaration') &&
1471
+ node.id
1472
+ ) {
1473
+ names.add(node.id.name);
1474
+ } else if (node.type === 'VariableDeclaration') {
1475
+ for (const declarator of node.declarations) {
1476
+ if (declarator.id.type === 'Identifier') {
1477
+ names.add(declarator.id.name);
1478
+ }
1479
+ }
1480
+ }
1481
+ }
1482
+ return names;
1483
+ }
1484
+
1426
1485
  /**
1427
1486
  * @param {string} pagePath
1428
1487
  * @returns {string[]}
@@ -1449,14 +1508,24 @@ export function extractComponents(pagePath) {
1449
1508
  while ((m = tagRegex.exec(src)) !== null) {
1450
1509
  matches.push(m[2]);
1451
1510
  }
1511
+
1512
+ // Exclude names bound by a top-level declaration in the same file: those are
1513
+ // page-local helpers (e.g. `function TimelineSection()`), not components a
1514
+ // builder can import. Read real bindings from the syntax tree so comments,
1515
+ // strings and type annotations cannot add or hide a name.
1516
+ const localDeclNames = topLevelBindingNames(src);
1517
+
1452
1518
  return [
1453
1519
  ...new Set(
1454
1520
  matches
1521
+ .filter(n => !localDeclNames.has(n))
1455
1522
  .filter(n => !['Theme', 'ThemeProvider'].includes(n))
1456
1523
  .filter(n => !UBIQUITOUS.has(n))
1457
1524
  .map(n =>
1458
1525
  n.replace(
1459
- /(Item|Section|Header|Content|Footer|Panel|Heading|CollapseButton|Column|Sortable|Selection|Group|Source)$/,
1526
+ // Strip only when a stem remains, so a bare `<Section>` keeps its
1527
+ // own name instead of emptying out.
1528
+ /(?<=.)(Item|Section|Header|Content|Footer|Panel|Heading|CollapseButton|Column|Sortable|Selection|Group|Source)$/,
1460
1529
  '',
1461
1530
  ),
1462
1531
  )
@@ -251,7 +251,7 @@ const RUNNERS = {
251
251
  sites: ['importDocModule'],
252
252
  },
253
253
  'foundation/discovery/template-adapter.mjs': {
254
- runs: '@babel/parser, to find the fixture references in template source; and a template doc that does not validate, to read the replacement it declares',
254
+ runs: '@babel/parser, to find the fixture references and page-local bindings in template source; and a template doc that does not validate, to read the replacement it declares',
255
255
  sites: ['importDocModule', 'require(@babel/parser)'],
256
256
  },
257
257
  'foundation/discovery/theme-discovery.mjs': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/cli",
3
- "version": "0.7.2-canary.c68f5e8",
3
+ "version": "0.7.2-canary.caec7ca",
4
4
  "displayName": "CLI",
5
5
  "description": "Scaffold projects, browse templates, generate themes, and get agent-ready docs from the command line.",
6
6
  "author": "Meta Open Source",
@@ -96,15 +96,15 @@
96
96
  "jscodeshift": "^17.3.0",
97
97
  "node-diff3": "3.2.1",
98
98
  "postcss": "^8.5.23",
99
- "postcss-selector-parser": "^7.1.5",
99
+ "postcss-selector-parser": "^7.1.6",
100
100
  "recast": "0.23.21",
101
101
  "zod": "^4.4.3"
102
102
  },
103
103
  "peerDependencies": {
104
- "@astryxdesign/charts": "0.7.2-canary.c68f5e8",
105
- "@astryxdesign/core": "0.7.2-canary.c68f5e8",
106
- "@astryxdesign/lab": "0.7.2-canary.c68f5e8",
107
- "@astryxdesign/theme-neutral": "0.7.2-canary.c68f5e8"
104
+ "@astryxdesign/charts": "0.7.2-canary.caec7ca",
105
+ "@astryxdesign/core": "0.7.2-canary.caec7ca",
106
+ "@astryxdesign/lab": "0.7.2-canary.caec7ca",
107
+ "@astryxdesign/theme-neutral": "0.7.2-canary.caec7ca"
108
108
  },
109
109
  "peerDependenciesMeta": {
110
110
  "@astryxdesign/charts": {
@@ -121,10 +121,10 @@
121
121
  }
122
122
  },
123
123
  "devDependencies": {
124
- "@astryxdesign/charts": "0.7.2-canary.c68f5e8",
125
- "@astryxdesign/core": "0.7.2-canary.c68f5e8",
126
- "@astryxdesign/lab": "0.7.2-canary.c68f5e8",
127
- "@astryxdesign/theme-neutral": "0.7.2-canary.c68f5e8",
124
+ "@astryxdesign/charts": "0.7.2-canary.caec7ca",
125
+ "@astryxdesign/core": "0.7.2-canary.caec7ca",
126
+ "@astryxdesign/lab": "0.7.2-canary.caec7ca",
127
+ "@astryxdesign/theme-neutral": "0.7.2-canary.caec7ca",
128
128
  "@heroicons/react": "^2.2.0",
129
129
  "@stylexjs/stylex": "^0.19.0",
130
130
  "@types/babel__core": "^7.20.5",