@astryxdesign/cli 0.4.7-canary.5d351e4 → 0.4.7-canary.63f398d
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/api/index.d.mts +1 -1
- package/api/index.mjs +1 -1
- package/api/theme/build/build.mjs +8 -38
- package/api/theme/targets/targets.d.mts +18 -0
- package/api/theme/targets/targets.mjs +87 -0
- package/api/theme/targets/targets.test.mjs +65 -0
- package/api/theme/theme.d.mts +1 -0
- package/api/theme/theme.mjs +3 -1
- package/api/theme/theme.type.d.mts +23 -0
- package/api/theme/theme.type.mjs +21 -1
- package/api/theme/themeTargets.doc.d.mts +11 -0
- package/api/theme/themeTargets.doc.mjs +58 -0
- package/assets/docs/README.md +50 -0
- package/assets/docs/theme.doc.dense.mjs +1 -1
- package/assets/docs/theme.doc.mjs +1 -1
- package/assets/templates/themes/neutral/neutralTheme.ts +13 -8
- package/clients/cli/commands/build-theme.mjs +85 -0
- package/clients/cli/commands/theme-targets.behavior.test.mjs +64 -0
- package/clients/cli/commands/theme-targets.doc.mjs +38 -0
- package/clients/cli/commands/theme.doc.mjs +4 -2
- package/clients/cli/index.mjs +1 -0
- package/clients/cli/lib/manifest.mjs +2 -0
- package/foundation/discovery/theming-targets.d.mts +48 -0
- package/foundation/discovery/theming-targets.mjs +135 -0
- package/foundation/discovery/theming-targets.test.mjs +127 -0
- package/foundation/response/response-types.doc.mjs +5 -0
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -419,6 +419,7 @@ Every response has a `type` discriminant. The full set is below (generated from
|
|
|
419
419
|
| `theme.list` | Every bundled theme as a ThemeListEntry[]: each with slug, displayName, description, and a maintained flag. |
|
|
420
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
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. |
|
|
422
423
|
| `upgrade.list` | Every available codemod, oldest→newest, as {name, title, version, optional}; returned for --list without running anything. |
|
|
423
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. |
|
|
424
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. |
|
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';
|
|
@@ -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
|
-
|
|
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
|
+
});
|
package/api/theme/theme.d.mts
CHANGED
|
@@ -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";
|
package/api/theme/theme.mjs
CHANGED
|
@@ -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
|
|
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
|
+
};
|
package/api/theme/theme.type.mjs
CHANGED
|
@@ -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
|
+
};
|
|
@@ -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.
|
|
@@ -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
|
|
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
|
|
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',
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
* icon = T30 / T80
|
|
25
25
|
* text = T30 / T80
|
|
26
26
|
*
|
|
27
|
-
* All 9 saturated badge values pass WCAG AA (5
|
|
27
|
+
* All 9 saturated badge values pass WCAG AA against their label (>= 4.5:1);
|
|
28
|
+
* `scripts/check-badge-contrast.test.mjs` holds every theme to that.
|
|
28
29
|
*
|
|
29
30
|
* Only overrides tokens that differ from the defaults.
|
|
30
31
|
*/
|
|
@@ -432,10 +433,14 @@ export const neutralTheme = defineTheme({
|
|
|
432
433
|
color: '#171717',
|
|
433
434
|
},
|
|
434
435
|
'variant:error': {
|
|
435
|
-
// Light: T55 #e33f4a
|
|
436
|
+
// Light: T58 #c9303a. The T55 stop #e33f4a pairs with white at only
|
|
437
|
+
// 4.14:1 — the label is 12px/500, so AA wants 4.5, not the 3:1
|
|
438
|
+
// large-text allowance. One tonal step down holds the hue
|
|
439
|
+
// (OKLCH H 21.9 -> 22.8, C 0.200 -> 0.189) and reaches 5.29:1.
|
|
436
440
|
// Dark : T60 stop from dark-mode tonal palette of Tailwind red-600
|
|
437
|
-
// source #dc2626 (kept on H=27 alarm-red rather than coral)
|
|
438
|
-
|
|
441
|
+
// source #dc2626 (kept on H=27 alarm-red rather than coral).
|
|
442
|
+
// Dark text on it is 6.60:1 and unchanged.
|
|
443
|
+
backgroundColor: 'light-dark(#c9303a, #ff705d)',
|
|
439
444
|
color: 'light-dark(#ffffff, #171717)',
|
|
440
445
|
},
|
|
441
446
|
|
|
@@ -500,7 +505,7 @@ export const neutralTheme = defineTheme({
|
|
|
500
505
|
//
|
|
501
506
|
// success → badge success bg (green T45 / dark-ramp T60)
|
|
502
507
|
// warning → badge warning bg (yellow T85, same hex both modes)
|
|
503
|
-
// error → badge error bg (red
|
|
508
|
+
// error → badge error bg (red T58 / dark-ramp T60)
|
|
504
509
|
// accent → badge info bg (blue T50 / dark-ramp T60) — the
|
|
505
510
|
// StatusDot "accent" is the info/attention color, so it
|
|
506
511
|
// pairs with the info badge rather than --color-accent
|
|
@@ -515,7 +520,7 @@ export const neutralTheme = defineTheme({
|
|
|
515
520
|
statusdot: {
|
|
516
521
|
'variant:success': {backgroundColor: 'light-dark(#198100, #64af4c)'},
|
|
517
522
|
'variant:warning': {backgroundColor: '#ffce2f'},
|
|
518
|
-
'variant:error': {backgroundColor: 'light-dark(#
|
|
523
|
+
'variant:error': {backgroundColor: 'light-dark(#c9303a, #ff705d)'},
|
|
519
524
|
'variant:accent': {backgroundColor: 'light-dark(#0074e2, #6d9cfe)'},
|
|
520
525
|
},
|
|
521
526
|
|
|
@@ -611,8 +616,8 @@ export const neutralTheme = defineTheme({
|
|
|
611
616
|
'--color-warning': '#ffce2f',
|
|
612
617
|
},
|
|
613
618
|
'variant:error': {
|
|
614
|
-
// Red
|
|
615
|
-
'--color-error': '#
|
|
619
|
+
// Red T58 saturated stop (= variant:error badge bg)
|
|
620
|
+
'--color-error': '#c9303a',
|
|
616
621
|
},
|
|
617
622
|
},
|
|
618
623
|
|
|
@@ -38,6 +38,7 @@ import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
|
|
|
38
38
|
import {themeAdd} from '../../../api/theme/add/add.mjs';
|
|
39
39
|
import {themeTemplate} from '../../../api/theme/template/template.mjs';
|
|
40
40
|
import {themeList} from '../../../api/theme/list/list.mjs';
|
|
41
|
+
import {themeTargets} from '../../../api/theme/targets/targets.mjs';
|
|
41
42
|
import {themeBuild, importSpecifier} from '../../../api/theme/build/build.mjs';
|
|
42
43
|
import {defineCommand} from '../lib/define-command.mjs';
|
|
43
44
|
import {doc as themeGroup} from './theme.doc.mjs';
|
|
@@ -45,10 +46,12 @@ import {doc as themeBuildCommand} from './theme-build.doc.mjs';
|
|
|
45
46
|
import {doc as themeListCommand} from './theme-list.doc.mjs';
|
|
46
47
|
import {doc as themeAddCommand} from './theme-add.doc.mjs';
|
|
47
48
|
import {doc as themeTemplateCommand} from './theme-template.doc.mjs';
|
|
49
|
+
import {doc as themeTargetsCommand} from './theme-targets.doc.mjs';
|
|
48
50
|
import {doc as themeBuildFn} from '../../../api/theme/themeBuild.doc.mjs';
|
|
49
51
|
import {doc as themeListFn} from '../../../api/theme/themeList.doc.mjs';
|
|
50
52
|
import {doc as themeAddFn} from '../../../api/theme/themeAdd.doc.mjs';
|
|
51
53
|
import {doc as themeTemplateFn} from '../../../api/theme/themeTemplate.doc.mjs';
|
|
54
|
+
import {doc as themeTargetsFn} from '../../../api/theme/themeTargets.doc.mjs';
|
|
52
55
|
|
|
53
56
|
/**
|
|
54
57
|
* Path to this CLI's real entry (clients/cli/bin/astryx.mjs), resolved from
|
|
@@ -194,6 +197,36 @@ function printThemeList(themes) {
|
|
|
194
197
|
);
|
|
195
198
|
}
|
|
196
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Render the targets as one greppable line each, under an aligned header. A
|
|
202
|
+
* `records()` block would be five lines per target — over a thousand for the
|
|
203
|
+
* full surface, which is the view this command exists to make readable.
|
|
204
|
+
* @param {import('../../../api/theme/theme.type.mjs').ThemeTargetEntry[]} targets
|
|
205
|
+
* @returns {string}
|
|
206
|
+
*/
|
|
207
|
+
function formatTargetsTable(targets) {
|
|
208
|
+
const rows = targets.map(t => ({
|
|
209
|
+
key: t.key,
|
|
210
|
+
component: t.component,
|
|
211
|
+
props: t.props.join(', ') || '-',
|
|
212
|
+
states: t.states.join(', ') || '-',
|
|
213
|
+
}));
|
|
214
|
+
const head = {key: 'key', component: 'component', props: 'props', states: 'states'};
|
|
215
|
+
const width = (/** @type {'key'|'component'|'props'} */ field) =>
|
|
216
|
+
[head, ...rows].reduce((max, r) => Math.max(max, r[field].length), 0);
|
|
217
|
+
const w = {key: width('key'), component: width('component'), props: width('props')};
|
|
218
|
+
const line = (/** @type {typeof head} */ r) =>
|
|
219
|
+
[
|
|
220
|
+
r.key.padEnd(w.key),
|
|
221
|
+
r.component.padEnd(w.component),
|
|
222
|
+
r.props.padEnd(w.props),
|
|
223
|
+
r.states,
|
|
224
|
+
]
|
|
225
|
+
.join(' ')
|
|
226
|
+
.trimEnd();
|
|
227
|
+
return [line(head), ...rows.map(line)].join('\n');
|
|
228
|
+
}
|
|
229
|
+
|
|
197
230
|
/**
|
|
198
231
|
* @param {import('commander').Command} program
|
|
199
232
|
*/
|
|
@@ -225,6 +258,18 @@ export function registerTheme(program) {
|
|
|
225
258
|
},
|
|
226
259
|
});
|
|
227
260
|
|
|
261
|
+
// Theming questions are asked at `theme`, but per-component overrides live
|
|
262
|
+
// under `component`. Without this pointer the group reads as a build-tool
|
|
263
|
+
// menu, and the component targets are unreachable from the noun the user
|
|
264
|
+
// started at.
|
|
265
|
+
theme.addHelpText(
|
|
266
|
+
'after',
|
|
267
|
+
`\nComponent style overrides:\n` +
|
|
268
|
+
` ${getCliInvocation()} theme targets Every themeable class, with its props and states\n` +
|
|
269
|
+
` ${getCliInvocation()} component <Name> One component's theming table\n` +
|
|
270
|
+
` ${getCliInvocation()} docs theme How component overrides work\n`,
|
|
271
|
+
);
|
|
272
|
+
|
|
228
273
|
defineCommand(theme, themeBuildCommand, {
|
|
229
274
|
fn: themeBuildFn,
|
|
230
275
|
action: async (
|
|
@@ -502,4 +547,44 @@ export function registerTheme(program) {
|
|
|
502
547
|
);
|
|
503
548
|
},
|
|
504
549
|
});
|
|
550
|
+
|
|
551
|
+
defineCommand(theme, themeTargetsCommand, {
|
|
552
|
+
fn: themeTargetsFn,
|
|
553
|
+
action: async (/** @type {string | undefined} */ filter) => {
|
|
554
|
+
const json = program.opts().json || false;
|
|
555
|
+
|
|
556
|
+
/** @type {import('../../../api/theme/theme.type.mjs').ThemeTargetsResponse} */
|
|
557
|
+
let result;
|
|
558
|
+
try {
|
|
559
|
+
result = await themeTargets(filter, {cwd: process.cwd()});
|
|
560
|
+
} catch (e) {
|
|
561
|
+
const err =
|
|
562
|
+
/** @type {import('../../../api/error.mjs').AstryxError} */ (e);
|
|
563
|
+
cliError(err.message, {
|
|
564
|
+
suggestions: err.suggestions || [],
|
|
565
|
+
code: err.code,
|
|
566
|
+
});
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (json) return jsonOut(result);
|
|
571
|
+
|
|
572
|
+
const run = getCliInvocation();
|
|
573
|
+
const {targets, componentCount} = result.data;
|
|
574
|
+
emit(
|
|
575
|
+
section(
|
|
576
|
+
'Theming targets',
|
|
577
|
+
`${targets.length} across ${componentCount} component${componentCount === 1 ? '' : 's'}`,
|
|
578
|
+
),
|
|
579
|
+
text(formatTargetsTable(targets)),
|
|
580
|
+
text(
|
|
581
|
+
[
|
|
582
|
+
`Each key goes under \`components\` in defineTheme; it paints \`.astryx-<key>\`.`,
|
|
583
|
+
`Props take a value (\`variant:secondary\`); states are written bare (\`checked\`).`,
|
|
584
|
+
`One component in full: ${run} component <Name>`,
|
|
585
|
+
].join('\n'),
|
|
586
|
+
),
|
|
587
|
+
);
|
|
588
|
+
},
|
|
589
|
+
});
|
|
505
590
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file CLI behavior for `astryx theme targets`.
|
|
5
|
+
*
|
|
6
|
+
* The API leaf is covered by api/theme/targets/targets.test.mjs; what is only
|
|
7
|
+
* reachable here is the terminal binding — the table a human reads, the JSON
|
|
8
|
+
* envelope a lint script reads, and the route into component overrides from
|
|
9
|
+
* `theme --help`, which is where the question gets asked.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {describe, it, expect} from 'vitest';
|
|
13
|
+
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
14
|
+
|
|
15
|
+
describe('astryx theme targets', () => {
|
|
16
|
+
it('prints one greppable line per target, with props and states', async () => {
|
|
17
|
+
const {status, stdout} = await runCli(['theme', 'targets', 'Switch']);
|
|
18
|
+
|
|
19
|
+
expect(status).toBe(0);
|
|
20
|
+
expect(stdout).toMatch(/^switch\s+Switch\s+size\s+checked, disabled$/m);
|
|
21
|
+
expect(stdout).toMatch(/^switch-thumb\s+Switch\s+size\s+checked$/m);
|
|
22
|
+
expect(stdout).toMatch(/3 across 1 component/);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('lists the whole surface when unfiltered', async () => {
|
|
26
|
+
const {status, stdout} = await runCli(['theme', 'targets']);
|
|
27
|
+
|
|
28
|
+
expect(status).toBe(0);
|
|
29
|
+
const rows = stdout.split('\n').filter(l => /^[a-z][a-z0-9-]*\s{2,}/.test(l));
|
|
30
|
+
expect(rows.length).toBeGreaterThan(100);
|
|
31
|
+
expect(stdout).toMatch(/^button\s/m);
|
|
32
|
+
expect(stdout).toMatch(/^switch-thumb\s/m);
|
|
33
|
+
}, 30_000);
|
|
34
|
+
|
|
35
|
+
it('returns a theme.targets envelope under --json', async () => {
|
|
36
|
+
const {status, stdout} = await runCli(['--json', 'theme', 'targets', 'Switch']);
|
|
37
|
+
|
|
38
|
+
expect(status).toBe(0);
|
|
39
|
+
const payload = JSON.parse(stdout);
|
|
40
|
+
expect(payload.type).toBe('theme.targets');
|
|
41
|
+
expect(payload.data.targets).toContainEqual({
|
|
42
|
+
key: 'switch-thumb',
|
|
43
|
+
className: 'astryx-switch-thumb',
|
|
44
|
+
component: 'Switch',
|
|
45
|
+
props: ['size'],
|
|
46
|
+
states: ['checked'],
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('fails a filter that matches nothing', async () => {
|
|
51
|
+
const {status, stderr} = await runCli(['theme', 'targets', 'nosuchthing']);
|
|
52
|
+
|
|
53
|
+
expect(status).toBe(1);
|
|
54
|
+
expect(stderr).toMatch(/No theming target matches "nosuchthing"/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('routes a theming question from `theme --help` to component overrides', async () => {
|
|
58
|
+
const {stdout} = await runCli(['theme', '--help']);
|
|
59
|
+
|
|
60
|
+
expect(stdout).toMatch(/Component style overrides:/);
|
|
61
|
+
expect(stdout).toMatch(/theme targets/);
|
|
62
|
+
expect(stdout).toMatch(/component <Name>/);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file CommandDoc for `astryx theme targets`. The terminal binding of the
|
|
5
|
+
* `themeTargets()` function (referenced via `fn`); its args map to that
|
|
6
|
+
* function's params so a converter can build Commander config + --help from one
|
|
7
|
+
* source of truth.
|
|
8
|
+
* @position packages/cli/clients/cli/commands — command documentation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** @type {import('@astryxdesign/cli/authoring').CommandDoc} */
|
|
12
|
+
export const doc = {
|
|
13
|
+
type: 'command',
|
|
14
|
+
name: 'theme targets',
|
|
15
|
+
displayName: 'astryx theme targets',
|
|
16
|
+
namespace: 'cli',
|
|
17
|
+
summary: 'List the component theming targets a theme can override',
|
|
18
|
+
description:
|
|
19
|
+
'Prints every `defineTheme` components key across the system: the stable class it paints, ' +
|
|
20
|
+
'the component that declares it, and the props and states that are legal override keys ' +
|
|
21
|
+
'under it. This is the whole themeable surface in one command — what auditing a theme, or ' +
|
|
22
|
+
'answering "which key paints this pixel?", used to need one `astryx component <Name>` per ' +
|
|
23
|
+
'component to assemble. Pass a component name to scope it; pass any substring to search ' +
|
|
24
|
+
'keys. `--json` for a list a repo can lint its own theme against.',
|
|
25
|
+
fn: 'themeTargets',
|
|
26
|
+
args: [{name: 'filter', param: 'filter', required: false}],
|
|
27
|
+
examples: [
|
|
28
|
+
{label: 'The whole themeable surface', cli: 'astryx theme targets'},
|
|
29
|
+
{label: "One component's targets", cli: 'astryx theme targets Switch'},
|
|
30
|
+
{label: 'Search keys', cli: 'astryx theme targets thumb'},
|
|
31
|
+
{label: 'For a lint or an audit script', cli: 'astryx --json theme targets'},
|
|
32
|
+
],
|
|
33
|
+
exitCodes: [
|
|
34
|
+
{code: 0, when: 'success'},
|
|
35
|
+
{code: 1, when: 'a filter matches no target, or core cannot be resolved'},
|
|
36
|
+
],
|
|
37
|
+
related: ['component', 'theme build', 'theme template'],
|
|
38
|
+
};
|
|
@@ -17,11 +17,13 @@ export const doc = {
|
|
|
17
17
|
description:
|
|
18
18
|
'The theme command group. Running astryx theme with no subcommand prints the ' +
|
|
19
19
|
'subcommand list; the work happens in the subcommands: compile a theme (build), ' +
|
|
20
|
-
'scaffold one into your project (add), start a custom one from the annotated template (template),
|
|
21
|
-
|
|
20
|
+
'scaffold one into your project (add), start a custom one from the annotated template (template), ' +
|
|
21
|
+
'list the bundled themes (list), or list the component theming targets a theme can override (targets).',
|
|
22
|
+
subcommands: ['build', 'add', 'list', 'template', 'targets'],
|
|
22
23
|
examples: [
|
|
23
24
|
{label: 'List bundled themes', cli: 'astryx theme list'},
|
|
24
25
|
{label: 'Scaffold a theme', cli: 'astryx theme add matcha'},
|
|
26
|
+
{label: 'See what a theme can override', cli: 'astryx theme targets'},
|
|
25
27
|
],
|
|
26
28
|
exitCodes: [
|
|
27
29
|
{code: 0, when: 'success (help shown, or a subcommand succeeded)'},
|
package/clients/cli/index.mjs
CHANGED
|
@@ -77,6 +77,7 @@ export const RESPONSE_TYPES = {
|
|
|
77
77
|
'theme list': ['theme.list'],
|
|
78
78
|
'theme add': ['theme.list', 'theme.add'],
|
|
79
79
|
'theme template': ['theme.template'],
|
|
80
|
+
'theme targets': ['theme.targets'],
|
|
80
81
|
upgrade: ['upgrade.list', 'upgrade.status', 'upgrade.run'],
|
|
81
82
|
manifest: ['manifest'],
|
|
82
83
|
doctor: ['doctor'],
|
|
@@ -116,6 +117,7 @@ const EXAMPLES = {
|
|
|
116
117
|
'astryx theme add matcha ./src/themes/matcha',
|
|
117
118
|
],
|
|
118
119
|
'theme template': ['astryx theme template', 'astryx theme template --json'],
|
|
120
|
+
'theme targets': ['astryx theme targets Switch', 'astryx --json theme targets'],
|
|
119
121
|
upgrade: ['astryx upgrade --json'],
|
|
120
122
|
manifest: ['astryx manifest --json', 'astryx --json'],
|
|
121
123
|
doctor: ['astryx doctor', 'astryx doctor --json'],
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
/**
|
|
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.
|
|
8
|
+
*
|
|
9
|
+
* Unreadable docs are skipped rather than fatal — a single malformed doc must
|
|
10
|
+
* not take out theme validation or the listing.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} coreSrc - absolute path to `<core>/src`
|
|
13
|
+
* @returns {Promise<ThemingTarget[]>}
|
|
14
|
+
*/
|
|
15
|
+
export function collectThemingTargets(coreSrc: string): Promise<ThemingTarget[]>;
|
|
16
|
+
/**
|
|
17
|
+
* Collapse the enumeration into the `{key: [props and states]}` map theme
|
|
18
|
+
* validation checks override keys against — both are legal override keys, so
|
|
19
|
+
* they share one list.
|
|
20
|
+
* @param {ThemingTarget[]} targets
|
|
21
|
+
* @returns {Record<string, string[]>}
|
|
22
|
+
*/
|
|
23
|
+
export function targetsByKey(targets: ThemingTarget[]): Record<string, string[]>;
|
|
24
|
+
/**
|
|
25
|
+
* One theming target, as a theme author has to write it.
|
|
26
|
+
*/
|
|
27
|
+
export type ThemingTarget = {
|
|
28
|
+
/**
|
|
29
|
+
* - the `defineTheme` `components` key (class minus the `astryx-` prefix)
|
|
30
|
+
*/
|
|
31
|
+
key: string;
|
|
32
|
+
/**
|
|
33
|
+
* - the stable class the component renders
|
|
34
|
+
*/
|
|
35
|
+
className: string;
|
|
36
|
+
/**
|
|
37
|
+
* - the component whose doc declares it
|
|
38
|
+
*/
|
|
39
|
+
component: string;
|
|
40
|
+
/**
|
|
41
|
+
* - visual props the target reflects (`variant:value` keys)
|
|
42
|
+
*/
|
|
43
|
+
props: string[];
|
|
44
|
+
/**
|
|
45
|
+
* - runtime states the target reflects (bare-name keys)
|
|
46
|
+
*/
|
|
47
|
+
states: string[];
|
|
48
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file The one enumeration of component theming targets.
|
|
5
|
+
*
|
|
6
|
+
* @input a core `src` directory
|
|
7
|
+
* @output every `theming.targets` entry authored in a component `.doc.mjs`,
|
|
8
|
+
* flattened into the `defineTheme` component key a theme author writes
|
|
9
|
+
* @position packages/cli/foundation/discovery — shared by `theme targets` (the
|
|
10
|
+
* listing) and `theme build` (override validation). Both read the
|
|
11
|
+
* component docs, which are the source of truth `astryx component
|
|
12
|
+
* <Name>` prints; nothing here is a second registry, so the list a
|
|
13
|
+
* theme author can enumerate and the set the compiler accepts cannot
|
|
14
|
+
* drift from the components or from each other.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as fs from 'node:fs';
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
import {loadComponentDoc} from './component-loader.mjs';
|
|
20
|
+
|
|
21
|
+
const SKIP_DIRS = new Set(['node_modules', '__tests__']);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* One theming target, as a theme author has to write it.
|
|
25
|
+
* @typedef {object} ThemingTarget
|
|
26
|
+
* @property {string} key - the `defineTheme` `components` key (class minus the `astryx-` prefix)
|
|
27
|
+
* @property {string} className - the stable class the component renders
|
|
28
|
+
* @property {string} component - the component whose doc declares it
|
|
29
|
+
* @property {string[]} props - visual props the target reflects (`variant:value` keys)
|
|
30
|
+
* @property {string[]} states - runtime states the target reflects (bare-name keys)
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Strip the namespace prefix to get the `defineTheme` key for a class name.
|
|
35
|
+
*
|
|
36
|
+
* Keep the `astryx-` literal in sync with packages/core/src/naming.ts
|
|
37
|
+
* (NAMESPACE / classPrefix), the same way component-format.mjs does.
|
|
38
|
+
* <!-- SYNC: packages/core/src/naming.ts (namespace prefix source of truth) -->
|
|
39
|
+
* @param {string} className
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
function targetKey(className) {
|
|
43
|
+
return className.replace(/^astryx-/, '');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
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.
|
|
50
|
+
*
|
|
51
|
+
* Unreadable docs are skipped rather than fatal — a single malformed doc must
|
|
52
|
+
* not take out theme validation or the listing.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} coreSrc - absolute path to `<core>/src`
|
|
55
|
+
* @returns {Promise<ThemingTarget[]>}
|
|
56
|
+
*/
|
|
57
|
+
export async function collectThemingTargets(coreSrc) {
|
|
58
|
+
if (!coreSrc || !fs.existsSync(coreSrc)) return [];
|
|
59
|
+
|
|
60
|
+
/** @type {ThemingTarget[]} */
|
|
61
|
+
const targets = [];
|
|
62
|
+
|
|
63
|
+
/** @param {string} dir */
|
|
64
|
+
async function scan(dir) {
|
|
65
|
+
for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
|
|
66
|
+
const full = path.join(dir, entry.name);
|
|
67
|
+
if (entry.isDirectory()) {
|
|
68
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
69
|
+
await scan(full);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (!entry.name.endsWith('.doc.mjs')) continue;
|
|
73
|
+
|
|
74
|
+
/** @type {any} */
|
|
75
|
+
let doc;
|
|
76
|
+
try {
|
|
77
|
+
doc = await loadComponentDoc(full);
|
|
78
|
+
} catch {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const component =
|
|
83
|
+
typeof doc?.name === 'string' && doc.name
|
|
84
|
+
? doc.name
|
|
85
|
+
: path.basename(path.dirname(full));
|
|
86
|
+
|
|
87
|
+
for (const target of doc?.theming?.targets || []) {
|
|
88
|
+
const className = target?.className;
|
|
89
|
+
if (typeof className !== 'string') continue;
|
|
90
|
+
const key = targetKey(className);
|
|
91
|
+
if (!key) continue;
|
|
92
|
+
targets.push({
|
|
93
|
+
key,
|
|
94
|
+
className,
|
|
95
|
+
component,
|
|
96
|
+
props: stringList(target.visualProps),
|
|
97
|
+
states: stringList(target.states),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await scan(coreSrc);
|
|
104
|
+
|
|
105
|
+
targets.sort(
|
|
106
|
+
(a, b) => a.key.localeCompare(b.key) || a.component.localeCompare(b.component),
|
|
107
|
+
);
|
|
108
|
+
return targets;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Collapse the enumeration into the `{key: [props and states]}` map theme
|
|
113
|
+
* validation checks override keys against — both are legal override keys, so
|
|
114
|
+
* they share one list.
|
|
115
|
+
* @param {ThemingTarget[]} targets
|
|
116
|
+
* @returns {Record<string, string[]>}
|
|
117
|
+
*/
|
|
118
|
+
export function targetsByKey(targets) {
|
|
119
|
+
/** @type {Record<string, string[]>} */
|
|
120
|
+
const byKey = {};
|
|
121
|
+
for (const t of targets) {
|
|
122
|
+
byKey[t.key] = [...new Set([...(byKey[t.key] || []), ...t.props, ...t.states])];
|
|
123
|
+
}
|
|
124
|
+
return byKey;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {unknown} value
|
|
129
|
+
* @returns {string[]}
|
|
130
|
+
*/
|
|
131
|
+
function stringList(value) {
|
|
132
|
+
return Array.isArray(value)
|
|
133
|
+
? value.filter((/** @type {unknown} */ v) => typeof v === 'string')
|
|
134
|
+
: [];
|
|
135
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file The enumerability guard for component theming targets.
|
|
5
|
+
*
|
|
6
|
+
* A theming target is only useful if a theme author can find it. These tests
|
|
7
|
+
* run against the REAL core docs and fail if any component's targets stop
|
|
8
|
+
* being enumerable — a doc that moves out of the scanned tree, a target shape
|
|
9
|
+
* that stops being read, or a component whose Theming table says one thing
|
|
10
|
+
* while `theme targets` says another. That divergence is the failure the
|
|
11
|
+
* listing exists to prevent: a target list that can drift from the components
|
|
12
|
+
* is worse than no list.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {describe, it, expect} from 'vitest';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
import {findCoreDir} from '../fs/paths.mjs';
|
|
18
|
+
import {
|
|
19
|
+
discoverComponents,
|
|
20
|
+
findComponentReadme,
|
|
21
|
+
} from './component-discovery.mjs';
|
|
22
|
+
import {loadComponentDoc} from './component-loader.mjs';
|
|
23
|
+
import {collectThemingTargets, targetsByKey} from './theming-targets.mjs';
|
|
24
|
+
|
|
25
|
+
const coreDir = /** @type {string} */ (findCoreDir(process.cwd()));
|
|
26
|
+
const coreSrc = path.join(coreDir, 'src');
|
|
27
|
+
|
|
28
|
+
/** @type {Promise<import('./theming-targets.mjs').ThemingTarget[]>} */
|
|
29
|
+
const enumerated = collectThemingTargets(coreSrc);
|
|
30
|
+
|
|
31
|
+
describe('collectThemingTargets', () => {
|
|
32
|
+
it('enumerates the whole surface, not a handful', async () => {
|
|
33
|
+
const targets = await enumerated;
|
|
34
|
+
expect(targets.length).toBeGreaterThan(100);
|
|
35
|
+
expect(new Set(targets.map(t => t.component)).size).toBeGreaterThan(50);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('drops the namespace prefix so each key is what defineTheme takes', async () => {
|
|
39
|
+
for (const t of await enumerated) {
|
|
40
|
+
expect(t.className).toBe(`astryx-${t.key}`);
|
|
41
|
+
expect(t.component).toBeTruthy();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('carries the props and states a target reflects', async () => {
|
|
46
|
+
const targets = await enumerated;
|
|
47
|
+
expect(targets.find(t => t.key === 'switch-thumb')).toEqual({
|
|
48
|
+
key: 'switch-thumb',
|
|
49
|
+
className: 'astryx-switch-thumb',
|
|
50
|
+
component: 'Switch',
|
|
51
|
+
props: ['size'],
|
|
52
|
+
states: ['checked'],
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('is sorted by key, so a diff of two runs is readable', async () => {
|
|
57
|
+
const keys = (await enumerated).map(t => t.key);
|
|
58
|
+
expect(keys).toEqual([...keys].sort((a, b) => a.localeCompare(b)));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// The listing and `theme build`'s override validation read this one
|
|
62
|
+
// enumeration. `targetsByKey` is the shape validation wants: props and
|
|
63
|
+
// states merged, because both are legal override keys.
|
|
64
|
+
it('collapses to the override keys, merging the components that share one', async () => {
|
|
65
|
+
const byKey = targetsByKey(await enumerated);
|
|
66
|
+
expect(byKey['switch']).toEqual(['size', 'checked', 'disabled']);
|
|
67
|
+
// `radio` is documented by both Indicator and RadioList.
|
|
68
|
+
const radio = (await enumerated).filter(t => t.key === 'radio');
|
|
69
|
+
expect(radio.length).toBeGreaterThan(1);
|
|
70
|
+
for (const t of radio) {
|
|
71
|
+
for (const name of [...t.props, ...t.states]) {
|
|
72
|
+
expect(byKey['radio']).toContain(name);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('every component doc that declares targets has them enumerated', async () => {
|
|
78
|
+
const targets = await enumerated;
|
|
79
|
+
/** @type {Map<string, Set<string>>} key -> props+states */
|
|
80
|
+
const byKey = new Map(
|
|
81
|
+
Object.entries(targetsByKey(targets)).map(([k, v]) => [k, new Set(v)]),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const names = Object.values(discoverComponents(coreDir)).flat();
|
|
85
|
+
/** @type {string[]} */
|
|
86
|
+
const missing = [];
|
|
87
|
+
/** @type {Set<string>} */
|
|
88
|
+
const seenDocs = new Set();
|
|
89
|
+
let checked = 0;
|
|
90
|
+
|
|
91
|
+
for (const name of names) {
|
|
92
|
+
const docPath = findComponentReadme(coreDir, name);
|
|
93
|
+
if (!docPath || seenDocs.has(docPath)) continue;
|
|
94
|
+
seenDocs.add(docPath);
|
|
95
|
+
|
|
96
|
+
/** @type {any} */
|
|
97
|
+
let doc;
|
|
98
|
+
try {
|
|
99
|
+
doc = await loadComponentDoc(docPath);
|
|
100
|
+
} catch {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const target of doc?.theming?.targets || []) {
|
|
105
|
+
if (typeof target?.className !== 'string') continue;
|
|
106
|
+
checked++;
|
|
107
|
+
const key = target.className.replace(/^astryx-/, '');
|
|
108
|
+
const known = byKey.get(key);
|
|
109
|
+
if (!known) {
|
|
110
|
+
missing.push(`${name}: ${target.className} is not enumerable`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
for (const prop of [
|
|
114
|
+
...(target.visualProps || []),
|
|
115
|
+
...(target.states || []),
|
|
116
|
+
]) {
|
|
117
|
+
if (!known.has(prop)) {
|
|
118
|
+
missing.push(`${name}: ${target.className} lost "${prop}"`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
expect(checked).toBeGreaterThan(100);
|
|
125
|
+
expect(missing).toEqual([]);
|
|
126
|
+
}, 60_000);
|
|
127
|
+
});
|
|
@@ -199,6 +199,11 @@ export const doc = {
|
|
|
199
199
|
description:
|
|
200
200
|
'A write receipt for the annotated theme template: the path (relative to cwd), whether it was written, and the reason it was not — `exists` when a file was already there, which is a success.',
|
|
201
201
|
},
|
|
202
|
+
{
|
|
203
|
+
value: 'theme.targets',
|
|
204
|
+
description:
|
|
205
|
+
'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.',
|
|
206
|
+
},
|
|
202
207
|
|
|
203
208
|
// upgrade
|
|
204
209
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/cli",
|
|
3
|
-
"version": "0.4.7-canary.
|
|
3
|
+
"version": "0.4.7-canary.63f398d",
|
|
4
4
|
"displayName": "CLI",
|
|
5
5
|
"description": "Scaffold projects, browse templates, generate themes, and get agent-ready docs from the command line.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -87,10 +87,10 @@
|
|
|
87
87
|
"zod": "^4.4.3"
|
|
88
88
|
},
|
|
89
89
|
"peerDependencies": {
|
|
90
|
-
"@astryxdesign/charts": "0.4.7-canary.
|
|
91
|
-
"@astryxdesign/core": "0.4.7-canary.
|
|
92
|
-
"@astryxdesign/lab": "0.4.7-canary.
|
|
93
|
-
"@astryxdesign/theme-neutral": "0.4.7-canary.
|
|
90
|
+
"@astryxdesign/charts": "0.4.7-canary.63f398d",
|
|
91
|
+
"@astryxdesign/core": "0.4.7-canary.63f398d",
|
|
92
|
+
"@astryxdesign/lab": "0.4.7-canary.63f398d",
|
|
93
|
+
"@astryxdesign/theme-neutral": "0.4.7-canary.63f398d",
|
|
94
94
|
"gpt-tokenizer": "^3.4.0"
|
|
95
95
|
},
|
|
96
96
|
"peerDependenciesMeta": {
|
|
@@ -108,10 +108,10 @@
|
|
|
108
108
|
}
|
|
109
109
|
},
|
|
110
110
|
"devDependencies": {
|
|
111
|
-
"@astryxdesign/charts": "0.4.7-canary.
|
|
112
|
-
"@astryxdesign/core": "0.4.7-canary.
|
|
113
|
-
"@astryxdesign/lab": "0.4.7-canary.
|
|
114
|
-
"@astryxdesign/theme-neutral": "0.4.7-canary.
|
|
111
|
+
"@astryxdesign/charts": "0.4.7-canary.63f398d",
|
|
112
|
+
"@astryxdesign/core": "0.4.7-canary.63f398d",
|
|
113
|
+
"@astryxdesign/lab": "0.4.7-canary.63f398d",
|
|
114
|
+
"@astryxdesign/theme-neutral": "0.4.7-canary.63f398d",
|
|
115
115
|
"gpt-tokenizer": "^3.4.0"
|
|
116
116
|
},
|
|
117
117
|
"scripts": {
|