staffa 0.6.1 → 0.7.0

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 (60) hide show
  1. package/README.md +34 -16
  2. package/dist/components/autocomplete.js +12 -9
  3. package/dist/components/box.js +15 -10
  4. package/dist/components/button.d.ts +3 -1
  5. package/dist/components/button.js +21 -28
  6. package/dist/components/buttonChooser.js +1 -1
  7. package/dist/components/buttonGroup.d.ts +3 -3
  8. package/dist/components/buttonGroup.js +3 -3
  9. package/dist/components/dialog.d.ts +3 -1
  10. package/dist/components/dialog.js +20 -10
  11. package/dist/components/field.js +7 -4
  12. package/dist/components/main.js +52 -15
  13. package/dist/components/menu.d.ts +23 -2
  14. package/dist/components/menu.js +54 -29
  15. package/dist/components/select.js +1 -1
  16. package/dist/components/tabs.js +3 -3
  17. package/dist/components/toast.js +8 -8
  18. package/dist/components/tooltip.js +4 -3
  19. package/dist/core.d.ts +14 -2
  20. package/dist/core.js +23 -0
  21. package/dist/staffa.esm.js +1 -1
  22. package/dist/theme.js +138 -224
  23. package/package.json +1 -1
  24. package/skill/ButtonOptions.md +2 -1
  25. package/skill/SKILL.md +108 -47
  26. package/skill/ToastOptions.md +1 -1
  27. package/skill/addContextMenu.md +28 -0
  28. package/skill/addTooltip.md +6 -2
  29. package/skill/autocomplete.md +9 -2
  30. package/skill/box.md +1 -0
  31. package/skill/button.md +5 -3
  32. package/skill/buttonGroup.md +5 -3
  33. package/skill/checkbox.md +1 -0
  34. package/skill/confirm.md +1 -1
  35. package/skill/dialog.md +2 -2
  36. package/skill/form.md +3 -2
  37. package/skill/main.md +5 -1
  38. package/skill/menuButton.md +1 -1
  39. package/skill/prompt.md +1 -1
  40. package/skill/select.md +1 -0
  41. package/skill/showFloatingMenu.md +5 -6
  42. package/skill/tabs.md +2 -2
  43. package/skill/textarea.md +3 -2
  44. package/skill/textline.md +1 -0
  45. package/skill/toast.md +2 -3
  46. package/src/components/autocomplete.ts +12 -9
  47. package/src/components/box.ts +15 -10
  48. package/src/components/button.ts +23 -32
  49. package/src/components/buttonChooser.ts +1 -1
  50. package/src/components/buttonGroup.ts +3 -3
  51. package/src/components/dialog.ts +22 -11
  52. package/src/components/field.ts +7 -4
  53. package/src/components/main.ts +47 -16
  54. package/src/components/menu.ts +73 -40
  55. package/src/components/select.ts +1 -1
  56. package/src/components/tabs.ts +3 -3
  57. package/src/components/toast.ts +8 -8
  58. package/src/components/tooltip.ts +4 -3
  59. package/src/core.ts +30 -2
  60. package/src/theme.ts +152 -234
package/src/theme.ts CHANGED
@@ -6,87 +6,58 @@ import A from "aberdeen";
6
6
  * # The surface model
7
7
  *
8
8
  * A Staffa app is a tree of **surfaces**. A surface is anything with its own
9
- * background and the text colour that goes on it — the page, a card, a raised
10
- * header, a coloured button. Mark an element as a surface with the `.s-s` class,
11
- * then add modifier classes to colour it. A surface carries two colours:
9
+ * background and matching ink — the page, a card, a coloured button. Mark an
10
+ * element with `.s-s` and (usually) one modifier class. There are two families:
12
11
  *
13
- * - `--s-a` its default **foreground** (ink)
14
- * - `--s-b` its default **background** (fill)
12
+ * - **Neutral surfaces** — `.s-s.neutral` (and the implicit page at `:root`). A calm
13
+ * neutral whose shade steps automatically with nesting depth (page → panel →
14
+ * raised, capped). No `tonal`/`outlined` variants. Use them for cards, bars,
15
+ * popovers — anything that just holds content.
16
+ * - **Accent surfaces** — `.s-s.primary`, `.s-s.danger`, `.s-s.success`,
17
+ * `.s-s.warning`, `.s-s.link` (a bare `.s-s` defaults to primary). A bright
18
+ * fill with white ink, painted as a subtle single-colour gradient. They support
19
+ * `.tonal` and `.outlined` variants. A surface nested *inside* an accent surface
20
+ * is always rendered filled, so it can't bleed into the vivid parent.
15
21
  *
16
- * set by a **level** modifier — `.base` (the page), `.panel` (the default card),
17
- * `.raised` (elevated chrome) — or an **accent role** modifier — `.primary`,
18
- * `.secondary`, `.gradient` (the brand sweep), `.neutral`, `.danger`,
19
- * `.success`, `.warning`.
22
+ * # Contextual tokens
20
23
  *
21
- * A **variant** modifier `.filled` (the default), `.tonal` or `.outlined` —
22
- * decides how `--s-a`/`--s-b` map onto the tokens widgets read
23
- * (`--s-fg`/`--s-bg`). A shared rule then derives muted/faint/border from that
24
- * pair and paints the element. Because the derivation reads `$s-fg`/`$s-bg`,
25
- * every surface gets its *own* legible secondary colours: drop a widget on any
26
- * surface and it adapts. (`:root` is an implicit filled surface, so the page
27
- * renders without extra classes.)
24
+ * Inside any surface (including `:root`) these inherited custom properties are
25
+ * defined, so widgets adapt to wherever they're nested:
28
26
  *
29
- * Components build surfaces by combining classes, e.g.
30
- * `A("div.s-s.panel.outlined", opts.attrs)` and because `opts.attrs` is the
31
- * caller's escape hatch, passing e.g. `.filled` or `.danger` overrides the
32
- * default look.
27
+ * | token | meaning |
28
+ * | ------------ | --------------------------------------------------------- |
29
+ * | `--s-bg` | the surface background |
30
+ * | `--s-text` | default ink (also applied as `color`) |
31
+ * | `--s-muted` | secondary text (subtitles, help) |
32
+ * | `--s-accent` | the surface's "pop" — the brand primary on neutral surfaces, the ink on accent surfaces |
33
+ * | `--s-faint` | hairline, derived from text/bg |
33
34
  *
34
- * | token | meaning |
35
- * | ---------------- | ---------------------------------------- |
36
- * | `--s-fg` | default text |
37
- * | `--s-bg` | background |
38
- * | `--s-fg-muted` | secondary text (subtitles, help) |
39
- * | `--s-fg-faint` | placeholders, disabled |
40
- * | `--s-border` / `--s-border-strong` | borders |
41
- * | `--s-accent` | brand "pop" colour (active indicators, etc.) |
42
- * | `--s-gradient` | full primary→secondary brand sweep (mark, active nav) |
43
- * | `--s-gradient-surface` | compressed sweep for filled gradient surfaces (buttons) |
44
- * | `--s-tint` | brand mid colour; the hue the neutral greys lean toward |
45
- * | `--s-glow` | soft coloured shadow for lit brand elements |
46
- * | `--s-link` / `--s-focus` | link & focus-ring colours |
47
- * | `--s-radius` / `--s-radius-lg` / `--s-shadow` | shape tokens |
35
+ * The brand/semantic colours are mode-independent and settable: `--s-primary`
36
+ * (the one brand colour — it tints the neutrals and defines `.s-s.primary`),
37
+ * `--s-danger`, `--s-success`, `--s-warning`, and `--s-link` (the link colour,
38
+ * also the fill of the `.s-s.link` surface). Links render in `--s-link` on
39
+ * neutral surfaces and in the ink on accent surfaces.
48
40
  *
49
- * `--s-accent` and `--s-link` default to the brand / link colour, but on a
50
- * bright coloured surface (`.primary`, `.danger`, …) they fall back to that
51
- * surface's own ink so they stay legible.
41
+ * # Borders & shadows
52
42
  *
53
- * # The palette
54
- *
55
- * All colours come from a small set of named **palette** tokens on `:root`, set
56
- * per mode the only place colours live, and the single place to re-skin:
57
- * `--s-primary`, `--s-secondary` (the two ends of `--s-gradient`), `--s-danger`,
58
- * `--s-success`, `--s-warning` (accent fills, which
59
- * double as semantic *ink* on neutral surfaces), `--s-neutral`, `--s-page`,
60
- * `--s-panel`, `--s-raised` (neutral fills), `--s-ink` (text on neutral) and
61
- * `--s-on-accent` (text on accent fills), plus `--s-link`/`--s-focus` and the
62
- * shape tokens. Every surface rule is wired to these, so they adapt with the
63
- * mode and with any override.
64
- *
65
- * # Variants
66
- *
67
- * Because the variant decides how `--s-a`/`--s-b` become `--s-fg`/`--s-bg`, the
68
- * three looks are generic and work on *any* role: `.tonal` reads the fill colour
69
- * as ink over a soft self-tint; `.outlined` reads it as ink over a transparent
70
- * fill with a coloured edge (inheriting the parent's background, so its derived
71
- * tokens read the real surroundings). Inside any of them `--s-fg`/`--s-bg` still
72
- * describe the real, rendered colours.
43
+ * Neutral surfaces carry a subtle hairline border by default (so a card reads as a
44
+ * card with no component help); it's applied through `:where()`, so a bar that
45
+ * wants only a divider overrides it trivially. Any surface can opt into elevation
46
+ * with `.shadow` or `.extra-shadow`, or drop a component's built-in shadow with
47
+ * `.no-shadow` (e.g. `S.button({ attrs: ".no-shadow" })`).
73
48
  *
74
49
  * # Customising
75
50
  *
76
- * There's no JS theme object the colours live in the palette `insertGlobalCss`
77
- * call below, branched on {@link getDarkMode}. Re-skin from your app by
78
- * overriding palette tokens (per mode if you like), or give a surface an
79
- * image/gradient background (set `--s-b` to the dominant fallback colour so
80
- * derived tokens stay sensible). Staffa uses global, `s-`-prefixed classes, so
81
- * nothing is scoped away from you.
51
+ * Re-skin by overriding the colour tokens (e.g. `--s-primary`). To add your own
52
+ * accent surface, just set `--s-bg` (and, if needed, `--s-text`) the gradient
53
+ * and the rest of the tokens follow automatically:
82
54
  *
83
55
  * ```ts
84
- * A(() => A.insertGlobalCss({ ":root": getDarkMode() ? "--s-primary:#28c4a0" : "--s-primary:#1f9d6b" }));
85
- * A.insertGlobalCss({ ".s-s.panel": "background: url(paper.png); --s-b: #efe9dd" });
56
+ * A.insertGlobalCss({ ".s-s.brand": "--s-bg:#ef6b00 --s-text:#fff" });
57
+ * S.button({ content: "Buy", attrs: ".brand" });
86
58
  * ```
87
59
  */
88
60
 
89
-
90
61
  const STORAGE_KEY = "staffa:darkMode";
91
62
 
92
63
  /** The explicit dark-mode choice; `undefined` follows the OS via {@link A.darkMode}. */
@@ -132,196 +103,149 @@ export function getDarkMode(allowAuto = false): boolean | undefined {
132
103
  }
133
104
 
134
105
  // ---------------------------------------------------------------------------
135
- // Reactive palette the ONE place colours live, and the only thing that
136
- // differs between light and dark. Everything else is wired to these named
137
- // tokens, so an app can re-skin by overriding just a few of them. The accent
138
- // names (--s-primary/-danger/-success/-warning) double as semantic *ink*
139
- // colours, legible as text/borders on neutral surfaces.
106
+ // The only mode-dependent thing: the neutral surface shades and their ink,
107
+ // written straight onto the surfaces (no intermediate palette vars). `:root` is
108
+ // the page (depth 0); each nested `.neutral` steps one shade up, capped at the
109
+ // `.neutral .neutral` rule. The accent (coloured) surfaces and everything else are
110
+ // mode-independent and live in the static block below.
140
111
  // ---------------------------------------------------------------------------
141
- // The neutral fills/inks aren't hard-coded greys: each one mixes a small dose
142
- // of `--s-tint` (the brand's mid colour, defined in the static block below)
143
- // into a true-grey base. Re-skin the brand and every "grey" — page, panels,
144
- // ink, the neutral fill — drifts subtly toward the new brand hue, light and
145
- // dark alike. The percentages are deliberately small: a tint you'd only spot
146
- // in a side-by-side, never a colour cast.
147
112
  A(() => {
148
113
  if (getDarkMode()) {
149
114
  A.insertGlobalCss({
150
- ":root":
151
- "--s-primary:#8b7bff --s-secondary:#ef7fd0 --s-danger:#ff6b6b --s-success:#46d39a --s-warning:#fbbf24 " +
152
- "--s-neutral: color-mix(in oklab, #3d4047, $s-tint 14%); " +
153
- "--s-page: color-mix(in oklab, #0e0f12, $s-tint 5%); " +
154
- "--s-panel: color-mix(in oklab, #17181c, $s-tint 6%); " +
155
- "--s-raised: color-mix(in oklab, #212327, $s-tint 8%); " +
156
- "--s-ink: color-mix(in oklab, #e9eaec, $s-tint 8%); " +
157
- "--s-on-accent:#0c0a14 --s-focus: color-mix(in srgb, $s-primary 45%, transparent); " +
158
- "--s-radius:12px --s-radius-lg:18px --s-shadow: 0 10px 34px rgba(0,0,0,0.5);",
159
- // Contextual link, restored across the neutral group (see static block).
160
- ":root, .s-s.base, .s-s.panel, .s-s.raised, .s-s.neutral": "--s-link:#6db3ff",
115
+ ":root, .s-s.neutral": "--s-bg:#0e0f12 --s-text:#e9eaec",
116
+ ".s-s.neutral": "--s-bg:#191b1f --s-text:#e9eaec",
117
+ ".s-s.neutral .s-s.neutral": "--s-bg:#23262b",
161
118
  });
162
119
  } else {
163
120
  A.insertGlobalCss({
164
- ":root":
165
- "--s-primary:#6c5ce7 --s-secondary:#d6459e --s-danger:#e23b3b --s-success:#1f9d6b --s-warning:#d97706 " +
166
- "--s-neutral: color-mix(in oklab, #c9cbd0, $s-tint 14%); " +
167
- "--s-page: color-mix(in oklab, #f3f4f6, $s-tint 5%); " +
168
- "--s-panel: color-mix(in oklab, #ffffff, $s-tint 2%); " +
169
- "--s-raised: color-mix(in oklab, #edeef0, $s-tint 7%); " +
170
- "--s-ink: color-mix(in oklab, #1d1f24, $s-tint 7%); " +
171
- "--s-on-accent:#0c0a14 --s-focus: color-mix(in srgb, $s-primary 35%, transparent); " +
172
- "--s-radius:12px --s-radius-lg:18px --s-shadow: 0 10px 30px rgba(20,24,40,0.13);",
173
- ":root, .s-s.base, .s-s.panel, .s-s.raised, .s-s.neutral": "--s-link:#2563eb",
121
+ ":root, .s-s.neutral": "--s-bg:#eef0f3 --s-text:#1d1f24",
122
+ ".s-s.neutral, .s-s.neutral": "--s-bg:#ffffff --s-text:#1d1f24",
123
+ ".s-s.neutral .s-s.neutral": "--s-bg:#f6f7f9",
174
124
  });
175
125
  }
176
126
  });
177
127
 
178
128
  // ---------------------------------------------------------------------------
179
- // Static structure — inserted once, all wired to the palette above.
180
- //
181
- // A surface is marked with `.s-s`. Its level/role modifier sets two anchors
182
- // (--s-a ink, --s-b fill); its variant modifier (`.filled` default, `.tonal`,
183
- // `.outlined`) decides how those anchors map onto the rendered fg/bg. `:root`
184
- // (the page) is an implicit filled surface. Every `.s-s` then runs the same
185
- // derive+paint step, reading the resolved fg/bg, so each one gets its own
186
- // legible secondary tokens regardless of variant.
187
- //
188
- // Rule order matters: the variant/role rules below all share specificity, so a
189
- // later one wins. `.filled` therefore comes *last* among the variants — that's
190
- // what lets a caller's `attrs: ".filled"` override a component's default
191
- // `.tonal`/`.outlined`.
129
+ // Static structure — inserted once. The colour tokens are mode-independent
130
+ // (saturated fills that carry white ink on either background); shape/effect
131
+ // tokens are single values kept only because they're reused across components.
132
+ // Rule order matters: role fills come after the `:not(.neutral)` default, so a
133
+ // caller's `attrs` override wins at equal specificity.
192
134
  // ---------------------------------------------------------------------------
193
135
 
194
- A.setSpacingCssVars();
195
-
196
- A.insertGlobalCss({
197
- // Derived brand tokens. These only reference the per-mode palette colours, so
198
- // they're defined once here and track the active mode (and any re-skin):
199
- // `--s-tint` is the brand's mid colour, the hue the neutral greys above lean
200
- // toward; `--s-gradient` is the full primary→secondary sweep used where the
201
- // brand should *show* (the headline mark, the active nav pill, tab edges);
202
- // `--s-gradient-surface` is a compressed, near-vertical cut of that same sweep
203
- // for *filled* gradient surfaces (buttons): it reads as one rich colour with
204
- // depth rather than a two-colour banner; `--s-glow` is a soft coloured shadow
205
- // that makes lit brand elements feel raised; `--s-page-bg` is a faint
206
- // twin-corner aurora wash painted on the page surface.
207
- ":root":
208
- "--s-tint: color-mix(in oklab, $s-primary, $s-secondary); " +
209
- "--s-gradient: linear-gradient(135deg, $s-primary, $s-secondary); " +
210
- "--s-gradient-surface: linear-gradient(170deg, color-mix(in oklab, $s-primary 85%, $s-secondary), color-mix(in oklab, $s-primary 30%, $s-secondary)); " +
211
- "--s-glow: 0 5px 16px color-mix(in srgb, $s-primary 26%, transparent); " +
212
- "--s-page-bg: radial-gradient(120% 80% at 100% 0%, color-mix(in oklab, $s-secondary, transparent 86%), transparent 56%), radial-gradient(120% 80% at 0% 0%, color-mix(in oklab, $s-primary, transparent 87%), transparent 56%), $s-page;",
213
-
214
- // Level/role modifier → anchors. Levels (neutral elevations) use the shared
215
- // ink; accent roles use the on-accent ink over their named fill. The page
216
- // (`.base`) additionally paints the aurora wash; `--s-b` keeps the solid
217
- // fallback so every derived token stays sensible.
218
- ":root, .s-s.base": "--s-a:$s-ink --s-b:$s-page",
219
- ".s-s.base": "background: $s-page-bg;",
220
- ".s-s.panel": "--s-a:$s-ink --s-b:$s-panel",
221
- ".s-s.raised": "--s-a:$s-ink --s-b:$s-raised",
222
- ".s-s.neutral": "--s-a:$s-ink --s-b:$s-neutral",
223
- ".s-s.primary": "--s-a:$s-on-accent --s-b:$s-primary",
224
- ".s-s.secondary": "--s-a:$s-on-accent --s-b:$s-secondary",
225
- ".s-s.danger": "--s-a:$s-on-accent --s-b:$s-danger",
226
- ".s-s.success": "--s-a:$s-on-accent --s-b:$s-success",
227
- ".s-s.warning": "--s-a:$s-on-accent --s-b:$s-warning",
228
- // `.gradient` is an accent role whose fill is the brand sweep. Its `--s-b`
229
- // anchor stays the solid `--s-primary` so the derived tokens (muted ink,
230
- // border, …) remain sensible; the gradient itself is painted further down,
231
- // only in the filled context (tonal/outlined read the solid fallback instead).
232
- ".s-s.gradient": "--s-a:$s-on-accent --s-b:$s-primary",
233
-
234
- // Filled default (bare `.s-s` and `:root`): map the anchors to fg/bg, derive
235
- // the secondary tokens from that pair, then paint. var() resolves at use time,
236
- // so the tonal/outlined remaps below feed back into the derived tokens.
237
- ":root, .s-s":
238
- "--s-fg:$s-a --s-bg:$s-b " +
239
- "--s-fg-muted: color-mix(in oklab, $s-fg, $s-bg 42%); " +
240
- "--s-fg-faint: color-mix(in oklab, $s-fg, $s-bg 64%); " +
241
- "--s-border: color-mix(in oklab, $s-fg, $s-bg 82%); " +
242
- "--s-border-strong: color-mix(in oklab, $s-fg, $s-bg 68%); " +
243
- // Themed scrollbars (standard properties, honoured by Firefox and modern
244
- // Chromium): the thumb uses the surface's derived border token, so the bar
245
- // tracks the active mode/re-skin and a nested surface's own colours.
246
- "scrollbar-width:thin scrollbar-color: $s-border-strong transparent; " +
247
- "background:$s-bg color:$s-fg r:$s-radius",
248
- // WebKit/older-Chromium counterpart to the standard `scrollbar-*` props above:
249
- // a transparent track and a rounded thumb (border-clipped to leave breathing
250
- // room) that brightens on hover.
251
- ".s-s::-webkit-scrollbar, .s-s ::-webkit-scrollbar": "width:10px height:10px",
252
- ".s-s::-webkit-scrollbar-track, .s-s ::-webkit-scrollbar-track": "background:transparent",
253
- ".s-s::-webkit-scrollbar-thumb, .s-s ::-webkit-scrollbar-thumb": "background:$s-border-strong border-radius:99px border: 2px solid transparent; background-clip:padding-box",
254
- ".s-s::-webkit-scrollbar-thumb:hover, .s-s ::-webkit-scrollbar-thumb:hover": "background:$s-fg-faint background-clip:padding-box",
255
- // Tonal: the fill colour becomes the ink, over a soft tint of itself.
256
- ".s-s.tonal": "--s-fg:$s-b --s-bg: color-mix(in srgb, $s-b 16%, transparent); border: 1px solid $s-border;",
257
- // Outlined: the fill colour is the ink; --s-bg *inherits* the parent's bg (the
258
- // token the derivations read, so the edge mixes ink with the real surroundings)
259
- // while the painted background is transparent, letting that parent fill — even
260
- // a gradient or image — show through.
261
- ".s-s.outlined": "--s-fg:$s-b --s-bg:inherit background:transparent --s-border: color-mix(in srgb, $s-fg 55%, $s-bg); border: 1px solid $s-border;",
262
- // Filled, explicit — last among the variants so a caller's `attrs: ".filled"`
263
- // overrides a component's default `.tonal`/`.outlined` (resetting both the
264
- // anchors and the painted background).
265
- ".s-s.filled": "--s-fg:$s-a --s-bg:$s-b background:$s-bg;",
266
- // Paint the brand sweep for a filled `.gradient` surface — the compressed
267
- // surface cut, not the full banner sweep. Sits after the variant rules and is
268
- // keyed on `:not(.tonal):not(.outlined)`, so those variants keep their
269
- // solid-primary tint/edge.
270
- ".s-s.gradient:not(.tonal):not(.outlined)": "background: $s-gradient-surface;",
271
-
272
- // Contextual accent: the brand pop colour on neutral surfaces. Declared on the
273
- // whole neutral group so re-entering a neutral surface under a coloured one
274
- // restores it. (--s-link gets the same treatment in the reactive block, where
275
- // its per-mode literal lives — it can't reference the palette, as the palette
276
- // source and the contextual token share the name --s-link.)
277
- ":root, .s-s.base, .s-s.panel, .s-s.raised, .s-s.neutral": "--s-accent:$s-primary",
278
- // On a bright coloured surface those wouldn't be legible, so they fall back to
279
- // the surface's own ink. (Keyed on the role modifier, so it holds across
280
- // variants — and tracks the tonal/outlined fg remap.)
281
- ".s-s.primary, .s-s.secondary, .s-s.danger, .s-s.success, .s-s.warning, .s-s.gradient": "--s-accent:$s-fg --s-link:$s-fg",
282
- // Tonal/outlined nested inside a filled accent surface would lose legibility:
283
- // their transparency bleeds into the vivid parent fill. Swap in the neutral
284
- // panel as a base so the role color reads as ink rather than fill.
285
- // Specificity (4 classes) beats the 2-class variant rules — no !important needed.
286
- ".s-s.primary .s-s.tonal, .s-s.secondary .s-s.tonal, .s-s.gradient .s-s.tonal, .s-s.danger .s-s.tonal, .s-s.success .s-s.tonal, .s-s.warning .s-s.tonal, .s-s.primary .s-s.outlined, .s-s.secondary .s-s.outlined, .s-s.gradient .s-s.outlined, .s-s.danger .s-s.outlined, .s-s.success .s-s.outlined, .s-s.warning .s-s.outlined":
287
- "--s-bg: $s-panel; background: $s-panel;",
288
- });
136
+ A.setSpacingCssVars(1.1);
289
137
 
290
- // A deliberately light reset. Colours/shape come from the contextual tokens, so
291
- // rich content (e.g. markdown-to-HTML) adapts to whatever surface holds it. The
292
- // vertical rhythm / typography of block elements is handled separately, below.
293
138
  A.insertGlobalCss({
139
+ // What follows is a lightweight CSS reset. Semantic HTML should keep working, but less ugly/with some reasonable defaults.
294
140
  "*, *::before, *::after": "box-sizing:border-box",
295
141
  html: "text-size-adjust:100%",
296
- body: "m:0 line-height:1.5 font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; -webkit-font-smoothing:antialiased",
297
- a: "color: $s-link; text-decoration:underline text-underline-offset:2px; transition: color 0.12s, filter 0.12s;",
142
+ body: "m:0 line-height:1.5 font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; -webkit-font-smoothing:antialiased background-color:$s-bg text:$s-text",
143
+ // Links resolve to the contextual link foreground: the link colour on nesting
144
+ // surfaces, the ink on accent surfaces.
145
+ a: "color: $s-link-fg; text-decoration:underline text-underline-offset:2px; transition: color 0.12s, filter 0.12s;",
298
146
  "a:hover": "filter: brightness(1.15)",
299
147
  "input, button, textarea, select": "font:inherit color:inherit",
300
148
  "code, kbd, samp, pre": "font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;",
301
- code: "background: color-mix(in oklab, $s-fg, $s-bg 86%); padding: 0.12em 0.34em; r:4px font-size:0.9em",
302
- pre: "background: color-mix(in oklab, $s-fg, $s-bg 92%); p:$3 r: $s-radius; overflow:auto",
149
+ code: "background: color-mix(in oklab, $s-text, $s-bg 86%); padding: 0.12em 0.34em; r:4px font-size:0.9em",
150
+ pre: "background: color-mix(in oklab, $s-text, $s-bg 92%); p:$3 r: $s-radius; overflow:auto",
303
151
  "pre code": "background:transparent p:0",
304
152
  "img, svg, video, canvas": "max-width:100% h:auto",
305
- hr: "border:0 border-top: 1px solid $s-border;",
306
- "::placeholder": "color: $s-fg-faint; opacity:1",
153
+ hr: "border:0 border-top: 1px solid $s-faint;",
154
+ "::placeholder": "color: $s-muted; opacity:1",
307
155
  ":focus-visible": "outline: 2px solid $s-focus; outline-offset:2px",
308
- small: "color:$s-fg-muted font-size:0.9em",
156
+ small: "color:$s-muted font-size:0.9em",
309
157
  // Respect users who prefer less motion: keep transitions essentially instant.
310
158
  "@media (prefers-reduced-motion: reduce)": {
311
159
  "*, *::before, *::after": "transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; scroll-behavior: auto !important;",
312
160
  },
161
+
162
+ // Color theming
163
+ ":root":
164
+ // Brand + semantic colours (settable). One brand colour drives everything.
165
+ "--s-primary:#00a884 --s-danger:#dc5b41 --s-success:#00a884 --s-warning:#ef6b00 --s-link:#3f8cd8 " +
166
+ // Shape/effect — single values, reused across components.
167
+ "--s-radius-sm:6px --s-radius:12px --s-radius-lg:18px " +
168
+ "--s-focus: color-mix(in srgb, $s-primary 38%, transparent); " +
169
+ // Brand sweep for the headline mark, the active nav pill, the selected tab.
170
+ "--s-gradient: linear-gradient(135deg, color-mix(in oklab, $s-primary, white 16%), color-mix(in oklab, $s-primary, black 14%));",
171
+
172
+ // Neutral surfaces (and the page): the pop colour is the brand primary, and
173
+ // links use the link colour. (The bg/ink come from the mode block above.)
174
+ ":root, .s-s.neutral": "--s-accent:$s-primary --s-link-fg:$s-link",
175
+
176
+ // Accent surfaces: bright fill, white ink. The bare `:not(.neutral)` carries the
177
+ // shared defaults (with a primary fallback fill); each role names its own fill.
178
+ ".s-s:not(.neutral)":
179
+ "--s-bg:$s-primary " + // Default .s-s to .primary
180
+ "border:0 " +
181
+ "--s-text:#eee --s-accent:#fff --s-link-fg:#eef " +
182
+ "--s-muted: color-mix(in srgb, #fff 70%, transparent); " +
183
+ "--s-faint: color-mix(in srgb, #fff 30%, transparent);",
184
+ ".s-s.danger": "--s-bg:$s-danger",
185
+ ".s-s.success": "--s-bg:$s-success",
186
+ ".s-s.warning": "--s-bg:$s-warning",
187
+ ".s-s.link": "--s-bg:$s-link",
188
+ ".s-s.primary": "--s-bg:$s-primary",
189
+
190
+ // Shared derive: every surface (and the page) gets a muted ink + hairline from
191
+ // its resolved text/bg pair, plus `color` and themed scrollbars.
192
+ ":root, .s-s":
193
+ "--s-muted: color-mix(in oklab, $s-text, $s-bg 42%); " +
194
+ "--s-faint: color-mix(in oklab, $s-text, $s-bg 80%); " +
195
+ "color:$s-text scrollbar-width:thin scrollbar-color: $s-faint transparent;",
196
+ // Subtle single-colour gradient sheen, painted on every surface (and the page).
197
+ ".s-s, body":
198
+ "background: linear-gradient(170deg, color-mix(in oklab, $s-bg, white 9%), color-mix(in oklab, $s-bg, black 9%));",
199
+ ".s-s": "r:$s-radius",
200
+ // Neutral surfaces own a subtle hairline border (a card reads as a card without
201
+ // any component help). `:where()` keeps it zero-specificity, so a bar/panel that
202
+ // wants only a divider (a box header, the app top bar, the nav panel) overrides
203
+ // it with a single plain rule. Accent (filled) surfaces don't get it — their fill
204
+ // is the edge. Buttons keep their own `border:0`.
205
+ ":where(.s-s.neutral)": "border: 1px solid $s-faint;",
206
+ ".s-s::-webkit-scrollbar, .s-s ::-webkit-scrollbar": "width:10px height:10px",
207
+ ".s-s::-webkit-scrollbar-track, .s-s ::-webkit-scrollbar-track": "background:transparent",
208
+ ".s-s::-webkit-scrollbar-thumb, .s-s ::-webkit-scrollbar-thumb":
209
+ "background:$s-faint border-radius:99px border: 2px solid transparent; background-clip:padding-box",
210
+
211
+ // Elevation utilities — add `.shadow` or `.extra-shadow` to any surface:
212
+ // • neutral surface → a neutral drop shadow
213
+ // • accent (filled) surface → a self-coloured glow, keyed on its own --s-bg (a
214
+ // lit button is just this on a `.primary` surface)
215
+ // • tonal/outlined surface → ignored (a translucent body has nothing to lift)
216
+ // A `.neutral` button stays flat (so segmented groups gain no stray
217
+ // shadows). `.no-shadow` is a hard override of any of the above — place it last
218
+ // and make it !important so it beats the higher-specificity glow rule.
219
+ ".s-s.shadow.neutral:not(.s-btn)": "box-shadow: 0 4px 14px rgba(0,0,0,0.13);",
220
+ ".s-s.extra-shadow.neutral:not(.s-btn)": "box-shadow: 0 18px 50px rgba(0,0,0,0.28);",
221
+ ".s-s.shadow:not(.neutral):not(.tonal):not(.outlined)": "box-shadow: 0 4px 14px color-mix(in srgb, $s-bg 30%, transparent);",
222
+ ".s-s.extra-shadow:not(.neutral):not(.tonal):not(.outlined)": "box-shadow: 0 14px 40px color-mix(in srgb, $s-bg 40%, transparent);",
223
+ ".s-s.no-shadow": "box-shadow: none !important;",
224
+
225
+ // Accent variants. `tonal`: the fill colour becomes the ink over a soft
226
+ // self-tint. `outlined`: the fill colour is the ink over a transparent body
227
+ // with a colour edge. (Neutral surfaces ignore these.)
228
+ ".s-s:not(.neutral).tonal, .s-s:not(.neutral).outlined":
229
+ "--s-text:$s-bg --s-accent:$s-bg --s-link-fg:$s-bg --s-faint: color-mix(in srgb, $s-bg 30%, transparent); --s-muted: color-mix(in srgb, $s-bg 70%, transparent);",
230
+ ".s-s:not(.neutral).tonal":
231
+ "background: color-mix(in srgb, $s-bg 15%, transparent); border: 1px solid $s-faint;",
232
+ ".s-s:not(.neutral).outlined":
233
+ "background: transparent; border: 1px solid color-mix(in srgb, $s-bg 45%, transparent);",
234
+ // A surface nested inside an accent surface is forced back to a filled look —
235
+ // a translucent tonal/outlined body would bleed into the vivid parent fill.
236
+ // Specificity (4 classes) beats the 2-class variant rules — no !important.
237
+ ".s-s:not(.neutral) .s-s.tonal, .s-s:not(.neutral) .s-s.outlined":
238
+ "--s-text:#fff --s-accent:#fff --s-link-fg:#fff " +
239
+ "background: linear-gradient(170deg, color-mix(in oklab, $s-bg, white 9%), color-mix(in oklab, $s-bg, black 9%)); border-color: transparent;",
313
240
  });
314
241
 
315
242
  // ── Suppress transitions during the initial load ─────────────────────────────
316
243
  // Buttons (and links) transition their colours, so a light↔dark switch animates
317
244
  // smoothly. On a *cold* load that's a liability: elements mount and paint in the
318
245
  // active theme in one pass, but a transitioning element animates from its
319
- // default (unstyled) colours into the theme so a fresh button visibly slides
320
- // from light to dark while the rest of the page is already correct. Tag <html>
321
- // until the first frame has painted and hard-disable transitions under that tag,
322
- // so the initial render snaps straight to the right colours. Two rAFs: the first
323
- // runs before the paint that shows the themed UI, the second clears the tag just
324
- // after it — later theme switches then animate as normal.
246
+ // default (unstyled) colours into the theme. Tag <html> until the first frame
247
+ // has painted and hard-disable transitions under that tag, so the initial render
248
+ // snaps straight to the right colours.
325
249
  A.insertGlobalCss({
326
250
  ".s-preload, .s-preload *, .s-preload *::before, .s-preload *::after":
327
251
  "transition: none !important; animation: none !important;",
@@ -346,10 +270,6 @@ A.insertGlobalCss({
346
270
  // Sensible block defaults for *any* content — your own UI just as much as
347
271
  // markdown-rendered HTML. The rhythm: strip the browser's block margins, then
348
272
  // give every block a *top* margin only when it isn't its parent's first child.
349
- // Content sits flush against its container's edges, and space appears solely
350
- // *between* siblings. These are intentionally low-specificity defaults — a
351
- // component or utility that sets its own margin (or an inline `mt:` shortcut)
352
- // always wins, so a class only ever changes what it actually names.
353
273
  const BLOCK = "p, ul, ol, dl, blockquote, pre, table, figure, hr, h1, h2, h3, h4, h5, h6";
354
274
 
355
275
  A.insertGlobalCss({
@@ -358,9 +278,7 @@ A.insertGlobalCss({
358
278
  "&:not(:first-child)": "margin-top:$3",
359
279
  },
360
280
 
361
- // Headings: bold, tight, balanced. Big levels get slightly negative tracking,
362
- // small levels become spaced "labels" — so adjacent levels stay distinct. The
363
- // em-based top margin gives larger headings a little more room above.
281
+ // Headings: bold, tight, balanced.
364
282
  "h1, h2, h3, h4, h5, h6": {
365
283
  "&": "line-height:1.15 font-weight:700 text-wrap:balance",
366
284
  "&:not(:first-child)": "margin-top:1.4em",
@@ -370,7 +288,7 @@ A.insertGlobalCss({
370
288
  h3: "font-size:1.3em letter-spacing:-0.011em",
371
289
  h4: "font-size:1.1em",
372
290
  h5: "font-size:0.95em letter-spacing:0.005em",
373
- h6: "font-size:0.8em fg:$s-fg-muted text-transform:uppercase letter-spacing:0.07em",
291
+ h6: "font-size:0.8em fg:$s-muted text-transform:uppercase letter-spacing:0.07em",
374
292
 
375
293
  // Lists: markers, a sensible indent, gently spaced items, tight nesting.
376
294
  "ul, ol": {
@@ -379,12 +297,12 @@ A.insertGlobalCss({
379
297
  },
380
298
 
381
299
  // Blockquote, tables, definition lists, figure captions.
382
- blockquote: "border-left: 3px solid $s-border; padding-left: $3; fg: $s-fg-muted",
300
+ blockquote: "border-left: 3px solid $s-faint; padding-left: $3; fg: $s-muted",
383
301
  table: "border-collapse:collapse",
384
- "th, td": "text-align:left padding: $1 $2; border-bottom: 1px solid $s-border; vertical-align:top",
302
+ "th, td": "text-align:left padding: $1 $2; border-bottom: 1px solid $s-faint; vertical-align:top",
385
303
  th: "font-weight:600",
386
- "thead th": "border-bottom: 2px solid $s-border-strong;",
304
+ "thead th": "border-bottom: 2px solid $s-faint;",
387
305
  dt: "font-weight:600",
388
306
  dd: "margin-left: 1.5em",
389
- figcaption: "fg:$s-fg-muted font-size:0.9em margin-top:$1 text-align:center",
307
+ figcaption: "fg:$s-muted font-size:0.9em margin-top:$1 text-align:center",
390
308
  });