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