@astryxdesign/cli 0.4.1 → 0.4.2-canary.356d2f9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +1 -0
  3. package/api/init/init.doc.mjs +2 -2
  4. package/api/init/init.test.mjs +19 -2
  5. package/api/init/init.type.d.mts +9 -1
  6. package/api/init/init.type.mjs +3 -1
  7. package/api/init/run/run.mjs +36 -6
  8. package/api/theme/add/add.mjs +2 -13
  9. package/api/theme/build/build.font-warning.test.mjs +161 -0
  10. package/api/theme/build/build.mjs +95 -26
  11. package/api/theme/build/build.test.mjs +227 -0
  12. package/api/theme/build/font-warning.d.mts +26 -0
  13. package/api/theme/build/font-warning.mjs +214 -0
  14. package/api/theme/build/font-warning.test.mjs +242 -0
  15. package/api/theme/template/template.d.mts +21 -0
  16. package/api/theme/template/template.mjs +69 -0
  17. package/api/theme/template/template.test.mjs +82 -0
  18. package/api/theme/theme.d.mts +1 -0
  19. package/api/theme/theme.mjs +1 -0
  20. package/api/theme/theme.type.d.mts +13 -0
  21. package/api/theme/theme.type.mjs +11 -1
  22. package/api/theme/themeTemplate.doc.d.mts +11 -0
  23. package/api/theme/themeTemplate.doc.mjs +64 -0
  24. package/assets/docs/getting-started.doc.mjs +11 -1
  25. package/assets/docs/theme.doc.dense.mjs +2 -2
  26. package/assets/docs/theme.doc.mjs +26 -10
  27. package/assets/docs/theme.doc.zh.mjs +1 -1
  28. package/assets/docs/typography.doc.mjs +38 -0
  29. package/assets/templates/blocks/components/ChatMessageBubble/ChatMessageBubbleCustomContent.doc.mjs +13 -0
  30. package/assets/templates/blocks/components/ChatMessageBubble/ChatMessageBubbleCustomContent.tsx +55 -0
  31. package/assets/templates/pages/ai-chat/page.tsx +18 -13
  32. package/assets/templates/themes/butter/butterTheme.ts +4 -1
  33. package/assets/templates/themes/chocolate/chocolateTheme.ts +4 -1
  34. package/assets/templates/themes/gothic/gothicTheme.ts +4 -1
  35. package/assets/templates/themes/stone/stoneTheme.ts +4 -1
  36. package/assets/theme.template.ts +322 -0
  37. package/authoring/doctypes/base/type.ts +9 -1
  38. package/authoring/doctypes/component/component.doc.mjs +1 -1
  39. package/clients/cli/commands/build-theme.font-warning.test.mjs +138 -0
  40. package/clients/cli/commands/build-theme.mjs +50 -0
  41. package/clients/cli/commands/init.behavior.test.mjs +13 -2
  42. package/clients/cli/commands/theme-template.behavior.test.mjs +85 -0
  43. package/clients/cli/commands/theme-template.doc.mjs +42 -0
  44. package/clients/cli/commands/theme.doc.mjs +2 -2
  45. package/clients/cli/index.mjs +1 -0
  46. package/clients/cli/lib/manifest.mjs +2 -0
  47. package/foundation/agent-docs/agent-docs.mjs +1 -1
  48. package/foundation/response/response-types.doc.mjs +5 -0
  49. package/foundation/text/copyright-header.d.mts +11 -0
  50. package/foundation/text/copyright-header.mjs +35 -0
  51. package/foundation/text/copyright-header.test.mjs +58 -0
  52. package/package.json +9 -9
@@ -0,0 +1,242 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * Unit tests for the font-loading warning helpers behind `astryx theme build`
5
+ * (#5015). `collectUnloadedFonts()` reads a RESOLVED theme — `--font-family-*`
6
+ * tokens plus component-override `fontFamily` values (typography configs are
7
+ * already collapsed into tokens by the time the build sees the theme) — and
8
+ * returns the families the theme names but nothing loads. CSS generics,
9
+ * CSS-wide keywords, `var()` references, and known preinstalled system
10
+ * families are treated as loaded; anything unrecognized is assumed to be a
11
+ * webfont, because a missed warning is worse than a spurious one.
12
+ * `formatFontLoadingHelp()` renders the human snippet (<link> pair +
13
+ * @font-face) printed after the install instructions.
14
+ */
15
+
16
+ import {describe, it, expect} from 'vitest';
17
+ import {collectUnloadedFonts, formatFontLoadingHelp} from './font-warning.mjs';
18
+
19
+ describe('collectUnloadedFonts', () => {
20
+ it('returns webfont families from font-family tokens, skipping known fallbacks', () => {
21
+ expect(
22
+ collectUnloadedFonts({
23
+ tokens: {'--font-family-body': '"Fraunces", Georgia, serif'},
24
+ }),
25
+ ).toEqual(['Fraunces']);
26
+ });
27
+
28
+ it('returns nothing for the default system stacks', () => {
29
+ expect(
30
+ collectUnloadedFonts({
31
+ tokens: {
32
+ '--font-family-body':
33
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
34
+ '--font-family-code': '"SF Mono", Monaco, Consolas, monospace',
35
+ },
36
+ }),
37
+ ).toEqual([]);
38
+ });
39
+
40
+ it('treats bare system keywords like ui-monospace as loaded', () => {
41
+ expect(
42
+ collectUnloadedFonts({tokens: {'--font-family-code': 'ui-monospace'}}),
43
+ ).toEqual([]);
44
+ });
45
+
46
+ it('handles single-quoted and unquoted multi-word family names', () => {
47
+ expect(
48
+ collectUnloadedFonts({
49
+ tokens: {
50
+ '--font-family-body': "'JetBrains Mono', monospace",
51
+ '--font-family-heading': 'Space Grotesk, sans-serif',
52
+ },
53
+ }),
54
+ ).toEqual(['JetBrains Mono', 'Space Grotesk']);
55
+ });
56
+
57
+ it('dedupes case-insensitively across roles, keeping the first casing', () => {
58
+ expect(
59
+ collectUnloadedFonts({
60
+ tokens: {
61
+ '--font-family-body': '"Space Grotesk", sans-serif',
62
+ '--font-family-heading': '"space grotesk", serif',
63
+ },
64
+ }),
65
+ ).toEqual(['Space Grotesk']);
66
+ });
67
+
68
+ it('skips var() references and CSS-wide keywords', () => {
69
+ expect(
70
+ collectUnloadedFonts({
71
+ tokens: {
72
+ '--font-family-heading': 'var(--font-family-body)',
73
+ '--font-family-code': 'inherit',
74
+ },
75
+ }),
76
+ ).toEqual([]);
77
+ });
78
+
79
+ it("strips whole var() calls — fallback arguments are not this theme's declarations", () => {
80
+ expect(
81
+ collectUnloadedFonts({
82
+ tokens: {
83
+ '--font-family-body': 'var(--x, "Web Font")',
84
+ '--font-family-heading': 'var(--x, "Web Font", serif)',
85
+ },
86
+ }),
87
+ ).toEqual([]);
88
+ // Families outside the var() call still count.
89
+ expect(
90
+ collectUnloadedFonts({
91
+ tokens: {'--font-family-code': 'var(--x), Bungee'},
92
+ }),
93
+ ).toEqual(['Bungee']);
94
+ });
95
+
96
+ it('keeps a comma inside a quoted name whole, even mid-list', () => {
97
+ expect(
98
+ collectUnloadedFonts({
99
+ tokens: {'--font-family-body': 'Georgia, "Foo, Bar", serif'},
100
+ }),
101
+ ).toEqual(['Foo, Bar']);
102
+ });
103
+
104
+ it('ignores an empty font-family token', () => {
105
+ expect(collectUnloadedFonts({tokens: {'--font-family-body': ''}})).toEqual(
106
+ [],
107
+ );
108
+ });
109
+
110
+ it('collects component-override fontFamily values, any style key', () => {
111
+ expect(
112
+ collectUnloadedFonts({
113
+ components: {
114
+ Button: {base: {fontFamily: '"Orbitron", sans-serif'}},
115
+ Card: {'variant:hero': {fontFamily: 'Bungee'}},
116
+ },
117
+ }),
118
+ ).toEqual(['Orbitron', 'Bungee']);
119
+ });
120
+
121
+ it('matches known system families case-insensitively', () => {
122
+ expect(
123
+ collectUnloadedFonts({
124
+ tokens: {'--font-family-body': 'arial, sans-serif'},
125
+ }),
126
+ ).toEqual([]);
127
+ });
128
+
129
+ it('returns nothing for a theme with no tokens or components', () => {
130
+ expect(collectUnloadedFonts({})).toEqual([]);
131
+ });
132
+
133
+ it('returns nothing for a null or undefined resolved theme', () => {
134
+ // Older build.mjs consumers guard with `resolvedTheme || themeDef` — if
135
+ // a path ever hands the collector nothing, it must shrug, not throw.
136
+ expect(collectUnloadedFonts(null)).toEqual([]);
137
+ expect(collectUnloadedFonts(undefined)).toEqual([]);
138
+ });
139
+
140
+ it('ignores non-string values without throwing: numeric or object-valued', () => {
141
+ // The generator only allows object values under ':pseudo' keys, so an
142
+ // object-valued fontFamily is not a family declaration — skip, not crash.
143
+ expect(
144
+ collectUnloadedFonts({
145
+ tokens: {'--font-family-body': 42},
146
+ components: {
147
+ button: {base: {fontFamily: 700}},
148
+ card: {base: {fontFamily: {default: '"Sneaky"'}}},
149
+ },
150
+ }),
151
+ ).toEqual([]);
152
+ });
153
+
154
+ it('collects fontFamily nested under a pseudo-class block', () => {
155
+ // generateThemeRules accepts {':hover': {fontFamily}} inside a style
156
+ // key — a family named only there must still warn.
157
+ expect(
158
+ collectUnloadedFonts({
159
+ components: {
160
+ button: {base: {':hover': {fontFamily: '"Rubik Doodle", cursive'}}},
161
+ },
162
+ }),
163
+ ).toEqual(['Rubik Doodle']);
164
+ });
165
+
166
+ it('strips a var() whose fallback list contains another var()', () => {
167
+ expect(
168
+ collectUnloadedFonts({
169
+ tokens: {
170
+ '--font-family-body': 'var(--brand, var(--fallback), "Web Font")',
171
+ },
172
+ }),
173
+ ).toEqual([]);
174
+ // …and still keeps a family declared outside the nested call.
175
+ expect(
176
+ collectUnloadedFonts({
177
+ tokens: {'--font-family-code': 'var(--a, var(--b, serif)), Bungee'},
178
+ }),
179
+ ).toEqual(['Bungee']);
180
+ });
181
+
182
+ it('matches generic keywords case-insensitively', () => {
183
+ expect(
184
+ collectUnloadedFonts({
185
+ tokens: {
186
+ '--font-family-body': 'Sans-Serif',
187
+ '--font-family-heading': 'SERIF',
188
+ '--font-family-code': 'Ui-Monospace',
189
+ },
190
+ }),
191
+ ).toEqual([]);
192
+ });
193
+
194
+ it('collapses newlines and whitespace runs inside a family name', () => {
195
+ // Template-literal theme sources wrap long stacks across lines.
196
+ expect(
197
+ collectUnloadedFonts({
198
+ tokens: {'--font-family-body': '"Space\n Grotesk",\n serif'},
199
+ }),
200
+ ).toEqual(['Space Grotesk']);
201
+ });
202
+ });
203
+
204
+ describe('formatFontLoadingHelp', () => {
205
+ const help = formatFontLoadingHelp('ocean', ['Fraunces', 'JetBrains Mono']);
206
+
207
+ it('names the theme and every family in the headline', () => {
208
+ expect(help).toContain('⚠');
209
+ expect(help).toContain('ocean');
210
+ expect(help).toContain('"Fraunces"');
211
+ expect(help).toContain('"JetBrains Mono"');
212
+ });
213
+
214
+ it('includes the Google Fonts recipe: preconnect pair + the exact css2 URL', () => {
215
+ expect(help).toContain(
216
+ '<link rel="preconnect" href="https://fonts.googleapis.com"',
217
+ );
218
+ expect(help).toContain('https://fonts.gstatic.com');
219
+ // The whole href, so a malformed URL cannot hide behind fragment checks.
220
+ expect(help).toContain(
221
+ 'href="https://fonts.googleapis.com/css2?family=Fraunces&family=JetBrains+Mono&display=swap"',
222
+ );
223
+ });
224
+
225
+ it('includes a self-hosted @font-face alternative with font-display: swap', () => {
226
+ expect(help).toContain('@font-face');
227
+ expect(help).toContain('font-family: "Fraunces"');
228
+ expect(help).toContain('woff2');
229
+ expect(help).toContain('font-display: swap');
230
+ });
231
+
232
+ it('points at the docs recipe', () => {
233
+ expect(help).toContain('astryx docs typography');
234
+ });
235
+
236
+ it('percent-encodes family names in the css2 URL', () => {
237
+ const spiced = formatFontLoadingHelp('spice', ['P&Co Sans', 'Sömething']);
238
+ expect(spiced).toContain(
239
+ 'href="https://fonts.googleapis.com/css2?family=P%26Co+Sans&family=S%C3%B6mething&display=swap"',
240
+ );
241
+ });
242
+ });
@@ -0,0 +1,21 @@
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
+ * Write the annotated theme template into a project.
6
+ *
7
+ * Refuses to overwrite without `overwrite`: an edited copy is the consumer's
8
+ * work, and this command is safe to re-run (init calls it on every setup).
9
+ *
10
+ * @param {{targetPath?: string, overwrite?: boolean, cwd?: string}} [options]
11
+ * @returns {import('../theme.type.mjs').ThemeTemplateResponse}
12
+ */
13
+ export function themeTemplate(options?: {
14
+ targetPath?: string;
15
+ overwrite?: boolean;
16
+ cwd?: string;
17
+ }): import("../theme.type.mjs").ThemeTemplateResponse;
18
+ /** The annotated `defineTheme` reference that ships with the CLI. */
19
+ export const THEME_TEMPLATE_SRC: string;
20
+ /** Where it lands when the caller does not say. */
21
+ export const THEME_TEMPLATE_DEFAULT_PATH: "theme.template.ts";
@@ -0,0 +1,69 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file `astryx theme template` leaf — writes the annotated theme template into the
5
+ * consumer's project.
6
+ *
7
+ * Sibling of `theme add`: both answer "put a theme starting point in my
8
+ * project", and they split on where you start. `add` copies a theme we ship
9
+ * (you like stone, you want to own it); `template` writes the blank annotated
10
+ * reference (you want your own, and need to know what the surface contains).
11
+ *
12
+ * The template is a doc that happens to compile, so it is one file at the
13
+ * project root by default rather than a package under src/themes/ — you read
14
+ * it, copy what you need into your own theme file, and delete it.
15
+ */
16
+
17
+ import * as fs from 'node:fs';
18
+ import * as path from 'node:path';
19
+ import {CLI_ROOT} from '../../../foundation/fs/paths.mjs';
20
+ import {assertWithin, PathSafetyError} from '../../../foundation/fs/path-safety.mjs';
21
+ import {stripCopyrightHeader} from '../../../foundation/text/copyright-header.mjs';
22
+ import {AstryxError} from '../../error.mjs';
23
+ import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
24
+
25
+ /** The annotated `defineTheme` reference that ships with the CLI. */
26
+ export const THEME_TEMPLATE_SRC = path.join(CLI_ROOT, 'assets', 'theme.template.ts');
27
+
28
+ /** Where it lands when the caller does not say. */
29
+ export const THEME_TEMPLATE_DEFAULT_PATH = 'theme.template.ts';
30
+
31
+ /**
32
+ * Write the annotated theme template into a project.
33
+ *
34
+ * Refuses to overwrite without `overwrite`: an edited copy is the consumer's
35
+ * work, and this command is safe to re-run (init calls it on every setup).
36
+ *
37
+ * @param {{targetPath?: string, overwrite?: boolean, cwd?: string}} [options]
38
+ * @returns {import('../theme.type.mjs').ThemeTemplateResponse}
39
+ */
40
+ export function themeTemplate(options = {}) {
41
+ const {
42
+ targetPath = THEME_TEMPLATE_DEFAULT_PATH,
43
+ overwrite = false,
44
+ cwd = process.cwd(),
45
+ } = options;
46
+
47
+ let resolved;
48
+ try {
49
+ resolved = assertWithin(targetPath, cwd, {label: 'theme template path'});
50
+ } catch (err) {
51
+ if (err instanceof PathSafetyError) {
52
+ throw new AstryxError(err.message, undefined, ERROR_CODES.ERR_PATH_TRAVERSAL);
53
+ }
54
+ throw err;
55
+ }
56
+
57
+ const relative = path.relative(cwd, resolved) || targetPath;
58
+
59
+ if (fs.existsSync(resolved) && !overwrite) {
60
+ return {type: 'theme.template', data: {path: relative, written: false, reason: 'exists'}};
61
+ }
62
+
63
+ // Our repo header has no business in someone else's source tree.
64
+ const contents = stripCopyrightHeader(fs.readFileSync(THEME_TEMPLATE_SRC, 'utf-8'));
65
+ fs.mkdirSync(path.dirname(resolved), {recursive: true});
66
+ fs.writeFileSync(resolved, contents);
67
+
68
+ return {type: 'theme.template', data: {path: relative, written: true, reason: null}};
69
+ }
@@ -0,0 +1,82 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Direct API tests for `themeTemplate()` — the function behind
5
+ * `astryx theme template` and `astryx init --features theme`.
6
+ *
7
+ * What matters here is that it never destroys work: the template lands once,
8
+ * a second run leaves an edited copy alone, and a path that escapes the
9
+ * project is refused.
10
+ */
11
+
12
+ import {describe, it, expect, beforeEach, afterEach} from 'vitest';
13
+ import * as fs from 'node:fs';
14
+ import * as path from 'node:path';
15
+ import * as os from 'node:os';
16
+ import {themeTemplate, THEME_TEMPLATE_DEFAULT_PATH} from './template.mjs';
17
+
18
+ let tmpDir;
19
+ beforeEach(() => {
20
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-theme-template-'));
21
+ });
22
+ afterEach(() => {
23
+ fs.rmSync(tmpDir, {recursive: true, force: true});
24
+ });
25
+
26
+ describe('themeTemplate()', () => {
27
+ it('writes the template and returns a theme.template receipt', () => {
28
+ const res = themeTemplate({cwd: tmpDir});
29
+
30
+ expect(res).toEqual({
31
+ type: 'theme.template',
32
+ data: {path: THEME_TEMPLATE_DEFAULT_PATH, written: true, reason: null},
33
+ });
34
+ const written = fs.readFileSync(path.join(tmpDir, THEME_TEMPLATE_DEFAULT_PATH), 'utf-8');
35
+ expect(written).toMatch(/defineTheme/);
36
+ });
37
+
38
+ it('does not carry our copyright header into the consumer tree', () => {
39
+ themeTemplate({cwd: tmpDir});
40
+ const written = fs.readFileSync(path.join(tmpDir, THEME_TEMPLATE_DEFAULT_PATH), 'utf-8');
41
+ expect(written).not.toMatch(/Copyright \(c\) Meta Platforms/);
42
+ expect(written.startsWith('/**')).toBe(true);
43
+ });
44
+
45
+ it('leaves an existing file alone and says so', () => {
46
+ const dest = path.join(tmpDir, THEME_TEMPLATE_DEFAULT_PATH);
47
+ fs.writeFileSync(dest, '// mine\n');
48
+
49
+ const res = themeTemplate({cwd: tmpDir});
50
+
51
+ expect(res.data).toEqual({
52
+ path: THEME_TEMPLATE_DEFAULT_PATH,
53
+ written: false,
54
+ reason: 'exists',
55
+ });
56
+ expect(fs.readFileSync(dest, 'utf-8')).toBe('// mine\n');
57
+ });
58
+
59
+ it('overwrites when asked', () => {
60
+ const dest = path.join(tmpDir, THEME_TEMPLATE_DEFAULT_PATH);
61
+ fs.writeFileSync(dest, '// mine\n');
62
+
63
+ const res = themeTemplate({cwd: tmpDir, overwrite: true});
64
+
65
+ expect(res.data.written).toBe(true);
66
+ expect(fs.readFileSync(dest, 'utf-8')).toMatch(/defineTheme/);
67
+ });
68
+
69
+ it('honors a custom path and creates its directory', () => {
70
+ const res = themeTemplate({cwd: tmpDir, targetPath: 'src/themes/starter.ts'});
71
+
72
+ expect(res.data.path).toBe(path.join('src', 'themes', 'starter.ts'));
73
+ expect(fs.existsSync(path.join(tmpDir, 'src/themes/starter.ts'))).toBe(true);
74
+ });
75
+
76
+ it('refuses a path that escapes the project', () => {
77
+ expect(() => themeTemplate({cwd: tmpDir, targetPath: '../escaped.ts'})).toThrow(
78
+ /theme template path/,
79
+ );
80
+ expect(fs.existsSync(path.join(path.dirname(tmpDir), 'escaped.ts'))).toBe(false);
81
+ });
82
+ });
@@ -2,6 +2,7 @@
2
2
  // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
3
 
4
4
  export { themeAdd } from "./add/add.mjs";
5
+ export { themeTemplate } from "./template/template.mjs";
5
6
  export { themeList } from "./list/list.mjs";
6
7
  export { listThemes } from "./_adapter.mjs";
7
8
  export { themeBuild, importSpecifier } from "./build/build.mjs";
@@ -10,5 +10,6 @@
10
10
 
11
11
  export {themeBuild, importSpecifier} from './build/build.mjs';
12
12
  export {themeAdd} from './add/add.mjs';
13
+ export {themeTemplate} from './template/template.mjs';
13
14
  export {themeList} from './list/list.mjs';
14
15
  export {listThemes} from './_adapter.mjs';
@@ -66,3 +66,16 @@ export type ThemeAddResponse = {
66
66
  files: string[];
67
67
  };
68
68
  };
69
+ /**
70
+ * xds --json theme template
71
+ * `written: false` with `reason: 'exists'` is a success: the command is safe to
72
+ * re-run, and an edited template is the consumer's file to keep.
73
+ */
74
+ export type ThemeTemplateResponse = {
75
+ type: "theme.template";
76
+ data: {
77
+ path: string;
78
+ written: boolean;
79
+ reason: "exists" | null;
80
+ };
81
+ };
@@ -12,9 +12,10 @@
12
12
  * xds --json theme build <file> --check -> theme.build.check
13
13
  * xds --json theme list -> theme.list
14
14
  * xds --json theme add <slug> -> theme.add
15
+ * xds --json theme template -> theme.template
15
16
  * (file not found / parse error) -> CLIError
16
17
  *
17
- * @position api — colocated typedefs for api/theme/{theme,build,add,list,_adapter}
18
+ * @position api — colocated typedefs for api/theme/{theme,build,add,list,template,_adapter}
18
19
  */
19
20
 
20
21
  /**
@@ -54,6 +55,15 @@
54
55
  * @property {{slug: string, displayName: string, maintained: boolean, outputDir: string, entry: string, exportName: string, files: string[]}} data
55
56
  */
56
57
 
58
+ /**
59
+ * xds --json theme template
60
+ * `written: false` with `reason: 'exists'` is a success: the command is safe to
61
+ * re-run, and an edited template is the consumer's file to keep.
62
+ * @typedef {object} ThemeTemplateResponse
63
+ * @property {'theme.template'} type
64
+ * @property {{path: string, written: boolean, reason: 'exists' | null}} data
65
+ */
66
+
57
67
  // Make this a module so the @typedefs above are importable as types via
58
68
  // `import('./theme.type.mjs').ThemeBuildResponse` (and re-exportable from a .d.ts).
59
69
  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 `themeTemplate()` / `astryx theme template`. Colocated with the
6
+ * API function it documents; the response-shape source of truth stays in
7
+ * `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,64 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file FunctionDoc for `themeTemplate()` / `astryx theme template`. Colocated with the
5
+ * API function it documents; the response-shape source of truth stays in
6
+ * `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: 'themeTemplate',
15
+ displayName: 'themeTemplate()',
16
+ summary: 'Write the annotated theme template into a project.',
17
+ description:
18
+ 'Writes theme.template.ts: the annotated reference for the whole theme surface — every ' +
19
+ 'defineTheme field, the token families, the component override syntax, and how a theme is ' +
20
+ 'consumed — with the CLI command that prints the authoritative reference for each section. ' +
21
+ 'Read it, copy what you need into your own theme file, delete it. Where `theme add` starts ' +
22
+ 'you from a theme we ship, this starts you from a blank one. Refuses to overwrite without ' +
23
+ '`overwrite`, so it is safe to re-run.',
24
+ importPath: '@astryxdesign/cli/api',
25
+ signature:
26
+ 'themeTemplate(options?: {targetPath?: string, overwrite?: boolean, cwd?: string}): ThemeNewResponse',
27
+ keywords: ['theme', 'template', 'starter', 'defineTheme', 'scaffold', 'reference', 'tokens'],
28
+ params: [
29
+ {
30
+ name: 'options.targetPath',
31
+ type: 'string',
32
+ description: 'Destination file. Must resolve within cwd.',
33
+ default: "'theme.template.ts'",
34
+ },
35
+ {
36
+ name: 'options.overwrite',
37
+ type: 'boolean',
38
+ description: 'Replace an existing file instead of reporting it untouched.',
39
+ default: 'false',
40
+ },
41
+ {
42
+ name: 'options.cwd',
43
+ type: 'string',
44
+ description: 'Directory the target path resolves against.',
45
+ },
46
+ ],
47
+ returns: [
48
+ {
49
+ type: 'theme.template',
50
+ description:
51
+ 'Receipt: 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, not a failure.',
52
+ },
53
+ ],
54
+ throws: [{code: 'ERR_PATH_TRAVERSAL', when: 'the target path escapes cwd'}],
55
+ examples: [
56
+ {label: 'Write it at the project root', code: 'themeTemplate();'},
57
+ {
58
+ label: 'Somewhere else, replacing what is there',
59
+ code: "themeTemplate({targetPath: 'src/themes/template.ts', overwrite: true});",
60
+ },
61
+ ],
62
+ command: 'theme template',
63
+ related: ['themeAdd', 'themeBuild', 'themeList'],
64
+ };
@@ -20,9 +20,19 @@ export const docs = {
20
20
  {
21
21
  type: 'code',
22
22
  lang: 'text',
23
- label: 'Paste this into your AI',
23
+ label: 'Set up the design system',
24
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
+ {
27
+ type: 'prose',
28
+ text: 'Then give it a look. Every app gets a theme whether or not anyone picks one, so it is worth one question at setup rather than revisiting screens later that were built around the wrong look:',
29
+ },
30
+ {
31
+ type: 'code',
32
+ lang: 'text',
33
+ label: 'Give it a look',
34
+ code: "Ask me what look and feel this app should have. Run `npx @astryxdesign/cli theme list` and start from the closest shipped theme with `theme add <slug>`, which copies it in as editable source; if none of them fit, run `npx @astryxdesign/cli theme template` and fill in the annotated template it writes. Default to neutral if I have no preference, and show me the result before moving on.",
35
+ },
26
36
  ],
27
37
  },
28
38
  {
@@ -8,9 +8,9 @@ export const docsDense = {
8
8
  { section: 'Quick Start', title: 'Quick Start', content: [null, null, null, null, { type: 'prose', text: 'default import = runtime injection. /built import = pre-compiled CSS (pair with theme.css).' }] },
9
9
  { section: 'Available Themes', title: 'Themes', content: [null, null, { type: 'prose', text: 'published: neutral (start here), butter, chocolate, gothic (dark-only), matcha, stone, y2k. @astryxdesign/theme-{name} = source (runtime). @astryxdesign/theme-{name}/built = optimized (+ theme.css).' }] },
10
10
  { section: 'Theme Props', title: 'Props', content: [null] },
11
- { section: 'Creating a Custom Theme', title: 'Custom Theme', content: [{ type: 'prose', text: 'CLI wizard or manual defineTheme. only override tokens that differ.' }, null] },
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 hex via HCT.' }, 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-press-scale etc) set directly. private vars (--_*) cannot be set — use CSS properties. run `astryx component <Name>` for details.' }, null, null, 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 details.' }, 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] },
@@ -142,13 +142,17 @@ function App() {
142
142
  content: [
143
143
  {
144
144
  type: 'prose',
145
- text: 'Use the CLI wizard (recommended) or create manually with defineTheme. Only override tokens that differ from defaults; omitted tokens use the design system defaults.',
145
+ text: 'Start from a theme we ship, or write one from scratch with defineTheme. Only override tokens that differ from defaults; omitted tokens use the design system defaults.',
146
146
  },
147
147
  {
148
148
  type: 'code',
149
149
  lang: 'bash',
150
- label: 'Scaffold with CLI',
151
- code: 'astryx theme',
150
+ label: 'Browse, then copy a theme in as editable source',
151
+ code: 'astryx theme list\nastryx theme add stone',
152
+ },
153
+ {
154
+ type: 'prose',
155
+ text: 'For an annotated map of the whole surface — every defineTheme field, the token families, and the component override syntax, each with the CLI command that prints its reference — run `astryx theme template`. It writes `theme.template.ts` into your project to read and copy from (`astryx init --features theme` writes it as part of project setup).',
152
156
  },
153
157
  ],
154
158
  },
@@ -249,10 +253,15 @@ const brandTheme = defineTheme({
249
253
  ['tokens', 'Base tokens are copied first, then child tokens override on top.'],
250
254
  ['components', 'Deep-merged: child component rules override matching keys from the base.'],
251
255
  ['icons', 'Shallow-merged: child icons override matching names from the base.'],
252
- ['fonts', 'Base fonts included first, then child fonts appended.'],
256
+ ['indicators', 'Shallow-merged: child indicators override matching names from the base.'],
257
+ ['onDark, onLight', "Deep-merged per surface: the base's resolved surface first, then the child's overrides."],
253
258
  ['typography, motion, radius, color', 'Child config replaces base entirely (these are scale inputs, not additive).'],
254
259
  ],
255
260
  },
261
+ {
262
+ type: 'prose',
263
+ text: 'Inheritance is resolved when the theme is defined, so an extended theme is flat: `astryx theme build` emits one self-contained stylesheet holding everything the child inherited, and the base theme\'s CSS does not need to be loaded next to it. A base that is not a theme — most often an import that missed — is a build error rather than a theme that silently inherits nothing.',
264
+ },
256
265
  ],
257
266
  },
258
267
  {
@@ -275,14 +284,17 @@ const brandTheme = defineTheme({
275
284
  base: { borderRadius: '20px', padding: '24px' },
276
285
  },
277
286
  button: {
278
- base: { borderRadius: '9999px', textTransform: 'uppercase' },
287
+ base: {
288
+ borderRadius: '9999px',
289
+ textTransform: 'uppercase',
290
+ // Some components have public CSS vars for properties that don't map
291
+ // to standard CSS. Set these directly. Take the name from
292
+ // \`astryx component <Name>\` — a var the component does not define
293
+ // compiles to CSS that never applies.
294
+ '--button-focus-offset': '3px',
295
+ },
279
296
  'variant:ghost': { borderWidth: '2px', borderStyle: 'solid' },
280
297
  },
281
- // Some components have public CSS vars for properties that don't map
282
- // to standard CSS. Set these directly.
283
- button: {
284
- base: { '--button-press-scale': 'scale(0.95)' },
285
- },
286
298
  }`,
287
299
  },
288
300
  {
@@ -410,6 +422,10 @@ import './themes/ocean.css';
410
422
  <App />
411
423
  </Theme>`,
412
424
  },
425
+ {
426
+ type: 'prose',
427
+ text: 'The build also warns when the theme names font families it does not load (webfonts like Fraunces) and prints the `<link>`/`@font-face` to add — the built CSS only sets font-family, so loading the font files stays the app\'s job. See `astryx docs typography` for the full recipe.',
428
+ },
413
429
  ],
414
430
  },
415
431
  {