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.
- package/README.md +34 -16
- package/dist/components/autocomplete.js +12 -9
- package/dist/components/box.js +15 -10
- package/dist/components/button.d.ts +3 -1
- package/dist/components/button.js +21 -28
- package/dist/components/buttonChooser.js +1 -1
- package/dist/components/buttonGroup.d.ts +3 -3
- package/dist/components/buttonGroup.js +3 -3
- package/dist/components/dialog.d.ts +3 -1
- package/dist/components/dialog.js +20 -10
- package/dist/components/field.js +7 -4
- package/dist/components/main.js +52 -15
- package/dist/components/menu.d.ts +23 -2
- package/dist/components/menu.js +54 -29
- package/dist/components/select.js +1 -1
- package/dist/components/tabs.js +3 -3
- package/dist/components/toast.js +8 -8
- package/dist/components/tooltip.js +4 -3
- package/dist/core.d.ts +14 -2
- package/dist/core.js +23 -0
- package/dist/staffa.esm.js +1 -1
- package/dist/theme.js +138 -224
- package/package.json +1 -1
- package/skill/ButtonOptions.md +2 -1
- package/skill/SKILL.md +108 -47
- package/skill/ToastOptions.md +1 -1
- package/skill/addContextMenu.md +28 -0
- package/skill/addTooltip.md +6 -2
- package/skill/autocomplete.md +9 -2
- package/skill/box.md +1 -0
- package/skill/button.md +5 -3
- package/skill/buttonGroup.md +5 -3
- package/skill/checkbox.md +1 -0
- package/skill/confirm.md +1 -1
- package/skill/dialog.md +2 -2
- package/skill/form.md +3 -2
- package/skill/main.md +5 -1
- package/skill/menuButton.md +1 -1
- package/skill/prompt.md +1 -1
- package/skill/select.md +1 -0
- package/skill/showFloatingMenu.md +5 -6
- package/skill/tabs.md +2 -2
- package/skill/textarea.md +3 -2
- package/skill/textline.md +1 -0
- package/skill/toast.md +2 -3
- package/src/components/autocomplete.ts +12 -9
- package/src/components/box.ts +15 -10
- package/src/components/button.ts +23 -32
- package/src/components/buttonChooser.ts +1 -1
- package/src/components/buttonGroup.ts +3 -3
- package/src/components/dialog.ts +22 -11
- package/src/components/field.ts +7 -4
- package/src/components/main.ts +47 -16
- package/src/components/menu.ts +73 -40
- package/src/components/select.ts +1 -1
- package/src/components/tabs.ts +3 -3
- package/src/components/toast.ts +8 -8
- package/src/components/tooltip.ts +4 -3
- package/src/core.ts +30 -2
- 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
|
|
10
|
-
*
|
|
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
|
-
* -
|
|
14
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
22
|
-
*
|
|
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
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* default
|
|
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
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
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
|
|
85
|
-
*
|
|
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
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
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
|
-
|
|
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-
|
|
302
|
-
pre: "background: color-mix(in oklab, $s-
|
|
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-
|
|
306
|
-
"::placeholder": "color: $s-
|
|
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-
|
|
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
|
|
320
|
-
//
|
|
321
|
-
//
|
|
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.
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
307
|
+
figcaption: "fg:$s-muted font-size:0.9em margin-top:$1 text-align:center",
|
|
390
308
|
});
|