@astryxdesign/cli 0.5.2-canary.e4f8e4e → 0.5.2-canary.edc0cfc

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.
@@ -39,6 +39,21 @@ describe('themeTargets (api/theme/targets)', () => {
39
39
  ]);
40
40
  }, 60_000);
41
41
 
42
+ it.each(['table-header', 'table-body', 'table-footer'])(
43
+ '%s appears once under the Table owner',
44
+ async target => {
45
+ const {data} = await themeTargets('Table');
46
+ const matches = data.targets.filter(entry => entry.key === target);
47
+ expect(data.componentCount).toBe(1);
48
+ expect(matches).toHaveLength(1);
49
+ expect(matches[0]).toMatchObject({
50
+ key: target,
51
+ component: 'Table',
52
+ });
53
+ },
54
+ 60_000,
55
+ );
56
+
42
57
  // Half the system's keys contain "button" (chat-send-button, toggle-button,
43
58
  // …). A component name has to mean the component, or `theme targets Button`
44
59
  // answers a different question than `component Button` and the two views
@@ -4,10 +4,10 @@
4
4
  export const doc = {
5
5
  type: 'block',
6
6
  exampleFor: 'useTableRowStatus',
7
- name: 'useTableRowStatus - Status Dots',
8
- displayName: 'useTableRowStatus - Status Dots',
7
+ name: 'useTableRowStatus - Status Icons',
8
+ displayName: 'useTableRowStatus - Status Icons',
9
9
  description:
10
- 'A job table using useTableRowStatus to render a colored status dot (or icon) per row (failed / running / queued). Rows with no status show no dot.',
10
+ 'A job table using useTableRowStatus. Semantic outcomes use the themed success, warning, and error icons; a palette-only queued state keeps the neutral dot.',
11
11
  isReady: true,
12
12
  aspectRatio: 16 / 9,
13
13
  componentsUsed: ['Table'],
@@ -34,13 +34,13 @@ const columns: TableColumn<Job>[] = [
34
34
  function jobStatus(job: Job): TableRowStatus | null {
35
35
  switch (job.state) {
36
36
  case 'failed':
37
- return {color: 'error', icon: 'error', label: 'Failed'};
37
+ return {color: 'error', label: 'Failed'};
38
38
  case 'running':
39
- return {color: 'warning', icon: 'warning', label: 'Running'};
39
+ return {color: 'warning', label: 'Running'};
40
40
  case 'queued':
41
41
  return {color: 'gray', label: 'Queued'};
42
- default:
43
- return null; // succeeded: no dot
42
+ case 'succeeded':
43
+ return {color: 'success', label: 'Succeeded'};
44
44
  }
45
45
  }
46
46
 
@@ -33,7 +33,13 @@ export namespace docs {
33
33
  }
34
34
  let props: never[];
35
35
  }
36
- export namespace docsDense {
36
+ export namespace docsZh {
37
+ let name_1: string;
38
+ export { name_1 as name };
39
+ let displayName_1: string;
40
+ export { displayName_1 as displayName };
41
+ let category_1: string;
42
+ export { category_1 as category };
37
43
  export namespace usage_1 {
38
44
  let description_1: string;
39
45
  export { description_1 as description };
@@ -42,3 +48,12 @@ export namespace docsDense {
42
48
  let props_1: never[];
43
49
  export { props_1 as props };
44
50
  }
51
+ export namespace docsDense {
52
+ export namespace usage_2 {
53
+ let description_2: string;
54
+ export { description_2 as description };
55
+ }
56
+ export { usage_2 as usage };
57
+ let props_2: never[];
58
+ export { props_2 as props };
59
+ }
@@ -34,15 +34,30 @@ export const docs = {
34
34
  ],
35
35
  anatomy: [
36
36
  {
37
- name: 'Base-only anatomy',
37
+ name: 'Track',
38
38
  required: true,
39
- description: 'This must not leak into translated output.',
39
+ description: 'The rail that shows the current on/off state.',
40
+ },
41
+ {
42
+ name: 'Thumb',
43
+ required: true,
44
+ description: 'The control that moves along the track.',
40
45
  },
41
46
  ],
42
47
  },
43
48
  props: [],
44
49
  };
45
50
 
51
+ export const docsZh = {
52
+ name: 'AccessibilityOverlayFixture',
53
+ displayName: 'Accessibility Overlay Fixture',
54
+ category: 'Test',
55
+ usage: {
56
+ description: 'Translated full-doc description.',
57
+ },
58
+ props: [],
59
+ };
60
+
46
61
  export const docsDense = {
47
62
  usage: {
48
63
  description: 'Dense description.',
@@ -0,0 +1,38 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in foundation/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ export namespace docs {
5
+ let name: string;
6
+ let displayName: string;
7
+ let category: string;
8
+ namespace usage {
9
+ let description: string;
10
+ let anatomy: {
11
+ name: string;
12
+ required: boolean;
13
+ description: string;
14
+ }[];
15
+ }
16
+ let props: never[];
17
+ }
18
+ export namespace docsZh {
19
+ let name_1: string;
20
+ export { name_1 as name };
21
+ let displayName_1: string;
22
+ export { displayName_1 as displayName };
23
+ let category_1: string;
24
+ export { category_1 as category };
25
+ export namespace usage_1 {
26
+ let description_1: string;
27
+ export { description_1 as description };
28
+ let anatomy_1: {
29
+ name: string;
30
+ required: boolean;
31
+ description: string;
32
+ }[];
33
+ export { anatomy_1 as anatomy };
34
+ }
35
+ export { usage_1 as usage };
36
+ let props_1: never[];
37
+ export { props_1 as props };
38
+ }
@@ -0,0 +1,45 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ export const docs = {
4
+ name: 'AnatomyOverrideFixture',
5
+ displayName: 'Anatomy Override Fixture',
6
+ category: 'Test',
7
+ usage: {
8
+ description: 'A multiline field for longer text.',
9
+ anatomy: [
10
+ {
11
+ name: 'Field',
12
+ required: true,
13
+ description: 'The multiline text entry area.',
14
+ },
15
+ {
16
+ name: 'Resize handle',
17
+ required: false,
18
+ description: 'Lets the user change the field height.',
19
+ },
20
+ ],
21
+ },
22
+ props: [],
23
+ };
24
+
25
+ export const docsZh = {
26
+ name: 'AnatomyOverrideFixture',
27
+ displayName: 'Anatomy Override Fixture',
28
+ category: 'Test',
29
+ usage: {
30
+ description: '用于输入较长文本的多行字段。',
31
+ anatomy: [
32
+ {
33
+ name: '文本区域',
34
+ required: true,
35
+ description: '用于输入多行文本的区域。',
36
+ },
37
+ {
38
+ name: '调整大小控件',
39
+ required: false,
40
+ description: '允许用户调整字段高度。',
41
+ },
42
+ ],
43
+ },
44
+ props: [],
45
+ };
@@ -51,7 +51,7 @@ export async function loadComponentDoc(
51
51
  /** @type {any} */
52
52
  const translation = mod[translationKey];
53
53
  if (translation.props || translation.components?.some((/** @type {any} */ c) => c.props)) {
54
- return translation;
54
+ return overlayComponentDoc(docs, translation);
55
55
  }
56
56
  return mergeTranslation(docs, translation);
57
57
  }
@@ -194,7 +194,8 @@ function overlayComponentDoc(docs, translation) {
194
194
  });
195
195
  };
196
196
 
197
- /** Preserve structured accessibility data without changing established translated output.
197
+ /** Preserve canonical structured guidance added after legacy full-doc translations,
198
+ * without changing established translated prose behavior.
198
199
  * @param {any} baseUsage
199
200
  * @param {any} translatedUsage
200
201
  */
@@ -210,6 +211,9 @@ function overlayComponentDoc(docs, translation) {
210
211
  baseUsage?.accessibilityThemeCoverage !== undefined
211
212
  ? {accessibilityThemeCoverage: baseUsage.accessibilityThemeCoverage}
212
213
  : null),
214
+ ...(translatedUsage.anatomy === undefined && baseUsage?.anatomy !== undefined
215
+ ? {anatomy: baseUsage.anatomy}
216
+ : null),
213
217
  };
214
218
  };
215
219
 
@@ -9,7 +9,8 @@
9
9
  */
10
10
 
11
11
  import {describe, it, expect} from 'vitest';
12
- import {mergeTranslation} from './component-loader.mjs';
12
+ import * as path from 'node:path';
13
+ import {loadComponentDoc, loadDocs, mergeTranslation} from './component-loader.mjs';
13
14
 
14
15
  /** Minimal HookDoc-shaped fixture with params + returns. */
15
16
  function hookDocs() {
@@ -105,3 +106,37 @@ describe('mergeTranslation — component prop descriptions (regression)', () =>
105
106
  expect(merged.props.find(p => p.name === 'disabled').description).toBe('Disabled state.');
106
107
  });
107
108
  });
109
+
110
+ describe('loadComponentDoc — full localized doc overlays', () => {
111
+ it('inherits canonical anatomy when docsZh omits it and matches loadDocs', async () => {
112
+ const docPath = path.join(
113
+ import.meta.dirname,
114
+ '__fixtures__',
115
+ 'component-accessibility-overlay.doc.mjs',
116
+ );
117
+
118
+ const direct = await loadComponentDoc(docPath, {zh: true});
119
+ const cli = await loadDocs(docPath, {zh: true});
120
+
121
+ expect(direct).toEqual(cli);
122
+ expect(direct.usage.description).toBe('Translated full-doc description.');
123
+ expect(direct.usage.anatomy).toEqual((await loadComponentDoc(docPath)).usage.anatomy);
124
+ expect(direct.usage.bestPractices).toBeUndefined();
125
+ });
126
+
127
+ it('keeps explicit localized anatomy and matches loadDocs', async () => {
128
+ const docPath = path.join(
129
+ import.meta.dirname,
130
+ '__fixtures__',
131
+ 'component-anatomy-override.doc.mjs',
132
+ );
133
+
134
+ const direct = await loadComponentDoc(docPath, {zh: true});
135
+ const cli = await loadDocs(docPath, {zh: true});
136
+ const canonical = await loadComponentDoc(docPath);
137
+
138
+ expect(direct).toEqual(cli);
139
+ expect(direct.usage.anatomy).not.toEqual(canonical.usage.anatomy);
140
+ expect(direct.usage.anatomy[0].name).toBe('文本区域');
141
+ });
142
+ });
@@ -90,7 +90,7 @@ describe('translated component docs never drop a prop', () => {
90
90
  }
91
91
  });
92
92
 
93
- describe('the reported symptom', () => {
93
+ describe('full component-doc overlays', () => {
94
94
  it('astryx component Button --zh still lists isInterruptible and isIconOnly', async () => {
95
95
  const docPath = path.join(CORE_SRC, 'Button', 'Button.doc.mjs');
96
96
  const zh = await loadDocs(docPath, {zh: true});
@@ -127,6 +127,33 @@ describe('the reported symptom', () => {
127
127
  'Decorative track — Not part of the contrast audit.',
128
128
  );
129
129
  expect(dense.usage.bestPractices).toBeUndefined();
130
- expect(dense.usage.anatomy).toBeUndefined();
130
+ expect(dense.usage.anatomy).toEqual(english.usage.anatomy);
131
+ });
132
+
133
+ it('inherits canonical anatomy when a full docsZh usage omits it', async () => {
134
+ const docPath = path.join(
135
+ import.meta.dirname,
136
+ '__fixtures__',
137
+ 'component-accessibility-overlay.doc.mjs',
138
+ );
139
+ const english = await loadDocs(docPath);
140
+ const zh = await loadDocs(docPath, {zh: true});
141
+
142
+ expect(zh.usage.description).toBe('Translated full-doc description.');
143
+ expect(zh.usage.anatomy).toEqual(english.usage.anatomy);
144
+ expect(zh.usage.bestPractices).toBeUndefined();
145
+ });
146
+
147
+ it('keeps explicit localized anatomy from a full docsZh usage', async () => {
148
+ const docPath = path.join(
149
+ import.meta.dirname,
150
+ '__fixtures__',
151
+ 'component-anatomy-override.doc.mjs',
152
+ );
153
+ const mod = await import(pathToFileURL(docPath).href);
154
+ const zh = await loadDocs(docPath, {zh: true});
155
+
156
+ expect(zh.usage.anatomy).toEqual(mod.docsZh.usage.anatomy);
157
+ expect(zh.usage.anatomy).not.toEqual(mod.docs.usage.anatomy);
131
158
  });
132
159
  });
@@ -3,16 +3,21 @@
3
3
 
4
4
  /**
5
5
  * Every theming target declared under a core `src` directory, sorted by key
6
- * then component. A key can appear more than once: a shared sub-element (the
7
- * radio indicator, say) is documented by every component that renders it.
6
+ * then component. When both a parent doc and one of its `subComponentOf`
7
+ * children declare the same class, the parent is the canonical discovery owner;
8
+ * the child keeps its direct docs but does not add a second listing row. Shared
9
+ * targets declared by unrelated components remain separate rows.
8
10
  *
9
11
  * Unreadable docs are skipped rather than fatal — a single malformed doc must
10
12
  * not take out theme validation or the listing.
11
13
  *
12
14
  * @param {string} coreSrc - absolute path to `<core>/src`
15
+ * @param {{includeDeprecated?: boolean}} [options] - preserve the CLI's full listing by default; ownership checks can request active targets only
13
16
  * @returns {Promise<ThemingTarget[]>}
14
17
  */
15
- export function collectThemingTargets(coreSrc: string): Promise<ThemingTarget[]>;
18
+ export function collectThemingTargets(coreSrc: string, { includeDeprecated }?: {
19
+ includeDeprecated?: boolean;
20
+ }): Promise<ThemingTarget[]>;
16
21
  /**
17
22
  * One public custom property a theme may set on a component's target.
18
23
  * @typedef {object} ThemingVar
@@ -45,19 +45,25 @@ function targetKey(className) {
45
45
 
46
46
  /**
47
47
  * Every theming target declared under a core `src` directory, sorted by key
48
- * then component. A key can appear more than once: a shared sub-element (the
49
- * radio indicator, say) is documented by every component that renders it.
48
+ * then component. When both a parent doc and one of its `subComponentOf`
49
+ * children declare the same class, the parent is the canonical discovery owner;
50
+ * the child keeps its direct docs but does not add a second listing row. Shared
51
+ * targets declared by unrelated components remain separate rows.
50
52
  *
51
53
  * Unreadable docs are skipped rather than fatal — a single malformed doc must
52
54
  * not take out theme validation or the listing.
53
55
  *
54
56
  * @param {string} coreSrc - absolute path to `<core>/src`
57
+ * @param {{includeDeprecated?: boolean}} [options] - preserve the CLI's full listing by default; ownership checks can request active targets only
55
58
  * @returns {Promise<ThemingTarget[]>}
56
59
  */
57
- export async function collectThemingTargets(coreSrc) {
60
+ export async function collectThemingTargets(
61
+ coreSrc,
62
+ {includeDeprecated = true} = {},
63
+ ) {
58
64
  if (!coreSrc || !fs.existsSync(coreSrc)) return [];
59
65
 
60
- /** @type {ThemingTarget[]} */
66
+ /** @type {Array<ThemingTarget & {parent: string|null}>} */
61
67
  const targets = [];
62
68
 
63
69
  /** @param {string} dir */
@@ -85,6 +91,7 @@ export async function collectThemingTargets(coreSrc) {
85
91
  : path.basename(path.dirname(full));
86
92
 
87
93
  for (const target of doc?.theming?.targets || []) {
94
+ if (!includeDeprecated && target?.deprecatedFor != null) continue;
88
95
  const className = target?.className;
89
96
  if (typeof className !== 'string') continue;
90
97
  const key = targetKey(className);
@@ -93,6 +100,8 @@ export async function collectThemingTargets(coreSrc) {
93
100
  key,
94
101
  className,
95
102
  component,
103
+ parent:
104
+ typeof doc?.subComponentOf === 'string' ? doc.subComponentOf : null,
96
105
  props: stringList(target.visualProps),
97
106
  states: stringList(target.states),
98
107
  });
@@ -102,10 +111,53 @@ export async function collectThemingTargets(coreSrc) {
102
111
 
103
112
  await scan(coreSrc);
104
113
 
105
- targets.sort(
114
+ const canonical = canonicalizeParentTargets(targets);
115
+ canonical.sort(
106
116
  (a, b) => a.key.localeCompare(b.key) || a.component.localeCompare(b.component),
107
117
  );
108
- return targets;
118
+ return canonical;
119
+ }
120
+
121
+ /**
122
+ * Collapse only an explicit parent/child duplicate. A child target is removed
123
+ * when its `subComponentOf` parent declares that same class exactly once; its
124
+ * props and states are merged into the parent's row so no capability is lost.
125
+ *
126
+ * Unrelated components sharing a class remain separate. An ambiguous parent
127
+ * declaration also remains untouched rather than guessing which row is
128
+ * canonical.
129
+ *
130
+ * @param {Array<ThemingTarget & {parent: string|null}>} targets
131
+ * @returns {ThemingTarget[]}
132
+ */
133
+ function canonicalizeParentTargets(targets) {
134
+ /** @type {Map<string, Array<ThemingTarget & {parent: string|null}>>} */
135
+ const rootsByComponentAndClass = new Map();
136
+ for (const target of targets) {
137
+ if (target.parent != null) continue;
138
+ const identity = `${target.component}\0${target.className}`;
139
+ const roots = rootsByComponentAndClass.get(identity) ?? [];
140
+ roots.push(target);
141
+ rootsByComponentAndClass.set(identity, roots);
142
+ }
143
+
144
+ /** @type {Set<ThemingTarget & {parent: string|null}>} */
145
+ const duplicates = new Set();
146
+ for (const target of targets) {
147
+ if (target.parent == null) continue;
148
+ const roots =
149
+ rootsByComponentAndClass.get(`${target.parent}\0${target.className}`) ?? [];
150
+ if (roots.length !== 1) continue;
151
+
152
+ const canonical = roots[0];
153
+ canonical.props = [...new Set([...canonical.props, ...target.props])];
154
+ canonical.states = [...new Set([...canonical.states, ...target.states])];
155
+ duplicates.add(target);
156
+ }
157
+
158
+ return targets
159
+ .filter(target => !duplicates.has(target))
160
+ .map(({parent: _parent, ...target}) => target);
109
161
  }
110
162
 
111
163
  /**
@@ -41,6 +41,10 @@ const coreSrc = path.join(coreDir, 'src');
41
41
 
42
42
  /** @type {Promise<import('./theming-targets.mjs').ThemingTarget[]>} */
43
43
  const enumerated = collectThemingTargets(coreSrc);
44
+ /** @type {Promise<import('./theming-targets.mjs').ThemingTarget[]>} */
45
+ const activeEnumerated = collectThemingTargets(coreSrc, {
46
+ includeDeprecated: false,
47
+ });
44
48
 
45
49
  describe('collectThemingTargets', () => {
46
50
  it('enumerates the whole surface, not a handful', async () => {
@@ -56,6 +60,20 @@ describe('collectThemingTargets', () => {
56
60
  }
57
61
  });
58
62
 
63
+ it('can limit ownership checks to active targets', async () => {
64
+ const all = await enumerated;
65
+ const active = await activeEnumerated;
66
+ expect(
67
+ all.some(t => t.component === 'CodeBlock' && t.key === 'codeblock'),
68
+ ).toBe(true);
69
+ expect(
70
+ active.some(t => t.component === 'CodeBlock' && t.key === 'codeblock'),
71
+ ).toBe(false);
72
+ expect(
73
+ active.some(t => t.component === 'CodeBlock' && t.key === 'code-block'),
74
+ ).toBe(true);
75
+ });
76
+
59
77
  it('carries the props and states a target reflects', async () => {
60
78
  const targets = await enumerated;
61
79
  expect(targets.find(t => t.key === 'switch-thumb')).toEqual({
@@ -67,6 +85,34 @@ describe('collectThemingTargets', () => {
67
85
  });
68
86
  });
69
87
 
88
+ it.each([
89
+ ['TableHeader', 'table-header'],
90
+ ['TableBody', 'table-body'],
91
+ ['TableFooter', 'table-footer'],
92
+ ])('keeps %s theming metadata available in its direct doc', async (name, key) => {
93
+ const doc = await loadComponentDoc(
94
+ path.join(coreSrc, 'Table', `${name}.doc.mjs`),
95
+ );
96
+ expect(doc.subComponentOf).toBe('Table');
97
+ expect(doc.theming.targets).toContainEqual({className: `astryx-${key}`});
98
+ });
99
+
100
+ it.each(['table-header', 'table-body', 'table-footer'])(
101
+ 'enumerates %s once under its canonical Table owner',
102
+ async key => {
103
+ const matches = (await enumerated).filter(target => target.key === key);
104
+ expect(matches).toEqual([
105
+ {
106
+ key,
107
+ className: `astryx-${key}`,
108
+ component: 'Table',
109
+ props: [],
110
+ states: [],
111
+ },
112
+ ]);
113
+ },
114
+ );
115
+
70
116
  it('is sorted by key, so a diff of two runs is readable', async () => {
71
117
  const keys = (await enumerated).map(t => t.key);
72
118
  expect(keys).toEqual([...keys].sort((a, b) => a.localeCompare(b)));
@@ -78,9 +124,10 @@ describe('collectThemingTargets', () => {
78
124
  it('collapses to the override keys, merging the components that share one', async () => {
79
125
  const byKey = targetsByKey(await enumerated);
80
126
  expect(byKey['switch']).toEqual(['size', 'checked', 'disabled']);
81
- // `radio` is documented by both Indicator and RadioList.
127
+ // `radio` is documented by two unrelated owners. Parent/child
128
+ // canonicalization must not collapse a shared target across families.
82
129
  const radio = (await enumerated).filter(t => t.key === 'radio');
83
- expect(radio.length).toBeGreaterThan(1);
130
+ expect(radio.map(t => t.component)).toEqual(['Indicator', 'RadioList']);
84
131
  for (const t of radio) {
85
132
  for (const name of [...t.props, ...t.states]) {
86
133
  expect(byKey['radio']).toContain(name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/cli",
3
- "version": "0.5.2-canary.e4f8e4e",
3
+ "version": "0.5.2-canary.edc0cfc",
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.5.2-canary.e4f8e4e",
91
- "@astryxdesign/core": "0.5.2-canary.e4f8e4e",
92
- "@astryxdesign/lab": "0.5.2-canary.e4f8e4e",
93
- "@astryxdesign/theme-neutral": "0.5.2-canary.e4f8e4e",
90
+ "@astryxdesign/charts": "0.5.2-canary.edc0cfc",
91
+ "@astryxdesign/core": "0.5.2-canary.edc0cfc",
92
+ "@astryxdesign/lab": "0.5.2-canary.edc0cfc",
93
+ "@astryxdesign/theme-neutral": "0.5.2-canary.edc0cfc",
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.5.2-canary.e4f8e4e",
112
- "@astryxdesign/core": "0.5.2-canary.e4f8e4e",
113
- "@astryxdesign/lab": "0.5.2-canary.e4f8e4e",
114
- "@astryxdesign/theme-neutral": "0.5.2-canary.e4f8e4e",
111
+ "@astryxdesign/charts": "0.5.2-canary.edc0cfc",
112
+ "@astryxdesign/core": "0.5.2-canary.edc0cfc",
113
+ "@astryxdesign/lab": "0.5.2-canary.edc0cfc",
114
+ "@astryxdesign/theme-neutral": "0.5.2-canary.edc0cfc",
115
115
  "gpt-tokenizer": "^3.4.0"
116
116
  },
117
117
  "scripts": {