@astryxdesign/cli 0.4.7 → 0.5.0-canary.009bcb3
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/CHANGELOG.md +64 -0
- package/README.md +53 -51
- package/api/docs/docs.doc.mjs +2 -2
- package/api/index.d.mts +1 -1
- package/api/index.mjs +1 -1
- package/api/search/search.mjs +50 -4
- package/api/search/search.test.mjs +71 -0
- package/api/template/data-token-fallbacks.test.mjs +85 -0
- package/api/template/template.doc.mjs +2 -2
- package/api/theme/build/build.mjs +43 -44
- package/api/theme/targets/targets.d.mts +18 -0
- package/api/theme/targets/targets.mjs +87 -0
- package/api/theme/targets/targets.test.mjs +66 -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/api/theme/themeTemplate.doc.mjs +3 -3
- package/assets/codemods/__tests__/registry.test.mjs +1 -0
- package/assets/codemods/registry.mjs +1 -0
- package/assets/codemods/transforms/v0.5.0/__tests__/next-codemods.test.mjs +127 -0
- package/assets/codemods/transforms/v0.5.0/banner-collapsible-content.mjs +171 -0
- package/assets/codemods/transforms/v0.5.0/index.mjs +20 -0
- package/assets/docs/README.md +50 -0
- package/assets/docs/cli-integrations.doc.mjs +4 -4
- package/assets/docs/theme.doc.dense.mjs +1 -1
- package/assets/docs/theme.doc.mjs +1 -1
- package/assets/docs/typography.doc.mjs +2 -2
- package/assets/templates/blocks/components/Banner/BannerCollapsibleContent.doc.mjs +1 -1
- package/assets/templates/blocks/components/Banner/BannerCollapsibleContent.tsx +1 -1
- package/assets/templates/blocks/components/DateInput/DateInputClearable.tsx +5 -1
- package/assets/templates/blocks/components/DateInput/DateInputDateRange.tsx +6 -1
- package/assets/templates/blocks/components/DateInput/DateInputFormats.tsx +5 -1
- package/assets/templates/blocks/components/DateInput/DateInputShowcase.tsx +4 -1
- package/assets/templates/blocks/components/DateInput/DateInputWithDescription.tsx +5 -1
- package/assets/templates/blocks/components/DateInput/DateInputWithValidation.tsx +5 -1
- package/assets/templates/blocks/components/Dialog/DialogAdaptivePresentation.doc.mjs +25 -0
- package/assets/templates/blocks/components/Dialog/DialogAdaptivePresentation.tsx +167 -0
- package/assets/templates/blocks/components/Dialog/DialogScrollingContent.tsx +1 -1
- package/assets/templates/blocks/components/HoverCard/HoverCardHookUsage.tsx +4 -1
- package/assets/templates/blocks/components/Step/StepContent.doc.mjs +14 -0
- package/assets/templates/blocks/components/Step/StepContent.tsx +32 -0
- package/assets/templates/blocks/components/Step/StepIndicator.doc.mjs +14 -0
- package/assets/templates/blocks/components/Step/StepIndicator.tsx +60 -0
- package/assets/templates/blocks/components/Step/StepShowcase.doc.mjs +15 -0
- package/assets/templates/blocks/components/Step/StepShowcase.tsx +26 -0
- package/assets/templates/blocks/components/Step/StepStates.doc.mjs +14 -0
- package/assets/templates/blocks/components/Step/StepStates.tsx +46 -0
- package/assets/templates/blocks/components/Stepper/StepperCustomContent.doc.mjs +22 -0
- package/assets/templates/blocks/components/Stepper/StepperCustomContent.tsx +126 -0
- package/assets/templates/blocks/components/Stepper/StepperIndicatorModes.doc.mjs +1 -1
- package/assets/templates/blocks/components/Stepper/StepperIndicatorModes.tsx +17 -5
- package/assets/templates/blocks/components/Stepper/StepperOnTrackHorizontal.doc.mjs +14 -0
- package/assets/templates/blocks/components/Stepper/StepperOnTrackHorizontal.tsx +25 -0
- package/assets/templates/blocks/components/Stepper/StepperOnTrackVertical.doc.mjs +2 -2
- package/assets/templates/blocks/components/Stepper/StepperOnTrackVertical.tsx +1 -1
- package/assets/templates/blocks/components/Stepper/StepperShowcase.doc.mjs +1 -1
- package/assets/templates/blocks/components/Stepper/StepperShowcase.tsx +6 -7
- package/assets/templates/blocks/components/Stepper/StepperStatus.tsx +1 -1
- package/assets/templates/pages/dashboard-cohort-funnel/page.tsx +1 -1
- package/assets/templates/pages/dashboard-data/page.tsx +1 -1
- package/assets/templates/pages/dashboard-portfolio/page.tsx +5 -5
- package/assets/templates/pages/dashboard-service-monitoring/page.tsx +1 -1
- package/assets/templates/pages/mixed-gallery/page.tsx +12 -3
- package/assets/templates/pages/settings-dialog/page.tsx +2543 -757
- package/assets/templates/pages/settings-dialog/template.doc.mjs +1 -2
- package/assets/templates/pages/table-filter/page.tsx +4093 -0
- package/assets/templates/pages/table-filter/template.doc.mjs +12 -0
- package/assets/templates/pages/table-grouped/page.tsx +151 -144
- package/assets/templates/pages/theme-showcase/page.tsx +36 -17
- package/assets/templates/themes/chocolate/chocolateTheme.ts +3 -1
- package/assets/templates/themes/matcha/matchaTheme.ts +3 -1
- package/assets/templates/themes/neutral/neutralTheme.ts +16 -9
- package/assets/templates/themes/stone/stoneTheme.ts +3 -1
- package/clients/cli/commands/build-theme.color-scheme.test.mjs +7 -6
- package/clients/cli/commands/build-theme.data-tokens.test.mjs +144 -0
- package/clients/cli/commands/build-theme.mjs +85 -0
- package/clients/cli/commands/build-theme.variants.test.mjs +3 -0
- package/clients/cli/commands/dialog-adaptive-template.test.mjs +24 -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-template.doc.mjs +2 -2
- package/clients/cli/commands/theme.doc.mjs +4 -2
- package/clients/cli/index.mjs +1 -0
- package/clients/cli/lib/component-format.mjs +2 -2
- package/clients/cli/lib/manifest.mjs +2 -0
- package/foundation/discovery/component-discovery.mjs +10 -9
- package/foundation/discovery/hook-discovery.mjs +2 -1
- package/foundation/discovery/hook-discovery.test.mjs +156 -37
- package/foundation/discovery/theming-targets.d.mts +86 -0
- package/foundation/discovery/theming-targets.mjs +202 -0
- package/foundation/discovery/theming-targets.test.mjs +245 -0
- package/foundation/fs/paths.d.mts +16 -0
- package/foundation/fs/paths.mjs +36 -0
- package/foundation/fs/paths.test.mjs +29 -1
- package/foundation/response/response-types.doc.mjs +7 -2
- package/package.json +9 -9
- package/assets/templates/blocks/components/Stepper/StepperHorizontal.doc.mjs +0 -14
- package/assets/templates/blocks/components/Stepper/StepperHorizontal.tsx +0 -24
- package/assets/templates/blocks/components/Stepper/StepperMultiStepForm.doc.mjs +0 -14
- package/assets/templates/blocks/components/Stepper/StepperMultiStepForm.tsx +0 -92
- package/assets/templates/blocks/components/Stepper/StepperVerticalOnboarding.doc.mjs +0 -14
- package/assets/templates/blocks/components/Stepper/StepperVerticalOnboarding.tsx +0 -40
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Asserts `astryx theme build` emits the `--color-data-*` defaults the
|
|
5
|
+
* way the `<Theme>` runtime does — once, at `:root`, in `@layer astryx-base`.
|
|
6
|
+
*
|
|
7
|
+
* Seeding them into the theme's own `@scope` block instead makes every nested
|
|
8
|
+
* `<Theme>` re-declare the default and shadow a parent theme's override, which
|
|
9
|
+
* no other token family does. Layer order is fixed by first declaration, so
|
|
10
|
+
* the base block also has to sit after `@layer reset` and before
|
|
11
|
+
* `@layer astryx-theme` for a consumer who imports this stylesheet alone.
|
|
12
|
+
*
|
|
13
|
+
* Building requires a compiled @astryxdesign/core, so this suite builds core
|
|
14
|
+
* once in beforeAll via the shared ensureCoreBuilt() helper.
|
|
15
|
+
*
|
|
16
|
+
* The build formats the defaults block itself, from the public
|
|
17
|
+
* `dataTokenDefaults` export; core formats it for the runtime. This suite is
|
|
18
|
+
* what keeps those two independent formattings byte-identical.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {describe, it, expect, beforeAll, beforeEach, afterEach} from 'vitest';
|
|
22
|
+
import * as fs from 'node:fs';
|
|
23
|
+
import * as path from 'node:path';
|
|
24
|
+
import * as os from 'node:os';
|
|
25
|
+
// Core's own generator, imported from source rather than from the package: it
|
|
26
|
+
// is deliberately not part of `@astryxdesign/core/theme`'s public surface, and
|
|
27
|
+
// this suite is the guard that the build's independent formatting matches it.
|
|
28
|
+
import {generateDataTokenDefaultsCSS} from '../../../../core/src/theme/generateThemeRules';
|
|
29
|
+
import {dataTokenDefaults as sourceDataTokenDefaults} from '../../../../core/src/theme/domainTokens/dataTokens';
|
|
30
|
+
import {ensureCoreBuilt} from './ensure-core-built.mjs';
|
|
31
|
+
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
32
|
+
|
|
33
|
+
function writeTheme(dir, name, theme) {
|
|
34
|
+
fs.mkdirSync(dir, {recursive: true});
|
|
35
|
+
const file = path.join(dir, `${name}.mjs`);
|
|
36
|
+
fs.writeFileSync(
|
|
37
|
+
file,
|
|
38
|
+
`export default ${JSON.stringify({name, ...theme})};\n`,
|
|
39
|
+
);
|
|
40
|
+
return file;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function buildTheme(tmpDir, name, theme) {
|
|
44
|
+
const project = path.join(tmpDir, 'project');
|
|
45
|
+
const themesDir = path.join(project, 'themes');
|
|
46
|
+
const themeFile = writeTheme(themesDir, name, theme);
|
|
47
|
+
|
|
48
|
+
const result = await runCli(
|
|
49
|
+
['theme', 'build', path.relative(project, themeFile)],
|
|
50
|
+
project,
|
|
51
|
+
);
|
|
52
|
+
expect(result.code).toBe(0);
|
|
53
|
+
|
|
54
|
+
return fs.readFileSync(path.join(themesDir, `${name}.css`), 'utf-8');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
beforeAll(() => {
|
|
58
|
+
ensureCoreBuilt();
|
|
59
|
+
}, 200_000);
|
|
60
|
+
|
|
61
|
+
let tmpDir;
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-build-theme-data-'));
|
|
64
|
+
});
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('theme build data token output', () => {
|
|
70
|
+
it('emits the defaults once, at :root, in @layer astryx-base', async () => {
|
|
71
|
+
const css = await buildTheme(tmpDir, 'charts-untouched', {
|
|
72
|
+
tokens: {'--color-accent': '#0077B6'},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const baseStart = css.indexOf('@layer astryx-base');
|
|
76
|
+
expect(baseStart).toBeGreaterThanOrEqual(0);
|
|
77
|
+
|
|
78
|
+
const baseBlock = css.slice(baseStart, css.indexOf('@layer astryx-theme'));
|
|
79
|
+
expect(baseBlock).toContain(':root {');
|
|
80
|
+
expect(baseBlock).toContain('--color-data-categorical-blue:');
|
|
81
|
+
expect(baseBlock).toContain('--color-data-gray-1:');
|
|
82
|
+
expect(css.match(/--color-data-categorical-blue:/g)).toHaveLength(1);
|
|
83
|
+
expect(css.match(/@layer astryx-base/g)).toHaveLength(1);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('declares astryx-base after reset and before astryx-theme', async () => {
|
|
87
|
+
const css = await buildTheme(tmpDir, 'charts-order', {
|
|
88
|
+
tokens: {'--color-accent': '#0077B6'},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const reset = css.indexOf('@layer reset');
|
|
92
|
+
const base = css.indexOf('@layer astryx-base');
|
|
93
|
+
const theme = css.indexOf('@layer astryx-theme');
|
|
94
|
+
|
|
95
|
+
expect(reset).toBeGreaterThanOrEqual(0);
|
|
96
|
+
expect(base).toBeGreaterThan(reset);
|
|
97
|
+
expect(theme).toBeGreaterThan(base);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("puts only the theme's own data token in its scope block", async () => {
|
|
101
|
+
const css = await buildTheme(tmpDir, 'charts-override', {
|
|
102
|
+
tokens: {'--color-data-categorical-blue': '#00A3FF'},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const themeBlock = css.slice(css.indexOf('@layer astryx-theme'));
|
|
106
|
+
expect(themeBlock).toContain('--color-data-categorical-blue: #00A3FF;');
|
|
107
|
+
expect(themeBlock).not.toContain('--color-data-categorical-orange');
|
|
108
|
+
expect(themeBlock).not.toContain('--color-data-gray-1');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('emits the bytes the runtime generator emits', async () => {
|
|
112
|
+
const css = await buildTheme(tmpDir, 'charts-parity', {
|
|
113
|
+
tokens: {'--color-accent': '#0077B6'},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// The expectation below comes from core's SOURCE; the stylesheet came from
|
|
117
|
+
// core's dist. ensureCoreBuilt() only checks that dist exists, never that
|
|
118
|
+
// it is current, so a dist older than the source would fail the byte
|
|
119
|
+
// comparison with a colour mismatch that reads as formatter drift. Name
|
|
120
|
+
// that case first.
|
|
121
|
+
const {dataTokenDefaults: builtDataTokenDefaults} = await import(
|
|
122
|
+
'@astryxdesign/core/theme'
|
|
123
|
+
);
|
|
124
|
+
expect(
|
|
125
|
+
builtDataTokenDefaults,
|
|
126
|
+
'packages/core/dist is stale — run `pnpm -F @astryxdesign/core build`',
|
|
127
|
+
).toEqual(sourceDataTokenDefaults);
|
|
128
|
+
|
|
129
|
+
// The build formats this block itself, from the public `dataTokenDefaults`
|
|
130
|
+
// export; the runtime formats it in core. Nothing else holds the two
|
|
131
|
+
// together, so compare the bytes — a changed indent or separator in either
|
|
132
|
+
// one fails here.
|
|
133
|
+
const runtimeBase = generateDataTokenDefaultsCSS();
|
|
134
|
+
const marker = '@layer astryx-base {\n';
|
|
135
|
+
const start = css.indexOf(marker);
|
|
136
|
+
expect(start).toBeGreaterThanOrEqual(0);
|
|
137
|
+
|
|
138
|
+
const bodyStart = start + marker.length;
|
|
139
|
+
expect(css.slice(bodyStart, bodyStart + runtimeBase.length)).toBe(
|
|
140
|
+
runtimeBase,
|
|
141
|
+
);
|
|
142
|
+
expect(css.slice(bodyStart + runtimeBase.length)).toMatch(/^\n\}/);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -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
|
}
|
|
@@ -164,6 +164,7 @@ describe('theme build custom-variant augmentations', () => {
|
|
|
164
164
|
},
|
|
165
165
|
breadcrumbs: { 'variant:customBreadcrumbs': { color: 'currentColor' } },
|
|
166
166
|
button: { 'variant:customButton': { backgroundColor: 'transparent' } },
|
|
167
|
+
card: { 'variant:customCard': { backgroundColor: 'transparent' } },
|
|
167
168
|
dialog: { 'variant:customDialog': { backgroundColor: 'transparent' } },
|
|
168
169
|
divider: { 'variant:customDivider': { borderColor: 'currentColor' } },
|
|
169
170
|
'field-status': { 'variant:customFieldStatus': { color: 'currentColor' } },
|
|
@@ -204,6 +205,7 @@ describe('theme build custom-variant augmentations', () => {
|
|
|
204
205
|
`import {Banner} from '@astryxdesign/core/Banner';\n` +
|
|
205
206
|
`import {Breadcrumbs} from '@astryxdesign/core/Breadcrumbs';\n` +
|
|
206
207
|
`import {Button} from '@astryxdesign/core/Button';\n` +
|
|
208
|
+
`import {Card} from '@astryxdesign/core/Card';\n` +
|
|
207
209
|
`import {Dialog} from '@astryxdesign/core/Dialog';\n` +
|
|
208
210
|
`import {Divider} from '@astryxdesign/core/Divider';\n` +
|
|
209
211
|
`import {FieldStatus} from '@astryxdesign/core/FieldStatus';\n` +
|
|
@@ -222,6 +224,7 @@ describe('theme build custom-variant augmentations', () => {
|
|
|
222
224
|
` <Banner title="Banner" status="customBannerStatus" container="customBannerContainer" />\n` +
|
|
223
225
|
` <Breadcrumbs variant="customBreadcrumbs">Crumbs</Breadcrumbs>\n` +
|
|
224
226
|
` <Button label="Button" variant="customButton" />\n` +
|
|
227
|
+
` <Card variant="customCard">Card</Card>\n` +
|
|
225
228
|
` <Dialog isOpen onOpenChange={() => {}} variant="customDialog">Body</Dialog>\n` +
|
|
226
229
|
` <Divider variant="customDivider" />\n` +
|
|
227
230
|
` <FieldStatus type="success" message="Ok" variant="customFieldStatus" />\n` +
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {describe, expect, it} from 'vitest';
|
|
4
|
+
import {component} from '../../../api/component/component.mjs';
|
|
5
|
+
|
|
6
|
+
const CWD = {cwd: '.'};
|
|
7
|
+
|
|
8
|
+
// Resolves built-in block templates by walking the source tree.
|
|
9
|
+
const SCAN_TIMEOUT = 30_000;
|
|
10
|
+
|
|
11
|
+
describe(
|
|
12
|
+
'Dialog adaptive presentation template',
|
|
13
|
+
() => {
|
|
14
|
+
it('is listed as a Dialog example block', async () => {
|
|
15
|
+
const result = await component('Dialog', {
|
|
16
|
+
...CWD,
|
|
17
|
+
blocks: true,
|
|
18
|
+
});
|
|
19
|
+
const exampleNames = result.data.examples.map(b => b.name);
|
|
20
|
+
expect(exampleNames).toContain('DialogAdaptivePresentation');
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
SCAN_TIMEOUT,
|
|
24
|
+
);
|
|
@@ -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(/4 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
|
+
};
|
|
@@ -16,9 +16,9 @@ export const doc = {
|
|
|
16
16
|
namespace: 'cli',
|
|
17
17
|
summary: 'Write the annotated theme template into your project',
|
|
18
18
|
description:
|
|
19
|
-
'Writes theme.template.ts: the annotated reference for the whole theme surface
|
|
19
|
+
'Writes theme.template.ts: the annotated reference for the whole theme surface, covering every ' +
|
|
20
20
|
'defineTheme field, the token families, the component override syntax, and how a theme is ' +
|
|
21
|
-
'consumed
|
|
21
|
+
'consumed, naming the CLI command that prints the authoritative reference for each. Read ' +
|
|
22
22
|
'it, copy what you need into your own theme file, delete it. Use `theme add <slug>` instead ' +
|
|
23
23
|
'to start from a theme we ship. Leaves an existing file untouched unless --overwrite.',
|
|
24
24
|
fn: 'themeTemplate',
|
|
@@ -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
|
@@ -13,8 +13,8 @@ import {getCliInvocation} from '../../../foundation/env/package-manager.mjs';
|
|
|
13
13
|
*
|
|
14
14
|
* Override keys are the component's stable class name with the `astryx-`
|
|
15
15
|
* namespace prefix stripped — `generateThemeRules` re-adds the prefix when it
|
|
16
|
-
* builds the `.astryx-*` selector. So `astryx-
|
|
17
|
-
* (→ `.astryx-
|
|
16
|
+
* builds the `.astryx-*` selector. So `astryx-table-cell` → key `table-cell`
|
|
17
|
+
* (→ `.astryx-table-cell`), and `astryx-button` → key `button`.
|
|
18
18
|
*
|
|
19
19
|
* Keep the `astryx-` literal in sync with packages/core/src/naming.ts
|
|
20
20
|
* (NAMESPACE / classPrefix), the same way build-theme.mjs mirrors it.
|
|
@@ -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'],
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import * as path from 'node:path';
|
|
9
|
+
import {existsCaseExact} from '../fs/paths.mjs';
|
|
9
10
|
|
|
10
11
|
const SKIP_DIRS = new Set(['hooks', 'utils', '__tests__', 'node_modules']);
|
|
11
12
|
|
|
@@ -243,27 +244,27 @@ export function findComponentReadme(coreDir, name) {
|
|
|
243
244
|
|
|
244
245
|
// Direct match: src/{name}/{Name}.doc.mjs or src/{name}/Astryx{Name}.doc.mjs
|
|
245
246
|
const direct = path.join(srcDir, name, exactDoc);
|
|
246
|
-
if (
|
|
247
|
+
if (existsCaseExact(direct, srcDir)) return direct;
|
|
247
248
|
const directXds = path.join(srcDir, name, xdsDoc);
|
|
248
|
-
if (
|
|
249
|
+
if (existsCaseExact(directXds, srcDir)) return directXds;
|
|
249
250
|
|
|
250
251
|
// Nested match: src/*/{name}/{Name}.doc.mjs or src/*/{name}/Astryx{Name}.doc.mjs
|
|
251
252
|
const entries = fs.readdirSync(srcDir, {withFileTypes: true});
|
|
252
253
|
for (const entry of entries) {
|
|
253
254
|
if (!entry.isDirectory()) continue;
|
|
254
255
|
const nested = path.join(srcDir, entry.name, name, exactDoc);
|
|
255
|
-
if (
|
|
256
|
+
if (existsCaseExact(nested, srcDir)) return nested;
|
|
256
257
|
const nestedXds = path.join(srcDir, entry.name, name, xdsDoc);
|
|
257
|
-
if (
|
|
258
|
+
if (existsCaseExact(nestedXds, srcDir)) return nestedXds;
|
|
258
259
|
}
|
|
259
260
|
|
|
260
261
|
// Per-component doc in a parent directory: src/*/{Name}.doc.mjs or src/*/Astryx{Name}.doc.mjs
|
|
261
262
|
for (const entry of entries) {
|
|
262
263
|
if (!entry.isDirectory()) continue;
|
|
263
264
|
const perComp = path.join(srcDir, entry.name, exactDoc);
|
|
264
|
-
if (
|
|
265
|
+
if (existsCaseExact(perComp, srcDir)) return perComp;
|
|
265
266
|
const perCompXds = path.join(srcDir, entry.name, xdsDoc);
|
|
266
|
-
if (
|
|
267
|
+
if (existsCaseExact(perCompXds, srcDir)) return perCompXds;
|
|
267
268
|
}
|
|
268
269
|
|
|
269
270
|
// Sub-component fallback: find the source file, then walk up
|
|
@@ -321,7 +322,7 @@ export function findComponentSource(coreDir, name) {
|
|
|
321
322
|
// Check for an exact match (prefixed or bare) first
|
|
322
323
|
for (const candidate of candidateFiles) {
|
|
323
324
|
const exact = path.join(dirPath, candidate);
|
|
324
|
-
if (
|
|
325
|
+
if (existsCaseExact(exact, dirPath)) return exact;
|
|
325
326
|
}
|
|
326
327
|
|
|
327
328
|
// Recurse into subdirectories
|
|
@@ -336,7 +337,7 @@ export function findComponentSource(coreDir, name) {
|
|
|
336
337
|
|
|
337
338
|
// Search in the component's directory
|
|
338
339
|
const directDir = path.join(srcDir, name);
|
|
339
|
-
if (
|
|
340
|
+
if (existsCaseExact(directDir, srcDir)) {
|
|
340
341
|
const found = searchDir(directDir);
|
|
341
342
|
if (found) return found;
|
|
342
343
|
}
|
|
@@ -346,7 +347,7 @@ export function findComponentSource(coreDir, name) {
|
|
|
346
347
|
for (const entry of entries) {
|
|
347
348
|
if (!entry.isDirectory()) continue;
|
|
348
349
|
const nestedDir = path.join(srcDir, entry.name, name);
|
|
349
|
-
if (
|
|
350
|
+
if (existsCaseExact(nestedDir, srcDir)) {
|
|
350
351
|
const found = searchDir(nestedDir);
|
|
351
352
|
if (found) return found;
|
|
352
353
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import * as path from 'node:path';
|
|
9
|
+
import {existsCaseExact} from '../fs/paths.mjs';
|
|
9
10
|
import {levenshteinDistance} from '../text/string-utils.mjs';
|
|
10
11
|
|
|
11
12
|
const SKIP_DIRS = new Set(['utils', '__tests__', 'node_modules']);
|
|
@@ -157,7 +158,7 @@ export function findHookDoc(coreDir, name) {
|
|
|
157
158
|
// 1. Check src/hooks/{name}.doc.mjs first
|
|
158
159
|
for (const candidate of uniqueCandidates) {
|
|
159
160
|
const direct = path.join(hooksDir, `${candidate}.doc.mjs`);
|
|
160
|
-
if (
|
|
161
|
+
if (existsCaseExact(direct, hooksDir)) return direct;
|
|
161
162
|
}
|
|
162
163
|
|
|
163
164
|
// 2. Case-insensitive search in hooks directory
|