@astryxdesign/cli 0.4.7-canary.de6d448 → 0.4.7-canary.dfdd778

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/README.md CHANGED
@@ -383,52 +383,52 @@ Every response has a `type` discriminant. The full set is below (generated from
383
383
 
384
384
  <!-- BEGIN GENERATED: response-types -->
385
385
 
386
- | Type | What `data` carries |
387
- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
388
- | `component.list` | The component catalog grouped by category: `detail` (the level: names \| compact \| full) and `components`, the grouped map of names+package, brief entries, or a full ComponentDoc per entry. |
389
- | `component.detail` | One component's authored ComponentDoc plus ownership metadata (owner package, import specifier, and whether source is available). |
390
- | `component.detail.props` | Just one component's props table (ComponentPropDoc[]). |
391
- | `component.detail.source` | One component's source file, as {component, source}. |
392
- | `component.detail.showcase` | One component's showcase example, as {component, aspectRatio, source}. |
393
- | `component.detail.blocks` | One component's example blocks, as {component, showcase, examples, related} of BlockEntry. |
394
- | `docs.list` | All reference-doc topics as DocsListEntry[] ({topic, description}), in discovery order. |
395
- | `docs.detail` | One topic's full ReferenceDoc, with token-ref blocks inlined. |
396
- | `docs.detail.section` | A single ReferenceSection of a topic: the first whose title contains the section query. |
397
- | `blog.list` | The feed URL plus every post parsed from the RSS feed, each with slug, title, description, date, type, authors, link, and plaintext URL. |
398
- | `blog.detail` | One post's metadata plus the feed URL and the post's full plaintext body. |
399
- | `discover.list` | The configured external packages (name, category, components, version, description); when empty it carries meta.configured to tell "nothing configured" from "nothing discovered". |
400
- | `discover.detail` | A single external package entry, for an @scope/name query. |
401
- | `discover.detail.doc` | The validated ComponentDoc for one external component: an @scope/name/Component query, or a free-text term resolving to exactly one component. |
402
- | `discover.search` | The echoed query plus the matching {package, component} pairs, when a free-text term matches several components. |
403
- | `search` | The echoed query plus a ranked SearchResultEntry[] (domain, name, score, reason, description, follow-up command, and import path where relevant). |
404
- | `build.help` | A marker (`playbook: true`) that the renderer expands into the how-to-build-a-page workflow; emitted when no query is given. |
405
- | `build.kit` | The grouped composition kit: echoed query, hasResults/directMatch flags, the closest page templates, drop-in block patterns, idea-specific components/hooks, and the always-on frame + foundation component-name arrays. |
406
- | `swizzle.list` | The names of swizzlable components discoverable from cwd's @astryxdesign/core. |
407
- | `swizzle.copy` | An eject receipt: component name, owning package, output directory, files-copied count, the written file names, whether any file uses StyleX, and an optional maintainer note. |
408
- | `template.list` | Every discovered template (page + block); each entry carries id, name, description, kind, owning package, optional category and componentsUsed, and readiness flags. |
409
- | `template.show` | The resolved template's raw source plus its description, kind, and the component names it composes. |
410
- | `template.skeleton` | A layout skeleton (structural tags with spatial annotations) plus the template's description and the components it composes. |
411
- | `template.copy` | A scaffold receipt: template id, output directory, written file name, and file count. |
412
- | `template.cdn` | A write receipt for the no-build-step CDN starter page: the path (relative to cwd), the Astryx version every CDN URL was pinned to, whether it was written, and the reason it was not `exists` when a file was already there, which is a success. |
413
- | `hook.list` | The hook catalog grouped by category: `detail` (the level: names \| compact \| full) and `components`, the grouped map of hook names, brief entries, or a full HookDoc per entry. |
414
- | `hook.detail` | One hook's full authored HookDoc. |
415
- | `hook.detail.params` | Just one hook's parameters table (HookParamDoc[]). |
416
- | `theme.build` | A theme build receipt: name, token- and component-override counts, output size, the written outputs {css, js, dts, and variantsDts when applicable}, and any validation warnings. |
417
- | `theme.build.check` | The --check receipt: theme name, an upToDate flag, the stale outputs (each {path, reason: missing \| outdated}), and the full list of checked paths. Writes nothing. |
418
- | `theme.build.batch` | Several themes built in one invocation: `count` plus one {file, receipt} per theme in argument order, where receipt is that theme's theme.build (or theme.build.check) envelope, or null when it produced no CSS. |
419
- | `theme.list` | Every bundled theme as a ThemeListEntry[]: each with slug, displayName, description, and a maintained flag. |
420
- | `theme.add` | A scaffold receipt: resolved slug, displayName, maintained flag, outputDir (relative to cwd), the theme entry file, its exportName, and the files written. |
421
- | `theme.template` | A write receipt for the annotated theme template: the path (relative to cwd), whether it was written, and the reason it was not `exists` when a file was already there, which is a success. |
422
- | `theme.targets` | The whole themeable surface: the echoed filter, the component count, and one entry per theming target — {key, className, component, props, states}, where props and states are its legal override keys. |
423
- | `upgrade.list` | Every available codemod, oldest→newest, as {name, title, version, optional}; returned for --list without running anything. |
424
- | `upgrade.status` | A short-circuit outcome with no codemods run (up_to_date, no_codemods, or config_fixable), each carrying the agent-docs summary. |
425
- | `upgrade.run` | The run receipt: from/to versions, codemod count, integrations processed, the agent-docs summary, and (apply mode) filesChanged, transformsApplied, and per-codemod errors. |
426
- | `manifest` | The self-describing CLI capability manifest: name, version, apiVersion, global options, the command tree (args, options, json flag, response types, examples), the jsonSupported allowlist, and the flat responseTypes index. |
427
- | `doctor` | The health-check report: `checks` (each with id, label, status: pass \| warn \| fail \| info, a message, and a fix when not passing) plus a `summary` of counts per status. |
428
- | `integration.validate` | The validation result: the package name and version (both null when no local manifest is found) plus issues, an AstryxIntegrationIssue[] of {code, severity: warning \| error, message}. |
429
- | `layout.expand` | The expansion: parsed form, generated TSX code, componentsUsed, states (count of useState hooks scaffolded), todos, blocksReferenced (each {name, mode}), warnings, and written (the output path, or null when nothing was written). |
430
- | `layout.check` | The validation result: a valid flag, the detected form, errors (each with line/col, message, formatted text, and suggestions), warnings, and the expression re-printed in both canonical surfaces (compact and outline). |
431
- | `layout.grammar` | The XLE/XLO grammar cheatsheet: a text field with the full reference plus an aliases map (short name → canonical component) generated from this install's registry. |
386
+ | Type | What `data` carries |
387
+ | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
388
+ | `component.list` | The component catalog grouped by category: `detail` (the level: names \| compact \| full) and `components`, the grouped map of names+package, brief entries, or a full ComponentDoc per entry. |
389
+ | `component.detail` | One component's authored ComponentDoc plus ownership metadata (owner package, import specifier, and whether source is available). |
390
+ | `component.detail.props` | Just one component's props table (ComponentPropDoc[]). |
391
+ | `component.detail.source` | One component's source file, as {component, source}. |
392
+ | `component.detail.showcase` | One component's showcase example, as {component, aspectRatio, source}. |
393
+ | `component.detail.blocks` | One component's example blocks, as {component, showcase, examples, related} of BlockEntry. |
394
+ | `docs.list` | All reference-doc topics as DocsListEntry[] ({topic, description}), in discovery order. |
395
+ | `docs.detail` | One topic's full ReferenceDoc, with token-ref blocks inlined. |
396
+ | `docs.detail.section` | A single ReferenceSection of a topic: the first whose title contains the section query. |
397
+ | `blog.list` | The feed URL plus every post parsed from the RSS feed, each with slug, title, description, date, type, authors, link, and plaintext URL. |
398
+ | `blog.detail` | One post's metadata plus the feed URL and the post's full plaintext body. |
399
+ | `discover.list` | The configured external packages (name, category, components, version, description); when empty it carries meta.configured to tell "nothing configured" from "nothing discovered". |
400
+ | `discover.detail` | A single external package entry, for an @scope/name query. |
401
+ | `discover.detail.doc` | The validated ComponentDoc for one external component: an @scope/name/Component query, or a free-text term resolving to exactly one component. |
402
+ | `discover.search` | The echoed query plus the matching {package, component} pairs, when a free-text term matches several components. |
403
+ | `search` | The echoed query plus a ranked SearchResultEntry[] (domain, name, score, reason, description, follow-up command, and import path where relevant). |
404
+ | `build.help` | A marker (`playbook: true`) that the renderer expands into the how-to-build-a-page workflow; emitted when no query is given. |
405
+ | `build.kit` | The grouped composition kit: echoed query, hasResults/directMatch flags, the closest page templates, drop-in block patterns, idea-specific components/hooks, and the always-on frame + foundation component-name arrays. |
406
+ | `swizzle.list` | The names of swizzlable components discoverable from cwd's @astryxdesign/core. |
407
+ | `swizzle.copy` | An eject receipt: component name, owning package, output directory, files-copied count, the written file names, whether any file uses StyleX, and an optional maintainer note. |
408
+ | `template.list` | Every discovered template (page + block); each entry carries id, name, description, kind, owning package, optional category and componentsUsed, and readiness flags. |
409
+ | `template.show` | The resolved template's raw source plus its description, kind, and the component names it composes. |
410
+ | `template.skeleton` | A layout skeleton (structural tags with spatial annotations) plus the template's description and the components it composes. |
411
+ | `template.copy` | A scaffold receipt: template id, output directory, written file name, and file count. |
412
+ | `template.cdn` | A write receipt for the no-build-step CDN starter page: the path (relative to cwd), the Astryx version every CDN URL was pinned to, whether it was written, and the reason it was not. `exists` when a file was already there, which is a success. |
413
+ | `hook.list` | The hook catalog grouped by category: `detail` (the level: names \| compact \| full) and `components`, the grouped map of hook names, brief entries, or a full HookDoc per entry. |
414
+ | `hook.detail` | One hook's full authored HookDoc. |
415
+ | `hook.detail.params` | Just one hook's parameters table (HookParamDoc[]). |
416
+ | `theme.build` | A theme build receipt: name, token- and component-override counts, output size, the written outputs {css, js, dts, and variantsDts when applicable}, and any validation warnings. |
417
+ | `theme.build.check` | The --check receipt: theme name, an upToDate flag, the stale outputs (each {path, reason: missing \| outdated}), and the full list of checked paths. Writes nothing. |
418
+ | `theme.build.batch` | Several themes built in one invocation: `count` plus one {file, receipt} per theme in argument order, where receipt is that theme's theme.build (or theme.build.check) envelope, or null when it produced no CSS. |
419
+ | `theme.list` | Every bundled theme as a ThemeListEntry[]: each with slug, displayName, description, and a maintained flag. |
420
+ | `theme.add` | A scaffold receipt: resolved slug, displayName, maintained flag, outputDir (relative to cwd), the theme entry file, its exportName, and the files written. |
421
+ | `theme.template` | A write receipt for the annotated theme template: the path (relative to cwd), whether it was written, and the reason it was not. `exists` when a file was already there, which is a success. |
422
+ | `theme.targets` | The whole themeable surface: the echoed filter, the component count, and one entry per theming target — {key, className, component, props, states}, where props and states are its legal override keys. |
423
+ | `upgrade.list` | Every available codemod, oldest→newest, as {name, title, version, optional}; returned for --list without running anything. |
424
+ | `upgrade.status` | A short-circuit outcome with no codemods run (up_to_date, no_codemods, or config_fixable), each carrying the agent-docs summary. |
425
+ | `upgrade.run` | The run receipt: from/to versions, codemod count, integrations processed, the agent-docs summary, and (apply mode) filesChanged, transformsApplied, and per-codemod errors. |
426
+ | `manifest` | The self-describing CLI capability manifest: name, version, apiVersion, global options, the command tree (args, options, json flag, response types, examples), the jsonSupported allowlist, and the flat responseTypes index. |
427
+ | `doctor` | The health-check report: `checks` (each with id, label, status: pass \| warn \| fail \| info, a message, and a fix when not passing) plus a `summary` of counts per status. |
428
+ | `integration.validate` | The validation result: the package name and version (both null when no local manifest is found) plus issues, an AstryxIntegrationIssue[] of {code, severity: warning \| error, message}. |
429
+ | `layout.expand` | The expansion: parsed form, generated TSX code, componentsUsed, states (count of useState hooks scaffolded), todos, blocksReferenced (each {name, mode}), warnings, and written (the output path, or null when nothing was written). |
430
+ | `layout.check` | The validation result: a valid flag, the detected form, errors (each with line/col, message, formatted text, and suggestions), warnings, and the expression re-printed in both canonical surfaces (compact and outline). |
431
+ | `layout.grammar` | The XLE/XLO grammar cheatsheet: a text field with the full reference plus an aliases map (short name → canonical component) generated from this install's registry. |
432
432
 
433
433
  <!-- END GENERATED: response-types -->
434
434
  <!-- Generated by scripts/generate-cli-readme.mjs from the response-types EnumDoc. Run `pnpm -F @astryxdesign/cli readme`. -->
@@ -581,12 +581,13 @@ export default {
581
581
  };
582
582
  ```
583
583
 
584
- | Field | Type | Purpose |
585
- | ------------ | -------- | --------------------------------------------------------------------- |
586
- | `components` | `string` | Directory holding the package's components and their `.doc.*` files. |
587
- | `templates` | `string` | Directory holding the package's page/block templates. |
588
- | `codemods` | `string` | Directory holding upgrade codemods run by `astryx upgrade`. |
589
- | `issuesUrl` | `string` | Where "report an issue" links for this package's contributions point. |
584
+ | Field | Type | Purpose |
585
+ | ------------ | -------- | --------------------------------------------------------------------------------- |
586
+ | `components` | `string` | Directory holding the package's components and their `.doc.*` files. |
587
+ | `templates` | `string` | Directory holding the package's page/block templates. |
588
+ | `codemods` | `string` | Directory holding upgrade codemods run by `astryx upgrade`. |
589
+ | `docs` | `string` | Directory of reference docs; each `{topic}.doc.*` becomes a topic the CLI serves. |
590
+ | `issuesUrl` | `string` | Where "report an issue" links for this package's contributions point. |
590
591
 
591
592
  Every field is optional; declare only the roots the package ships. There is no
592
593
  factory: write a plain object, and annotate it with the `AstryxIntegration` type
@@ -19,8 +19,8 @@ export const doc = {
19
19
  'returns that full ReferenceDoc (with token-ref blocks inlined); a topic ' +
20
20
  'plus a section returns the first section whose title contains the ' +
21
21
  '(case-insensitive) query. The topic set is the CLI\'s own docs plus the ' +
22
- 'ones the project\'s configured integrations contribute including any ' +
23
- 'topic an integration replaces or extends so it depends on the cwd. ' +
22
+ 'ones the project\'s configured integrations contribute, including any ' +
23
+ 'topic an integration replaces or extends, so it depends on the cwd. ' +
24
24
  'Overlay options select localized or dense variants.',
25
25
  importPath: '@astryxdesign/cli/api',
26
26
  signature:
@@ -104,7 +104,7 @@ export const doc = {
104
104
  {
105
105
  type: 'template.skeleton',
106
106
  description:
107
- "A layout skeleton structural tags with spatial annotations plus the template's description and the components it composes.",
107
+ "A layout skeleton (structural tags with spatial annotations) plus the template's description and the components it composes.",
108
108
  },
109
109
  {
110
110
  type: 'template.copy',
@@ -114,7 +114,7 @@ export const doc = {
114
114
  {
115
115
  type: 'template.cdn',
116
116
  description:
117
- 'A write receipt for the CDN starter page: the path (relative to cwd), the Astryx version every CDN URL was pinned to, whether it was written, and the reason it was not `exists` when a file was already there, which is a success.',
117
+ 'A write receipt for the CDN starter page: the path (relative to cwd), the Astryx version every CDN URL was pinned to, whether it was written, and the reason it was not. `exists` when a file was already there, which is a success.',
118
118
  },
119
119
  ],
120
120
  throws: [
@@ -15,9 +15,9 @@ export const doc = {
15
15
  displayName: 'themeTemplate()',
16
16
  summary: 'Write the annotated theme template into a project.',
17
17
  description:
18
- 'Writes theme.template.ts: the annotated reference for the whole theme surface every ' +
18
+ 'Writes theme.template.ts: the annotated reference for the whole theme surface, covering every ' +
19
19
  'defineTheme field, the token families, the component override syntax, and how a theme is ' +
20
- 'consumed with the CLI command that prints the authoritative reference for each section. ' +
20
+ 'consumed, with the CLI command that prints the authoritative reference for each section. ' +
21
21
  'Read it, copy what you need into your own theme file, delete it. Where `theme add` starts ' +
22
22
  'you from a theme we ship, this starts you from a blank one. Refuses to overwrite without ' +
23
23
  '`overwrite`, so it is safe to re-run.',
@@ -48,7 +48,7 @@ export const doc = {
48
48
  {
49
49
  type: 'theme.template',
50
50
  description:
51
- 'Receipt: the path (relative to cwd), whether it was written, and the reason it was not `exists` when a file was already there, which is a success, not a failure.',
51
+ 'Receipt: the path (relative to cwd), whether it was written, and the reason it was not. `exists` when a file was already there, which is a success, not a failure.',
52
52
  },
53
53
  ],
54
54
  throws: [{code: 'ERR_PATH_TRAVERSAL', when: 'the target path escapes cwd'}],
@@ -0,0 +1,127 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Unit tests for the staged (next-release) codemods.
5
+ *
6
+ * Mirrors v0.4.0/__tests__/next-codemods.test.mjs, which covers the codemods
7
+ * after promotion. Keeping a copy here means a staged transform is tested from
8
+ * the day it is written rather than the day it is released.
9
+ */
10
+
11
+ import {describe, expect, it} from 'vitest';
12
+ import jscodeshift from 'jscodeshift';
13
+
14
+ const j = jscodeshift.withParser('tsx');
15
+ const api = {jscodeshift: j, stats: () => {}, report: () => {}};
16
+
17
+ async function apply(name, source) {
18
+ const {default: transform} = await import(`../${name}.mjs`);
19
+ return transform({source, path: 'test.tsx'}, api) ?? source;
20
+ }
21
+
22
+ const TRANSFORM = 'banner-collapsible-content';
23
+
24
+ const IMPORT = "import {Banner} from '@astryxdesign/core/Banner';\n";
25
+
26
+ describe('banner-collapsible-content', () => {
27
+ it('rewrites a bare defaultIsExpanded to a starts-open config', async () => {
28
+ const output = await apply(
29
+ TRANSFORM,
30
+ `${IMPORT}const el = <Banner status="info" title="T" defaultIsExpanded><p>d</p></Banner>;`,
31
+ );
32
+ expect(output).toContain('defaultIsOpen: true');
33
+ expect(output).not.toContain('defaultIsExpanded');
34
+ });
35
+
36
+ it('rewrites defaultIsExpanded={true} the same way', async () => {
37
+ const output = await apply(
38
+ TRANSFORM,
39
+ `${IMPORT}const el = <Banner status="info" title="T" defaultIsExpanded={true}><p>d</p></Banner>;`,
40
+ );
41
+ expect(output).toContain('defaultIsOpen: true');
42
+ expect(output).not.toContain('defaultIsExpanded');
43
+ });
44
+
45
+ it('drops defaultIsExpanded={false}, which is the default', async () => {
46
+ const output = await apply(
47
+ TRANSFORM,
48
+ `${IMPORT}const el = <Banner status="info" title="T" defaultIsExpanded={false}><p>d</p></Banner>;`,
49
+ );
50
+ expect(output).not.toContain('defaultIsExpanded');
51
+ // No config needed: starting collapsed is what a Banner does by default.
52
+ expect(output).not.toContain('collapsible');
53
+ // Untouched attributes keep their original text (recast only reprints
54
+ // what changed), so the element is exactly the base minus the prop.
55
+ expect(output).toContain('<Banner status="info" title="T">');
56
+ });
57
+
58
+ it('keeps a dynamic default dynamic', async () => {
59
+ const output = await apply(
60
+ TRANSFORM,
61
+ `${IMPORT}const el = <Banner status="info" title="T" defaultIsExpanded={isOpen}><p>d</p></Banner>;`,
62
+ );
63
+ expect(output).toContain('defaultIsOpen: isOpen');
64
+ expect(output).not.toContain('defaultIsExpanded');
65
+ });
66
+
67
+ it('leaves a banner that never set the prop alone', async () => {
68
+ // The default is unchanged, so this banner still behaves as it did. The
69
+ // migration must not touch it — that is the whole point of the shape.
70
+ const source = `${IMPORT}const el = <Banner status="error" title="T"><ul><li>a</li></ul></Banner>;`;
71
+ const output = await apply(TRANSFORM, source);
72
+ expect(output).toBe(source);
73
+ });
74
+
75
+ it('leaves a childless banner alone', async () => {
76
+ const source = `${IMPORT}const el = <Banner status="info" title="T" />;`;
77
+ const output = await apply(TRANSFORM, source);
78
+ expect(output).toBe(source);
79
+ });
80
+
81
+ it('leaves a banner that already uses collapsible alone', async () => {
82
+ const source = `${IMPORT}const el = <Banner status="info" title="T" collapsible={false} defaultIsExpanded><p>d</p></Banner>;`;
83
+ const output = await apply(TRANSFORM, source);
84
+ expect(output).toBe(source);
85
+ });
86
+
87
+ it('does not guess around a spread', async () => {
88
+ const source = `${IMPORT}const el = <Banner status="info" title="T" defaultIsExpanded {...rest}><p>d</p></Banner>;`;
89
+ const output = await apply(TRANSFORM, source);
90
+ expect(output).toBe(source);
91
+ });
92
+
93
+ it("leaves another component's defaultIsExpanded alone", async () => {
94
+ // ChatToolCalls has a prop of the same name that this migration must not
95
+ // touch.
96
+ const source = `import {ChatToolCalls} from '@astryxdesign/core/Chat';
97
+ const el = <ChatToolCalls calls={calls} defaultIsExpanded />;`;
98
+ const output = await apply(TRANSFORM, source);
99
+ expect(output).toBe(source);
100
+ });
101
+
102
+ it('leaves a Banner that is not the core Banner alone', async () => {
103
+ const source = `import {Banner} from './ui/Banner';
104
+ const el = <Banner status="info" title="T" defaultIsExpanded><p>d</p></Banner>;`;
105
+ const output = await apply(TRANSFORM, source);
106
+ expect(output).toBe(source);
107
+ });
108
+
109
+ it('leaves the prop inside a props object alone', async () => {
110
+ // Out of scope by design: which component the object feeds is a guess,
111
+ // and the removed prop makes those sites a type error anyway.
112
+ const source = `${IMPORT}const args = {status: 'info', title: 'T', defaultIsExpanded: true};
113
+ const el = <Banner {...args} />;`;
114
+ const output = await apply(TRANSFORM, source);
115
+ expect(output).toBe(source);
116
+ });
117
+
118
+ it('migrates a Banner imported from the package root', async () => {
119
+ const output = await apply(
120
+ TRANSFORM,
121
+ `import {Banner, Button} from '@astryxdesign/core';
122
+ const el = <Banner status="info" title="T" defaultIsExpanded><p>d</p></Banner>;`,
123
+ );
124
+ expect(output).toContain('defaultIsOpen: true');
125
+ expect(output).not.toContain('defaultIsExpanded');
126
+ });
127
+ });
@@ -0,0 +1,171 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Codemod: migrate Banner's `defaultIsExpanded` onto `collapsible`
5
+ *
6
+ * Banner's collapse axis used to be a single knob, `defaultIsExpanded`, with
7
+ * the disclosure itself inferred from the presence of `children`. There was no
8
+ * way to have content without a toggle, and no controlled mode. The axis now
9
+ * lives on one `boolean | CollapsibleConfig` prop, per the boolean-or-config
10
+ * convention:
11
+ *
12
+ * <Banner>{children}</Banner> → collapsible, starts closed
13
+ * <Banner collapsible={{defaultIsOpen: true}}> → collapsible, starts open
14
+ * <Banner collapsible={{isOpen, onOpenChange}}> → controlled
15
+ * <Banner collapsible={false}> → always visible, no toggle
16
+ *
17
+ * The default is unchanged, so a Banner that never mentioned the old prop needs
18
+ * no rewrite at all — this transform is a prop rename and nothing more:
19
+ *
20
+ * defaultIsExpanded → collapsible={{defaultIsOpen: true}}
21
+ * defaultIsExpanded={true} → collapsible={{defaultIsOpen: true}}
22
+ * defaultIsExpanded={false} → (removed — it is the default)
23
+ * defaultIsExpanded={expr} → collapsible={{defaultIsOpen: expr}}
24
+ *
25
+ * Only elements named `Banner` are touched, and only when the file imports that
26
+ * name from `@astryxdesign/core`: `defaultIsExpanded` is also a ChatToolCalls
27
+ * prop, which this migration must leave alone.
28
+ *
29
+ * Scope: JSX attributes only. `defaultIsExpanded` inside a props object (a
30
+ * Storybook `args`, a spread built up in a variable) is left alone rather than
31
+ * rewritten on a guess about which component the object is for — removing the
32
+ * prop from the type makes those sites a type error, which is loud enough to
33
+ * find them.
34
+ */
35
+
36
+ export const meta = {
37
+ title: "Rename Banner's `defaultIsExpanded` to the `collapsible` config",
38
+ description:
39
+ "Banner's collapse axis is now a single `collapsible?: boolean | " +
40
+ 'CollapsibleConfig` prop. Rewrites `defaultIsExpanded` to the equivalent ' +
41
+ 'config (`{defaultIsOpen: true}`), and drops ' +
42
+ '`defaultIsExpanded={false}`, which is the default. Banners that never ' +
43
+ 'set the prop are untouched — the default is unchanged. Pass ' +
44
+ '`collapsible={false}` for content that is always visible.',
45
+ pr: '#5255',
46
+ };
47
+
48
+ const OLD_PROP = 'defaultIsExpanded';
49
+ const NEW_PROP = 'collapsible';
50
+ const COMPONENT = 'Banner';
51
+
52
+ /**
53
+ * Does this file use the core `Banner`?
54
+ *
55
+ * `defaultIsExpanded` is a ChatToolCalls prop too, and a local component may
56
+ * well be called Banner, so an unqualified element-name match is not enough.
57
+ *
58
+ * @param {any} j
59
+ * @param {any} root
60
+ * @returns {boolean}
61
+ */
62
+ function importsCoreBanner(j, root) {
63
+ let found = false;
64
+ root.find(j.ImportDeclaration).forEach((/** @type {any} */ path) => {
65
+ const source = path.node.source?.value;
66
+ if (typeof source !== 'string' || !source.startsWith('@astryxdesign/core')) {
67
+ return;
68
+ }
69
+ for (const spec of path.node.specifiers ?? []) {
70
+ if (
71
+ (spec.type === 'ImportSpecifier' && spec.imported?.name === COMPONENT) ||
72
+ (spec.local?.name === COMPONENT &&
73
+ (spec.type === 'ImportDefaultSpecifier' ||
74
+ spec.type === 'ImportSpecifier'))
75
+ ) {
76
+ found = true;
77
+ }
78
+ }
79
+ });
80
+ return found;
81
+ }
82
+
83
+ /**
84
+ * @param {import('../../../../authoring/codemod/type').AstryxCodemodFile} file
85
+ * @param {import('../../../../authoring/codemod/type').CodemodTransformApi} api
86
+ * @returns {string | null | undefined}
87
+ */
88
+ export default function transformer(file, api) {
89
+ // Cheap bail-out: nothing to rename without the old prop.
90
+ if (!file.source.includes(OLD_PROP)) {
91
+ return undefined;
92
+ }
93
+
94
+ const j = api.jscodeshift;
95
+ const root = j(file.source);
96
+
97
+ if (!importsCoreBanner(j, root)) {
98
+ return undefined;
99
+ }
100
+
101
+ let hasChanges = false;
102
+
103
+ /** `collapsible={{defaultIsOpen: <expr>}}` */
104
+ const collapsibleWithDefault = (/** @type {any} */ expression) =>
105
+ j.jsxAttribute(
106
+ j.jsxIdentifier(NEW_PROP),
107
+ j.jsxExpressionContainer(
108
+ j.objectExpression([
109
+ j.objectProperty(j.identifier('defaultIsOpen'), expression),
110
+ ]),
111
+ ),
112
+ );
113
+
114
+ root.find(j.JSXOpeningElement).forEach((/** @type {any} */ path) => {
115
+ const name = path.node.name;
116
+ if (name?.type !== 'JSXIdentifier' || name.name !== COMPONENT) {
117
+ return;
118
+ }
119
+
120
+ const attrs = path.node.attributes ?? [];
121
+ const oldIndex = attrs.findIndex(
122
+ (/** @type {any} */ a) =>
123
+ a.type === 'JSXAttribute' && a.name?.name === OLD_PROP,
124
+ );
125
+ if (oldIndex === -1) {
126
+ return;
127
+ }
128
+
129
+ // Already migrated by hand, or a spread that may carry either prop:
130
+ // in both cases a rewrite would be guesswork.
131
+ const hasNewProp = attrs.some(
132
+ (/** @type {any} */ a) =>
133
+ a.type === 'JSXAttribute' && a.name?.name === NEW_PROP,
134
+ );
135
+ const hasSpread = attrs.some(
136
+ (/** @type {any} */ a) => a.type === 'JSXSpreadAttribute',
137
+ );
138
+ if (hasNewProp || hasSpread) {
139
+ return;
140
+ }
141
+
142
+ const value = attrs[oldIndex].value;
143
+
144
+ if (value == null) {
145
+ // Bare `defaultIsExpanded` — starts open.
146
+ attrs[oldIndex] = collapsibleWithDefault(j.booleanLiteral(true));
147
+ } else if (value.type === 'JSXExpressionContainer') {
148
+ const expression = value.expression;
149
+ const isBooleanLiteral =
150
+ expression.type === 'BooleanLiteral' ||
151
+ (expression.type === 'Literal' && typeof expression.value === 'boolean');
152
+ if (isBooleanLiteral && expression.value === false) {
153
+ // Starting closed is the default now, so the prop simply goes.
154
+ attrs.splice(oldIndex, 1);
155
+ } else if (isBooleanLiteral) {
156
+ attrs[oldIndex] = collapsibleWithDefault(j.booleanLiteral(true));
157
+ } else {
158
+ // A dynamic default stays dynamic.
159
+ attrs[oldIndex] = collapsibleWithDefault(expression);
160
+ }
161
+ } else {
162
+ // `defaultIsExpanded="something"` is not valid for a boolean prop;
163
+ // leave it for a human rather than inventing a meaning.
164
+ return;
165
+ }
166
+
167
+ hasChanges = true;
168
+ });
169
+
170
+ return hasChanges ? root.toSource({quote: 'single'}) : undefined;
171
+ }
@@ -7,4 +7,14 @@
7
7
  * this file into the resolved version folder.
8
8
  */
9
9
 
10
- export default [];
10
+ import bannerCollapsibleContent, {
11
+ meta as bannerCollapsibleContentMeta,
12
+ } from './banner-collapsible-content.mjs';
13
+
14
+ export default [
15
+ {
16
+ name: 'banner-collapsible-content',
17
+ transform: bannerCollapsibleContent,
18
+ meta: bannerCollapsibleContentMeta,
19
+ },
20
+ ];
@@ -51,7 +51,7 @@ export const docs = {
51
51
  content: [
52
52
  {
53
53
  type: 'prose',
54
- text: 'Astryx never loads font files. defineTheme and the built CSS only set font-family naming a webfont (Fraunces, JetBrains Mono, ) makes every browser look for it, and quietly fall back when the app has not loaded it. `astryx theme build` warns when a theme names families that are neither CSS generics nor common system fonts and prints the snippet to add; loading the font is always the app\'s job.',
54
+ text: 'Astryx never loads font files. defineTheme and the built CSS only set font-family: naming a webfont (Fraunces, JetBrains Mono, and so on) makes every browser look for it, and quietly fall back when the app has not loaded it. `astryx theme build` warns when a theme names families that are neither CSS generics nor common system fonts and prints the snippet to add; loading the font is always the app\'s job.',
55
55
  },
56
56
  {
57
57
  type: 'code',
@@ -77,7 +77,7 @@ export const docs = {
77
77
  },
78
78
  {
79
79
  type: 'prose',
80
- text: "Always pair a webfont with a real fallback stack metric-similar system fonts plus a generic so text stays readable before the font loads and wherever it never does: defineTheme({typography: {heading: {family: 'Fraunces', fallbacks: 'Georgia, serif'}}}).",
80
+ text: "Always pair a webfont with a real fallback stack (metric-similar system fonts plus a generic) so text stays readable before the font loads and wherever it never does: defineTheme({typography: {heading: {family: 'Fraunces', fallbacks: 'Georgia, serif'}}}).",
81
81
  },
82
82
  ],
83
83
  },
@@ -6,7 +6,7 @@ export const doc = {
6
6
  exampleFor: 'Banner',
7
7
  name: 'Banner — Collapsible',
8
8
  displayName: 'Banner — Collapsible',
9
- description: 'Combine an action button, dismiss control, and expandable detail area in one banner. Use for complex notifications like config changes or deployment summaries.',
9
+ description: 'Combine an action button, dismiss control, and a collapsible detail area in one banner. Children sit behind the toggle by default; `collapsible={{defaultIsOpen: true}}` starts it open, and `collapsible={false}` drops the toggle entirely. Use for complex notifications like config changes or deployment summaries.',
10
10
  isReady: true,
11
11
  aspectRatio: 16 / 9,
12
12
  componentsUsed: ['Banner', 'Button', 'List', 'Layout', 'Text'],
@@ -16,7 +16,7 @@ export default function BannerCollapsibleContent() {
16
16
  description="Review the changes before they take effect."
17
17
  endContent={<Button label="Review" variant="secondary" size="sm" />}
18
18
  isDismissable
19
- defaultIsExpanded>
19
+ collapsible={{defaultIsOpen: true}}>
20
20
  <Stack direction="vertical" gap={2}>
21
21
  <Text type="supporting" color="secondary">
22
22
  Changed settings:
@@ -36,7 +36,11 @@ const clipStyle: CSSProperties = {
36
36
  // 3 columns on desktop, dropping straight to 1 column below 720px (no 2-col
37
37
  // middle state). minmax(0, 1fr) (not 1fr) so tracks split evenly and ignore the
38
38
  // images' intrinsic min-width. The hero spans 2 columns on desktop, then fills
39
- // the row once it's single-column.
39
+ // the row once it's single-column — and switches from 3:1 to 3:2 with a plain
40
+ // aspect-ratio override. AspectRatio's ratio is a class-level declaration in
41
+ // @layer astryx-base, and this <style> tag is unlayered, so the override wins
42
+ // regardless of specificity — every stacked tile is uniform without rendering
43
+ // the hero twice.
40
44
  const GALLERY_CSS = `
41
45
  .mixed-gallery-grid {
42
46
  display: grid;
@@ -52,6 +56,7 @@ const GALLERY_CSS = `
52
56
  }
53
57
  .mixed-gallery-hero {
54
58
  grid-column: 1 / -1;
59
+ aspect-ratio: 3 / 2;
55
60
  }
56
61
  }
57
62
  `;
@@ -134,9 +139,13 @@ export default function MixedGalleryTemplate() {
134
139
  of three. Every tile is 3:2 except the hero, which is 3:1 so that
135
140
  (being 2 columns wide) it matches the row height exactly. All
136
141
  rows are therefore the same height. Responsive via @container:
137
- 3 columns → 1 column at ≤720px. */}
142
+ 3 columns → 1 column at ≤720px, where the hero drops to 3:2 so
143
+ the stacked tiles are uniform. */}
138
144
  <div className="mixed-gallery-grid">
139
- {/* Hero — spans 2 columns; 3:1 keeps it level with the sidebar */}
145
+ {/* Hero — spans 2 columns; 3:1 keeps it level with the sidebar.
146
+ Stacked, the unlayered aspect-ratio override switches it to
147
+ 3:2 (see GALLERY_CSS) — one element, no duplicate hero
148
+ markup. */}
140
149
  <GalleryCard
141
150
  image={IMAGES[0]}
142
151
  ratio={3 / 1}
@@ -16,9 +16,9 @@ export const doc = {
16
16
  namespace: 'cli',
17
17
  summary: 'Write the annotated theme template into your project',
18
18
  description:
19
- 'Writes theme.template.ts: the annotated reference for the whole theme surface every ' +
19
+ 'Writes theme.template.ts: the annotated reference for the whole theme surface, covering every ' +
20
20
  'defineTheme field, the token families, the component override syntax, and how a theme is ' +
21
- 'consumed naming the CLI command that prints the authoritative reference for each. Read ' +
21
+ 'consumed, naming the CLI command that prints the authoritative reference for each. Read ' +
22
22
  'it, copy what you need into your own theme file, delete it. Use `theme add <slug>` instead ' +
23
23
  'to start from a theme we ship. Leaves an existing file untouched unless --overwrite.',
24
24
  fn: 'themeTemplate',
@@ -153,7 +153,7 @@ export const doc = {
153
153
  {
154
154
  value: 'template.cdn',
155
155
  description:
156
- 'A write receipt for the no-build-step CDN starter page: the path (relative to cwd), the Astryx version every CDN URL was pinned to, whether it was written, and the reason it was not `exists` when a file was already there, which is a success.',
156
+ 'A write receipt for the no-build-step CDN starter page: the path (relative to cwd), the Astryx version every CDN URL was pinned to, whether it was written, and the reason it was not. `exists` when a file was already there, which is a success.',
157
157
  },
158
158
 
159
159
  // hook
@@ -197,7 +197,7 @@ export const doc = {
197
197
  {
198
198
  value: 'theme.template',
199
199
  description:
200
- 'A write receipt for the annotated theme template: the path (relative to cwd), whether it was written, and the reason it was not `exists` when a file was already there, which is a success.',
200
+ 'A write receipt for the annotated theme template: the path (relative to cwd), whether it was written, and the reason it was not. `exists` when a file was already there, which is a success.',
201
201
  },
202
202
  {
203
203
  value: 'theme.targets',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/cli",
3
- "version": "0.4.7-canary.de6d448",
3
+ "version": "0.4.7-canary.dfdd778",
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",
@@ -87,10 +87,10 @@
87
87
  "zod": "^4.4.3"
88
88
  },
89
89
  "peerDependencies": {
90
- "@astryxdesign/charts": "0.4.7-canary.de6d448",
91
- "@astryxdesign/core": "0.4.7-canary.de6d448",
92
- "@astryxdesign/lab": "0.4.7-canary.de6d448",
93
- "@astryxdesign/theme-neutral": "0.4.7-canary.de6d448",
90
+ "@astryxdesign/charts": "0.4.7-canary.dfdd778",
91
+ "@astryxdesign/core": "0.4.7-canary.dfdd778",
92
+ "@astryxdesign/lab": "0.4.7-canary.dfdd778",
93
+ "@astryxdesign/theme-neutral": "0.4.7-canary.dfdd778",
94
94
  "gpt-tokenizer": "^3.4.0"
95
95
  },
96
96
  "peerDependenciesMeta": {
@@ -108,10 +108,10 @@
108
108
  }
109
109
  },
110
110
  "devDependencies": {
111
- "@astryxdesign/charts": "0.4.7-canary.de6d448",
112
- "@astryxdesign/core": "0.4.7-canary.de6d448",
113
- "@astryxdesign/lab": "0.4.7-canary.de6d448",
114
- "@astryxdesign/theme-neutral": "0.4.7-canary.de6d448",
111
+ "@astryxdesign/charts": "0.4.7-canary.dfdd778",
112
+ "@astryxdesign/core": "0.4.7-canary.dfdd778",
113
+ "@astryxdesign/lab": "0.4.7-canary.dfdd778",
114
+ "@astryxdesign/theme-neutral": "0.4.7-canary.dfdd778",
115
115
  "gpt-tokenizer": "^3.4.0"
116
116
  },
117
117
  "scripts": {