aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/theme.template.ts
blob: 64b61a8db2951c058437ec3ba333e3b7cca358b7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
/**
 * Astryx theme template — every `defineTheme` field, with a note on what it
 * does and when to reach for it.
 *
 * Only `name` is required. Delete what you do not need: the values here are
 * placeholders that reproduce the built-in defaults, so a field you leave
 * untouched changes nothing.
 *
 * Each section names the CLI command that prints the authoritative reference
 * for it. These comments are a map; the CLI is the territory — it reads the
 * same source the components do.  `astryx docs` lists every topic.
 *
 * ═══════════════════════════════════════════════════════════════════════
 * HOW A THEME IS CONSUMED
 * ═══════════════════════════════════════════════════════════════════════
 *
 * 1. WRAP THE APP. A theme does nothing until it is provided.
 *
 *      import {Theme} from '@astryxdesign/core/theme';
 *      import {myTheme} from './theme';
 *
 *      <Theme theme={myTheme} mode="system">   // 'system' | 'light' | 'dark'
 *        <App />
 *      </Theme>
 *
 *    `system` follows the OS. Nest a second `<Theme>` to give one region its
 *    own theme or mode — a dark sidebar inside a light page.
 *
 * 2. SHIP THE FONTS YOU NAME. Astryx sets the `--font-family-*` tokens; it
 *    never loads a font file, so use whatever typeface the design needs and
 *    load it yourself. Either add it to your app's <head>:
 *
 *      <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
 *      <link rel="stylesheet"
 *            href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" />
 *
 *    or self-host it in your global CSS:
 *
 *      @font-face {
 *        font-family: 'Inter';
 *        src: url('/fonts/Inter.woff2') format('woff2');
 *        font-weight: 100 900;
 *        font-display: swap;
 *      }
 *
 *    Always give `fallbacks` as well. A named family with no file loads
 *    nothing and warns about nothing — the fallback silently becomes your
 *    theme.
 *
 * 3. BUILD IT FOR PRODUCTION.
 *
 *      astryx theme build src/theme.ts
 *
 *    writes `<name>.css` (tokens, component overrides and prose styles in
 *    `@scope` rules), `<name>.js` (the theme with `__built: true` and
 *    pre-resolved values), `<name>.d.ts`, and `<name>.variants.d.ts` when the
 *    theme adds custom prop values. Import the module and the CSS together:
 *
 *      import {myThemeTheme} from './theme/my-theme';
 *      import './theme/my-theme.css';
 *
 *    Unbuilt, `<Theme>` injects a <style> tag at hydration instead — fine for
 *    dev and client-only apps, but under SSR the component overrides flash on
 *    hydration. Build for Next.js and Remix. Run the build after every edit:
 *    it also validates the theme and regenerates the types for custom
 *    variants.
 *
 * 4. LOOK AT IT IN BOTH COLOUR MODES before you ship, including the states you
 *    did not write. Full guide: `astryx docs theme`.
 *
 * SYNC: this template mirrors the theme system. When you change one of these,
 * update it here — scripts/check-theme-template.test.mjs fails when they drift:
 * - /packages/core/src/theme/defineTheme.ts     (the fields)
 * - /packages/core/src/theme/tokens.stylex.ts   (the token families)
 * - /packages/core/src/theme/expandColorScale.ts
 * - /packages/core/src/theme/expandTypeScale.ts
 * - /packages/core/src/theme/expandRadiusScale.ts
 * - /packages/core/src/theme/expandMotionScale.ts
 */

import {defineTheme} from '@astryxdesign/core/theme';
// import {palette} from './palette.generated';
// import {dracula} from '@astryxdesign/core/theme/syntax';
// import {neutralTheme} from '@astryxdesign/theme-neutral';

export const myTheme = defineTheme({
  /** Required. Becomes the `data-astryx-theme` attribute and the registry key. */
  name: 'my-theme',

  /**
   * Start from another theme instead of the defaults. Tokens are copied then
   * overridden, `components` deep-merge, `icons` shallow-merge — but the scale
   * configs below REPLACE the base's rather than merging, because they are
   * inputs to a generator, not values.
   *
   * Reference: `astryx theme list` (themes you can install and extend).
   */
  // extends: neutralTheme,

  // ───────────────────────────────────────────────────────────────────────
  // Scale configs — concise inputs for typography, radius, and motion.
  // The color scale remains supported for existing themes, but it is not the
  // recommended starting point for a new palette; see its note below.
  // ───────────────────────────────────────────────────────────────────────

  /**
   * Supported compatibility path for existing themes. This expands one accent
   * seed into the neutral ramp, surfaces, text, icons, borders, muted fills,
   * hover and pressed overlays — light and dark both. It is not an accent-only
   * edit.
   *
   * For a new complete palette, run:
   *
   *   astryx theme palette generate palette.config.json --out palette.generated.ts
   *
   * Review the generated candidate, then commit the palette module and receipt.
   * Uncomment the `palette` import above and reference its stops from semantic
   * tokens below. Regenerating an accepted palette then updates every mapped
   * token that keeps those references. Generation does not rewrite the theme.
   *
   *   accent        seed hex, or a [light, dark] pair to seed each scheme's
   *                 palette from its own colour; omit to keep the default
   *                 accent and re-tone only the neutrals
   *   neutralStyle  'warm' | 'cool' | 'neutral' — the temperature of the greys
   *   contrast      'standard' | 'high' — 'high' widens the text/surface tone
   *                 gap, for dense data UI or bright and clinical screens
   *
   * Whatever seed you pass, generated text holds >= 4.5:1 against its surface
   * and --color-border-emphasized >= 3:1. Two limits on that guarantee:
   *
   *   - It covers what this config generates. Write one side of a pair by hand
   *     in `tokens` below — an accent without its --color-on-accent, a surface
   *     without its text — and you own the contrast for that pair.
   *   - Status colours (success, warning, error) and the categorical data hues
   *     are not derived from the accent. They keep their defaults, which are
   *     tuned for the default surfaces; check them against yours.
   *
   * Either way the check is the same: resolve the pair in BOTH modes and
   * measure it. `useTheme()` and `resolveThemeTokens()` return the resolved
   * values, and the rendered DOM is the final word.
   *
   * Reference: `astryx docs color` for what each semantic role means,
   * `astryx docs tokens/tokens-color` for every colour token and its light/dark
   * default.
   */
  // color: {accent: '#0064E0', neutralStyle: 'cool', contrast: 'standard'},

  /**
   * Type scale and the three font roles.
   *   scale.base   body size in px; scale.ratio  step between sizes
   *                (1.125 tight/dense · 1.2 default · 1.333 dramatic)
   *   body / heading / code: {family, fallbacks, weight, weights}
   *   heading inherits family and fallbacks from body when omitted; `weights`
   *   sets per-level weight for headings 1–6.
   *
   * See §2 above — naming a family here does not load it.
   *
   * Reference: `astryx docs typography`.
   */
  typography: {
    scale: {base: 16, ratio: 1.2},
    body: {family: 'Inter', fallbacks: '-apple-system, system-ui, sans-serif'},
    heading: {weight: 'semibold', weights: {1: 'bold', 2: 'bold'}},
    code: {
      family: 'Geist Mono',
      fallbacks: '"SF Mono", ui-monospace, monospace',
    },
  },

  /**
   * Radius scale. `base` is the unit in px; `multiplier` scales every step
   * (0 = square and brutalist, 2 = very soft). --radius-none and --radius-full
   * are fixed and never scaled.
   *
   * Reference: `astryx docs shape`.
   */
  radius: {base: 4, multiplier: 1},

  /**
   * Duration scale in ms. Each of fast/medium/slow also gets a -min and -max
   * sibling computed with `ratio` (min = base × ratio, max = base ÷ ratio), so
   * a component can pick a longer or shorter variant of the same tempo.
   *   Snappy {fast: 100, medium: 250} · Default {fast: 175, medium: 410, slow: 975}
   *   Cinematic {fast: 200, medium: 500, slow: 1200}
   * `easing` overrides --ease-standard.
   *
   * Reference: `astryx docs motion`.
   */
  motion: {fast: 175, medium: 410, slow: 975, ratio: 0.75},

  // ───────────────────────────────────────────────────────────────────────
  // tokens — explicit overrides, which beat anything the scales generated.
  // A string applies to both colour modes; a [light, dark] tuple compiles to
  // CSS light-dark(). List only what you want to change.
  //
  // `astryx docs tokens/tokens-color` PRINTS THE WHOLE TABLE — every colour token
  // with its light and dark default (`astryx docs tokens` lists every category). Read it before hand-writing colours: most of what you
  // want already has a semantic name, and the defaults tell you what you are
  // moving away from.
  //
  // The families, so you know what exists:
  //   --color-*        accent, on-accent · background-{body,surface,card,popover,
  //                    muted,inverted} · text-* · icon-* · border,
  //                    border-emphasized · overlay{,-hover,-pressed} ·
  //                    success/warning/error (+ -muted, on-*) · skeleton, track,
  //                    shadow, tint-hover · categorical {blue,cyan,gray,green,
  //                    orange,pink,purple,red,teal,yellow} ×
  //                    {background,border,icon,text}
  //   --spacing-*      0 · 0-5 · 1 … 12
  //   --size-element-* sm · md · lg (control heights)
  //   --focus-outline-* width · style · color · offset
  //   --border-width   hairline thickness shared by bordered surfaces
  //   --radius-*       none · inner · element · container · page · chat · full
  //   --shadow-*       low/med/high + inset-{hover,selected,success,warning,error}
  //   --duration-*     {fast,medium,slow} × {-min, base, -max}
  //   --ease-standard
  //   --font-family-*  body · heading · code
  //   --font-size-*    4xs … 5xl
  //   --font-weight-*  normal · medium · semibold · bold
  //   --text-*         {heading-1…6, body, large, label, code, supporting,
  //                    display-1…3} × {-size, -weight, -leading}
  //   --color-syntax-* code highlighting · --color-data-* charts
  // ───────────────────────────────────────────────────────────────────────
  tokens: {
    // Keep generated roles connected to the accepted palette. Uncomment the
    // import above, then map each reviewed semantic role this way.
    // '--color-background-surface': [
    //   palette.neutral.light[100],
    //   palette.neutral.dark[15],
    // ],
    '--color-accent': ['#0064E0', '#2694FE'],
    // Changing an accent means owning its on-colour: this is the label that
    // sits on top of the fill above, in each mode.
    '--color-on-accent': ['#FFFFFF', '#FFFFFF'],
    '--color-background-body': ['#F1F4F7', '#111112'],
    '--focus-outline-color': 'var(--color-accent)',
    '--shadow-low': '0 1px 2px light-dark(#0000001A, #00000066)',
  },

  /** Optional theme-family-local roles accept any valid CSS custom-property name;
   *  prefixes do not establish ownership. */
  // localTokens: {},

  // ───────────────────────────────────────────────────────────────────────
  // components — per-component CSS, emitted inside
  // `@scope ([data-astryx-theme="my-theme"])`, so it only touches your theme.
  //
  // Keys are the component's stable class minus the `astryx-` prefix
  // (`astryx-button` → `button`, `astryx-side-nav-item` → `side-nav-item`),
  // including inner parts like `progress-bar-fill`.
  //
  // `astryx component <Name>` IS THE REFERENCE HERE. It prints that
  // component's theming targets, the visual props and states you can key on,
  // its public CSS variables with defaults, and which standard CSS properties
  // it expands. `astryx component --list` names every component, and
  // `astryx docs styling` covers the escape hatches outside a theme.
  //
  // Style-key forms:
  //   'base'                    every instance
  //   'variant:ghost'           one prop value (any visual prop, not just variant)
  //   'variant:ghost+size:sm'   intersection of two
  //   'checked'                 a state the component reflects
  //   ':hover' / ':focus-visible' / ':disabled' — nested inside any of the above
  //
  // Write ordinary CSS properties: `borderRadius` and `padding` expand into
  // the component's internal vars for you, concentric radius math included.
  // Set a public CSS var directly when no standard property reaches it — take
  // the name from `astryx component <Name>`, since a var no component defines
  // compiles to CSS that never applies. Never set a private `--_`-prefixed
  // var; the build rejects it.
  // ───────────────────────────────────────────────────────────────────────
  components: {
    button: {
      base: {
        borderRadius: 'var(--radius-full)',
        fontWeight: 'var(--font-weight-semibold)',
        // A public var from `astryx component Button` — no CSS-property
        // equivalent exists for it.
        '--button-focus-offset': '2px',
        // Interaction states live inside the same block.
        ':hover': {transform: 'translateY(-1px)'},
        ':active': {transform: 'translateY(0)'},
      },
      'variant:ghost': {borderWidth: '1px', borderStyle: 'solid'},
      'variant:destructive+size:sm': {letterSpacing: '0.02em'},
      // A value that is not built in becomes a NEW variant: `astryx theme
      // build` writes the module augmentation, so <Button variant="quiet" />
      // typechecks wherever this theme is active. Add one only where the
      // product will render it — an unused variant is invisible work.
      'variant:quiet': {
        backgroundColor: 'var(--color-background-muted)',
        color: 'var(--color-text-secondary)',
      },
    },
    // Container components take `padding` too; it expands to their layout tokens.
    card: {
      base: {
        borderRadius: 'var(--radius-container)',
        padding: 'var(--spacing-6)',
      },
    },
    // The same mechanism adds custom Text types: <Text type="hero" />.
    text: {'type:hero': {fontSize: 'var(--font-size-4xl)', lineHeight: '1.05'}},
  },

  // ───────────────────────────────────────────────────────────────────────
  // Everything below takes React values, so it needs a .tsx file or an
  // imported module. Commented out for that reason.
  // ───────────────────────────────────────────────────────────────────────

  /**
   * Swap the artwork behind semantic icon names — every component that asks
   * for `check`, `close`, `chevron-down` gets yours.
   * Reference: `astryx docs icons` lists the names.
   */
  // icons: {check: <MyCheck />, close: <MyClose />},

  /**
   * Replace the small components that DRAW control state — the checkbox box,
   * the radio dot, the mark on a chosen option — by indicator name rather than
   * per call site. Replacing `check` re-skins every single-selection mark in
   * the app at once.
   * Reference: `astryx component Indicator`.
   */
  // indicators: {check: RadioIndicator},

  /**
   * Code highlighting: sets the --color-syntax-* tokens. Presets live in
   * `@astryxdesign/core/theme/syntax`.
   */
  // syntax: dracula,

  /**
   * Content sitting on an inverted surface — a dark toast in light mode, a
   * light popover in dark mode. Same shape as the theme itself (tokens and
   * components). Sensible values are generated if you say nothing, so override
   * only where your palette needs it.
   * Reference: `astryx component MediaTheme`, which reads these.
   */
  // onDark: {tokens: {'--color-accent': '#90CAF9'}, components: {button: {'variant:ghost': {borderWidth: '1px'}}}},
  // onLight: {tokens: {'--color-accent': '#0B5FCC'}},

  // ───────────────────────────────────────────────────────────────────────
  // Theme adaptations
  // ───────────────────────────────────────────────────────────────────────

  /**
   * Opt-in values for reviewed environmental conditions. Width points use the
   * fixed names `sm`, `md`, `lg`, `xl`, and `2xl`; overriding the map alone
   * emits no CSS. Fields inside `when` are ANDed, and matching rules cascade in
   * authored order so later writes win.
   *
   * `from` includes its point; `below` excludes it. The other supported axes
   * are primary-pointer precision, contrast preference, and motion preference.
   * Component writes in a rule are validated exactly like root `components`.
   * The one addition: a custom value that is valid only because a theme
   * enrolls it must be declared at the root first, since generated type
   * augmentation is unconditional; a rule may then restyle it.
   */
  // Adaptations are opt-in. Uncomment and edit the example below when the
  // theme needs environmental values.
  // adaptations: {
  //   widthBreakpoints: {
  //     sm: 640,
  //     md: 768,
  //     lg: 1024,
  //     xl: 1280,
  //     '2xl': 1536,
  //   },
  //   rules: [
  //     {
  //       // Narrow at every pointer precision.
  //       when: {width: {below: 'md'}},
  //       value: {tokens: {'--spacing-4': '0.75rem'}},
  //     },
  //     {
  //       // Taller controls under a finger or stylus at every width.
  //       when: {pointer: 'coarse'},
  //       value: {
  //         tokens: {
  //           '--size-element-sm': '36px',
  //           '--size-element-md': '40px',
  //           '--size-element-lg': '44px',
  //         },
  //       },
  //     },
  //     // Combined conditions are peers, not nested syntax:
  //     // {
  //     //   when: {
  //     //     width: {from: 'lg', below: 'xl'},
  //     //     pointer: 'coarse',
  //     //     contrast: 'more',
  //     //     motion: 'reduce',
  //     //   },
  //     //   value: {components: {card: {base: {borderWidth: '2px'}}}},
  //     // },
  //   ],
  // },
});