@astryxdesign/cli 0.4.6 → 0.4.7-canary.20bae6c

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 (65) 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/template/template.doc.mjs +2 -2
  7. package/api/theme/build/build.mjs +8 -38
  8. package/api/theme/targets/targets.d.mts +18 -0
  9. package/api/theme/targets/targets.mjs +87 -0
  10. package/api/theme/targets/targets.test.mjs +65 -0
  11. package/api/theme/theme.d.mts +1 -0
  12. package/api/theme/theme.mjs +3 -1
  13. package/api/theme/theme.type.d.mts +23 -0
  14. package/api/theme/theme.type.mjs +21 -1
  15. package/api/theme/themeTargets.doc.d.mts +11 -0
  16. package/api/theme/themeTargets.doc.mjs +58 -0
  17. package/api/theme/themeTemplate.doc.mjs +3 -3
  18. package/assets/docs/README.md +50 -0
  19. package/assets/docs/cli-integrations.doc.mjs +4 -4
  20. package/assets/docs/theme.doc.dense.mjs +1 -1
  21. package/assets/docs/theme.doc.mjs +1 -1
  22. package/assets/docs/typography.doc.mjs +2 -2
  23. package/assets/templates/blocks/components/Dialog/DialogAdaptivePresentation.doc.mjs +25 -0
  24. package/assets/templates/blocks/components/Dialog/DialogAdaptivePresentation.tsx +167 -0
  25. package/assets/templates/blocks/components/Dialog/DialogScrollingContent.tsx +1 -1
  26. package/assets/templates/blocks/components/Step/StepContent.doc.mjs +14 -0
  27. package/assets/templates/blocks/components/Step/StepContent.tsx +32 -0
  28. package/assets/templates/blocks/components/Step/StepIndicator.doc.mjs +14 -0
  29. package/assets/templates/blocks/components/Step/StepIndicator.tsx +60 -0
  30. package/assets/templates/blocks/components/Step/StepShowcase.doc.mjs +15 -0
  31. package/assets/templates/blocks/components/Step/StepShowcase.tsx +26 -0
  32. package/assets/templates/blocks/components/Step/StepStates.doc.mjs +14 -0
  33. package/assets/templates/blocks/components/Step/StepStates.tsx +46 -0
  34. package/assets/templates/blocks/components/Stepper/StepperCustomContent.doc.mjs +22 -0
  35. package/assets/templates/blocks/components/Stepper/StepperCustomContent.tsx +126 -0
  36. package/assets/templates/blocks/components/Stepper/StepperIndicatorModes.doc.mjs +1 -1
  37. package/assets/templates/blocks/components/Stepper/StepperIndicatorModes.tsx +17 -5
  38. package/assets/templates/blocks/components/Stepper/StepperOnTrackHorizontal.doc.mjs +14 -0
  39. package/assets/templates/blocks/components/Stepper/StepperOnTrackHorizontal.tsx +25 -0
  40. package/assets/templates/blocks/components/Stepper/StepperOnTrackVertical.doc.mjs +2 -2
  41. package/assets/templates/blocks/components/Stepper/StepperOnTrackVertical.tsx +1 -1
  42. package/assets/templates/blocks/components/Stepper/StepperShowcase.doc.mjs +1 -1
  43. package/assets/templates/blocks/components/Stepper/StepperShowcase.tsx +6 -7
  44. package/assets/templates/blocks/components/Stepper/StepperStatus.tsx +1 -1
  45. package/assets/templates/pages/mixed-gallery/page.tsx +12 -3
  46. package/assets/templates/themes/neutral/neutralTheme.ts +13 -8
  47. package/clients/cli/commands/build-theme.mjs +85 -0
  48. package/clients/cli/commands/dialog-adaptive-template.test.mjs +24 -0
  49. package/clients/cli/commands/theme-targets.behavior.test.mjs +64 -0
  50. package/clients/cli/commands/theme-targets.doc.mjs +38 -0
  51. package/clients/cli/commands/theme-template.doc.mjs +2 -2
  52. package/clients/cli/commands/theme.doc.mjs +4 -2
  53. package/clients/cli/index.mjs +1 -0
  54. package/clients/cli/lib/manifest.mjs +2 -0
  55. package/foundation/discovery/theming-targets.d.mts +48 -0
  56. package/foundation/discovery/theming-targets.mjs +135 -0
  57. package/foundation/discovery/theming-targets.test.mjs +127 -0
  58. package/foundation/response/response-types.doc.mjs +7 -2
  59. package/package.json +9 -9
  60. package/assets/templates/blocks/components/Stepper/StepperHorizontal.doc.mjs +0 -14
  61. package/assets/templates/blocks/components/Stepper/StepperHorizontal.tsx +0 -24
  62. package/assets/templates/blocks/components/Stepper/StepperMultiStepForm.doc.mjs +0 -14
  63. package/assets/templates/blocks/components/Stepper/StepperMultiStepForm.tsx +0 -92
  64. package/assets/templates/blocks/components/Stepper/StepperVerticalOnboarding.doc.mjs +0 -14
  65. package/assets/templates/blocks/components/Stepper/StepperVerticalOnboarding.tsx +0 -40
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @xds/cli
2
2
 
3
+ # 0.4.7
4
+
5
+ ---
6
+
3
7
  # 0.4.6
4
8
 
5
9
  #### New Features
package/README.md CHANGED
@@ -383,51 +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
- | `upgrade.list` | Every available codemod, oldest→newest, as {name, title, version, optional}; returned for --list without running anything. |
423
- | `upgrade.status` | A short-circuit outcome with no codemods run (up_to_date, no_codemods, or config_fixable), each carrying the agent-docs summary. |
424
- | `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. |
425
- | `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. |
426
- | `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. |
427
- | `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}. |
428
- | `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). |
429
- | `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). |
430
- | `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. |
431
432
 
432
433
  <!-- END GENERATED: response-types -->
433
434
  <!-- Generated by scripts/generate-cli-readme.mjs from the response-types EnumDoc. Run `pnpm -F @astryxdesign/cli readme`. -->
@@ -580,12 +581,13 @@ export default {
580
581
  };
581
582
  ```
582
583
 
583
- | Field | Type | Purpose |
584
- | ------------ | -------- | --------------------------------------------------------------------- |
585
- | `components` | `string` | Directory holding the package's components and their `.doc.*` files. |
586
- | `templates` | `string` | Directory holding the package's page/block templates. |
587
- | `codemods` | `string` | Directory holding upgrade codemods run by `astryx upgrade`. |
588
- | `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. |
589
591
 
590
592
  Every field is optional; declare only the roots the package ships. There is no
591
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:
package/api/index.d.mts CHANGED
@@ -31,6 +31,6 @@ export * from "./doctor/doctor.type.mjs";
31
31
  export * from "./layout/layout.type.mjs";
32
32
  export * from "./integration/validate-integration.type.mjs";
33
33
  export type Logger = import("./logger.mjs").Logger;
34
- export { themeBuild, themeAdd, themeList, listThemes } from "./theme/theme.mjs";
34
+ export { themeBuild, themeAdd, themeList, themeTargets, listThemes } from "./theme/theme.mjs";
35
35
  export { layoutExpand, layoutCheck, layoutGrammar } from "./layout/layout.mjs";
36
36
  export { validateIntegration, summarizeIssues } from "./integration/validate-integration.mjs";
package/api/index.mjs CHANGED
@@ -25,7 +25,7 @@ export {docs} from './docs/docs.mjs';
25
25
  export {blog} from './blog/blog.mjs';
26
26
  export {discover} from './discover/discover.mjs';
27
27
  export {template} from './template/template.mjs';
28
- export {themeBuild, themeAdd, themeList, listThemes} from './theme/theme.mjs';
28
+ export {themeBuild, themeAdd, themeList, themeTargets, listThemes} from './theme/theme.mjs';
29
29
  export {hook} from './hook/hook.mjs';
30
30
  export {search} from './search/search.mjs';
31
31
  export {build} from './build/build.mjs';
@@ -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: [
@@ -43,6 +43,10 @@ import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
43
43
  import {AstryxError} from '../../error.mjs';
44
44
  import {logger} from '../../logger.mjs';
45
45
  import {loadComponentDoc} from '../../../foundation/discovery/component-loader.mjs';
46
+ import {
47
+ collectThemingTargets,
48
+ targetsByKey,
49
+ } from '../../../foundation/discovery/theming-targets.mjs';
46
50
  import {
47
51
  collectUnloadedFonts,
48
52
  formatFontLoadingHelp,
@@ -882,6 +886,9 @@ ${iconType}export declare const ${toIdentifier(themeDef.name)}Theme: DefinedThem
882
886
  * Returns null when docs are unavailable so validation can skip unknown-key
883
887
  * warnings rather than guessing from a second registry.
884
888
  *
889
+ * Shares its enumeration with `theme targets`, so what a theme author can list
890
+ * is exactly what this validator accepts.
891
+ *
885
892
  * @returns {Promise<Record<string, string[]> | null>}
886
893
  */
887
894
  async function loadKnownComponents() {
@@ -889,44 +896,7 @@ async function loadKnownComponents() {
889
896
  const coreSrc = coreRoot ? path.join(coreRoot, 'src') : null;
890
897
  if (!coreSrc || !fs.existsSync(coreSrc)) return null;
891
898
 
892
- /** @type {Record<string, string[]>} */
893
- const targets = {};
894
-
895
- /** @param {string} dir */
896
- async function scan(dir) {
897
- const entries = fs.readdirSync(dir, {withFileTypes: true});
898
- for (const entry of entries) {
899
- const full = path.join(dir, entry.name);
900
- if (entry.isDirectory()) {
901
- if (entry.name === 'node_modules' || entry.name === '__tests__') continue;
902
- await scan(full);
903
- continue;
904
- }
905
- if (!entry.name.endsWith('.doc.mjs')) continue;
906
-
907
- /** @type {any} */
908
- let doc;
909
- try {
910
- doc = await loadComponentDoc(full);
911
- } catch {
912
- continue;
913
- }
914
-
915
- for (const target of doc?.theming?.targets || []) {
916
- const className = target?.className;
917
- if (typeof className !== 'string') continue;
918
- const key = className.replace(/^astryx-/, '');
919
- if (!key) continue;
920
- const props = [target.visualProps, target.states]
921
- .filter(list => Array.isArray(list))
922
- .flat()
923
- .filter((/** @type {unknown} */ p) => typeof p === 'string');
924
- targets[key] = [...new Set([...(targets[key] || []), ...props])];
925
- }
926
- }
927
- }
928
-
929
- await scan(coreSrc);
899
+ const targets = targetsByKey(await collectThemingTargets(coreSrc));
930
900
  return Object.keys(targets).length > 0 ? targets : null;
931
901
  }
932
902
 
@@ -0,0 +1,18 @@
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
+ * List every component theming target — the `defineTheme` `components` keys,
6
+ * with the props and states each one accepts.
7
+ *
8
+ * A filter naming a component exactly wins over a substring search, so
9
+ * `theme targets Button` is Button's own set (what `astryx component Button`
10
+ * prints) rather than every key that happens to contain "button".
11
+ *
12
+ * @param {string} [filter] - component name, or a substring of a target key
13
+ * @param {{cwd?: string}} [ctx]
14
+ * @returns {Promise<import('../theme.type.mjs').ThemeTargetsResponse>}
15
+ */
16
+ export function themeTargets(filter?: string, { cwd }?: {
17
+ cwd?: string;
18
+ }): Promise<import("../theme.type.mjs").ThemeTargetsResponse>;
@@ -0,0 +1,87 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file `astryx theme targets` leaf — the whole themeable surface, enumerated.
5
+ *
6
+ * @input a cwd (to resolve the project's `@astryxdesign/core`) and an optional
7
+ * component/key filter
8
+ * @output the `theme.targets` envelope: one row per theming target
9
+ * @position api/theme/targets — projection over
10
+ * foundation/discovery/theming-targets.mjs, the same component docs
11
+ * `astryx component <Name>` prints its Theming table from.
12
+ */
13
+
14
+ import * as path from 'node:path';
15
+ import {findCoreDir} from '../../../foundation/fs/paths.mjs';
16
+ import {collectThemingTargets} from '../../../foundation/discovery/theming-targets.mjs';
17
+ import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
18
+ import {AstryxError} from '../../error.mjs';
19
+
20
+ /**
21
+ * Whether a target matches the caller's filter loosely: any target whose key,
22
+ * class, or component contains it — so `theme targets thumb` finds the switch
23
+ * thumb without knowing which component owns it.
24
+ * @param {import('../../../foundation/discovery/theming-targets.mjs').ThemingTarget} target
25
+ * @param {string} filter - already lowercased
26
+ * @returns {boolean}
27
+ */
28
+ function matchesLoosely(target, filter) {
29
+ return (
30
+ target.key.toLowerCase().includes(filter) ||
31
+ target.className.toLowerCase().includes(filter) ||
32
+ target.component.toLowerCase().includes(filter)
33
+ );
34
+ }
35
+
36
+ /**
37
+ * List every component theming target — the `defineTheme` `components` keys,
38
+ * with the props and states each one accepts.
39
+ *
40
+ * A filter naming a component exactly wins over a substring search, so
41
+ * `theme targets Button` is Button's own set (what `astryx component Button`
42
+ * prints) rather than every key that happens to contain "button".
43
+ *
44
+ * @param {string} [filter] - component name, or a substring of a target key
45
+ * @param {{cwd?: string}} [ctx]
46
+ * @returns {Promise<import('../theme.type.mjs').ThemeTargetsResponse>}
47
+ */
48
+ export async function themeTargets(filter, {cwd = process.cwd()} = {}) {
49
+ const coreDir = findCoreDir(cwd);
50
+ if (!coreDir) {
51
+ throw new AstryxError(
52
+ 'Could not find @astryxdesign/core package',
53
+ undefined,
54
+ ERROR_CODES.ERR_CORE_NOT_FOUND,
55
+ );
56
+ }
57
+
58
+ const all = await collectThemingTargets(path.join(coreDir, 'src'));
59
+ const needle = filter ? String(filter).toLowerCase() : null;
60
+ let targets = all;
61
+ if (needle) {
62
+ const named = all.filter(t => t.component.toLowerCase() === needle);
63
+ targets = named.length > 0 ? named : all.filter(t => matchesLoosely(t, needle));
64
+ }
65
+
66
+ if (needle && targets.length === 0) {
67
+ const near = [...new Set(all.map(t => t.component))]
68
+ .filter(name => name.toLowerCase().startsWith(needle.slice(0, 3)))
69
+ .sort()
70
+ .slice(0, 5)
71
+ .map(name => ({name, reason: 'has theming targets'}));
72
+ throw new AstryxError(
73
+ `No theming target matches "${filter}". Run \`theme targets\` with no filter for the whole list.`,
74
+ near.length > 0 ? near : undefined,
75
+ ERROR_CODES.ERR_UNKNOWN_COMPONENT,
76
+ );
77
+ }
78
+
79
+ return {
80
+ type: 'theme.targets',
81
+ data: {
82
+ filter: filter ?? null,
83
+ componentCount: new Set(targets.map(t => t.component)).size,
84
+ targets,
85
+ },
86
+ };
87
+ }
@@ -0,0 +1,65 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Direct-API tests for the `theme targets` leaf. Runs against the real
5
+ * core docs, so it doubles as a guard that the themeable surface stays
6
+ * readable and shaped as `theme.targets` promises.
7
+ */
8
+
9
+ import {describe, it, expect} from 'vitest';
10
+ import {themeTargets} from './targets.mjs';
11
+
12
+ describe('themeTargets (api/theme/targets)', () => {
13
+ it('returns a theme.targets envelope covering the whole surface', async () => {
14
+ const result = await themeTargets();
15
+ expect(result.type).toBe('theme.targets');
16
+ expect(result.data.filter).toBeNull();
17
+ expect(result.data.targets.length).toBeGreaterThan(100);
18
+ expect(result.data.componentCount).toBeGreaterThan(50);
19
+ for (const t of result.data.targets) {
20
+ expect(Object.keys(t).sort()).toEqual([
21
+ 'className',
22
+ 'component',
23
+ 'key',
24
+ 'props',
25
+ 'states',
26
+ ]);
27
+ }
28
+ }, 60_000);
29
+
30
+ it('scopes to one component by name', async () => {
31
+ const {data} = await themeTargets('Switch');
32
+ expect(data.filter).toBe('Switch');
33
+ expect(data.componentCount).toBe(1);
34
+ expect(data.targets.map(t => t.key)).toEqual([
35
+ 'switch',
36
+ 'switch-field',
37
+ 'switch-thumb',
38
+ ]);
39
+ }, 60_000);
40
+
41
+ // Half the system's keys contain "button" (chat-send-button, toggle-button,
42
+ // …). A component name has to mean the component, or `theme targets Button`
43
+ // answers a different question than `component Button` and the two views
44
+ // look like they disagree.
45
+ it('prefers an exact component name over a substring match', async () => {
46
+ const {data} = await themeTargets('Button');
47
+ expect(data.targets.map(t => t.key)).toEqual(['button']);
48
+ }, 60_000);
49
+
50
+ // This command answers "which theme slot paints the switch thumb?" — a
51
+ // question you can only ask by the part, not the component, until you
52
+ // already know which component owns it.
53
+ it('searches keys by substring, across components', async () => {
54
+ const {data} = await themeTargets('thumb');
55
+ expect(data.componentCount).toBeGreaterThan(1);
56
+ expect(data.targets.map(t => t.key)).toContain('switch-thumb');
57
+ for (const t of data.targets) expect(t.key).toContain('thumb');
58
+ }, 60_000);
59
+
60
+ it('rejects a filter that matches nothing, with components to try', async () => {
61
+ await expect(themeTargets('nosuchthing')).rejects.toMatchObject({
62
+ code: 'ERR_UNKNOWN_COMPONENT',
63
+ });
64
+ }, 60_000);
65
+ });
@@ -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
+ };