@astryxdesign/cli 0.4.6 → 0.4.7-canary.019ae5a

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 (73) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +53 -51
  3. package/api/docs/docs.doc.mjs +2 -2
  4. package/api/index.d.mts +1 -1
  5. package/api/index.mjs +1 -1
  6. package/api/search/search.mjs +50 -4
  7. package/api/search/search.test.mjs +71 -0
  8. package/api/template/template.doc.mjs +2 -2
  9. package/api/theme/build/build.mjs +8 -38
  10. package/api/theme/targets/targets.d.mts +18 -0
  11. package/api/theme/targets/targets.mjs +87 -0
  12. package/api/theme/targets/targets.test.mjs +65 -0
  13. package/api/theme/theme.d.mts +1 -0
  14. package/api/theme/theme.mjs +3 -1
  15. package/api/theme/theme.type.d.mts +23 -0
  16. package/api/theme/theme.type.mjs +21 -1
  17. package/api/theme/themeTargets.doc.d.mts +11 -0
  18. package/api/theme/themeTargets.doc.mjs +58 -0
  19. package/api/theme/themeTemplate.doc.mjs +3 -3
  20. package/assets/codemods/transforms/next/__tests__/next-codemods.test.mjs +127 -0
  21. package/assets/codemods/transforms/next/banner-collapsible-content.mjs +171 -0
  22. package/assets/codemods/transforms/next/index.mjs +11 -1
  23. package/assets/docs/README.md +50 -0
  24. package/assets/docs/cli-integrations.doc.mjs +4 -4
  25. package/assets/docs/theme.doc.dense.mjs +1 -1
  26. package/assets/docs/theme.doc.mjs +1 -1
  27. package/assets/docs/typography.doc.mjs +2 -2
  28. package/assets/templates/blocks/components/Banner/BannerCollapsibleContent.doc.mjs +1 -1
  29. package/assets/templates/blocks/components/Banner/BannerCollapsibleContent.tsx +1 -1
  30. package/assets/templates/blocks/components/Dialog/DialogAdaptivePresentation.doc.mjs +25 -0
  31. package/assets/templates/blocks/components/Dialog/DialogAdaptivePresentation.tsx +167 -0
  32. package/assets/templates/blocks/components/Dialog/DialogScrollingContent.tsx +1 -1
  33. package/assets/templates/blocks/components/Step/StepContent.doc.mjs +14 -0
  34. package/assets/templates/blocks/components/Step/StepContent.tsx +32 -0
  35. package/assets/templates/blocks/components/Step/StepIndicator.doc.mjs +14 -0
  36. package/assets/templates/blocks/components/Step/StepIndicator.tsx +60 -0
  37. package/assets/templates/blocks/components/Step/StepShowcase.doc.mjs +15 -0
  38. package/assets/templates/blocks/components/Step/StepShowcase.tsx +26 -0
  39. package/assets/templates/blocks/components/Step/StepStates.doc.mjs +14 -0
  40. package/assets/templates/blocks/components/Step/StepStates.tsx +46 -0
  41. package/assets/templates/blocks/components/Stepper/StepperCustomContent.doc.mjs +22 -0
  42. package/assets/templates/blocks/components/Stepper/StepperCustomContent.tsx +126 -0
  43. package/assets/templates/blocks/components/Stepper/StepperIndicatorModes.doc.mjs +1 -1
  44. package/assets/templates/blocks/components/Stepper/StepperIndicatorModes.tsx +17 -5
  45. package/assets/templates/blocks/components/Stepper/StepperOnTrackHorizontal.doc.mjs +14 -0
  46. package/assets/templates/blocks/components/Stepper/StepperOnTrackHorizontal.tsx +25 -0
  47. package/assets/templates/blocks/components/Stepper/StepperOnTrackVertical.doc.mjs +2 -2
  48. package/assets/templates/blocks/components/Stepper/StepperOnTrackVertical.tsx +1 -1
  49. package/assets/templates/blocks/components/Stepper/StepperShowcase.doc.mjs +1 -1
  50. package/assets/templates/blocks/components/Stepper/StepperShowcase.tsx +6 -7
  51. package/assets/templates/blocks/components/Stepper/StepperStatus.tsx +1 -1
  52. package/assets/templates/pages/mixed-gallery/page.tsx +12 -3
  53. package/assets/templates/pages/table-grouped/page.tsx +151 -144
  54. package/assets/templates/themes/neutral/neutralTheme.ts +13 -8
  55. package/clients/cli/commands/build-theme.mjs +85 -0
  56. package/clients/cli/commands/dialog-adaptive-template.test.mjs +24 -0
  57. package/clients/cli/commands/theme-targets.behavior.test.mjs +64 -0
  58. package/clients/cli/commands/theme-targets.doc.mjs +38 -0
  59. package/clients/cli/commands/theme-template.doc.mjs +2 -2
  60. package/clients/cli/commands/theme.doc.mjs +4 -2
  61. package/clients/cli/index.mjs +1 -0
  62. package/clients/cli/lib/manifest.mjs +2 -0
  63. package/foundation/discovery/theming-targets.d.mts +86 -0
  64. package/foundation/discovery/theming-targets.mjs +202 -0
  65. package/foundation/discovery/theming-targets.test.mjs +245 -0
  66. package/foundation/response/response-types.doc.mjs +7 -2
  67. package/package.json +9 -9
  68. package/assets/templates/blocks/components/Stepper/StepperHorizontal.doc.mjs +0 -14
  69. package/assets/templates/blocks/components/Stepper/StepperHorizontal.tsx +0 -24
  70. package/assets/templates/blocks/components/Stepper/StepperMultiStepForm.doc.mjs +0 -14
  71. package/assets/templates/blocks/components/Stepper/StepperMultiStepForm.tsx +0 -92
  72. package/assets/templates/blocks/components/Stepper/StepperVerticalOnboarding.doc.mjs +0 -14
  73. package/assets/templates/blocks/components/Stepper/StepperVerticalOnboarding.tsx +0 -40
@@ -3,6 +3,7 @@
3
3
 
4
4
  export { themeAdd } from "./add/add.mjs";
5
5
  export { themeTemplate } from "./template/template.mjs";
6
+ export { themeTargets } from "./targets/targets.mjs";
6
7
  export { themeList } from "./list/list.mjs";
7
8
  export { listThemes } from "./_adapter.mjs";
8
9
  export { themeBuild, importSpecifier } from "./build/build.mjs";
@@ -1,7 +1,8 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
3
  /**
4
- * @file `theme` command barrel — re-exports the build/add/list leaves so the CLI
4
+ * @file `theme` command barrel — re-exports the build/add/list/template/targets
5
+ * leaves so the CLI
5
6
  * (cli/commands/build-theme.mjs) and scripted callers import from one place.
6
7
  * Each leaf is also importable directly (e.g. api/theme/add/add.mjs). `theme`
7
8
  * has real subcommands, so there is no flag-dispatch here — the CLI calls the
@@ -11,5 +12,6 @@
11
12
  export {themeBuild, importSpecifier} from './build/build.mjs';
12
13
  export {themeAdd} from './add/add.mjs';
13
14
  export {themeTemplate} from './template/template.mjs';
15
+ export {themeTargets} from './targets/targets.mjs';
14
16
  export {themeList} from './list/list.mjs';
15
17
  export {listThemes} from './_adapter.mjs';
@@ -101,3 +101,26 @@ export type ThemeTemplateResponse = {
101
101
  reason: "exists" | null;
102
102
  };
103
103
  };
104
+ /**
105
+ * One themeable target: the `defineTheme` `components` key, the class it
106
+ * renders as, the component whose doc declares it, and the props and states
107
+ * that are legal override keys under it.
108
+ */
109
+ export type ThemeTargetEntry = {
110
+ key: string;
111
+ className: string;
112
+ component: string;
113
+ props: string[];
114
+ states: string[];
115
+ };
116
+ /**
117
+ * xds --json theme targets [filter]
118
+ */
119
+ export type ThemeTargetsResponse = {
120
+ type: "theme.targets";
121
+ data: {
122
+ filter: string | null;
123
+ componentCount: number;
124
+ targets: ThemeTargetEntry[];
125
+ };
126
+ };
@@ -14,9 +14,10 @@
14
14
  * xds --json theme list -> theme.list
15
15
  * xds --json theme add <slug> -> theme.add
16
16
  * xds --json theme template -> theme.template
17
+ * xds --json theme targets [filter] -> theme.targets
17
18
  * (file not found / parse error) -> CLIError
18
19
  *
19
- * @position api — colocated typedefs for api/theme/{theme,build,add,list,template,_adapter}
20
+ * @position api — colocated typedefs for api/theme/{theme,build,add,list,template,targets,_adapter}
20
21
  */
21
22
 
22
23
  /**
@@ -78,6 +79,25 @@
78
79
  * @property {{path: string, written: boolean, reason: 'exists' | null}} data
79
80
  */
80
81
 
82
+ /**
83
+ * One themeable target: the `defineTheme` `components` key, the class it
84
+ * renders as, the component whose doc declares it, and the props and states
85
+ * that are legal override keys under it.
86
+ * @typedef {object} ThemeTargetEntry
87
+ * @property {string} key
88
+ * @property {string} className
89
+ * @property {string} component
90
+ * @property {string[]} props
91
+ * @property {string[]} states
92
+ */
93
+
94
+ /**
95
+ * xds --json theme targets [filter]
96
+ * @typedef {object} ThemeTargetsResponse
97
+ * @property {'theme.targets'} type
98
+ * @property {{filter: string | null, componentCount: number, targets: ThemeTargetEntry[]}} data
99
+ */
100
+
81
101
  // Make this a module so the @typedefs above are importable as types via
82
102
  // `import('./theme.type.mjs').ThemeBuildResponse` (and re-exportable from a .d.ts).
83
103
  export {};
@@ -0,0 +1,11 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in api/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ /**
5
+ * @file FunctionDoc for `themeTargets()` / `astryx theme targets`. Colocated
6
+ * with the API function it documents; the response-shape source of truth stays
7
+ * in `theme.type.mjs`.
8
+ * @position packages/cli/api/theme — function documentation
9
+ */
10
+ /** @type {import('@astryxdesign/cli/authoring').FunctionDoc} */
11
+ export const doc: import("@astryxdesign/cli/authoring").FunctionDoc;
@@ -0,0 +1,58 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file FunctionDoc for `themeTargets()` / `astryx theme targets`. Colocated
5
+ * with the API function it documents; the response-shape source of truth stays
6
+ * in `theme.type.mjs`.
7
+ * @position packages/cli/api/theme — function documentation
8
+ */
9
+
10
+ /** @type {import('@astryxdesign/cli/authoring').FunctionDoc} */
11
+ export const doc = {
12
+ type: 'function',
13
+ kind: 'api',
14
+ name: 'themeTargets',
15
+ displayName: 'themeTargets()',
16
+ summary: 'List every component theming target a theme can override.',
17
+ description:
18
+ 'Enumerates the whole themeable surface: each `defineTheme` components key, the stable ' +
19
+ 'class it paints, the component that declares it, and the props and states that are legal ' +
20
+ 'override keys under it. Same source as the Theming table `astryx component <Name>` prints ' +
21
+ '— the component docs — so the list cannot drift from the components, and `theme build` ' +
22
+ 'validates overrides against this exact set. A filter naming a component gives that ' +
23
+ 'component\u2019s set; anything else is a substring search over the keys.',
24
+ importPath: '@astryxdesign/cli/api',
25
+ signature:
26
+ 'themeTargets(filter?: string, ctx?: {cwd?: string}): Promise<ThemeTargetsResponse>',
27
+ keywords: ['theme', 'targets', 'defineTheme', 'components', 'override', 'class', 'states', 'audit'],
28
+ params: [
29
+ {
30
+ name: 'filter',
31
+ type: 'string',
32
+ description:
33
+ 'A component name (exact, case-insensitive) or a substring of a target key. Omit for the whole surface.',
34
+ },
35
+ {
36
+ name: 'ctx.cwd',
37
+ type: 'string',
38
+ description: 'Directory the project’s @astryxdesign/core is resolved from.',
39
+ },
40
+ ],
41
+ returns: [
42
+ {
43
+ type: 'theme.targets',
44
+ description:
45
+ 'The echoed filter, how many components are represented, and the targets: each {key, className, component, props, states}.',
46
+ },
47
+ ],
48
+ throws: [
49
+ {code: 'ERR_CORE_NOT_FOUND', when: '@astryxdesign/core cannot be resolved from cwd'},
50
+ {code: 'ERR_UNKNOWN_COMPONENT', when: 'a filter matches no target'},
51
+ ],
52
+ examples: [
53
+ {label: 'The whole themeable surface', code: 'await themeTargets();'},
54
+ {label: "One component's targets", code: "await themeTargets('Switch');"},
55
+ ],
56
+ command: 'theme targets',
57
+ related: ['themeBuild', 'themeTemplate', 'component'],
58
+ };
@@ -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
+ ];
@@ -0,0 +1,50 @@
1
+ # /packages/cli/assets/docs
2
+
3
+ Reference topics for people **building with** Astryx. Not docs about building Astryx itself.
4
+
5
+ One `{topic}.doc.mjs` per topic, plus optional `{topic}.doc.dense.mjs` / `{topic}.doc.zh.mjs` prose overlays. `foundation/discovery/docs-discovery.mjs` picks up any `{topic}.doc.mjs` here with no registration; `api/docs/_adapter.mjs` merges the overlays.
6
+
7
+ What you add reaches `astryx docs <topic>`, `astryx search`, the `--json` API, the agent-docs block and the doc site — and ships on npm.
8
+
9
+ ## Who you are writing for
10
+
11
+ Someone building a product with Astryx. Their questions:
12
+
13
+ - what a component is for, and when to reach for something else
14
+ - the props, their defaults, and what each does to what they see
15
+ - how to compose it, and the pattern to copy
16
+ - what it costs — bundle size, the a11y obligations they inherit
17
+ - how to theme it, and which targets are stable
18
+
19
+ **The test for anything you add: does a caller act on it?** They are not reviewing a PR, promoting a component out of lab, or attaching evidence to a checklist.
20
+
21
+ ## Tells that you are writing for us instead
22
+
23
+ - second person aimed at the wrong reader — "reviewers should…", "before promoting a component…", "attach evidence for…"
24
+ - **rubric, readiness, gate, audit, checklist, sign-off, promotion, evidence** as things the reader must produce
25
+ - a table of things to verify rather than things to use
26
+ - anything about lab → core, which is our lifecycle, not theirs
27
+ - Storybook, Playwright, CI or the Simulator named as tools the reader runs
28
+
29
+ One subtlety: a statement about the **system's behavior** is caller-facing even when it sounds like process. "A component's theme targets are stable once published" tells a caller what they can rely on; "reviewers must check that theme targets are stable" is ours. Same fact, different reader — **rewrite it rather than move it**.
30
+
31
+ ## Where the rest goes
32
+
33
+ The material is usually good; the finding is placement, not quality. It goes in the [wiki](https://github.com/facebook/astryx/wiki) — **as a section on the page that already covers it, not a new page.** The wiki is at nearly 60 pages, several of them overlapping, because every stray section got its own.
34
+
35
+ | what you wrote | where it goes |
36
+ | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
37
+ | how a component is graded — checks, scoring | [Component-Audit-Rubric](https://github.com/facebook/astryx/wiki/Component-Audit-Rubric) |
38
+ | lab → core promotion, what a component must satisfy | [Component-Lifecycle](https://github.com/facebook/astryx/wiki/Component-Lifecycle) |
39
+ | how to build a new component | [Component-Authoring-Guide](https://github.com/facebook/astryx/wiki/Component-Authoring-Guide), [Creating-New-Components](https://github.com/facebook/astryx/wiki/Creating-New-Components) |
40
+ | what a component must be hardened against | [Component-Hardening-Protocol](https://github.com/facebook/astryx/wiki/Component-Hardening-Protocol), [Hardening-Audit-Guide](https://github.com/facebook/astryx/wiki/Hardening-Audit-Guide) |
41
+ | a11y requirements as checks we run | [Accessibility-Checklist](https://github.com/facebook/astryx/wiki/Accessibility-Checklist) |
42
+ | how the system is put together | [System-Architecture](https://github.com/facebook/astryx/wiki/System-Architecture), [Theming-Infrastructure](https://github.com/facebook/astryx/wiki/Theming-Infrastructure) |
43
+ | API naming and shape decisions | [API-Conventions](https://github.com/facebook/astryx/wiki/API-Conventions), [API-Arbitration](https://github.com/facebook/astryx/wiki/API-Arbitration) |
44
+ | contributor workflow, PR process | [Contributing](https://github.com/facebook/astryx/wiki/Contributing), [Contributing-with-AI-Assistants](https://github.com/facebook/astryx/wiki/Contributing-with-AI-Assistants) |
45
+ | release mechanics | [Release-Process](https://github.com/facebook/astryx/wiki/Release-Process) |
46
+ | what a nightly agent role does | the Night-Watch pages, from [Night-Watch-Overview](https://github.com/facebook/astryx/wiki/Night-Watch-Overview) |
47
+
48
+ **Fits no row?** It is still not caller-facing. Default it to [Contributing](https://github.com/facebook/astryx/wiki/Contributing), or `CONTRIBUTING.md` when it is a step someone follows with the repo cloned. Never default it back to this directory.
49
+
50
+ Worked example: a responsive-and-interaction readiness rubric is grading criteria → **Component-Audit-Rubric**, or **Component-Lifecycle** if it is a promotion gate.
@@ -115,7 +115,7 @@ export const docs = {
115
115
  content: [
116
116
  {
117
117
  type: 'prose',
118
- text: "Point the integration file's `docs` field at a directory of reference docs and every `{topic}.doc.{ts,mjs,js}` under it becomes a topic the CLI serves `astryx docs` lists it, `astryx docs <topic>` prints it, `astryx search` indexes it, and `astryx init` names it in the agent block. A topic is a plain object stamped `type: 'generic'`, the same shape core's own topics use.",
118
+ text: "Point the integration file's `docs` field at a directory of reference docs and every `{topic}.doc.{ts,mjs,js}` under it becomes a topic the CLI serves: `astryx docs` lists it, `astryx docs <topic>` prints it, `astryx search` indexes it, and `astryx init` names it in the agent block. A topic is a plain object stamped `type: 'generic'`, the same shape core's own topics use.",
119
119
  },
120
120
  {
121
121
  type: 'code',
@@ -124,7 +124,7 @@ export const docs = {
124
124
  },
125
125
  {
126
126
  type: 'prose',
127
- text: "A topic can also speak about one that already exists. `replaces: 'x'` takes over topic x core's, or another integration's so a package whose consumers install it differently can serve its own Getting Started instead of the built-in one. Give the replacement a different `name` and the old name keeps resolving to it, so a link or an agent that learned the old topic still lands in the right place.",
127
+ text: "A topic can also speak about one that already exists. `replaces: 'x'` takes over topic x (core's, or another integration's) so a package whose consumers install it differently can serve its own Getting Started instead of the built-in one. Give the replacement a different `name` and the old name keeps resolving to it, so a link or an agent that learned the old topic still lands in the right place.",
128
128
  },
129
129
  {
130
130
  type: 'code',
@@ -133,14 +133,14 @@ export const docs = {
133
133
  },
134
134
  {
135
135
  type: 'prose',
136
- text: "`extends: 'x'` merges onto a topic instead of owning it: a section whose title matches one in the base replaces that section, and a section the base does not have is appended. Reach for it to correct or add to a topic you do not want to fork a fork of someone else's guide stops receiving their fixes the day you write it.",
136
+ text: "`extends: 'x'` merges onto a topic instead of owning it: a section whose title matches one in the base replaces that section, and a section the base does not have is appended. Reach for it to correct or add to a topic you do not want to fork: a fork of someone else's guide stops receiving their fixes the day you write it.",
137
137
  },
138
138
  {
139
139
  type: 'list',
140
140
  style: 'unordered',
141
141
  items: [
142
142
  'A topic name is a CLI argument and a docsite path, so it may hold only letters, digits, `_` and `-`.',
143
- "A name that collides with an existing topic and declares neither `replaces` nor `extends` is an error, not a silent override the CLI will not guess which one you meant.",
143
+ "A name that collides with an existing topic and declares neither `replaces` nor `extends` is an error, not a silent override; the CLI will not guess which one you meant.",
144
144
  '`replaces` and `extends` are exclusive: a topic either takes another\'s place or merges onto it.',
145
145
  'Two integrations replacing one topic is a warning, and the one configured later in `astryx.config` wins.',
146
146
  ],
@@ -10,7 +10,7 @@ export const docsDense = {
10
10
  { section: 'Theme Props', title: 'Props', content: [null] },
11
11
  { section: 'Creating a Custom Theme', title: 'Custom Theme', content: [{ type: 'prose', text: '`theme list` + `theme add <slug>` to start from a shipped theme, or defineTheme from scratch. only override tokens that differ.' }, null, { type: 'prose', text: '`astryx theme template` writes theme.template.ts: every defineTheme field + token families + override syntax, annotated, with the CLI command that prints each reference.' }] },
12
12
  { section: 'defineTheme', title: 'defineTheme', content: [{ type: 'prose', text: 'scale configs (color, typography, radius, motion) + explicit token overrides + component overrides. color derives full palette from accent via HCT; accent = hex or [light, dark] tuple (per-scheme palettes). tokens overrides win token-by-token; --color-on-accent stays baked from color.accent, so prefer a tuple accent over overriding --color-accent.' }, null, null] },
13
- { section: 'Component Style Overrides', title: 'Component Overrides', content: [{ type: 'prose', text: 'components field uses semantic component keys + style keys (base, variant:value, stateName), not raw selectors. for external CSS, prefer data-* selectors from `astryx docs styling`. write standard CSS (borderRadius, padding) — pipeline expands to internal vars. public vars (--button-focus-offset etc) set directly. private vars (--_*) cannot be set — use CSS properties. run `astryx component <Name>` for details.' }, null, null, null, null] },
13
+ { section: 'Component Style Overrides', title: 'Component Overrides', content: [{ type: 'prose', text: 'components field uses semantic component keys + style keys (base, variant:value, stateName), not raw selectors. for external CSS, prefer data-* selectors from `astryx docs styling`. write standard CSS (borderRadius, padding) — pipeline expands to internal vars. public vars (--button-focus-offset etc) set directly. private vars (--_*) cannot be set — use CSS properties. run `astryx theme targets [Name]` to enumerate every themeable key (--json for lint), `astryx component <Name>` for one component.' }, null, null, null, null] },
14
14
  { section: 'Custom Variants', title: 'Custom Variants', content: [{ type: 'prose', text: 'any unknown prop:value in components becomes a new variant. astryx theme build generates TS augmentations. works on any extensible prop axis (variant, status, etc).' }, null, null, null, null] },
15
15
  { section: 'Building Themes for Production', title: 'Build for Production', content: [{ type: 'prose', text: 'astryx theme build compiles defineTheme to static CSS. outputs .css + .js (__built:true) + .d.ts.' }, null, null, null, null] },
16
16
  { section: 'Runtime vs Built Themes', title: 'Runtime vs Built', content: [{ type: 'prose', text: 'runtime: useInsertionEffect injects styles client-side. built: static CSS on first paint. USE /built + theme.css FOR SSR.' }, null, null, null] },
@@ -300,7 +300,7 @@ const brandTheme = defineTheme({
300
300
  },
301
301
  {
302
302
  type: 'prose',
303
- text: 'Run `astryx component <Name>` to see a component\'s theming targets, public CSS variables, and which standard CSS properties are supported.',
303
+ text: 'Run `astryx theme targets` for every themeable key in the system (`astryx theme targets <Name>` to scope it, `--json` to lint a theme against it), and `astryx component <Name>` for one component\'s theming targets, public CSS variables, and which standard CSS properties are supported.',
304
304
  },
305
305
  {
306
306
  type: 'list',
@@ -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'],