@astryxdesign/cli 0.3.0-canary.ec85ba0 → 0.3.0-canary.f4607ea
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/api/theme/build/build.d.mts +6 -1
- package/api/theme/build/build.mjs +29 -4
- package/api/theme/themeBuild.doc.mjs +14 -2
- package/assets/codemods/transforms/next/__tests__/next-codemods.test.mjs +92 -0
- package/assets/codemods/transforms/next/index.mjs +11 -1
- package/assets/codemods/transforms/next/rename-dropdown-menu-radio-dot-target.mjs +152 -0
- package/assets/docs/getting-started.doc.mjs +3 -3
- package/assets/templates/blocks/components/Stepper/StepperHorizontal.doc.mjs +14 -0
- package/assets/templates/blocks/components/Stepper/StepperHorizontal.tsx +24 -0
- package/assets/templates/blocks/components/Stepper/StepperIndicatorModes.doc.mjs +14 -0
- package/assets/templates/blocks/components/Stepper/StepperIndicatorModes.tsx +68 -0
- package/assets/templates/blocks/components/Stepper/StepperMultiStepForm.doc.mjs +14 -0
- package/assets/templates/blocks/components/Stepper/StepperMultiStepForm.tsx +92 -0
- package/assets/templates/blocks/components/Stepper/StepperOnTrackVertical.doc.mjs +14 -0
- package/assets/templates/blocks/components/Stepper/StepperOnTrackVertical.tsx +41 -0
- package/assets/templates/blocks/components/Stepper/StepperShowcase.doc.mjs +15 -0
- package/assets/templates/blocks/components/Stepper/StepperShowcase.tsx +25 -0
- package/assets/templates/blocks/components/Stepper/StepperStatus.doc.mjs +14 -0
- package/assets/templates/blocks/components/Stepper/StepperStatus.tsx +50 -0
- package/assets/templates/blocks/components/Stepper/StepperVerticalOnboarding.doc.mjs +14 -0
- package/assets/templates/blocks/components/Stepper/StepperVerticalOnboarding.tsx +40 -0
- package/assets/templates/blocks/components/TextArea/TextAreaStates.tsx +1 -1
- package/authoring/doctypes/base/type.ts +6 -0
- package/clients/cli/commands/build-theme.icons-specifier.test.mjs +225 -0
- package/clients/cli/commands/build-theme.mjs +10 -4
- package/clients/cli/commands/theme-build.doc.mjs +13 -1
- package/package.json +9 -9
|
@@ -21,13 +21,18 @@ export function importSpecifier(relDir: string, base: string): string;
|
|
|
21
21
|
* `logger` (silent by default).
|
|
22
22
|
*
|
|
23
23
|
* @param {string} file - Theme file path, resolved against `cwd`.
|
|
24
|
-
* @param {{out?: string, check?: boolean}} [options] -
|
|
24
|
+
* @param {{out?: string, check?: boolean, iconsSpecifier?: string}} [options] -
|
|
25
|
+
* `out` overrides the output CSS path; `check` compares against on-disk outputs
|
|
26
|
+
* instead of writing. `iconsSpecifier` overrides the icon registry import
|
|
27
|
+
* specifier in the generated module (e.g. `./icons.mjs`); when omitted, the
|
|
28
|
+
* specifier scraped from the theme source is emitted unchanged.
|
|
25
29
|
* @param {{cwd?: string}} [ctx]
|
|
26
30
|
* @returns {Promise<import('../theme.type.mjs').ThemeBuildResponse | import('../theme.type.mjs').ThemeBuildCheckResponse | null>}
|
|
27
31
|
*/
|
|
28
32
|
export function themeBuild(file: string, options?: {
|
|
29
33
|
out?: string;
|
|
30
34
|
check?: boolean;
|
|
35
|
+
iconsSpecifier?: string;
|
|
31
36
|
}, { cwd }?: {
|
|
32
37
|
cwd?: string;
|
|
33
38
|
}): Promise<import("../theme.type.mjs").ThemeBuildResponse | import("../theme.type.mjs").ThemeBuildCheckResponse | null>;
|
|
@@ -726,13 +726,30 @@ function extractIconInfo(filePath) {
|
|
|
726
726
|
* Generate a minimal JS module for a built theme.
|
|
727
727
|
* Includes the theme name, marker, and re-exports the icon registry.
|
|
728
728
|
* All styling is in the CSS file.
|
|
729
|
+
*
|
|
730
|
+
* The icon registry is imported rather than inlined because it holds React
|
|
731
|
+
* elements, which cannot be serialized. `extractIconInfo` lifts the specifier
|
|
732
|
+
* out of the TypeScript source, where an extensionless `./icons` is resolved by
|
|
733
|
+
* the TypeScript resolver — but the artifact here is ESM JavaScript, which
|
|
734
|
+
* requires a fully specified path. Only the caller knows what its own build
|
|
735
|
+
* will emit and under what name, so `iconsSpecifier` lets it say. When it is
|
|
736
|
+
* not given, the scraped specifier is emitted unchanged.
|
|
737
|
+
*
|
|
729
738
|
* @param {any} themeDef
|
|
730
739
|
* @param {{exportName: string, importPath: string} | null} iconInfo
|
|
740
|
+
* @param {string} [iconsSpecifier] - Overrides the scraped icon import specifier.
|
|
731
741
|
* @returns {string}
|
|
732
742
|
*/
|
|
733
|
-
function generateBuiltModule(themeDef, iconInfo) {
|
|
743
|
+
function generateBuiltModule(themeDef, iconInfo, iconsSpecifier) {
|
|
744
|
+
// Preserve the historical generated bytes when no override is supplied.
|
|
745
|
+
// User-provided specifiers need string-literal encoding so quotes and
|
|
746
|
+
// backslashes cannot produce invalid JavaScript.
|
|
747
|
+
const renderedSpecifier =
|
|
748
|
+
iconsSpecifier === undefined
|
|
749
|
+
? `'${iconInfo?.importPath}'`
|
|
750
|
+
: JSON.stringify(iconsSpecifier);
|
|
734
751
|
const iconImport = iconInfo
|
|
735
|
-
? `import { ${iconInfo.exportName} } from
|
|
752
|
+
? `import { ${iconInfo.exportName} } from ${renderedSpecifier};\n`
|
|
736
753
|
: '';
|
|
737
754
|
const iconsField = iconInfo ? ` icons: ${iconInfo.exportName},` : '';
|
|
738
755
|
const iconReExport = iconInfo ? `\nexport { ${iconInfo.exportName} };\n` : '';
|
|
@@ -970,7 +987,11 @@ function validatePrivateVars(themeDef) {
|
|
|
970
987
|
* `logger` (silent by default).
|
|
971
988
|
*
|
|
972
989
|
* @param {string} file - Theme file path, resolved against `cwd`.
|
|
973
|
-
* @param {{out?: string, check?: boolean}} [options] -
|
|
990
|
+
* @param {{out?: string, check?: boolean, iconsSpecifier?: string}} [options] -
|
|
991
|
+
* `out` overrides the output CSS path; `check` compares against on-disk outputs
|
|
992
|
+
* instead of writing. `iconsSpecifier` overrides the icon registry import
|
|
993
|
+
* specifier in the generated module (e.g. `./icons.mjs`); when omitted, the
|
|
994
|
+
* specifier scraped from the theme source is emitted unchanged.
|
|
974
995
|
* @param {{cwd?: string}} [ctx]
|
|
975
996
|
* @returns {Promise<import('../theme.type.mjs').ThemeBuildResponse | import('../theme.type.mjs').ThemeBuildCheckResponse | null>}
|
|
976
997
|
*/
|
|
@@ -1185,7 +1206,11 @@ export async function themeBuild(
|
|
|
1185
1206
|
generatedHeader(sourceRelative, 'css', buildCommand, versions) + css;
|
|
1186
1207
|
const jsContent =
|
|
1187
1208
|
generatedHeader(sourceRelative, 'js', buildCommand, versions) +
|
|
1188
|
-
generateBuiltModule(
|
|
1209
|
+
generateBuiltModule(
|
|
1210
|
+
resolvedTheme || themeDef,
|
|
1211
|
+
iconInfo,
|
|
1212
|
+
options.iconsSpecifier,
|
|
1213
|
+
);
|
|
1189
1214
|
const dtsContent =
|
|
1190
1215
|
generatedHeader(sourceRelative, 'ts', buildCommand, versions) +
|
|
1191
1216
|
generateBuiltTypes(themeDef, iconInfo, variantsFileName);
|
|
@@ -19,11 +19,13 @@ export const doc = {
|
|
|
19
19
|
"via @astryxdesign/core's shared generator (the single source of truth, so the build " +
|
|
20
20
|
'emits the exact CSS the <Theme> runtime does), writes a scoped CSS file, a JS module ' +
|
|
21
21
|
'that re-exports the built theme, and a .d.ts (plus an optional .variants.d.ts when the ' +
|
|
22
|
-
'theme adds custom prop values).
|
|
22
|
+
'theme adds custom prop values). When another build step emits the icon registry, ' +
|
|
23
|
+
'{iconsSpecifier} declares the fully specified module path for the generated JS import. ' +
|
|
24
|
+
'With {check: true} it writes nothing and instead compares ' +
|
|
23
25
|
'each output against disk, returning the drift: the CI guard for committed, generated theme CSS.',
|
|
24
26
|
importPath: '@astryxdesign/cli/api',
|
|
25
27
|
signature:
|
|
26
|
-
'themeBuild(file: string, options?: {out?: string, check?: boolean}, ctx?: {cwd?: string}): Promise<ThemeBuildResponse | ThemeBuildCheckResponse | null>',
|
|
28
|
+
'themeBuild(file: string, options?: {out?: string, check?: boolean, iconsSpecifier?: string}, ctx?: {cwd?: string}): Promise<ThemeBuildResponse | ThemeBuildCheckResponse | null>',
|
|
27
29
|
keywords: [
|
|
28
30
|
'theme',
|
|
29
31
|
'build',
|
|
@@ -54,6 +56,12 @@ export const doc = {
|
|
|
54
56
|
'Compile in memory and compare each output against what is on disk instead of writing: the CI drift guard.',
|
|
55
57
|
default: 'false',
|
|
56
58
|
},
|
|
59
|
+
{
|
|
60
|
+
name: 'options.iconsSpecifier',
|
|
61
|
+
type: 'string',
|
|
62
|
+
description:
|
|
63
|
+
'Override the icon-registry import specifier in the generated JS module, for example ./icons.mjs. When omitted, the source specifier is preserved.',
|
|
64
|
+
},
|
|
57
65
|
{
|
|
58
66
|
name: 'ctx.cwd',
|
|
59
67
|
type: 'string',
|
|
@@ -102,6 +110,10 @@ export const doc = {
|
|
|
102
110
|
label: 'Check for drift (CI)',
|
|
103
111
|
code: "const r = await themeBuild('src/themes/ocean.ts', {check: true});",
|
|
104
112
|
},
|
|
113
|
+
{
|
|
114
|
+
label: 'Use a separately compiled icon registry',
|
|
115
|
+
code: "const r = await themeBuild('src/themes/ocean.ts', {iconsSpecifier: './icons.mjs'});",
|
|
116
|
+
},
|
|
105
117
|
],
|
|
106
118
|
command: 'theme build',
|
|
107
119
|
related: ['themeAdd', 'themeList', 'listThemes'],
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Unit tests for the staged (next-release) codemods.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors v0.3.0/__tests__/next-codemods.test.mjs, which covers the codemods
|
|
7
|
+
* after promotion. Keeping a copy here means a staged transform is tested from
|
|
8
|
+
* the day it is written rather than the day it is released.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {describe, expect, it} from 'vitest';
|
|
12
|
+
import jscodeshift from 'jscodeshift';
|
|
13
|
+
|
|
14
|
+
const j = jscodeshift.withParser('tsx');
|
|
15
|
+
const api = {jscodeshift: j, stats: () => {}, report: () => {}};
|
|
16
|
+
|
|
17
|
+
async function apply(name, source) {
|
|
18
|
+
const {default: transform} = await import(`../${name}.mjs`);
|
|
19
|
+
return transform({source, path: 'test.tsx'}, api) ?? source;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const TRANSFORM = 'rename-dropdown-menu-radio-dot-target';
|
|
23
|
+
|
|
24
|
+
describe('rename-dropdown-menu-radio-dot-target', () => {
|
|
25
|
+
it('renames the theme target key in a defineTheme components map', async () => {
|
|
26
|
+
const input = `import {defineTheme} from '@astryxdesign/core/theme';
|
|
27
|
+
export const theme = defineTheme({
|
|
28
|
+
name: 'brand',
|
|
29
|
+
components: {
|
|
30
|
+
'dropdown-menu-radio-dot': {base: {backgroundColor: 'var(--color-accent)'}},
|
|
31
|
+
},
|
|
32
|
+
});`;
|
|
33
|
+
const output = await apply(TRANSFORM, input);
|
|
34
|
+
expect(output).toContain("'radio-indicator-dot':");
|
|
35
|
+
// The old name survives only inside the TODO comment the rename attaches.
|
|
36
|
+
expect(output).not.toContain("'dropdown-menu-radio-dot':");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('warns that the new target is app-wide, not menu-only', async () => {
|
|
40
|
+
const input = `const components = {
|
|
41
|
+
'dropdown-menu-radio-dot': {base: {width: '10px'}},
|
|
42
|
+
};`;
|
|
43
|
+
const output = await apply(TRANSFORM, input);
|
|
44
|
+
// The rename cannot preserve scope — there is no menu-only dot element
|
|
45
|
+
// left — so the author has to decide, and must be told.
|
|
46
|
+
expect(output).toContain('TODO(astryx upgrade)');
|
|
47
|
+
expect(output).toContain('EVERY radio dot');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('renames the rendered class inside a selector string', async () => {
|
|
51
|
+
const input = `const sel = '.astryx-dropdown-menu-radio-dot';
|
|
52
|
+
const nested = '.astryx-dropdown-menu-radio .astryx-dropdown-menu-radio-dot';`;
|
|
53
|
+
const output = await apply(TRANSFORM, input);
|
|
54
|
+
expect(output).toContain("'.astryx-radio-indicator-dot'");
|
|
55
|
+
expect(output).toContain(
|
|
56
|
+
'.astryx-dropdown-menu-radio .astryx-radio-indicator-dot',
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('renames the class inside a template literal', async () => {
|
|
61
|
+
const input =
|
|
62
|
+
'const css = `.astryx-dropdown-menu-radio-dot { background: ${c}; }`;';
|
|
63
|
+
const output = await apply(TRANSFORM, input);
|
|
64
|
+
expect(output).toContain('.astryx-radio-indicator-dot {');
|
|
65
|
+
expect(output).not.toContain('astryx-dropdown-menu-radio-dot');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('leaves the surviving dropdown-menu-radio target alone', async () => {
|
|
69
|
+
// Only the DOT target was removed; the circle still carries the
|
|
70
|
+
// menu-specific target, so a theme keyed on it must not be rewritten.
|
|
71
|
+
const input = `const components = {
|
|
72
|
+
'dropdown-menu-radio': {base: {borderWidth: '2px'}},
|
|
73
|
+
};`;
|
|
74
|
+
const output = await apply(TRANSFORM, input);
|
|
75
|
+
expect(output).toContain("'dropdown-menu-radio'");
|
|
76
|
+
expect(output).not.toContain('radio-indicator');
|
|
77
|
+
expect(output).not.toContain('TODO(astryx upgrade)');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('is a no-op on files that never mention the target', async () => {
|
|
81
|
+
const input = `const components = {button: {base: {fontWeight: '600'}}};`;
|
|
82
|
+
expect(await apply(TRANSFORM, input)).toBe(input);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('is idempotent', async () => {
|
|
86
|
+
const input = `const components = {
|
|
87
|
+
'dropdown-menu-radio-dot': {base: {width: '10px'}},
|
|
88
|
+
};`;
|
|
89
|
+
const once = await apply(TRANSFORM, input);
|
|
90
|
+
expect(await apply(TRANSFORM, once)).toBe(once);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -7,4 +7,14 @@
|
|
|
7
7
|
* this file into the resolved version folder.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
import renameDropdownMenuRadioDotTarget, {
|
|
11
|
+
meta as renameDropdownMenuRadioDotTargetMeta,
|
|
12
|
+
} from './rename-dropdown-menu-radio-dot-target.mjs';
|
|
13
|
+
|
|
14
|
+
export default [
|
|
15
|
+
{
|
|
16
|
+
name: 'rename-dropdown-menu-radio-dot-target',
|
|
17
|
+
transform: renameDropdownMenuRadioDotTarget,
|
|
18
|
+
meta: renameDropdownMenuRadioDotTargetMeta,
|
|
19
|
+
},
|
|
20
|
+
];
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Codemod: rename the removed `dropdown-menu-radio-dot` theme target
|
|
5
|
+
*
|
|
6
|
+
* The menu radio's dot is no longer drawn by DropdownMenuRadioItem. That row
|
|
7
|
+
* now renders the shared radio indicator, so its dot is the indicator's dot and
|
|
8
|
+
* carries `radio-indicator-dot` (plus the legacy `radio-dot`) instead of the
|
|
9
|
+
* menu-specific `dropdown-menu-radio-dot`, which is gone.
|
|
10
|
+
*
|
|
11
|
+
* Runtime themes are not validated: a theme keyed on the old target keeps
|
|
12
|
+
* compiling and simply stops matching, with no error anywhere. That silence is
|
|
13
|
+
* why this rename needs a codemod rather than a changelog line.
|
|
14
|
+
*
|
|
15
|
+
* The rename is NOT scope-preserving, and that cannot be fixed here — there is
|
|
16
|
+
* no menu-only dot element left to address. `radio-indicator-dot` reaches every
|
|
17
|
+
* radio dot in the app, including RadioList's. So every rewritten site also
|
|
18
|
+
* gets a TODO comment (api.report is a stub; comments are the only warning
|
|
19
|
+
* channel) telling the author to check whether the rule was meant to be
|
|
20
|
+
* menu-only, and pointing at the containing-target route if it was.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export const meta = {
|
|
24
|
+
title: 'Rename the removed dropdown-menu-radio-dot theme target',
|
|
25
|
+
description:
|
|
26
|
+
'Renames the `dropdown-menu-radio-dot` theme target (and the ' +
|
|
27
|
+
'`astryx-dropdown-menu-radio-dot` class it rendered) to ' +
|
|
28
|
+
'`radio-indicator-dot` / `astryx-radio-indicator-dot`. Menu radios draw ' +
|
|
29
|
+
'the shared radio indicator now, so the menu-specific dot target no ' +
|
|
30
|
+
'longer exists. The new target is app-wide rather than menu-only, so each ' +
|
|
31
|
+
'rewritten site gets a TODO comment to confirm that widening is intended.',
|
|
32
|
+
pr: '#4712',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const OLD_TARGET = 'dropdown-menu-radio-dot';
|
|
36
|
+
const NEW_TARGET = 'radio-indicator-dot';
|
|
37
|
+
const OLD_CLASS = `astryx-${OLD_TARGET}`;
|
|
38
|
+
const NEW_CLASS = `astryx-${NEW_TARGET}`;
|
|
39
|
+
|
|
40
|
+
const TODO_COMMENT =
|
|
41
|
+
' TODO(astryx upgrade): `dropdown-menu-radio-dot` became `radio-indicator-dot`,' +
|
|
42
|
+
' which styles EVERY radio dot, not just the ones in a menu. If this rule was' +
|
|
43
|
+
' meant to be menu-only, scope it under the containing `dropdown-menu-radio`' +
|
|
44
|
+
' target instead of this one. ';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Rewrite one string value, or return null when it holds nothing to rename.
|
|
48
|
+
*
|
|
49
|
+
* Handles the target name on its own (a theme's `components` key) and the
|
|
50
|
+
* rendered class inside a larger string (a selector such as
|
|
51
|
+
* `.astryx-dropdown-menu-radio-dot`, or a className list).
|
|
52
|
+
*
|
|
53
|
+
* @param {string} value
|
|
54
|
+
* @returns {string | null}
|
|
55
|
+
*/
|
|
56
|
+
function renameIn(value) {
|
|
57
|
+
if (value === OLD_TARGET) {
|
|
58
|
+
return NEW_TARGET;
|
|
59
|
+
}
|
|
60
|
+
if (value.includes(OLD_CLASS)) {
|
|
61
|
+
return value.split(OLD_CLASS).join(NEW_CLASS);
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {import('../../../../authoring/codemod/type').AstryxCodemodFile} file
|
|
68
|
+
* @param {import('../../../../authoring/codemod/type').CodemodTransformApi} api
|
|
69
|
+
* @returns {string | null | undefined}
|
|
70
|
+
*/
|
|
71
|
+
export default function transformer(file, api) {
|
|
72
|
+
// Cheap bail-out: most files in a consumer repo mention neither name.
|
|
73
|
+
if (!file.source.includes(OLD_TARGET)) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const j = api.jscodeshift;
|
|
78
|
+
const root = j(file.source);
|
|
79
|
+
let hasChanges = false;
|
|
80
|
+
|
|
81
|
+
/** Attach the widening warning once to the nearest statement-ish node. */
|
|
82
|
+
function attachTodo(/** @type {any} */ path) {
|
|
83
|
+
// The property (or its statement) reads better than the bare literal, and
|
|
84
|
+
// matches where a human would look for the note.
|
|
85
|
+
const host =
|
|
86
|
+
path.parent?.node?.type === 'ObjectProperty' ||
|
|
87
|
+
path.parent?.node?.type === 'Property'
|
|
88
|
+
? path.parent.node
|
|
89
|
+
: path.node;
|
|
90
|
+
if (!host.comments) {
|
|
91
|
+
host.comments = [];
|
|
92
|
+
}
|
|
93
|
+
if (
|
|
94
|
+
host.comments.some((/** @type {any} */ c) => c.value === TODO_COMMENT)
|
|
95
|
+
) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
host.comments.push(j.commentBlock(TODO_COMMENT, true, false));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
root
|
|
102
|
+
.find(j.StringLiteral)
|
|
103
|
+
.forEach((/** @type {any} */ path) => {
|
|
104
|
+
const renamed = renameIn(path.node.value);
|
|
105
|
+
if (renamed == null) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
path.node.value = renamed;
|
|
109
|
+
attachTodo(path);
|
|
110
|
+
hasChanges = true;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Older parsers surface string literals as `Literal`.
|
|
114
|
+
root.find(j.Literal).forEach((/** @type {any} */ path) => {
|
|
115
|
+
if (typeof path.node.value !== 'string') {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const renamed = renameIn(path.node.value);
|
|
119
|
+
if (renamed == null) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
path.node.value = renamed;
|
|
123
|
+
if (typeof path.node.raw === 'string') {
|
|
124
|
+
path.node.raw = path.node.raw
|
|
125
|
+
.split(OLD_TARGET)
|
|
126
|
+
.join(NEW_TARGET);
|
|
127
|
+
}
|
|
128
|
+
attachTodo(path);
|
|
129
|
+
hasChanges = true;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Template literals carry the class in CSS strings: `.${OLD_CLASS} > span`.
|
|
133
|
+
root.find(j.TemplateElement).forEach((/** @type {any} */ path) => {
|
|
134
|
+
const cooked = path.node.value?.cooked;
|
|
135
|
+
if (typeof cooked !== 'string') {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const renamed = renameIn(cooked);
|
|
139
|
+
if (renamed == null) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
path.node.value.cooked = renamed;
|
|
143
|
+
path.node.value.raw = path.node.value.raw
|
|
144
|
+
.split(OLD_CLASS)
|
|
145
|
+
.join(NEW_CLASS)
|
|
146
|
+
.split(OLD_TARGET)
|
|
147
|
+
.join(NEW_TARGET);
|
|
148
|
+
hasChanges = true;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return hasChanges ? root.toSource({quote: 'single'}) : undefined;
|
|
152
|
+
}
|
|
@@ -21,7 +21,7 @@ export const docs = {
|
|
|
21
21
|
type: 'code',
|
|
22
22
|
lang: 'text',
|
|
23
23
|
label: 'Paste this into your AI',
|
|
24
|
-
code: 'Install @astryxdesign/core, @astryxdesign/theme-neutral, and @astryxdesign/cli in this project, then run `npx @astryxdesign/cli init` to set up agent docs. Read the generated files to learn the conventions.',
|
|
24
|
+
code: 'Install @astryxdesign/core, @stylexjs/stylex, @astryxdesign/theme-neutral, and @astryxdesign/cli in this project, then run `npx @astryxdesign/cli init` to set up agent docs. Read the generated files to learn the conventions.',
|
|
25
25
|
},
|
|
26
26
|
],
|
|
27
27
|
},
|
|
@@ -34,13 +34,13 @@ export const docs = {
|
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
type: 'prose',
|
|
37
|
-
text: 'Add the core package, a theme
|
|
37
|
+
text: 'Add the core package and its `@stylexjs/stylex` peer dependency, plus a theme and the CLI.',
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
type: 'code',
|
|
41
41
|
lang: 'bash',
|
|
42
42
|
label: 'Terminal',
|
|
43
|
-
code: `npm install @astryxdesign/core @astryxdesign/theme-neutral @astryxdesign/cli`,
|
|
43
|
+
code: `npm install @astryxdesign/core @stylexjs/stylex @astryxdesign/theme-neutral @astryxdesign/cli`,
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
type: 'prose',
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
|
|
4
|
+
export const doc = {
|
|
5
|
+
type: 'block',
|
|
6
|
+
exampleFor: 'Stepper',
|
|
7
|
+
name: 'Stepper — Horizontal Progress',
|
|
8
|
+
displayName: 'Stepper — Horizontal Progress',
|
|
9
|
+
description:
|
|
10
|
+
'A horizontal separated stepper: each step owns a segment of the progress bar above its label. Filled segments track how far the flow has progressed.',
|
|
11
|
+
isReady: true,
|
|
12
|
+
aspectRatio: 16 / 9,
|
|
13
|
+
componentsUsed: ['Stepper'],
|
|
14
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
import {useState} from 'react';
|
|
6
|
+
import {Stepper, Step} from '@astryxdesign/lab';
|
|
7
|
+
|
|
8
|
+
export default function StepperHorizontal() {
|
|
9
|
+
const [active, setActive] = useState(1);
|
|
10
|
+
return (
|
|
11
|
+
<div style={{width: '100%', maxWidth: 600}}>
|
|
12
|
+
<Stepper
|
|
13
|
+
activeStep={active}
|
|
14
|
+
orientation="horizontal"
|
|
15
|
+
onStepClick={setActive}>
|
|
16
|
+
<Step step={0} label="Workspace" />
|
|
17
|
+
<Step step={1} label="Team" />
|
|
18
|
+
<Step step={2} label="Integrations" />
|
|
19
|
+
<Step step={3} label="Import" />
|
|
20
|
+
<Step step={4} label="Launch" />
|
|
21
|
+
</Stepper>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
|
|
4
|
+
export const doc = {
|
|
5
|
+
type: 'block',
|
|
6
|
+
exampleFor: 'Stepper',
|
|
7
|
+
name: 'Stepper — Indicator Modes',
|
|
8
|
+
displayName: 'Stepper — Indicator Modes',
|
|
9
|
+
description:
|
|
10
|
+
'The indicator prop side by side: auto (check when done, ring when current, number ahead), always-number, and a custom icon per step.',
|
|
11
|
+
isReady: true,
|
|
12
|
+
aspectRatio: 4 / 3,
|
|
13
|
+
componentsUsed: ['Stepper', 'Text', 'Icon'],
|
|
14
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
import {useState} from 'react';
|
|
6
|
+
import {Stepper, Step} from '@astryxdesign/lab';
|
|
7
|
+
import {Text} from '@astryxdesign/core/Text';
|
|
8
|
+
import {Icon} from '@astryxdesign/core/Icon';
|
|
9
|
+
|
|
10
|
+
export default function StepperIndicatorModes() {
|
|
11
|
+
const [active, setActive] = useState(2);
|
|
12
|
+
return (
|
|
13
|
+
<div style={{display: 'flex', gap: 48, flexWrap: 'wrap'}}>
|
|
14
|
+
<div style={{maxWidth: 220}}>
|
|
15
|
+
<Text type="label">Auto</Text>
|
|
16
|
+
<Stepper
|
|
17
|
+
activeStep={active}
|
|
18
|
+
orientation="vertical"
|
|
19
|
+
onStepClick={setActive}>
|
|
20
|
+
<Step step={0} label="Account" />
|
|
21
|
+
<Step step={1} label="Profile" />
|
|
22
|
+
<Step step={2} label="Settings" />
|
|
23
|
+
<Step step={3} label="Review" />
|
|
24
|
+
</Stepper>
|
|
25
|
+
</div>
|
|
26
|
+
<div style={{maxWidth: 220}}>
|
|
27
|
+
<Text type="label">Number</Text>
|
|
28
|
+
<Stepper
|
|
29
|
+
activeStep={active}
|
|
30
|
+
orientation="vertical"
|
|
31
|
+
onStepClick={setActive}>
|
|
32
|
+
<Step step={0} label="Account" indicator="number" />
|
|
33
|
+
<Step step={1} label="Profile" indicator="number" />
|
|
34
|
+
<Step step={2} label="Settings" indicator="number" />
|
|
35
|
+
<Step step={3} label="Review" indicator="number" />
|
|
36
|
+
</Stepper>
|
|
37
|
+
</div>
|
|
38
|
+
<div style={{maxWidth: 220}}>
|
|
39
|
+
<Text type="label">Custom icon</Text>
|
|
40
|
+
<Stepper
|
|
41
|
+
activeStep={active}
|
|
42
|
+
orientation="vertical"
|
|
43
|
+
onStepClick={setActive}>
|
|
44
|
+
<Step
|
|
45
|
+
step={0}
|
|
46
|
+
label="Account"
|
|
47
|
+
icon={<Icon icon="info" size="sm" />}
|
|
48
|
+
/>
|
|
49
|
+
<Step
|
|
50
|
+
step={1}
|
|
51
|
+
label="Profile"
|
|
52
|
+
icon={<Icon icon="search" size="sm" />}
|
|
53
|
+
/>
|
|
54
|
+
<Step
|
|
55
|
+
step={2}
|
|
56
|
+
label="Settings"
|
|
57
|
+
icon={<Icon icon="wrench" size="sm" />}
|
|
58
|
+
/>
|
|
59
|
+
<Step
|
|
60
|
+
step={3}
|
|
61
|
+
label="Review"
|
|
62
|
+
icon={<Icon icon="check" size="sm" />}
|
|
63
|
+
/>
|
|
64
|
+
</Stepper>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
|
|
4
|
+
export const doc = {
|
|
5
|
+
type: 'block',
|
|
6
|
+
exampleFor: 'Stepper',
|
|
7
|
+
name: 'Stepper — Multi-Step Form',
|
|
8
|
+
displayName: 'Stepper — Multi-Step Form',
|
|
9
|
+
description:
|
|
10
|
+
'A vertical stepper driving a multi-step form. Each step renders its own fields in the content slot for the active step, with Back/Continue buttons advancing activeStep.',
|
|
11
|
+
isReady: true,
|
|
12
|
+
aspectRatio: 4 / 3,
|
|
13
|
+
componentsUsed: ['Stepper', 'TextInput', 'Button', 'Text'],
|
|
14
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
import {useState} from 'react';
|
|
6
|
+
import {Stepper, Step} from '@astryxdesign/lab';
|
|
7
|
+
import {TextInput} from '@astryxdesign/core/TextInput';
|
|
8
|
+
import {Button} from '@astryxdesign/core/Button';
|
|
9
|
+
import {Text} from '@astryxdesign/core/Text';
|
|
10
|
+
|
|
11
|
+
export default function StepperMultiStepForm() {
|
|
12
|
+
const [active, setActive] = useState(0);
|
|
13
|
+
return (
|
|
14
|
+
<div style={{width: '100%', maxWidth: 480}}>
|
|
15
|
+
<Stepper
|
|
16
|
+
activeStep={active}
|
|
17
|
+
orientation="vertical"
|
|
18
|
+
onStepClick={setActive}>
|
|
19
|
+
<Step step={0} label="Project details" indicator="number">
|
|
20
|
+
{active === 0 && (
|
|
21
|
+
<div style={{display: 'flex', flexDirection: 'column', gap: 12}}>
|
|
22
|
+
<TextInput
|
|
23
|
+
label="Project name"
|
|
24
|
+
placeholder="My awesome project"
|
|
25
|
+
value=""
|
|
26
|
+
/>
|
|
27
|
+
<TextInput
|
|
28
|
+
label="Repository URL"
|
|
29
|
+
placeholder="https://github.com/..."
|
|
30
|
+
value=""
|
|
31
|
+
/>
|
|
32
|
+
<div>
|
|
33
|
+
<Button
|
|
34
|
+
label="Continue"
|
|
35
|
+
variant="primary"
|
|
36
|
+
onClick={() => setActive(1)}
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
)}
|
|
41
|
+
</Step>
|
|
42
|
+
<Step step={1} label="Environment" indicator="number">
|
|
43
|
+
{active === 1 && (
|
|
44
|
+
<div style={{display: 'flex', flexDirection: 'column', gap: 12}}>
|
|
45
|
+
<TextInput label="Node version" placeholder="20" value="" />
|
|
46
|
+
<TextInput
|
|
47
|
+
label="Build command"
|
|
48
|
+
placeholder="npm run build"
|
|
49
|
+
value=""
|
|
50
|
+
/>
|
|
51
|
+
<div style={{display: 'flex', gap: 8}}>
|
|
52
|
+
<Button
|
|
53
|
+
label="Back"
|
|
54
|
+
variant="secondary"
|
|
55
|
+
onClick={() => setActive(0)}
|
|
56
|
+
/>
|
|
57
|
+
<Button
|
|
58
|
+
label="Continue"
|
|
59
|
+
variant="primary"
|
|
60
|
+
onClick={() => setActive(2)}
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
</Step>
|
|
66
|
+
<Step step={2} label="Deploy" indicator="number">
|
|
67
|
+
{active === 2 && (
|
|
68
|
+
<div style={{display: 'flex', flexDirection: 'column', gap: 12}}>
|
|
69
|
+
<Text type="body">
|
|
70
|
+
Ready to deploy. This creates a production build and pushes to
|
|
71
|
+
your configured hosting.
|
|
72
|
+
</Text>
|
|
73
|
+
<div style={{display: 'flex', gap: 8}}>
|
|
74
|
+
<Button
|
|
75
|
+
label="Back"
|
|
76
|
+
variant="secondary"
|
|
77
|
+
onClick={() => setActive(1)}
|
|
78
|
+
/>
|
|
79
|
+
<Button
|
|
80
|
+
label="Deploy now"
|
|
81
|
+
variant="primary"
|
|
82
|
+
onClick={() => setActive(3)}
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
</Step>
|
|
88
|
+
<Step step={3} label="Done" indicator="number" />
|
|
89
|
+
</Stepper>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
|
|
4
|
+
export const doc = {
|
|
5
|
+
type: 'block',
|
|
6
|
+
exampleFor: 'Stepper',
|
|
7
|
+
name: 'Stepper — On-Track Vertical',
|
|
8
|
+
displayName: 'Stepper — On-Track Vertical',
|
|
9
|
+
description:
|
|
10
|
+
'The on-track layout in vertical orientation: indicators sit inline on a continuous connector rail, with each label and description beside its node.',
|
|
11
|
+
isReady: true,
|
|
12
|
+
aspectRatio: 4 / 3,
|
|
13
|
+
componentsUsed: ['Stepper'],
|
|
14
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
import {useState} from 'react';
|
|
6
|
+
import {Stepper, Step} from '@astryxdesign/lab';
|
|
7
|
+
|
|
8
|
+
export default function StepperOnTrackVertical() {
|
|
9
|
+
const [active, setActive] = useState(2);
|
|
10
|
+
return (
|
|
11
|
+
<div style={{width: '100%', maxWidth: 400}}>
|
|
12
|
+
<Stepper
|
|
13
|
+
activeStep={active}
|
|
14
|
+
orientation="vertical"
|
|
15
|
+
indicatorPosition="on-track"
|
|
16
|
+
onStepClick={setActive}>
|
|
17
|
+
<Step
|
|
18
|
+
step={0}
|
|
19
|
+
label="Create workspace"
|
|
20
|
+
description="Name and configure your workspace"
|
|
21
|
+
/>
|
|
22
|
+
<Step
|
|
23
|
+
step={1}
|
|
24
|
+
label="Invite team members"
|
|
25
|
+
description="Add collaborators by email"
|
|
26
|
+
/>
|
|
27
|
+
<Step
|
|
28
|
+
step={2}
|
|
29
|
+
label="Set up integrations"
|
|
30
|
+
description="Connect Slack, GitHub, Jira"
|
|
31
|
+
/>
|
|
32
|
+
<Step
|
|
33
|
+
step={3}
|
|
34
|
+
label="Import data"
|
|
35
|
+
description="Bring in existing projects"
|
|
36
|
+
/>
|
|
37
|
+
<Step step={4} label="Launch" description="Go live with your team" />
|
|
38
|
+
</Stepper>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
|
|
4
|
+
export const doc = {
|
|
5
|
+
type: 'block',
|
|
6
|
+
exampleFor: 'Stepper',
|
|
7
|
+
name: 'Stepper — Checkout Progress',
|
|
8
|
+
displayName: 'Stepper — Checkout Progress',
|
|
9
|
+
description:
|
|
10
|
+
'A horizontal on-track stepper for a checkout flow: numbered nodes sit on a continuous connector, with completed and current steps filled. Click any step to jump.',
|
|
11
|
+
isReady: true,
|
|
12
|
+
isShowcase: true,
|
|
13
|
+
aspectRatio: 16 / 9,
|
|
14
|
+
componentsUsed: ['Stepper'],
|
|
15
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
import {useState} from 'react';
|
|
6
|
+
import {Stepper, Step} from '@astryxdesign/lab';
|
|
7
|
+
|
|
8
|
+
export default function StepperShowcase() {
|
|
9
|
+
const [active, setActive] = useState(2);
|
|
10
|
+
return (
|
|
11
|
+
<div style={{width: '100%', maxWidth: 640}}>
|
|
12
|
+
<Stepper
|
|
13
|
+
activeStep={active}
|
|
14
|
+
orientation="horizontal"
|
|
15
|
+
indicatorPosition="on-track"
|
|
16
|
+
onStepClick={setActive}>
|
|
17
|
+
<Step step={0} label="Cart" indicator="number" />
|
|
18
|
+
<Step step={1} label="Shipping" indicator="number" />
|
|
19
|
+
<Step step={2} label="Payment" indicator="number" />
|
|
20
|
+
<Step step={3} label="Review" indicator="number" />
|
|
21
|
+
<Step step={4} label="Confirm" indicator="number" />
|
|
22
|
+
</Stepper>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
|
|
4
|
+
export const doc = {
|
|
5
|
+
type: 'block',
|
|
6
|
+
exampleFor: 'Stepper',
|
|
7
|
+
name: 'Stepper — Validation Status',
|
|
8
|
+
displayName: 'Stepper — Validation Status',
|
|
9
|
+
description:
|
|
10
|
+
'Semantic status per step in a verification flow: success shows a green check, error a red glyph, accent the in-progress step. Status sets the indicator color and glyph only, never the connector, and is announced to assistive tech as text.',
|
|
11
|
+
isReady: true,
|
|
12
|
+
aspectRatio: 4 / 3,
|
|
13
|
+
componentsUsed: ['Stepper'],
|
|
14
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
import {useState} from 'react';
|
|
6
|
+
import {Stepper, Step} from '@astryxdesign/lab';
|
|
7
|
+
|
|
8
|
+
export default function StepperStatus() {
|
|
9
|
+
const [active, setActive] = useState(3);
|
|
10
|
+
return (
|
|
11
|
+
<div style={{width: '100%', maxWidth: 400}}>
|
|
12
|
+
<Stepper
|
|
13
|
+
activeStep={active}
|
|
14
|
+
orientation="vertical"
|
|
15
|
+
onStepClick={setActive}>
|
|
16
|
+
<Step
|
|
17
|
+
step={0}
|
|
18
|
+
label="Email verified"
|
|
19
|
+
description="you@example.com"
|
|
20
|
+
status="success"
|
|
21
|
+
/>
|
|
22
|
+
<Step
|
|
23
|
+
step={1}
|
|
24
|
+
label="Phone verified"
|
|
25
|
+
description="+1 (555) 012-3456"
|
|
26
|
+
status="success"
|
|
27
|
+
/>
|
|
28
|
+
<Step
|
|
29
|
+
step={2}
|
|
30
|
+
label="Identity document"
|
|
31
|
+
description="Passport upload failed"
|
|
32
|
+
status="error"
|
|
33
|
+
/>
|
|
34
|
+
<Step
|
|
35
|
+
step={3}
|
|
36
|
+
label="Address verification"
|
|
37
|
+
description="Pending review"
|
|
38
|
+
status="accent"
|
|
39
|
+
/>
|
|
40
|
+
<Step
|
|
41
|
+
step={4}
|
|
42
|
+
label="Background check"
|
|
43
|
+
isOptional
|
|
44
|
+
description="Skipped"
|
|
45
|
+
/>
|
|
46
|
+
<Step step={5} label="Account activated" />
|
|
47
|
+
</Stepper>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
|
|
4
|
+
export const doc = {
|
|
5
|
+
type: 'block',
|
|
6
|
+
exampleFor: 'Stepper',
|
|
7
|
+
name: 'Stepper — Vertical Onboarding',
|
|
8
|
+
displayName: 'Stepper — Vertical Onboarding',
|
|
9
|
+
description:
|
|
10
|
+
'A vertical stepper for an onboarding flow, with a label and description per step. The auto indicator shows a check for completed steps, a ring for the current step, and a number for upcoming ones.',
|
|
11
|
+
isReady: true,
|
|
12
|
+
aspectRatio: 4 / 3,
|
|
13
|
+
componentsUsed: ['Stepper'],
|
|
14
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
import {useState} from 'react';
|
|
6
|
+
import {Stepper, Step} from '@astryxdesign/lab';
|
|
7
|
+
|
|
8
|
+
export default function StepperVerticalOnboarding() {
|
|
9
|
+
const [active, setActive] = useState(2);
|
|
10
|
+
return (
|
|
11
|
+
<div style={{width: '100%', maxWidth: 400}}>
|
|
12
|
+
<Stepper
|
|
13
|
+
activeStep={active}
|
|
14
|
+
orientation="vertical"
|
|
15
|
+
onStepClick={setActive}>
|
|
16
|
+
<Step
|
|
17
|
+
step={0}
|
|
18
|
+
label="Create workspace"
|
|
19
|
+
description="Name and configure your workspace"
|
|
20
|
+
/>
|
|
21
|
+
<Step
|
|
22
|
+
step={1}
|
|
23
|
+
label="Invite team members"
|
|
24
|
+
description="Add collaborators by email"
|
|
25
|
+
/>
|
|
26
|
+
<Step
|
|
27
|
+
step={2}
|
|
28
|
+
label="Set up integrations"
|
|
29
|
+
description="Connect Slack, GitHub, Jira"
|
|
30
|
+
/>
|
|
31
|
+
<Step
|
|
32
|
+
step={3}
|
|
33
|
+
label="Import data"
|
|
34
|
+
description="Bring in existing projects"
|
|
35
|
+
/>
|
|
36
|
+
<Step step={4} label="Launch" description="Go live with your team" />
|
|
37
|
+
</Stepper>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -260,6 +260,8 @@ export interface ComponentSlotElement {
|
|
|
260
260
|
* { property: 'padding', vars: ['--_card-padding'] },
|
|
261
261
|
* ```
|
|
262
262
|
*/
|
|
263
|
+
// SYNC: apps/docsite/scripts/generate-data.mjs (interface DerivedVar) — see the
|
|
264
|
+
// note on ComponentThemingTarget below.
|
|
263
265
|
export interface ComponentThemingDerivedVar {
|
|
264
266
|
/** The standard CSS property name (camelCase) that theme authors write.
|
|
265
267
|
* e.g. `'borderRadius'`, `'padding'`, `'paddingBlock'` */
|
|
@@ -291,6 +293,10 @@ export interface ComponentThemingDerivedVar {
|
|
|
291
293
|
* {className: 'astryx-card'}
|
|
292
294
|
* ```
|
|
293
295
|
*/
|
|
296
|
+
// SYNC: When adding a field here, add it to the docsite's generated-registry
|
|
297
|
+
// copy too — apps/docsite/scripts/generate-data.mjs (interface ThemingTarget).
|
|
298
|
+
// `next build` type-checks the emitted componentRegistry.ts against that copy,
|
|
299
|
+
// so a field present in a .doc.mjs but missing there fails the docsite build.
|
|
294
300
|
export interface ComponentThemingTarget {
|
|
295
301
|
/** The stable CSS class name rendered by the component.
|
|
296
302
|
* Always starts with `astryx-`.
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Regression tests for `astryx theme build --icons-specifier`.
|
|
5
|
+
*
|
|
6
|
+
* The generated module imports the icon registry rather than inlining it,
|
|
7
|
+
* because the registry holds React elements. `extractIconInfo` lifts that
|
|
8
|
+
* specifier out of the TypeScript source, where an extensionless `./icons` is
|
|
9
|
+
* resolved by the TypeScript resolver — but the artifact is ESM JavaScript,
|
|
10
|
+
* which requires a fully specified path, so Node cannot load it. See #4620.
|
|
11
|
+
*
|
|
12
|
+
* Only the caller knows what its own build emits and under what name (the same
|
|
13
|
+
* source compiled by tsup lands at `icons.mjs` in a package with no `"type"`
|
|
14
|
+
* field and at `icons.js` in one with `"type": "module"`), so the specifier is
|
|
15
|
+
* declared rather than inferred. Absent the flag, output is byte-for-byte what
|
|
16
|
+
* it was before, which keeps the default no-`--out` flow — where the neighbour
|
|
17
|
+
* is an uncompiled `icons.tsx` that only a bundler can resolve — working.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {describe, it, expect, beforeAll, beforeEach, afterEach} from 'vitest';
|
|
21
|
+
import * as fs from 'node:fs';
|
|
22
|
+
import * as path from 'node:path';
|
|
23
|
+
import * as os from 'node:os';
|
|
24
|
+
import {ensureCoreBuilt} from './ensure-core-built.mjs';
|
|
25
|
+
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The emitted icon import, or null. Reads the statement rather than the whole
|
|
29
|
+
* file: the `@generated` header quotes the source filename and a usage example,
|
|
30
|
+
* so a substring search over the file matches comment text too.
|
|
31
|
+
*/
|
|
32
|
+
function iconImportLine(generated) {
|
|
33
|
+
const match = generated.match(/^import .*$/m);
|
|
34
|
+
return match ? match[0] : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A theme whose registry arrives via a relative import, plus that module. */
|
|
38
|
+
function writeThemeWithIcons(dir, name) {
|
|
39
|
+
fs.mkdirSync(dir, {recursive: true});
|
|
40
|
+
fs.writeFileSync(
|
|
41
|
+
path.join(dir, 'icons.mjs'),
|
|
42
|
+
'export const testIcons = {};\n',
|
|
43
|
+
);
|
|
44
|
+
const file = path.join(dir, `${name}.mjs`);
|
|
45
|
+
fs.writeFileSync(
|
|
46
|
+
file,
|
|
47
|
+
`import {testIcons} from './icons';\n` +
|
|
48
|
+
`export default {\n` +
|
|
49
|
+
` name: ${JSON.stringify(name)},\n` +
|
|
50
|
+
` icons: testIcons,\n` +
|
|
51
|
+
` tokens: {'--color-bg': '#fff'},\n` +
|
|
52
|
+
`};\n`,
|
|
53
|
+
);
|
|
54
|
+
return file;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
beforeAll(() => {
|
|
58
|
+
ensureCoreBuilt();
|
|
59
|
+
}, 200_000);
|
|
60
|
+
|
|
61
|
+
let tmpDir;
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-icons-specifier-'));
|
|
64
|
+
});
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('theme build --icons-specifier', () => {
|
|
70
|
+
it('emits the declared specifier', async () => {
|
|
71
|
+
const project = path.join(tmpDir, 'project');
|
|
72
|
+
const themeFile = writeThemeWithIcons(project, 'declared');
|
|
73
|
+
|
|
74
|
+
const result = await runCli(
|
|
75
|
+
[
|
|
76
|
+
'theme',
|
|
77
|
+
'build',
|
|
78
|
+
path.relative(project, themeFile),
|
|
79
|
+
'--icons-specifier',
|
|
80
|
+
'./icons.mjs',
|
|
81
|
+
],
|
|
82
|
+
project,
|
|
83
|
+
);
|
|
84
|
+
expect(result.code).toBe(0);
|
|
85
|
+
|
|
86
|
+
const generated = fs.readFileSync(
|
|
87
|
+
path.join(project, 'declared.js'),
|
|
88
|
+
'utf8',
|
|
89
|
+
);
|
|
90
|
+
expect(iconImportLine(generated)).toBe(
|
|
91
|
+
'import { testIcons } from "./icons.mjs";',
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('accepts a specifier the generator could never have inferred', async () => {
|
|
96
|
+
const project = path.join(tmpDir, 'project');
|
|
97
|
+
const themeFile = writeThemeWithIcons(project, 'custom');
|
|
98
|
+
|
|
99
|
+
const result = await runCli(
|
|
100
|
+
[
|
|
101
|
+
'theme',
|
|
102
|
+
'build',
|
|
103
|
+
path.relative(project, themeFile),
|
|
104
|
+
'--icons-specifier',
|
|
105
|
+
'../build/registry/icons.js',
|
|
106
|
+
],
|
|
107
|
+
project,
|
|
108
|
+
);
|
|
109
|
+
expect(result.code).toBe(0);
|
|
110
|
+
|
|
111
|
+
const generated = fs.readFileSync(path.join(project, 'custom.js'), 'utf8');
|
|
112
|
+
expect(iconImportLine(generated)).toBe(
|
|
113
|
+
'import { testIcons } from "../build/registry/icons.js";',
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('encodes the declared specifier as a valid JavaScript string', async () => {
|
|
118
|
+
const project = path.join(tmpDir, 'project');
|
|
119
|
+
const themeFile = writeThemeWithIcons(project, 'encoded');
|
|
120
|
+
|
|
121
|
+
const result = await runCli(
|
|
122
|
+
[
|
|
123
|
+
'theme',
|
|
124
|
+
'build',
|
|
125
|
+
path.relative(project, themeFile),
|
|
126
|
+
'--icons-specifier',
|
|
127
|
+
"./icon's.mjs",
|
|
128
|
+
],
|
|
129
|
+
project,
|
|
130
|
+
);
|
|
131
|
+
expect(result.code).toBe(0);
|
|
132
|
+
|
|
133
|
+
const generated = fs.readFileSync(path.join(project, 'encoded.js'), 'utf8');
|
|
134
|
+
expect(iconImportLine(generated)).toBe(
|
|
135
|
+
'import { testIcons } from "./icon\'s.mjs";',
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('combines with the current --check flow', async () => {
|
|
140
|
+
const project = path.join(tmpDir, 'project');
|
|
141
|
+
const themeFile = writeThemeWithIcons(project, 'checked');
|
|
142
|
+
const relativeTheme = path.relative(project, themeFile);
|
|
143
|
+
|
|
144
|
+
const built = await runCli(
|
|
145
|
+
[
|
|
146
|
+
'theme',
|
|
147
|
+
'build',
|
|
148
|
+
relativeTheme,
|
|
149
|
+
'--icons-specifier',
|
|
150
|
+
'./icons.mjs',
|
|
151
|
+
],
|
|
152
|
+
project,
|
|
153
|
+
);
|
|
154
|
+
expect(built.code).toBe(0);
|
|
155
|
+
|
|
156
|
+
const withoutSpecifier = await runCli(
|
|
157
|
+
['theme', 'build', relativeTheme, '--check'],
|
|
158
|
+
project,
|
|
159
|
+
);
|
|
160
|
+
expect(withoutSpecifier.code).toBe(1);
|
|
161
|
+
|
|
162
|
+
const withSpecifier = await runCli(
|
|
163
|
+
[
|
|
164
|
+
'theme',
|
|
165
|
+
'build',
|
|
166
|
+
relativeTheme,
|
|
167
|
+
'--check',
|
|
168
|
+
'--icons-specifier',
|
|
169
|
+
'./icons.mjs',
|
|
170
|
+
],
|
|
171
|
+
project,
|
|
172
|
+
);
|
|
173
|
+
expect(withSpecifier.code).toBe(0);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('leaves the scraped specifier untouched when the flag is absent', async () => {
|
|
177
|
+
const project = path.join(tmpDir, 'project');
|
|
178
|
+
const themeFile = writeThemeWithIcons(project, 'untouched');
|
|
179
|
+
|
|
180
|
+
const result = await runCli(
|
|
181
|
+
['theme', 'build', path.relative(project, themeFile)],
|
|
182
|
+
project,
|
|
183
|
+
);
|
|
184
|
+
expect(result.code).toBe(0);
|
|
185
|
+
|
|
186
|
+
// The pre-flag behaviour, preserved: no extension is invented. In this
|
|
187
|
+
// layout the neighbour is a source file only a bundler can resolve, so
|
|
188
|
+
// adding one would break a build that works today.
|
|
189
|
+
const generated = fs.readFileSync(
|
|
190
|
+
path.join(project, 'untouched.js'),
|
|
191
|
+
'utf8',
|
|
192
|
+
);
|
|
193
|
+
expect(iconImportLine(generated)).toBe(
|
|
194
|
+
"import { testIcons } from './icons';",
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('is inert for a theme with no icons field', async () => {
|
|
199
|
+
const project = path.join(tmpDir, 'project');
|
|
200
|
+
fs.mkdirSync(project, {recursive: true});
|
|
201
|
+
const themeFile = path.join(project, 'plain.mjs');
|
|
202
|
+
fs.writeFileSync(
|
|
203
|
+
themeFile,
|
|
204
|
+
`export default {name: 'plain', tokens: {'--color-bg': '#fff'}};\n`,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const result = await runCli(
|
|
208
|
+
[
|
|
209
|
+
'theme',
|
|
210
|
+
'build',
|
|
211
|
+
path.relative(project, themeFile),
|
|
212
|
+
'--icons-specifier',
|
|
213
|
+
'./icons.mjs',
|
|
214
|
+
],
|
|
215
|
+
project,
|
|
216
|
+
);
|
|
217
|
+
expect(result.code).toBe(0);
|
|
218
|
+
|
|
219
|
+
// No icons field means no import to rewrite, so the flag has nothing to act
|
|
220
|
+
// on and must not introduce one.
|
|
221
|
+
const generated = fs.readFileSync(path.join(project, 'plain.js'), 'utf8');
|
|
222
|
+
expect(iconImportLine(generated)).toBeNull();
|
|
223
|
+
expect(generated).not.toContain('icons:');
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -57,13 +57,15 @@ function resolveCliBin() {
|
|
|
57
57
|
* Resolves with the child's exit code; never rejects.
|
|
58
58
|
*
|
|
59
59
|
* @param {string} file - The theme file argument, as the user passed it.
|
|
60
|
-
* @param {{out?: string}} options - Parsed command
|
|
60
|
+
* @param {{out?: string, iconsSpecifier?: string}} options - Parsed command
|
|
61
|
+
* options that affect generated output.
|
|
61
62
|
* @returns {Promise<number>}
|
|
62
63
|
*/
|
|
63
64
|
function runThemeBuildOnceChild(file, options) {
|
|
64
65
|
const cliBin = resolveCliBin();
|
|
65
66
|
const args = [cliBin, 'theme', 'build', file];
|
|
66
67
|
if (options.out) args.push('--out', options.out);
|
|
68
|
+
if (options.iconsSpecifier) args.push('--icons-specifier', options.iconsSpecifier);
|
|
67
69
|
return new Promise((/** @type {(code: number) => void} */ resolve) => {
|
|
68
70
|
const child = spawn(process.execPath, args, {
|
|
69
71
|
stdio: 'inherit',
|
|
@@ -83,7 +85,7 @@ function runThemeBuildOnceChild(file, options) {
|
|
|
83
85
|
*
|
|
84
86
|
* @param {string} file - The theme file argument, as the user passed it.
|
|
85
87
|
* @param {string} filePath - Absolute path to the theme file.
|
|
86
|
-
* @param {{out?: string}} options - Parsed command options.
|
|
88
|
+
* @param {{out?: string, iconsSpecifier?: string}} options - Parsed command options.
|
|
87
89
|
* @returns {Promise<void>} Resolves when the watcher is stopped (Ctrl-C).
|
|
88
90
|
*/
|
|
89
91
|
async function runThemeBuildWatch(file, filePath, options) {
|
|
@@ -204,7 +206,7 @@ export function registerTheme(program) {
|
|
|
204
206
|
fn: themeBuildFn,
|
|
205
207
|
action: async (
|
|
206
208
|
/** @type {string} */ file,
|
|
207
|
-
/** @type {{out?: string, watch?: boolean, check?: boolean}} */ options,
|
|
209
|
+
/** @type {{out?: string, watch?: boolean, check?: boolean, iconsSpecifier?: string}} */ options,
|
|
208
210
|
) => {
|
|
209
211
|
const filePath = path.resolve(process.cwd(), file);
|
|
210
212
|
const json = program.opts().json || false;
|
|
@@ -247,7 +249,11 @@ export function registerTheme(program) {
|
|
|
247
249
|
try {
|
|
248
250
|
const result = await themeBuild(
|
|
249
251
|
file,
|
|
250
|
-
{
|
|
252
|
+
{
|
|
253
|
+
out: options.out,
|
|
254
|
+
check: options.check,
|
|
255
|
+
iconsSpecifier: options.iconsSpecifier,
|
|
256
|
+
},
|
|
251
257
|
{cwd: process.cwd()},
|
|
252
258
|
);
|
|
253
259
|
if (json && result) jsonOut(result);
|
|
@@ -18,11 +18,19 @@ export const doc = {
|
|
|
18
18
|
description:
|
|
19
19
|
'Compiles a file that calls defineTheme() into a scoped CSS file, a JS module, and ' +
|
|
20
20
|
'type declarations: the exact CSS the <Theme> runtime emits. With --check it writes ' +
|
|
21
|
-
'nothing and instead reports whether the committed outputs have drifted from source.'
|
|
21
|
+
'nothing and instead reports whether the committed outputs have drifted from source. ' +
|
|
22
|
+
'When a separate build step emits the icon registry, --icons-specifier declares the ' +
|
|
23
|
+
'fully specified module path that the generated JS should import.',
|
|
22
24
|
fn: 'themeBuild',
|
|
23
25
|
args: [{name: 'file', param: 'file', required: true}],
|
|
24
26
|
options: [
|
|
25
27
|
{flag: '-o, --out <path>', param: 'options.out', description: 'Output CSS file path'},
|
|
28
|
+
{
|
|
29
|
+
flag: '--icons-specifier <specifier>',
|
|
30
|
+
param: 'options.iconsSpecifier',
|
|
31
|
+
description:
|
|
32
|
+
'Override the icon-registry import in the generated JS module (for example, ./icons.mjs)',
|
|
33
|
+
},
|
|
26
34
|
{
|
|
27
35
|
flag: '-w, --watch',
|
|
28
36
|
description: 'Rebuild automatically when the theme file changes (Ctrl-C to stop)',
|
|
@@ -43,6 +51,10 @@ export const doc = {
|
|
|
43
51
|
label: 'Check for drift (CI)',
|
|
44
52
|
cli: 'astryx theme build ./src/themes/ocean.ts --check',
|
|
45
53
|
},
|
|
54
|
+
{
|
|
55
|
+
label: 'Build against a separately compiled icon registry',
|
|
56
|
+
cli: 'astryx theme build ./src/themes/ocean.ts --icons-specifier ./icons.mjs',
|
|
57
|
+
},
|
|
46
58
|
],
|
|
47
59
|
exitCodes: [
|
|
48
60
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/cli",
|
|
3
|
-
"version": "0.3.0-canary.
|
|
3
|
+
"version": "0.3.0-canary.f4607ea",
|
|
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",
|
|
@@ -84,10 +84,10 @@
|
|
|
84
84
|
"zod": "^4.4.3"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
|
-
"@astryxdesign/charts": "0.3.0-canary.
|
|
88
|
-
"@astryxdesign/core": "0.3.0-canary.
|
|
89
|
-
"@astryxdesign/lab": "0.3.0-canary.
|
|
90
|
-
"@astryxdesign/theme-neutral": "0.3.0-canary.
|
|
87
|
+
"@astryxdesign/charts": "0.3.0-canary.f4607ea",
|
|
88
|
+
"@astryxdesign/core": "0.3.0-canary.f4607ea",
|
|
89
|
+
"@astryxdesign/lab": "0.3.0-canary.f4607ea",
|
|
90
|
+
"@astryxdesign/theme-neutral": "0.3.0-canary.f4607ea",
|
|
91
91
|
"gpt-tokenizer": "^3.4.0"
|
|
92
92
|
},
|
|
93
93
|
"peerDependenciesMeta": {
|
|
@@ -105,10 +105,10 @@
|
|
|
105
105
|
}
|
|
106
106
|
},
|
|
107
107
|
"devDependencies": {
|
|
108
|
-
"@astryxdesign/charts": "0.3.0-canary.
|
|
109
|
-
"@astryxdesign/core": "0.3.0-canary.
|
|
110
|
-
"@astryxdesign/lab": "0.3.0-canary.
|
|
111
|
-
"@astryxdesign/theme-neutral": "0.3.0-canary.
|
|
108
|
+
"@astryxdesign/charts": "0.3.0-canary.f4607ea",
|
|
109
|
+
"@astryxdesign/core": "0.3.0-canary.f4607ea",
|
|
110
|
+
"@astryxdesign/lab": "0.3.0-canary.f4607ea",
|
|
111
|
+
"@astryxdesign/theme-neutral": "0.3.0-canary.f4607ea",
|
|
112
112
|
"gpt-tokenizer": "^3.4.0"
|
|
113
113
|
},
|
|
114
114
|
"scripts": {
|