@astryxdesign/cli 0.4.1 → 0.4.2-canary.464a445
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +1 -0
- package/api/init/init.doc.mjs +2 -2
- package/api/init/init.test.mjs +19 -2
- package/api/init/init.type.d.mts +9 -1
- package/api/init/init.type.mjs +3 -1
- package/api/init/run/run.mjs +36 -6
- package/api/theme/add/add.mjs +2 -13
- package/api/theme/build/build.font-warning.test.mjs +161 -0
- package/api/theme/build/build.mjs +19 -12
- package/api/theme/build/build.test.mjs +53 -0
- package/api/theme/build/font-warning.d.mts +26 -0
- package/api/theme/build/font-warning.mjs +214 -0
- package/api/theme/build/font-warning.test.mjs +242 -0
- package/api/theme/template/template.d.mts +21 -0
- package/api/theme/template/template.mjs +69 -0
- package/api/theme/template/template.test.mjs +82 -0
- package/api/theme/theme.d.mts +1 -0
- package/api/theme/theme.mjs +1 -0
- package/api/theme/theme.type.d.mts +13 -0
- package/api/theme/theme.type.mjs +11 -1
- package/api/theme/themeTemplate.doc.d.mts +11 -0
- package/api/theme/themeTemplate.doc.mjs +64 -0
- package/assets/docs/getting-started.doc.mjs +11 -1
- package/assets/docs/theme.doc.dense.mjs +2 -2
- package/assets/docs/theme.doc.mjs +20 -9
- package/assets/docs/theme.doc.zh.mjs +1 -1
- package/assets/docs/typography.doc.mjs +38 -0
- package/assets/templates/blocks/components/ChatMessageBubble/ChatMessageBubbleCustomContent.doc.mjs +13 -0
- package/assets/templates/blocks/components/ChatMessageBubble/ChatMessageBubbleCustomContent.tsx +55 -0
- package/assets/templates/pages/ai-chat/page.tsx +18 -13
- package/assets/templates/themes/butter/butterTheme.ts +4 -1
- package/assets/templates/themes/chocolate/chocolateTheme.ts +4 -1
- package/assets/templates/themes/gothic/gothicTheme.ts +4 -1
- package/assets/templates/themes/stone/stoneTheme.ts +4 -1
- package/assets/theme.template.ts +322 -0
- package/authoring/doctypes/base/type.ts +9 -1
- package/authoring/doctypes/component/component.doc.mjs +1 -1
- package/clients/cli/commands/build-theme.font-warning.test.mjs +138 -0
- package/clients/cli/commands/build-theme.mjs +50 -0
- package/clients/cli/commands/init.behavior.test.mjs +13 -2
- package/clients/cli/commands/theme-template.behavior.test.mjs +85 -0
- package/clients/cli/commands/theme-template.doc.mjs +42 -0
- package/clients/cli/commands/theme.doc.mjs +2 -2
- package/clients/cli/index.mjs +1 -0
- package/clients/cli/lib/manifest.mjs +2 -0
- package/foundation/agent-docs/agent-docs.mjs +1 -1
- package/foundation/response/response-types.doc.mjs +5 -0
- package/foundation/text/copyright-header.d.mts +11 -0
- package/foundation/text/copyright-header.mjs +35 -0
- package/foundation/text/copyright-header.test.mjs +58 -0
- package/package.json +9 -9
|
@@ -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-
|
|
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',
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file End-to-end test for the `astryx theme build` font-loading warning
|
|
5
|
+
* (#5015). A theme that names webfont families gets, AFTER the install
|
|
6
|
+
* instructions, a stdout notice naming the fonts plus the copy-pasteable
|
|
7
|
+
* fix — the Google Fonts <link> pair and a self-hosted @font-face with
|
|
8
|
+
* font-display: swap — and still exits 0 (it is a warning, not an error).
|
|
9
|
+
* Themes that only name generics or known system stacks get none of it.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {describe, it, expect, beforeAll, 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 {ensureCoreBuilt} from './ensure-core-built.mjs';
|
|
17
|
+
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
18
|
+
|
|
19
|
+
function writeTheme(dir, name, source) {
|
|
20
|
+
fs.mkdirSync(dir, {recursive: true});
|
|
21
|
+
const file = path.join(dir, `${name}.mjs`);
|
|
22
|
+
fs.writeFileSync(file, source);
|
|
23
|
+
return file;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// `astryx theme build` imports the compiled @astryxdesign/core/theme entry.
|
|
27
|
+
// Build core once if it isn't already present so the suite works in any CI
|
|
28
|
+
// job, regardless of job ordering.
|
|
29
|
+
beforeAll(() => {
|
|
30
|
+
ensureCoreBuilt();
|
|
31
|
+
}, 200_000);
|
|
32
|
+
|
|
33
|
+
let tmpDir;
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-build-theme-fonts-'));
|
|
36
|
+
});
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('theme build font-loading warning', () => {
|
|
42
|
+
it('prints the unloaded fonts and the <link>/@font-face fix after the install instructions', async () => {
|
|
43
|
+
const project = path.join(tmpDir, 'project');
|
|
44
|
+
const themeFile = writeTheme(
|
|
45
|
+
project,
|
|
46
|
+
'fonty',
|
|
47
|
+
`export default {
|
|
48
|
+
name: 'fonty',
|
|
49
|
+
typography: {
|
|
50
|
+
body: {family: 'Space Grotesk', fallbacks: 'Arial, sans-serif'},
|
|
51
|
+
code: {family: 'JetBrains Mono'},
|
|
52
|
+
},
|
|
53
|
+
};\n`,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const result = await runCli(
|
|
57
|
+
['theme', 'build', path.relative(project, themeFile)],
|
|
58
|
+
project,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(result.code).toBe(0);
|
|
62
|
+
// The install instructions still come out intact…
|
|
63
|
+
expect(result.stdout).toContain("from './fonty'");
|
|
64
|
+
// …followed by the warning naming the theme and every unloaded family…
|
|
65
|
+
expect(result.stdout).toContain('names fonts it does not load');
|
|
66
|
+
expect(result.stdout).toContain('"Space Grotesk"');
|
|
67
|
+
expect(result.stdout).toContain('"JetBrains Mono"');
|
|
68
|
+
// …and the copy-pasteable fix, both flavors.
|
|
69
|
+
expect(result.stdout).toContain(
|
|
70
|
+
'<link rel="preconnect" href="https://fonts.googleapis.com"',
|
|
71
|
+
);
|
|
72
|
+
expect(result.stdout).toContain('family=Space+Grotesk');
|
|
73
|
+
expect(result.stdout).toContain('family=JetBrains+Mono');
|
|
74
|
+
expect(result.stdout).toContain('display=swap');
|
|
75
|
+
expect(result.stdout).toContain('@font-face');
|
|
76
|
+
expect(result.stdout).toContain('font-display: swap');
|
|
77
|
+
expect(result.stdout).toContain('astryx docs typography');
|
|
78
|
+
// The one-line summaries follow the CLI's stream contract: warnings on
|
|
79
|
+
// stderr, like the override-validation warnings in the same build.
|
|
80
|
+
expect(result.stderr).toContain('Font "Space Grotesk"');
|
|
81
|
+
expect(result.stderr).toContain('Font "JetBrains Mono"');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('keeps --json stdout one valid envelope: warnings inside, snippet suppressed', async () => {
|
|
85
|
+
const project = path.join(tmpDir, 'project');
|
|
86
|
+
const themeFile = writeTheme(
|
|
87
|
+
project,
|
|
88
|
+
'fonty',
|
|
89
|
+
`export default { name: 'fonty', tokens: { '--font-family-body': '"Space Grotesk", sans-serif' } };\n`,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const result = await runCli(
|
|
93
|
+
['--json', 'theme', 'build', path.relative(project, themeFile)],
|
|
94
|
+
project,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect(result.code).toBe(0);
|
|
98
|
+
// The whole stdout must parse — any human snippet leaking into --json
|
|
99
|
+
// mode corrupts the envelope, so this asserts the "JSON is always JSON"
|
|
100
|
+
// contract, not just substring presence.
|
|
101
|
+
const envelope = JSON.parse(result.stdout);
|
|
102
|
+
expect(envelope.type).toBe('theme.build');
|
|
103
|
+
expect(envelope.data.warnings).toEqual(
|
|
104
|
+
expect.arrayContaining([expect.stringContaining('Font "Space Grotesk"')]),
|
|
105
|
+
);
|
|
106
|
+
expect(result.stdout).not.toContain('fonts.googleapis.com');
|
|
107
|
+
// Human one-liners are silenced too — machine mode stays quiet.
|
|
108
|
+
expect(result.stderr).not.toContain('Font "');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('prints nothing font-related for a theme of generics and system stacks', async () => {
|
|
112
|
+
const project = path.join(tmpDir, 'project');
|
|
113
|
+
const themeFile = writeTheme(
|
|
114
|
+
project,
|
|
115
|
+
'sys',
|
|
116
|
+
`export default {
|
|
117
|
+
name: 'sys',
|
|
118
|
+
tokens: {
|
|
119
|
+
'--color-bg': '#fff',
|
|
120
|
+
'--font-family-body': 'Helvetica, Arial, sans-serif',
|
|
121
|
+
'--font-family-code': 'ui-monospace',
|
|
122
|
+
},
|
|
123
|
+
};\n`,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const result = await runCli(
|
|
127
|
+
['theme', 'build', path.relative(project, themeFile)],
|
|
128
|
+
project,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
expect(result.code).toBe(0);
|
|
132
|
+
expect(result.stdout).toContain("from './sys'");
|
|
133
|
+
expect(result.stdout).not.toContain('names fonts it does not load');
|
|
134
|
+
expect(result.stdout).not.toContain('fonts.googleapis.com');
|
|
135
|
+
expect(result.stdout).not.toContain('@font-face');
|
|
136
|
+
expect(result.stderr).not.toContain('Font "');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -30,6 +30,7 @@ import {logger} from '../../../api/logger.mjs';
|
|
|
30
30
|
import {cliError} from '../lib/cli-error.mjs';
|
|
31
31
|
import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
|
|
32
32
|
import {themeAdd} from '../../../api/theme/add/add.mjs';
|
|
33
|
+
import {themeTemplate} from '../../../api/theme/template/template.mjs';
|
|
33
34
|
import {themeList} from '../../../api/theme/list/list.mjs';
|
|
34
35
|
import {themeBuild, importSpecifier} from '../../../api/theme/build/build.mjs';
|
|
35
36
|
import {defineCommand} from '../lib/define-command.mjs';
|
|
@@ -37,9 +38,11 @@ import {doc as themeGroup} from './theme.doc.mjs';
|
|
|
37
38
|
import {doc as themeBuildCommand} from './theme-build.doc.mjs';
|
|
38
39
|
import {doc as themeListCommand} from './theme-list.doc.mjs';
|
|
39
40
|
import {doc as themeAddCommand} from './theme-add.doc.mjs';
|
|
41
|
+
import {doc as themeTemplateCommand} from './theme-template.doc.mjs';
|
|
40
42
|
import {doc as themeBuildFn} from '../../../api/theme/themeBuild.doc.mjs';
|
|
41
43
|
import {doc as themeListFn} from '../../../api/theme/themeList.doc.mjs';
|
|
42
44
|
import {doc as themeAddFn} from '../../../api/theme/themeAdd.doc.mjs';
|
|
45
|
+
import {doc as themeTemplateFn} from '../../../api/theme/themeTemplate.doc.mjs';
|
|
43
46
|
|
|
44
47
|
/**
|
|
45
48
|
* Path to this CLI's real entry (clients/cli/bin/astryx.mjs), resolved from
|
|
@@ -363,4 +366,51 @@ export function registerTheme(program) {
|
|
|
363
366
|
);
|
|
364
367
|
},
|
|
365
368
|
});
|
|
369
|
+
|
|
370
|
+
defineCommand(theme, themeTemplateCommand, {
|
|
371
|
+
fn: themeTemplateFn,
|
|
372
|
+
action: (
|
|
373
|
+
/** @type {string | undefined} */ targetPath,
|
|
374
|
+
/** @type {{overwrite?: boolean}} */ options,
|
|
375
|
+
) => {
|
|
376
|
+
const json = program.opts().json || false;
|
|
377
|
+
|
|
378
|
+
/** @type {import('../../../api/theme/theme.type.mjs').ThemeTemplateResponse} */
|
|
379
|
+
let result;
|
|
380
|
+
try {
|
|
381
|
+
result = themeTemplate({
|
|
382
|
+
targetPath,
|
|
383
|
+
overwrite: options.overwrite,
|
|
384
|
+
cwd: process.cwd(),
|
|
385
|
+
});
|
|
386
|
+
} catch (e) {
|
|
387
|
+
const err =
|
|
388
|
+
/** @type {import('../../../api/error.mjs').AstryxError} */ (e);
|
|
389
|
+
cliError(err.message, {
|
|
390
|
+
suggestions: err.suggestions || [],
|
|
391
|
+
code: err.code,
|
|
392
|
+
});
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (json) return jsonOut(result);
|
|
397
|
+
|
|
398
|
+
const invocation = getCliInvocation(process.cwd());
|
|
399
|
+
if (!result.data.written) {
|
|
400
|
+
emit(
|
|
401
|
+
text(`[skip] ${result.data.path} already exists — left as is.`),
|
|
402
|
+
text(`Pass --overwrite to replace it with a fresh copy.`),
|
|
403
|
+
);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
emit(
|
|
407
|
+
text(`[ok] Wrote ${result.data.path}`),
|
|
408
|
+
text(
|
|
409
|
+
'It documents every defineTheme field, the token families, and the component override syntax. ' +
|
|
410
|
+
'Copy what you need into your own theme file, then delete it.',
|
|
411
|
+
),
|
|
412
|
+
code(`${invocation} theme build ${result.data.path}`),
|
|
413
|
+
);
|
|
414
|
+
},
|
|
415
|
+
});
|
|
366
416
|
}
|
|
@@ -71,13 +71,24 @@ describe('astryx init --features', () => {
|
|
|
71
71
|
expect(exists('AGENTS.md')).toBe(true);
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
-
it('--features theme writes
|
|
74
|
+
it('--features theme writes the annotated theme template', async () => {
|
|
75
75
|
const {status, stdout} = await runCli(['init', '--features', 'theme'], {cwd: tmpDir});
|
|
76
76
|
expect(status).toBe(0);
|
|
77
|
-
expect(
|
|
77
|
+
expect(exists('theme.template.ts')).toBe(true);
|
|
78
|
+
expect(read('theme.template.ts')).toMatch(/defineTheme/);
|
|
79
|
+
expect(read('theme.template.ts')).not.toMatch(/Copyright \(c\) Meta Platforms/);
|
|
78
80
|
expect(stdout).toMatch(/theme/i);
|
|
79
81
|
});
|
|
80
82
|
|
|
83
|
+
it('--features theme never clobbers an existing theme.template.ts', async () => {
|
|
84
|
+
// Someone's edited copy outranks ours; re-running init must be safe.
|
|
85
|
+
fs.writeFileSync(path.join(tmpDir, 'theme.template.ts'), '// mine\n');
|
|
86
|
+
const {status, stdout} = await runCli(['init', '--features', 'theme'], {cwd: tmpDir});
|
|
87
|
+
expect(status).toBe(0);
|
|
88
|
+
expect(read('theme.template.ts')).toBe('// mine\n');
|
|
89
|
+
expect(stdout).toMatch(/already exists/);
|
|
90
|
+
});
|
|
91
|
+
|
|
81
92
|
it('rejects an unknown feature with exit 1 and a helpful message', async () => {
|
|
82
93
|
const {status, stderr} = await runCli(['init', '--features', 'bogus'], {cwd: tmpDir});
|
|
83
94
|
expect(status).toBe(1);
|