@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
@@ -8,7 +8,7 @@ export const docsZh = {
8
8
  { section: 'Quick Start', title: '快速开始', content: [null, null, null, null, { type: 'prose', text: '默认导入使用运行时样式注入。/built 导入使用预编译 CSS(需配合 theme.css)。' }] },
9
9
  { section: 'Available Themes', title: '可用主题', content: [null, null, { type: 'prose', text: '已发布主题:neutral(推荐起点)、butter、chocolate、gothic(仅暗色)、matcha、stone、y2k。@astryxdesign/theme-{name} = 源码版(运行时注入)。@astryxdesign/theme-{name}/built = 优化版(配合 theme.css)。' }] },
10
10
  { section: 'Theme Props', title: 'Theme 属性', content: [null] },
11
- { section: 'Creating a Custom Theme', title: '创建自定义主题', content: [{ type: 'prose', text: '使用 CLI 向导(推荐)或手动 defineTheme。只覆盖与默认值不同的令牌。' }, null] },
11
+ { section: 'Creating a Custom Theme', title: '创建自定义主题', content: [{ type: 'prose', text: ' `theme list` + `theme add <slug>` 从内置主题开始,或用 defineTheme 从零编写。只覆盖与默认值不同的令牌。' }, null, { type: 'prose', text: '`astryx theme template` 会写入 theme.template.ts:带注释的完整参考,涵盖每个 defineTheme 字段、令牌族和覆盖语法,并标明打印各自参考的 CLI 命令。' }] },
12
12
  { section: 'defineTheme', title: 'defineTheme', content: [{ type: 'prose', text: '支持比例配置(typography、radius、motion)+ 显式令牌覆盖 + 组件覆盖。' }, null, null] },
13
13
  { section: 'Building Themes for Production', title: '生产构建', content: [{ type: 'prose', text: 'astryx theme build 将 defineTheme 编译为静态 CSS。输出 .css + .js(__built:true)+ .d.ts。' }, null, null, null, null] },
14
14
  { section: 'Runtime vs Built Themes', title: '运行时 vs 构建', content: [{ type: 'prose', text: '运行时:useInsertionEffect 在客户端注入样式。构建:静态 CSS 在首次渲染时就存在。SSR 应用请使用 /built + theme.css。' }, null, null, null] },
@@ -44,6 +44,44 @@ export const docs = {
44
44
  ],
45
45
  },
46
46
 
47
+ // ── Loading Custom Fonts ────────────────────────────────────────────────
48
+ {
49
+ title: 'Loading Custom Fonts',
50
+ category: 'foundations',
51
+ content: [
52
+ {
53
+ type: 'prose',
54
+ text: 'Astryx never loads font files. defineTheme and the built CSS only set font-family — naming a webfont (Fraunces, JetBrains Mono, …) makes every browser look for it, and quietly fall back when the app has not loaded it. `astryx theme build` warns when a theme names families that are neither CSS generics nor common system fonts and prints the snippet to add; loading the font is always the app\'s job.',
55
+ },
56
+ {
57
+ type: 'code',
58
+ lang: 'html',
59
+ label: 'Google Fonts — add to your document <head>',
60
+ code: `<link rel="preconnect" href="https://fonts.googleapis.com" />
61
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
62
+ <link
63
+ rel="stylesheet"
64
+ href="https://fonts.googleapis.com/css2?family=Fraunces:wght@400;600;700&family=JetBrains+Mono&display=swap"
65
+ />`,
66
+ },
67
+ {
68
+ type: 'code',
69
+ lang: 'css',
70
+ label: 'Self-hosted — one @font-face per family and weight',
71
+ code: `@font-face {
72
+ font-family: 'Fraunces';
73
+ src: url('/fonts/fraunces.woff2') format('woff2');
74
+ font-weight: 400 700; /* variable-font range, or one weight per file */
75
+ font-display: swap; /* fallback text stays visible while the font loads */
76
+ }`,
77
+ },
78
+ {
79
+ type: 'prose',
80
+ text: "Always pair a webfont with a real fallback stack — metric-similar system fonts plus a generic — so text stays readable before the font loads and wherever it never does: defineTheme({typography: {heading: {family: 'Fraunces', fallbacks: 'Georgia, serif'}}}).",
81
+ },
82
+ ],
83
+ },
84
+
47
85
  // ── Font Sizes ──────────────────────────────────────────────────────────
48
86
  {
49
87
  title: 'Font Sizes',
@@ -0,0 +1,13 @@
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: 'ChatMessageBubble',
7
+ name: 'ChatMessageBubble — Custom Content',
8
+ displayName: 'ChatMessageBubble — Custom Content',
9
+ description: 'Custom in-message content aligned to the bubble text column. An artifact card is wrapped in a ghost bubble with width="100%", so its left edge matches the bubble text and it spans the full message column instead of the default bubble width cap; the timestamp rides the bubble metadata slot.',
10
+ isReady: true,
11
+ aspectRatio: 4 / 3,
12
+ componentsUsed: ['Chat', 'ChatMessage', 'ChatMessageBubble', 'ChatMessageMetadata', 'ClickableCard', 'Text', 'Timestamp'],
13
+ };
@@ -0,0 +1,55 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ import {
6
+ ChatMessageList,
7
+ ChatMessage,
8
+ ChatMessageBubble,
9
+ ChatMessageMetadata,
10
+ } from '@astryxdesign/core/Chat';
11
+ import {ClickableCard} from '@astryxdesign/core/ClickableCard';
12
+ import {Text} from '@astryxdesign/core/Text';
13
+ import {Timestamp} from '@astryxdesign/core/Timestamp';
14
+
15
+ export default function ChatMessageBubbleCustomContent() {
16
+ return (
17
+ <ChatMessageList style={{maxWidth: 520}}>
18
+ <ChatMessage sender="user">
19
+ <ChatMessageBubble>Can you pull up the Q3 report?</ChatMessageBubble>
20
+ </ChatMessage>
21
+ <ChatMessage sender="assistant">
22
+ <ChatMessageBubble name="Navi">
23
+ Sure, here is the artifact from last quarter.
24
+ </ChatMessageBubble>
25
+ {/* A ghost bubble aligns custom content with the bubble text column;
26
+ width="100%" spans the full message column instead of the default
27
+ bubble width cap. Metadata rides the bubble's metadata slot. */}
28
+ <ChatMessageBubble
29
+ variant="ghost"
30
+ width="100%"
31
+ metadata={
32
+ <ChatMessageMetadata
33
+ timestamp={
34
+ <Timestamp value="2026-04-28T09:45:00" format="time" />
35
+ }
36
+ footer={
37
+ <Text type="supporting" color="secondary">
38
+ Claude Opus 4.6
39
+ </Text>
40
+ }
41
+ />
42
+ }>
43
+ <ClickableCard label="Open Q3 performance report" onClick={() => {}}>
44
+ <Text type="body" weight="semibold" display="block">
45
+ Q3 Performance Report
46
+ </Text>
47
+ <Text type="supporting" color="secondary" display="block">
48
+ 12 pages · updated 2 days ago
49
+ </Text>
50
+ </ClickableCard>
51
+ </ChatMessageBubble>
52
+ </ChatMessage>
53
+ </ChatMessageList>
54
+ );
55
+ }
@@ -70,9 +70,6 @@ const chatLayout: CSSProperties = {
70
70
  flex: 1,
71
71
  minHeight: 0,
72
72
  };
73
- const artifactCard: CSSProperties = {
74
- marginBlockStart: 'var(--spacing-2)',
75
- };
76
73
  const artifactScroll: CSSProperties = {
77
74
  flex: 1,
78
75
  overflowY: 'auto',
@@ -250,8 +247,7 @@ function ArtifactCard({onOpen}: {onOpen: () => void}) {
250
247
  onClick={onOpen}
251
248
  variant="muted"
252
249
  padding={3}
253
- maxWidth={360}
254
- style={artifactCard}>
250
+ maxWidth={360}>
255
251
  <HStack gap={3} vAlign="center" width="100%">
256
252
  <Icon icon={DocumentTextIcon} size="md" color="secondary" />
257
253
  <StackItem size="fill">
@@ -614,15 +610,24 @@ The fix is to catch \`TokenExpiredError\` specifically and attempt a refresh bef
614
610
  {`I've drafted a design doc covering the problem, the fix, and the test matrix — pulling straight from the changes we just made.\n\nOpen the document below to review it. Want me to expand any section?`}
615
611
  </Markdown>
616
612
  </ChatMessageBubble>
617
- <ArtifactCard onOpen={openArtifact} />
618
- <ChatMessageMetadata
619
- timestamp={
620
- <Timestamp
621
- value="2026-04-29T10:18:40"
622
- format="time"
613
+ {/* Ghost + width="100%" aligns the card with the
614
+ bubble text column and spans the full message
615
+ column; metadata rides the bubble slot. */}
616
+ <ChatMessageBubble
617
+ variant="ghost"
618
+ width="100%"
619
+ metadata={
620
+ <ChatMessageMetadata
621
+ timestamp={
622
+ <Timestamp
623
+ value="2026-04-29T10:18:40"
624
+ format="time"
625
+ />
626
+ }
623
627
  />
624
- }
625
- />
628
+ }>
629
+ <ArtifactCard onOpen={openArtifact} />
630
+ </ChatMessageBubble>
626
631
  </ChatMessage>
627
632
 
628
633
  {/* User follow-up */}
@@ -202,8 +202,11 @@ export const butterTheme = defineTheme({
202
202
  // Radius
203
203
  // --radius-element drives buttons, badges, inputs — 8px per design
204
204
  // --radius-container drives cards, banners, popovers — 12px per design
205
+ // --radius-none and --radius-full are always fixed and must never be
206
+ // scaled by a theme (see defineTheme's radius config docs) — 0 and
207
+ // 9999px respectively, matching @astryxdesign/core's own defaults.
205
208
  // =========================================================================
206
- '--radius-none': '0.125rem',
209
+ '--radius-none': '0px',
207
210
  '--radius-inner': '0.375rem',
208
211
  '--radius-element': '0.5rem', // 8px
209
212
  '--radius-container': '0.75rem', // 12px
@@ -176,8 +176,11 @@ export const chocolateTheme = defineTheme({
176
176
 
177
177
  // =========================================================================
178
178
  // Radius — soft and rounded
179
+ // --radius-none and --radius-full are always fixed and must never be
180
+ // scaled by a theme (see defineTheme's radius config docs) — 0 and
181
+ // 9999px respectively, matching @astryxdesign/core's own defaults.
179
182
  // =========================================================================
180
- '--radius-none': '0.125rem',
183
+ '--radius-none': '0px',
181
184
  '--radius-inner': '0.375rem',
182
185
  '--radius-element': '0.625rem',
183
186
  '--radius-container': '0.75rem',
@@ -201,8 +201,11 @@ export const gothicTheme = defineTheme({
201
201
 
202
202
  // =========================================================================
203
203
  // Radius — subtle rounding (original gothic)
204
+ // --radius-none and --radius-full are always fixed and must never be
205
+ // scaled by a theme (see defineTheme's radius config docs) — 0 and
206
+ // 9999px respectively, matching @astryxdesign/core's own defaults.
204
207
  // =========================================================================
205
- '--radius-none': '0.125rem',
208
+ '--radius-none': '0px',
206
209
  '--radius-inner': '0.25rem',
207
210
  '--radius-element': '0.5rem',
208
211
  '--radius-container': '0.75rem',
@@ -219,8 +219,11 @@ export const stoneTheme = defineTheme({
219
219
 
220
220
  // =========================================================================
221
221
  // Radius — clean and subtle
222
+ // --radius-none and --radius-full are always fixed and must never be
223
+ // scaled by a theme (see defineTheme's radius config docs) — 0 and
224
+ // 9999px respectively, matching @astryxdesign/core's own defaults.
222
225
  // =========================================================================
223
- '--radius-none': '0.125rem',
226
+ '--radius-none': '0px',
224
227
  '--radius-inner': '0.25rem',
225
228
  '--radius-element': '0.5rem',
226
229
  '--radius-container': '0.75rem',
@@ -0,0 +1,322 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * Astryx theme template — every `defineTheme` field, with a note on what it
5
+ * does and when to reach for it.
6
+ *
7
+ * Only `name` is required. Delete what you do not need: the values here are
8
+ * placeholders that reproduce the built-in defaults, so a field you leave
9
+ * untouched changes nothing.
10
+ *
11
+ * Each section names the CLI command that prints the authoritative reference
12
+ * for it. These comments are a map; the CLI is the territory — it reads the
13
+ * same source the components do. `astryx docs` lists every topic.
14
+ *
15
+ * ═══════════════════════════════════════════════════════════════════════
16
+ * HOW A THEME IS CONSUMED
17
+ * ═══════════════════════════════════════════════════════════════════════
18
+ *
19
+ * 1. WRAP THE APP. A theme does nothing until it is provided.
20
+ *
21
+ * import {Theme} from '@astryxdesign/core';
22
+ * import {myTheme} from './theme';
23
+ *
24
+ * <Theme theme={myTheme} mode="system"> // 'system' | 'light' | 'dark'
25
+ * <App />
26
+ * </Theme>
27
+ *
28
+ * `system` follows the OS. Nest a second `<Theme>` to give one region its
29
+ * own theme or mode — a dark sidebar inside a light page.
30
+ *
31
+ * 2. SHIP THE FONTS YOU NAME. Astryx sets the `--font-family-*` tokens; it
32
+ * never loads a font file, so use whatever typeface the design needs and
33
+ * load it yourself. Either add it to your app's <head>:
34
+ *
35
+ * <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
36
+ * <link rel="stylesheet"
37
+ * href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" />
38
+ *
39
+ * or self-host it in your global CSS:
40
+ *
41
+ * @font-face {
42
+ * font-family: 'Inter';
43
+ * src: url('/fonts/Inter.woff2') format('woff2');
44
+ * font-weight: 100 900;
45
+ * font-display: swap;
46
+ * }
47
+ *
48
+ * Always give `fallbacks` as well. A named family with no file loads
49
+ * nothing and warns about nothing — the fallback silently becomes your
50
+ * theme.
51
+ *
52
+ * 3. BUILD IT FOR PRODUCTION.
53
+ *
54
+ * astryx theme build src/theme.ts
55
+ *
56
+ * writes `<name>.css` (tokens, component overrides and prose styles in
57
+ * `@scope` rules), `<name>.js` (the theme with `__built: true` and
58
+ * pre-resolved values), `<name>.d.ts`, and `<name>.variants.d.ts` when the
59
+ * theme adds custom prop values. Import the module and the CSS together:
60
+ *
61
+ * import {myThemeTheme} from './theme/my-theme';
62
+ * import './theme/my-theme.css';
63
+ *
64
+ * Unbuilt, `<Theme>` injects a <style> tag at hydration instead — fine for
65
+ * dev and client-only apps, but under SSR the component overrides flash on
66
+ * hydration. Build for Next.js and Remix. Run the build after every edit:
67
+ * it also validates the theme and regenerates the types for custom
68
+ * variants.
69
+ *
70
+ * 4. LOOK AT IT IN BOTH COLOUR MODES before you ship, including the states you
71
+ * did not write. Full guide: `astryx docs theme`.
72
+ *
73
+ * SYNC: this template mirrors the theme system. When you change one of these,
74
+ * update it here — scripts/check-theme-template.test.mjs fails when they drift:
75
+ * - /packages/core/src/theme/defineTheme.ts (the fields)
76
+ * - /packages/core/src/theme/tokens.stylex.ts (the token families)
77
+ * - /packages/core/src/theme/expandColorScale.ts
78
+ * - /packages/core/src/theme/expandTypeScale.ts
79
+ * - /packages/core/src/theme/expandRadiusScale.ts
80
+ * - /packages/core/src/theme/expandMotionScale.ts
81
+ */
82
+
83
+ import {defineTheme} from '@astryxdesign/core/theme';
84
+ // import {dracula} from '@astryxdesign/core/theme/syntax';
85
+ // import {neutralTheme} from '@astryxdesign/theme-neutral';
86
+
87
+ export const myTheme = defineTheme({
88
+ /** Required. Becomes the `data-astryx-theme` attribute and the registry key. */
89
+ name: 'my-theme',
90
+
91
+ /**
92
+ * Start from another theme instead of the defaults. Tokens are copied then
93
+ * overridden, `components` deep-merge, `icons` shallow-merge — but the scale
94
+ * configs below REPLACE the base's rather than merging, because they are
95
+ * inputs to a generator, not values.
96
+ *
97
+ * Reference: `astryx theme list` (themes you can install and extend).
98
+ */
99
+ // extends: neutralTheme,
100
+
101
+ // ───────────────────────────────────────────────────────────────────────
102
+ // Scale configs — a few parameters generate a whole family of tokens.
103
+ // Reach for these first. They keep a theme internally consistent and cover
104
+ // far more ground than you would hand-write.
105
+ // ───────────────────────────────────────────────────────────────────────
106
+
107
+ /**
108
+ * Generates the neutral ramp and the accent tokens from one seed colour
109
+ * using the HCT perceptual model: surfaces, text, icons, borders, muted
110
+ * fills, hover and pressed overlays — light and dark both.
111
+ *
112
+ * accent seed hex; omit to keep the default accent and re-tone only
113
+ * the neutrals
114
+ * neutralStyle 'warm' | 'cool' | 'neutral' — the temperature of the greys
115
+ * contrast 'standard' | 'high' — 'high' widens the text/surface tone
116
+ * gap, for dense data UI or bright and clinical screens
117
+ *
118
+ * Whatever seed you pass, generated text holds >= 4.5:1 against its surface
119
+ * and --color-border-emphasized >= 3:1. Two limits on that guarantee:
120
+ *
121
+ * - It covers what this config generates. Write one side of a pair by hand
122
+ * in `tokens` below — an accent without its --color-on-accent, a surface
123
+ * without its text — and you own the contrast for that pair.
124
+ * - Status colours (success, warning, error) and the categorical data hues
125
+ * are not derived from the accent. They keep their defaults, which are
126
+ * tuned for the default surfaces; check them against yours.
127
+ *
128
+ * Either way the check is the same: resolve the pair in BOTH modes and
129
+ * measure it. `useTheme()` and `resolveThemeTokens()` return the resolved
130
+ * values, and the rendered DOM is the final word.
131
+ *
132
+ * Reference: `astryx docs color` for what each semantic role means,
133
+ * `astryx docs tokens` for every colour token and its light/dark default.
134
+ */
135
+ color: {accent: '#0064E0', neutralStyle: 'cool', contrast: 'standard'},
136
+
137
+ /**
138
+ * Type scale and the three font roles.
139
+ * scale.base body size in px; scale.ratio step between sizes
140
+ * (1.125 tight/dense · 1.2 default · 1.333 dramatic)
141
+ * body / heading / code: {family, fallbacks, weight, weights}
142
+ * heading inherits family and fallbacks from body when omitted; `weights`
143
+ * sets per-level weight for headings 1–6.
144
+ *
145
+ * See §2 above — naming a family here does not load it.
146
+ *
147
+ * Reference: `astryx docs typography`.
148
+ */
149
+ typography: {
150
+ scale: {base: 16, ratio: 1.2},
151
+ body: {family: 'Inter', fallbacks: '-apple-system, system-ui, sans-serif'},
152
+ heading: {weight: 'semibold', weights: {1: 'bold', 2: 'bold'}},
153
+ code: {
154
+ family: 'Geist Mono',
155
+ fallbacks: '"SF Mono", ui-monospace, monospace',
156
+ },
157
+ },
158
+
159
+ /**
160
+ * Radius scale. `base` is the unit in px; `multiplier` scales every step
161
+ * (0 = square and brutalist, 2 = very soft). --radius-none and --radius-full
162
+ * are fixed and never scaled.
163
+ *
164
+ * Reference: `astryx docs shape`.
165
+ */
166
+ radius: {base: 4, multiplier: 1},
167
+
168
+ /**
169
+ * Duration scale in ms. Each of fast/medium/slow also gets a -min and -max
170
+ * sibling computed with `ratio` (min = base × ratio, max = base ÷ ratio), so
171
+ * a component can pick a longer or shorter variant of the same tempo.
172
+ * Snappy {fast: 100, medium: 250} · Default {fast: 175, medium: 410, slow: 975}
173
+ * Cinematic {fast: 200, medium: 500, slow: 1200}
174
+ * `easing` overrides --ease-standard.
175
+ *
176
+ * Reference: `astryx docs motion`.
177
+ */
178
+ motion: {fast: 175, medium: 410, slow: 975, ratio: 0.75},
179
+
180
+ // ───────────────────────────────────────────────────────────────────────
181
+ // tokens — explicit overrides, which beat anything the scales generated.
182
+ // A string applies to both colour modes; a [light, dark] tuple compiles to
183
+ // CSS light-dark(). List only what you want to change.
184
+ //
185
+ // `astryx docs tokens` PRINTS THE WHOLE TABLE — every token with its light
186
+ // and dark default. Read it before hand-writing colours: most of what you
187
+ // want already has a semantic name, and the defaults tell you what you are
188
+ // moving away from.
189
+ //
190
+ // The families, so you know what exists:
191
+ // --color-* accent, on-accent · background-{body,surface,card,popover,
192
+ // muted,inverted} · text-* · icon-* · border,
193
+ // border-emphasized · overlay{,-hover,-pressed} ·
194
+ // success/warning/error (+ -muted, on-*) · skeleton, track,
195
+ // shadow, tint-hover · categorical {blue,cyan,gray,green,
196
+ // orange,pink,purple,red,teal,yellow} ×
197
+ // {background,border,icon,text}
198
+ // --spacing-* 0 · 0-5 · 1 … 12
199
+ // --size-element-* sm · md · lg (control heights)
200
+ // --focus-outline-* width · style · color · offset
201
+ // --border-width hairline thickness shared by bordered surfaces
202
+ // --radius-* none · inner · element · container · page · chat · full
203
+ // --shadow-* low/med/high + inset-{hover,selected,success,warning,error}
204
+ // --duration-* {fast,medium,slow} × {-min, base, -max}
205
+ // --ease-standard
206
+ // --font-family-* body · heading · code
207
+ // --font-size-* 4xs … 5xl
208
+ // --font-weight-* normal · medium · semibold · bold
209
+ // --text-* {heading-1…6, body, large, label, code, supporting,
210
+ // display-1…3} × {-size, -weight, -leading}
211
+ // --color-syntax-* code highlighting · --color-data-* charts
212
+ // ───────────────────────────────────────────────────────────────────────
213
+ tokens: {
214
+ '--color-accent': ['#0064E0', '#2694FE'],
215
+ // Changing an accent means owning its on-colour: this is the label that
216
+ // sits on top of the fill above, in each mode.
217
+ '--color-on-accent': ['#FFFFFF', '#FFFFFF'],
218
+ '--color-background-body': ['#F1F4F7', '#111112'],
219
+ '--color-background-surface': ['#FFFFFF', '#1F1F22'],
220
+ '--focus-outline-color': 'var(--color-accent)',
221
+ '--shadow-low': '0 1px 2px light-dark(#0000001A, #00000066)',
222
+ },
223
+
224
+ // ───────────────────────────────────────────────────────────────────────
225
+ // components — per-component CSS, emitted inside
226
+ // `@scope ([data-astryx-theme="my-theme"])`, so it only touches your theme.
227
+ //
228
+ // Keys are the component's stable class minus the `astryx-` prefix
229
+ // (`astryx-button` → `button`, `astryx-side-nav-item` → `side-nav-item`),
230
+ // including inner parts like `progressbar-fill`.
231
+ //
232
+ // `astryx component <Name>` IS THE REFERENCE HERE. It prints that
233
+ // component's theming targets, the visual props and states you can key on,
234
+ // its public CSS variables with defaults, and which standard CSS properties
235
+ // it expands. `astryx component --list` names every component, and
236
+ // `astryx docs styling` covers the escape hatches outside a theme.
237
+ //
238
+ // Style-key forms:
239
+ // 'base' every instance
240
+ // 'variant:ghost' one prop value (any visual prop, not just variant)
241
+ // 'variant:ghost+size:sm' intersection of two
242
+ // 'checked' a state the component reflects
243
+ // ':hover' / ':focus-visible' / ':disabled' — nested inside any of the above
244
+ //
245
+ // Write ordinary CSS properties: `borderRadius` and `padding` expand into
246
+ // the component's internal vars for you, concentric radius math included.
247
+ // Set a public CSS var directly when no standard property reaches it — take
248
+ // the name from `astryx component <Name>`, since a var no component defines
249
+ // compiles to CSS that never applies. Never set a private `--_`-prefixed
250
+ // var; the build rejects it.
251
+ // ───────────────────────────────────────────────────────────────────────
252
+ components: {
253
+ button: {
254
+ base: {
255
+ borderRadius: 'var(--radius-full)',
256
+ fontWeight: 'var(--font-weight-semibold)',
257
+ // A public var from `astryx component Button` — no CSS-property
258
+ // equivalent exists for it.
259
+ '--button-focus-offset': '2px',
260
+ // Interaction states live inside the same block.
261
+ ':hover': {transform: 'translateY(-1px)'},
262
+ ':active': {transform: 'translateY(0)'},
263
+ },
264
+ 'variant:ghost': {borderWidth: '1px', borderStyle: 'solid'},
265
+ 'variant:destructive+size:sm': {letterSpacing: '0.02em'},
266
+ // A value that is not built in becomes a NEW variant: `astryx theme
267
+ // build` writes the module augmentation, so <Button variant="quiet" />
268
+ // typechecks wherever this theme is active. Add one only where the
269
+ // product will render it — an unused variant is invisible work.
270
+ 'variant:quiet': {
271
+ backgroundColor: 'var(--color-background-muted)',
272
+ color: 'var(--color-text-secondary)',
273
+ },
274
+ },
275
+ // Container components take `padding` too; it expands to their layout tokens.
276
+ card: {
277
+ base: {
278
+ borderRadius: 'var(--radius-container)',
279
+ padding: 'var(--spacing-6)',
280
+ },
281
+ },
282
+ // The same mechanism adds custom Text types: <Text type="hero" />.
283
+ text: {'type:hero': {fontSize: 'var(--font-size-4xl)', lineHeight: '1.05'}},
284
+ },
285
+
286
+ // ───────────────────────────────────────────────────────────────────────
287
+ // Everything below takes React values, so it needs a .tsx file or an
288
+ // imported module. Commented out for that reason.
289
+ // ───────────────────────────────────────────────────────────────────────
290
+
291
+ /**
292
+ * Swap the artwork behind semantic icon names — every component that asks
293
+ * for `check`, `close`, `chevron-down` gets yours.
294
+ * Reference: `astryx docs icons` lists the names.
295
+ */
296
+ // icons: {check: <MyCheck />, close: <MyClose />},
297
+
298
+ /**
299
+ * Replace the small components that DRAW control state — the checkbox box,
300
+ * the radio dot, the mark on a chosen option — by indicator name rather than
301
+ * per call site. Replacing `check` re-skins every single-selection mark in
302
+ * the app at once.
303
+ * Reference: `astryx component Indicator`.
304
+ */
305
+ // indicators: {check: RadioIndicator},
306
+
307
+ /**
308
+ * Code highlighting: sets the --color-syntax-* tokens. Presets live in
309
+ * `@astryxdesign/core/theme/syntax`.
310
+ */
311
+ // syntax: dracula,
312
+
313
+ /**
314
+ * Content sitting on an inverted surface — a dark toast in light mode, a
315
+ * light popover in dark mode. Same shape as the theme itself (tokens and
316
+ * components). Sensible values are generated if you say nothing, so override
317
+ * only where your palette needs it.
318
+ * Reference: `astryx component MediaTheme`, which reads these.
319
+ */
320
+ // onDark: {tokens: {'--color-accent': '#90CAF9'}, components: {button: {'variant:ghost': {borderWidth: '1px'}}}},
321
+ // onLight: {tokens: {'--color-accent': '#0B5FCC'}},
322
+ });
@@ -117,6 +117,14 @@ export interface ComponentPlaygroundConfig {
117
117
  * the component is visible on load and knobs stay usable, whereas a real
118
118
  * top-layer modal makes the rest of the page inert (#3657). */
119
119
  overlay?: boolean;
120
+ /** The component reads AppShell mobile context and renders nothing
121
+ * without it (e.g. `MobileNavToggle` returns null unless the context
122
+ * reports an enabled mobile viewport — the default value outside
123
+ * AppShell never does). The interactive preview provides a simulated
124
+ * mobile AppShell context so the stage is not an empty box, keeps the
125
+ * drawer open state interactive, and notes the simulation under the
126
+ * rendered component (#4983). */
127
+ appShellMobile?: boolean;
120
128
  /** Required parent wrapper for sub-components that depend on a parent
121
129
  * context provider (e.g. `Tab` calls `useTabListContext()` and throws
122
130
  * standalone). The preview wraps the component in this parent before
@@ -339,7 +347,7 @@ export interface ComponentThemingTarget {
339
347
  * ```
340
348
  */
341
349
  export interface ComponentThemingVar {
342
- /** CSS custom property name, e.g. '--_card-radius' or '--button-press-scale' */
350
+ /** CSS custom property name, e.g. '--_card-radius' or '--button-focus-offset' */
343
351
  name: string;
344
352
  /** What this var controls */
345
353
  description: string;
@@ -146,7 +146,7 @@ export const doc = {
146
146
  name: 'playground',
147
147
  type: 'ComponentPlaygroundConfig',
148
148
  description:
149
- 'Interactive-preview config: initial prop `defaults`, `overlay` for modal-only components, and a `wrapper` for context-dependent sub-components.',
149
+ 'Interactive-preview config: initial prop `defaults`, `overlay` for modal-only components, `appShellMobile` for components gated on AppShell mobile context, and a `wrapper` for context-dependent sub-components.',
150
150
  },
151
151
  {
152
152
  name: 'props',