staffa 0.1.0 → 0.2.1

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 (50) hide show
  1. package/README.md +65 -27
  2. package/dist/components/autocomplete.js +20 -20
  3. package/dist/components/box.d.ts +8 -6
  4. package/dist/components/box.js +15 -11
  5. package/dist/components/button.d.ts +10 -33
  6. package/dist/components/button.js +17 -38
  7. package/dist/components/buttonChooser.d.ts +42 -0
  8. package/dist/components/buttonChooser.js +38 -0
  9. package/dist/components/buttonGroup.d.ts +3 -3
  10. package/dist/components/buttonGroup.js +18 -18
  11. package/dist/components/checkbox.js +7 -7
  12. package/dist/components/dialog.d.ts +20 -25
  13. package/dist/components/dialog.js +81 -91
  14. package/dist/components/field.d.ts +8 -6
  15. package/dist/components/field.js +18 -17
  16. package/dist/components/form.d.ts +3 -3
  17. package/dist/components/form.js +4 -4
  18. package/dist/components/main.d.ts +7 -5
  19. package/dist/components/main.js +25 -23
  20. package/dist/components/select.d.ts +1 -1
  21. package/dist/components/select.js +5 -5
  22. package/dist/components/tabs.d.ts +5 -3
  23. package/dist/components/tabs.js +25 -19
  24. package/dist/components/textarea.js +4 -4
  25. package/dist/components/textline.js +1 -1
  26. package/dist/core.d.ts +26 -39
  27. package/dist/core.js +6 -5
  28. package/dist/index.d.ts +10 -8
  29. package/dist/index.js +9 -8
  30. package/dist/staffa.esm.js +1 -0
  31. package/dist/theme.d.ts +9 -75
  32. package/dist/theme.js +178 -82
  33. package/package.json +3 -2
  34. package/src/components/autocomplete.ts +20 -20
  35. package/src/components/box.ts +20 -14
  36. package/src/components/button.ts +24 -72
  37. package/src/components/buttonChooser.ts +66 -0
  38. package/src/components/buttonGroup.ts +18 -18
  39. package/src/components/checkbox.ts +7 -7
  40. package/src/components/dialog.ts +101 -102
  41. package/src/components/field.ts +24 -21
  42. package/src/components/form.ts +7 -7
  43. package/src/components/main.ts +30 -26
  44. package/src/components/select.ts +4 -4
  45. package/src/components/tabs.ts +29 -22
  46. package/src/components/textarea.ts +4 -4
  47. package/src/components/textline.ts +1 -1
  48. package/src/core.ts +26 -40
  49. package/src/index.ts +10 -8
  50. package/src/theme.ts +190 -135
package/dist/theme.d.ts CHANGED
@@ -1,87 +1,21 @@
1
1
  /**
2
- * Theming and global base styles for Skye.
3
- *
4
- * Skye is themed entirely through CSS custom properties (via Aberdeen's
5
- * {@link A.cssVars}). Components reference these with `var(--sPrimary)` etc., so
6
- * changing a single variable restyles the whole app — at runtime, reactively.
7
- *
8
- * Unlike typical Aberdeen apps (which use component-local `insertCss`), Skye uses
9
- * **global** CSS (`insertGlobalCss`) with class names prefixed `S_`. This is a
10
- * deliberate trade-off: it lets application authors override any Skye style from
11
- * their own stylesheet without fighting scoped class names.
12
- */
13
- /**
14
- * The set of CSS custom properties Skye understands. All are plain CSS color /
15
- * length strings. Override any subset by mutating {@link darkTheme} /
16
- * {@link lightTheme}.
17
- */
18
- export interface Theme {
19
- /** Page background — the darkest surface. */
20
- sBg: string;
21
- /** Default surface for cards, inputs, menus. */
22
- sSurface: string;
23
- /** Raised surface for headers, footers, chips, hover states. */
24
- sSurfaceHi: string;
25
- /** Primary foreground / text color. */
26
- sFg: string;
27
- /** Muted text (help text, subtitles). */
28
- sFgMuted: string;
29
- /** Faint text (placeholders, disabled). */
30
- sFgFaint: string;
31
- /** Default border color. */
32
- sBorder: string;
33
- /** Stronger border / neutral control color. */
34
- sBorderStrong: string;
35
- /** Brand / accent color. */
36
- sPrimary: string;
37
- /** Brand color, hover/brighter. */
38
- sPrimaryHover: string;
39
- /** Text drawn on top of {@link Theme.sPrimary}. */
40
- sPrimaryFg: string;
41
- /** Destructive / error color. */
42
- sDanger: string;
43
- /** Positive / success color. */
44
- sSuccess: string;
45
- /** Caution color. */
46
- sWarning: string;
47
- /** Focus-ring color (usually a translucent primary). */
48
- sFocus: string;
49
- /** Default corner radius. */
50
- sRadius: string;
51
- /** Larger corner radius (e.g. the {@link import("./components/main").main} sheet). */
52
- sRadiusLg: string;
53
- /** Elevation shadow for menus, dialogs, the framed content sheet. */
54
- sShadow: string;
55
- }
56
- /**
57
- * The default dark Skye theme: modern and intentionally a little vivid so it
58
- * stands out of the box.
59
- *
60
- * This is a live Aberdeen proxy — mutate it (e.g. `darkTheme.sPrimary = "..."`)
61
- * and, while dark mode is active, the change flows straight into the CSS
62
- * variables. Use this to retheme dark and {@link lightTheme} independently.
63
- */
64
- export declare const darkTheme: Theme;
65
- /**
66
- * The light Skye theme — the same lavender brand, retuned for a bright,
67
- * modern surface: white cards on a soft grey page, a deeper primary so it
68
- * reads well on light backgrounds, and a softer elevation shadow.
69
- *
70
- * Like {@link darkTheme}, a live proxy: mutate it to retheme light mode.
2
+ * The named accent roles for interactive elements (buttons, tabs, badges, …).
3
+ * Each maps to a `.s-s.<role>` class that sets `--s-a` (ink) and `--s-b` (fill).
71
4
  */
72
- export declare const lightTheme: Theme;
5
+ export type SurfaceRole = "primary" | "neutral" | "danger" | "success" | "warning";
6
+ /** How a surface's two colours are rendered. `filled` is the default. */
7
+ export type Variant = "filled" | "tonal" | "outlined";
73
8
  /**
74
9
  * Force dark mode (`true`), light mode (`false`), or follow the OS preference
75
- * (`undefined`). Takes effect immediately and is persisted to localStorage, so
76
- * the choice survives reloads.
10
+ * (`undefined`). Takes effect immediately and is persisted to localStorage.
77
11
  */
78
12
  export declare function setDarkMode(value: boolean | undefined): void;
79
13
  /**
80
14
  * Whether dark mode is currently active. Reactive — read it inside a scope to
81
15
  * re-run on changes.
82
16
  *
83
- * @param allowAuto - When `true`, returns `undefined` (rather than resolving to
84
- * a boolean) if the user is following the OS preference, so a dark/light/auto
85
- * control can tell the three states apart.
17
+ * @param allowAuto - When `true`, returns `undefined` (rather than a boolean) if
18
+ * the user is following the OS preference, so a dark/light/auto control can
19
+ * tell the three states apart.
86
20
  */
87
21
  export declare function getDarkMode(allowAuto?: boolean): boolean | undefined;
package/dist/theme.js CHANGED
@@ -1,65 +1,85 @@
1
1
  import A from "aberdeen";
2
2
  /**
3
- * The default dark Skye theme: modern and intentionally a little vivid so it
4
- * stands out of the box.
3
+ * Theming and global base styles for Staffa.
5
4
  *
6
- * This is a live Aberdeen proxy — mutate it (e.g. `darkTheme.sPrimary = "..."`)
7
- * and, while dark mode is active, the change flows straight into the CSS
8
- * variables. Use this to retheme dark and {@link lightTheme} independently.
9
- */
10
- export const darkTheme = A.proxy({
11
- sBg: "#0e1015",
12
- sSurface: "#181b22",
13
- sSurfaceHi: "#222632",
14
- sFg: "#e8eaf0",
15
- sFgMuted: "#a6acba",
16
- sFgFaint: "#6b7280",
17
- sBorder: "#2c313c",
18
- sBorderStrong: "#3c4352",
19
- sPrimary: "#8b7bff",
20
- sPrimaryHover: "#a99dff",
21
- sPrimaryFg: "#0c0a1a",
22
- sDanger: "#ff6b6b",
23
- sSuccess: "#46d39a",
24
- sWarning: "#fbbf24",
25
- sFocus: "rgba(139, 123, 255, 0.45)",
26
- sRadius: "10px",
27
- sRadiusLg: "16px",
28
- sShadow: "0 8px 30px rgba(0, 0, 0, 0.45)",
29
- });
30
- /**
31
- * The light Skye theme — the same lavender brand, retuned for a bright,
32
- * modern surface: white cards on a soft grey page, a deeper primary so it
33
- * reads well on light backgrounds, and a softer elevation shadow.
5
+ * # The surface model
34
6
  *
35
- * Like {@link darkTheme}, a live proxy: mutate it to retheme light mode.
36
- */
37
- export const lightTheme = A.proxy({
38
- sBg: "#f3f4f8",
39
- sSurface: "#ffffff",
40
- sSurfaceHi: "#eceef4",
41
- sFg: "#1b1e27",
42
- sFgMuted: "#5b6273",
43
- sFgFaint: "#9aa1b2",
44
- sBorder: "#e2e5ee",
45
- sBorderStrong: "#c7ccda",
46
- sPrimary: "#6c5ce7",
47
- sPrimaryHover: "#5847d4",
48
- sPrimaryFg: "#ffffff",
49
- sDanger: "#e23b3b",
50
- sSuccess: "#1f9d6b",
51
- sWarning: "#d97706",
52
- sFocus: "rgba(108, 92, 231, 0.35)",
53
- sRadius: "10px",
54
- sRadiusLg: "16px",
55
- sShadow: "0 6px 24px rgba(20, 24, 40, 0.12)",
56
- });
57
- const STORAGE_KEY = "skye:darkMode";
58
- /**
59
- * The explicit dark-mode choice — `true` (force dark), `false` (force light) or
60
- * `undefined` (follow the OS via {@link A.darkMode}). A reactive proxy, seeded
61
- * from localStorage so the persisted preference applies on the first paint.
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:
11
+ *
12
+ * - `--s-a` — its default **foreground** (ink)
13
+ * - `--s-b` — its default **background** (fill)
14
+ *
15
+ * set by a **level** modifier — `.base` (the page), `.panel` (the default card),
16
+ * `.raised` (elevated chrome) — or an **accent role** modifier — `.primary`,
17
+ * `.neutral`, `.danger`, `.success`, `.warning`.
18
+ *
19
+ * A **variant** modifier — `.filled` (the default), `.tonal` or `.outlined` —
20
+ * decides how `--s-a`/`--s-b` map onto the tokens widgets read
21
+ * (`--s-fg`/`--s-bg`). A shared rule then derives muted/faint/border from that
22
+ * pair and paints the element. Because the derivation reads `$s-fg`/`$s-bg`,
23
+ * every surface gets its *own* legible secondary colours: drop a widget on any
24
+ * surface and it adapts. (`:root` is an implicit filled surface, so the page
25
+ * renders without extra classes.)
26
+ *
27
+ * Components build surfaces by combining classes, e.g.
28
+ * `A("div.s-s.panel.outlined", opts.attrs)` — and because `opts.attrs` is the
29
+ * caller's escape hatch, passing e.g. `.filled` or `.danger` overrides the
30
+ * default look.
31
+ *
32
+ * | token | meaning |
33
+ * | ---------------- | ---------------------------------------- |
34
+ * | `--s-fg` | default text |
35
+ * | `--s-bg` | background |
36
+ * | `--s-fg-muted` | secondary text (subtitles, help) |
37
+ * | `--s-fg-faint` | placeholders, disabled |
38
+ * | `--s-border` / `--s-border-strong` | borders |
39
+ * | `--s-accent` | brand "pop" colour (active indicators, etc.) |
40
+ * | `--s-link` / `--s-focus` | link & focus-ring colours |
41
+ * | `--s-radius` / `--s-radius-lg` / `--s-shadow` | shape tokens |
42
+ *
43
+ * `--s-accent` and `--s-link` default to the brand / link colour, but on a
44
+ * bright coloured surface (`.primary`, `.danger`, …) they fall back to that
45
+ * surface's own ink so they stay legible.
46
+ *
47
+ * # The palette
48
+ *
49
+ * All colours come from a small set of named **palette** tokens on `:root`, set
50
+ * per mode — the only place colours live, and the single place to re-skin:
51
+ * `--s-primary`, `--s-danger`, `--s-success`, `--s-warning` (accent fills, which
52
+ * double as semantic *ink* on neutral surfaces), `--s-neutral`, `--s-page`,
53
+ * `--s-panel`, `--s-raised` (neutral fills), `--s-ink` (text on neutral) and
54
+ * `--s-on-accent` (text on accent fills), plus `--s-link`/`--s-focus` and the
55
+ * shape tokens. Every surface rule is wired to these, so they adapt with the
56
+ * mode and with any override.
57
+ *
58
+ * # Variants
59
+ *
60
+ * Because the variant decides how `--s-a`/`--s-b` become `--s-fg`/`--s-bg`, the
61
+ * three looks are generic and work on *any* role: `.tonal` reads the fill colour
62
+ * as ink over a soft self-tint; `.outlined` reads it as ink over a transparent
63
+ * fill with a coloured edge (inheriting the parent's background, so its derived
64
+ * tokens read the real surroundings). Inside any of them `--s-fg`/`--s-bg` still
65
+ * describe the real, rendered colours.
66
+ *
67
+ * # Customising
68
+ *
69
+ * There's no JS theme object — the colours live in the palette `insertGlobalCss`
70
+ * call below, branched on {@link getDarkMode}. Re-skin from your app by
71
+ * overriding palette tokens (per mode if you like), or give a surface an
72
+ * image/gradient background (set `--s-b` to the dominant fallback colour so
73
+ * derived tokens stay sensible). Staffa uses global, `s-`-prefixed classes, so
74
+ * nothing is scoped away from you.
75
+ *
76
+ * ```ts
77
+ * A(() => A.insertGlobalCss({ ":root": getDarkMode() ? "--s-primary:#28c4a0" : "--s-primary:#1f9d6b" }));
78
+ * A.insertGlobalCss({ ".s-s.panel": "background: url(paper.png); --s-b: #efe9dd" });
79
+ * ```
62
80
  */
81
+ const STORAGE_KEY = "staffa:darkMode";
82
+ /** The explicit dark-mode choice; `undefined` follows the OS via {@link A.darkMode}. */
63
83
  const $override = A.proxy({ value: readStoredOverride() });
64
84
  /** Read the persisted dark-mode override from localStorage (defensively). */
65
85
  function readStoredOverride() {
@@ -77,8 +97,7 @@ function readStoredOverride() {
77
97
  }
78
98
  /**
79
99
  * Force dark mode (`true`), light mode (`false`), or follow the OS preference
80
- * (`undefined`). Takes effect immediately and is persisted to localStorage, so
81
- * the choice survives reloads.
100
+ * (`undefined`). Takes effect immediately and is persisted to localStorage.
82
101
  */
83
102
  export function setDarkMode(value) {
84
103
  $override.value = value;
@@ -96,40 +115,117 @@ export function setDarkMode(value) {
96
115
  * Whether dark mode is currently active. Reactive — read it inside a scope to
97
116
  * re-run on changes.
98
117
  *
99
- * @param allowAuto - When `true`, returns `undefined` (rather than resolving to
100
- * a boolean) if the user is following the OS preference, so a dark/light/auto
101
- * control can tell the three states apart.
118
+ * @param allowAuto - When `true`, returns `undefined` (rather than a boolean) if
119
+ * the user is following the OS preference, so a dark/light/auto control can
120
+ * tell the three states apart.
102
121
  */
103
122
  export function getDarkMode(allowAuto = false) {
104
123
  const v = $override.value;
105
124
  return v === undefined && !allowAuto ? A.darkMode() : v;
106
125
  }
107
- // Set up everything as this module loads. Spacing scale first ($1 = 0.25rem,
108
- // $2 = 0.5rem, $3 = 1rem, ...), then reactively merge the active theme into the
109
- // CSS variables. This scope runs synchronously now before the first paint —
110
- // so the correct colors are in place immediately (no flash), and A.merge
111
- // subscribes to the theme it reads, so toggling the mode or mutating
112
- // darkTheme / lightTheme re-applies automatically.
113
- A.setSpacingCssVars();
126
+ // ---------------------------------------------------------------------------
127
+ // Reactive palette the ONE place colours live, and the only thing that
128
+ // differs between light and dark. Everything else is wired to these named
129
+ // tokens, so an app can re-skin by overriding just a few of them. The accent
130
+ // names (--s-primary/-danger/-success/-warning) double as semantic *ink*
131
+ // colours, legible as text/borders on neutral surfaces.
132
+ // ---------------------------------------------------------------------------
114
133
  A(() => {
115
- A.merge(A.cssVars, getDarkMode() ? darkTheme : lightTheme);
134
+ if (getDarkMode()) {
135
+ A.insertGlobalCss({
136
+ ":root": "--s-primary:#8b7bff --s-danger:#ff6b6b --s-success:#46d39a --s-warning:#fbbf24 " +
137
+ "--s-neutral:#3c4352 --s-page:#0e1015 --s-panel:#181b22 --s-raised:#222632 " +
138
+ "--s-ink:#e8eaf0 --s-on-accent:#0c0a14 --s-focus:rgba(139,123,255,0.45) " +
139
+ "--s-radius:10px --s-radius-lg:16px --s-shadow: 0 8px 30px rgba(0,0,0,0.45);",
140
+ // Contextual link, restored across the neutral group (see static block).
141
+ ":root, .s-s.base, .s-s.panel, .s-s.raised, .s-s.neutral": "--s-link:#6db3ff",
142
+ });
143
+ }
144
+ else {
145
+ A.insertGlobalCss({
146
+ ":root": "--s-primary:#6c5ce7 --s-danger:#e23b3b --s-success:#1f9d6b --s-warning:#d97706 " +
147
+ "--s-neutral:#c7ccda --s-page:#f3f4f8 --s-panel:#ffffff --s-raised:#eceef4 " +
148
+ "--s-ink:#1b1e27 --s-on-accent:#ffffff --s-focus:rgba(108,92,231,0.35) " +
149
+ "--s-radius:10px --s-radius-lg:16px --s-shadow: 0 6px 24px rgba(20,24,40,0.12);",
150
+ ":root, .s-s.base, .s-s.panel, .s-s.raised, .s-s.neutral": "--s-link:#2563eb",
151
+ });
152
+ }
153
+ });
154
+ // ---------------------------------------------------------------------------
155
+ // Static structure — inserted once, all wired to the palette above.
156
+ //
157
+ // A surface is marked with `.s-s`. Its level/role modifier sets two anchors
158
+ // (--s-a ink, --s-b fill); its variant modifier (`.filled` default, `.tonal`,
159
+ // `.outlined`) decides how those anchors map onto the rendered fg/bg. `:root`
160
+ // (the page) is an implicit filled surface. Every `.s-s` then runs the same
161
+ // derive+paint step, reading the resolved fg/bg, so each one gets its own
162
+ // legible secondary tokens regardless of variant.
163
+ //
164
+ // Rule order matters: the variant/role rules below all share specificity, so a
165
+ // later one wins. `.filled` therefore comes *last* among the variants — that's
166
+ // what lets a caller's `attrs: ".filled"` override a component's default
167
+ // `.tonal`/`.outlined`.
168
+ // ---------------------------------------------------------------------------
169
+ A.setSpacingCssVars();
170
+ A.insertGlobalCss({
171
+ // Level/role modifier → anchors. Levels (neutral elevations) use the shared
172
+ // ink; accent roles use the on-accent ink over their named fill.
173
+ ":root, .s-s.base": "--s-a:$s-ink --s-b:$s-page",
174
+ ".s-s.panel": "--s-a:$s-ink --s-b:$s-panel",
175
+ ".s-s.raised": "--s-a:$s-ink --s-b:$s-raised",
176
+ ".s-s.neutral": "--s-a:$s-ink --s-b:$s-neutral",
177
+ ".s-s.primary": "--s-a:$s-on-accent --s-b:$s-primary",
178
+ ".s-s.danger": "--s-a:$s-on-accent --s-b:$s-danger",
179
+ ".s-s.success": "--s-a:$s-on-accent --s-b:$s-success",
180
+ ".s-s.warning": "--s-a:$s-on-accent --s-b:$s-warning",
181
+ // Filled default (bare `.s-s` and `:root`): map the anchors to fg/bg, derive
182
+ // the secondary tokens from that pair, then paint. var() resolves at use time,
183
+ // so the tonal/outlined remaps below feed back into the derived tokens.
184
+ ":root, .s-s": "--s-fg:$s-a --s-bg:$s-b " +
185
+ "--s-fg-muted: color-mix(in oklab, $s-fg, $s-bg 42%); " +
186
+ "--s-fg-faint: color-mix(in oklab, $s-fg, $s-bg 64%); " +
187
+ "--s-border: color-mix(in oklab, $s-fg, $s-bg 82%); " +
188
+ "--s-border-strong: color-mix(in oklab, $s-fg, $s-bg 68%); " +
189
+ "background:$s-bg color:$s-fg",
190
+ // Tonal: the fill colour becomes the ink, over a soft tint of itself.
191
+ ".s-s.tonal": "--s-fg:$s-b --s-bg: color-mix(in srgb, $s-b 16%, transparent);",
192
+ // Outlined: the fill colour is the ink; --s-bg *inherits* the parent's bg (the
193
+ // token the derivations read, so the edge mixes ink with the real surroundings)
194
+ // while the painted background is transparent, letting that parent fill — even
195
+ // a gradient or image — show through.
196
+ ".s-s.outlined": "--s-fg:$s-b --s-bg:inherit background:transparent --s-border: color-mix(in srgb, $s-fg 55%, $s-bg);",
197
+ // Filled, explicit — last among the variants so a caller's `attrs: ".filled"`
198
+ // overrides a component's default `.tonal`/`.outlined` (resetting both the
199
+ // anchors and the painted background).
200
+ ".s-s.filled": "--s-fg:$s-a --s-bg:$s-b background:$s-bg;",
201
+ // Contextual accent: the brand pop colour on neutral surfaces. Declared on the
202
+ // whole neutral group so re-entering a neutral surface under a coloured one
203
+ // restores it. (--s-link gets the same treatment in the reactive block, where
204
+ // its per-mode literal lives — it can't reference the palette, as the palette
205
+ // source and the contextual token share the name --s-link.)
206
+ ":root, .s-s.base, .s-s.panel, .s-s.raised, .s-s.neutral": "--s-accent:$s-primary",
207
+ // On a bright coloured surface those wouldn't be legible, so they fall back to
208
+ // the surface's own ink. (Keyed on the role modifier, so it holds across
209
+ // variants — and tracks the tonal/outlined fg remap.)
210
+ ".s-s.primary, .s-s.danger, .s-s.success, .s-s.warning": "--s-accent:$s-fg --s-link:$s-fg",
116
211
  });
117
- // A deliberately light reset. It sets box-sizing and sensible colors/fonts, but
118
- // does NOT strip margins from headings/paragraphs/lists, so rendered rich
119
- // content (e.g. markdown-to-HTML) keeps reasonable default rhythm.
212
+ // A deliberately light reset. Colours/shape come from the contextual tokens, so
213
+ // rich content (e.g. markdown-to-HTML) adapts to whatever surface holds it. It
214
+ // does NOT strip margins from headings/paragraphs/lists.
120
215
  A.insertGlobalCss({
121
216
  "*, *::before, *::after": "box-sizing:border-box",
122
217
  html: "text-size-adjust:100%",
123
- body: "m:0 bg:$sBg fg:$sFg line-height:1.5 font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; -webkit-font-smoothing:antialiased",
124
- a: "fg:$sPrimary text-decoration:underline text-underline-offset:2px",
125
- "a:hover": "fg:$sPrimaryHover",
218
+ body: "m:0 line-height:1.5 font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; -webkit-font-smoothing:antialiased",
219
+ a: "color: $s-link; text-decoration:underline text-underline-offset:2px",
220
+ "a:hover": "filter: brightness(1.15)",
126
221
  "input, button, textarea, select": "font:inherit color:inherit",
127
222
  "code, kbd, samp, pre": "font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;",
128
- code: "bg:$sSurfaceHi padding: 0.12em 0.34em; r:4px font-size:0.9em",
129
- pre: "bg:$sSurface p:$3 r:$sRadius overflow:auto",
130
- "pre code": "bg:transparent p:0",
223
+ code: "background: color-mix(in oklab, $s-fg, $s-bg 86%); padding: 0.12em 0.34em; r:4px font-size:0.9em",
224
+ pre: "background: color-mix(in oklab, $s-fg, $s-bg 92%); p:$3 r: $s-radius; overflow:auto",
225
+ "pre code": "background:transparent p:0",
131
226
  "img, svg, video, canvas": "max-width:100% h:auto",
132
- hr: "border:0 border-top: 1px solid $sBorder; margin: $3 0;",
133
- "::placeholder": "fg:$sFgFaint opacity:1",
134
- ":focus-visible": "outline: 2px solid $sFocus; outline-offset:2px",
227
+ hr: "border:0 border-top: 1px solid $s-border; margin: $3 0;",
228
+ "::placeholder": "color: $s-fg-faint; opacity:1",
229
+ ":focus-visible": "outline: 2px solid $s-focus; outline-offset:2px",
230
+ small: "color:$s-fg-muted font-size:0.9em",
135
231
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "staffa",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "An opinionated component library for the Aberdeen reactive UI library.",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -21,7 +21,8 @@
21
21
  "scripts": {
22
22
  "build": "tsc && tsc -p demo && esbuild src/index.ts --bundle --external:aberdeen --minify --format=esm --outfile=dist/staffa.esm.js",
23
23
  "typecheck": "tsc --noEmit && tsc -p demo --noEmit",
24
- "smoke": "tsc && node smoke.mjs"
24
+ "smoke": "tsc && node smoke.mjs",
25
+ "prepublish": "npm run build"
25
26
  },
26
27
  "peerDependencies": {
27
28
  "aberdeen": "^1.15.0"
@@ -33,21 +33,21 @@ export interface AutocompleteOptions extends FieldOptions {
33
33
  }
34
34
 
35
35
  A.insertGlobalCss({
36
- ".S_ac": {
36
+ ".s-ac": {
37
37
  "&": "position:relative",
38
- "> .S_control": "display:flex flex-wrap:wrap align-items:center gap:$1 bg:$sSurface fg:$sFg border: 1px solid $sBorder; r:$sRadius padding: 0.3em 0.4em; cursor:text; transition: border-color 0.15s, box-shadow 0.15s;",
39
- "> .S_control:hover": "border-color:$sBorderStrong",
40
- "> .S_control:focus-within": "border-color:$sPrimary box-shadow: 0 0 0 3px $sFocus;",
41
- "&[aria-invalid=true] > .S_control": "border-color:$sDanger",
42
- ".S_chip": "display:inline-flex align-items:center gap:$1 font-size:0.85em bg:$sSurfaceHi border: 1px solid $sBorder; r:$sRadius padding: 0.1em 0.2em 0.1em 0.5em;",
43
- ".S_chip > button": "cursor:pointer border:0 background:transparent fg:$sFgMuted font-size:1.1em line-height:1 padding: 0 0.2em; r:4px",
44
- ".S_chip > button:hover": "fg:$sFg background:$sBorder",
38
+ "> .s-control": "display:flex flex-wrap:wrap align-items:center gap:$1 bg:$s-panel fg:$s-ink border: 1px solid $s-border; r:$s-radius padding: 0.3em 0.4em; cursor:text; transition: border-color 0.15s, box-shadow 0.15s;",
39
+ "> .s-control:hover": "border-color:$s-border-strong",
40
+ "> .s-control:focus-within": "border-color:$s-accent box-shadow: 0 0 0 3px $s-focus;",
41
+ "&[aria-invalid=true] > .s-control": "border-color:$s-danger",
42
+ ".s-chip": "display:inline-flex align-items:center gap:$1 font-size:0.85em bg:$s-raised border: 1px solid $s-border; r:$s-radius padding: 0.1em 0.2em 0.1em 0.5em;",
43
+ ".s-chip > button": "cursor:pointer border:0 background:transparent fg:$s-fg-muted font-size:1.1em line-height:1 padding: 0 0.2em; r:4px",
44
+ ".s-chip > button:hover": "fg:$s-fg background:$s-border",
45
45
  "input": "flex:1 min-width:6ch border:0 background:transparent color:inherit outline:none padding:0.25em",
46
- "> .S_menu": "position:absolute top:100% left:0 right:0 z-index:20 margin-top:4px max-height:15rem overflow-y:auto list-style:none p:$1 margin-bottom:0 bg:$sSurface border: 1px solid $sBorder; r:$sRadius box-shadow:$sShadow",
47
- ".S_option": "padding: 0.45em 0.6em; r:6px cursor:pointer",
48
- ".S_option[aria-selected=true]": "background:$sSurfaceHi",
49
- ".S_add": "fg:$sPrimary font-style:italic",
50
- ".S_empty": "padding: 0.45em 0.6em; fg:$sFgMuted",
46
+ "> .s-menu": "position:absolute top:100% left:0 right:0 z-index:20 margin-top:4px max-height:15rem overflow-y:auto list-style:none p:$1 margin-bottom:0 bg:$s-panel border: 1px solid $s-border; r:$s-radius box-shadow:$s-shadow",
47
+ ".s-option": "padding: 0.45em 0.6em; r:6px cursor:pointer",
48
+ ".s-option[aria-selected=true]": "background:$s-raised",
49
+ ".s-add": "fg:$s-accent font-style:italic",
50
+ ".s-empty": "padding: 0.45em 0.6em; fg:$s-fg-muted",
51
51
  },
52
52
  });
53
53
 
@@ -123,19 +123,19 @@ export function autocomplete(opts: AutocompleteOptions): void {
123
123
  };
124
124
 
125
125
  drawField(opts, (id, isInvalid) => {
126
- A("div.S_ac", opts.control, () => {
126
+ A("div.s-ac", opts.inputAttrs, () => {
127
127
  A(() => A("aria-invalid=", isInvalid() ? "true" : "false"));
128
128
 
129
129
  let inputEl: HTMLInputElement | undefined;
130
130
 
131
- A("div.S_control", () => {
131
+ A("div.s-control", () => {
132
132
  A("click=", () => inputEl?.focus());
133
133
 
134
134
  // Chips for multi-select.
135
135
  A(() => {
136
136
  if (!opts.multi) return;
137
137
  for (const value of selectedValues()) {
138
- A("span.S_chip", () => {
138
+ A("span.s-chip", () => {
139
139
  A("span #", A.peek(() => labelFor(value)));
140
140
  A("button type=button aria-label=", `Remove ${value}`, () => {
141
141
  A("#×");
@@ -183,9 +183,9 @@ export function autocomplete(opts: AutocompleteOptions): void {
183
183
  const q = $st.query.trim();
184
184
  const showAdd = opts.allowCustom !== false && q !== "" && !list.some((o) => o.label.toLowerCase() === q.toLowerCase());
185
185
 
186
- A("ul.S_menu role=listbox", `id=${menuId}`, () => {
186
+ A("ul.s-menu role=listbox", `id=${menuId}`, () => {
187
187
  list.forEach((option, i) => {
188
- A("li.S_option role=option", `id=${menuId}-opt-${i}`, () => {
188
+ A("li.s-option role=option", `id=${menuId}-opt-${i}`, () => {
189
189
  A(() => A("aria-selected=", $st.active === i ? "true" : "false"));
190
190
  A("#", option.label);
191
191
  A("mousedown=", (e: Event) => e.preventDefault());
@@ -196,14 +196,14 @@ export function autocomplete(opts: AutocompleteOptions): void {
196
196
  });
197
197
  });
198
198
  if (showAdd) {
199
- A("li.S_option.S_add role=option", () => {
199
+ A("li.s-option.s-add role=option", () => {
200
200
  A("#", `Add "${q}"`);
201
201
  A("mousedown=", (e: Event) => e.preventDefault());
202
202
  A("click=", () => commit(q, inputEl));
203
203
  });
204
204
  }
205
205
  if (list.length === 0 && !showAdd) {
206
- A("li.S_empty #No matches");
206
+ A("li.s-empty #No matches");
207
207
  }
208
208
  });
209
209
  });
@@ -1,5 +1,5 @@
1
1
  import A from "aberdeen";
2
- import { type Content, type ContentOptions, type Slot, type Styling, drawSlot } from "../core.js";
2
+ import { type Content, type ContentOptions, type Slot, type Attributes, drawSlot } from "../core.js";
3
3
 
4
4
  /** Options for {@link box}. */
5
5
  export interface BoxOptions extends ContentOptions {
@@ -7,17 +7,23 @@ export interface BoxOptions extends ContentOptions {
7
7
  header?: Slot;
8
8
  /** Footer content, drawn in a styled bar below the body. */
9
9
  footer?: Slot;
10
+ /** Aberdeen attr/style string applied to the body (content-holding) element. */
11
+ contentAttrs?: Attributes;
10
12
  /** Aberdeen attr/style string applied to the header bar. */
11
- headerInner?: Styling;
13
+ headerAttrs?: Attributes;
12
14
  /** Aberdeen attr/style string applied to the footer bar. */
13
- footerInner?: Styling;
15
+ footerAttrs?: Attributes;
14
16
  }
15
17
 
18
+ // The box itself is a `.panel` surface; its header/footer are `.raised`
19
+ // surfaces (classes set on the elements in `box()` below). Colours and borders
20
+ // come from the contextual tokens, so a box stays legible on whatever surface
21
+ // it's nested in.
16
22
  A.insertGlobalCss({
17
- ".S_box": {
18
- "&": "display:flex flex-direction:column bg:$sSurface border: 1px solid $sBorder; r:$sRadius overflow:hidden",
19
- "> header": "display:flex align-items:center gap:$2 padding: $2 $3; bg:$sSurfaceHi border-bottom: 1px solid $sBorder; font-weight:600",
20
- "> footer": "display:flex align-items:center gap:$2 padding: $2 $3; bg:$sSurfaceHi border-top: 1px solid $sBorder;",
23
+ ".s-box": {
24
+ "&": "display:flex flex-direction:column border: 1px solid $s-border; r: $s-radius; overflow:hidden",
25
+ "> header": "display:flex align-items:center gap:$2 padding: $2 $3; border-bottom: 1px solid $s-border; font-weight:600",
26
+ "> footer": "display:flex align-items:center gap:$2 padding: $2 $3; border-top: 1px solid $s-border;",
21
27
  // The body is the only plain <div> child; give it the default padding+gap.
22
28
  "> div": "p:$3 gap:$3",
23
29
  },
@@ -28,14 +34,14 @@ A.insertGlobalCss({
28
34
  * header and footer, and a padded body that holds {@link ContentOptions.content}.
29
35
  *
30
36
  * The body gets default `padding` and matching `gap`; add `display:flex` via
31
- * {@link ContentOptions.inner | inner} if you want its children laid out as a
32
- * flex container.
37
+ * {@link BoxOptions.contentAttrs | contentAttrs} if you want its children laid
38
+ * out as a flex container.
33
39
  *
34
40
  * Shortcut: pass a function to use it directly as the body content.
35
41
  *
36
42
  * @example
37
43
  * ```ts
38
- * S.box({ header: "Profile", inner: "display:flex flex-direction:column", content: () => {
44
+ * S.box({ header: "Profile", contentAttrs: "display:flex flex-direction:column", content: () => {
39
45
  * S.textline({ label: "Name", bind: A.ref($user, "name") });
40
46
  * }});
41
47
  * S.box(() => A("p#Just some content")); // shorthand
@@ -44,19 +50,19 @@ A.insertGlobalCss({
44
50
  export function box(opts: BoxOptions | Content = {}): void {
45
51
  const o: BoxOptions = typeof opts === "function" ? { content: opts } : opts;
46
52
 
47
- A("section.S_box", o.root, () => {
53
+ A("section.s-box.s-s.panel", o.attrs, () => {
48
54
  // Header and footer get their own scopes so toggling them doesn't recreate
49
55
  // the body (which may hold focused inputs / lots of content).
50
56
  A(() => {
51
- if (o.header != null) A("header", o.headerInner, () => drawSlot(o.header));
57
+ if (o.header != null) A("header.s-s.raised", o.headerAttrs, () => drawSlot(o.header));
52
58
  });
53
59
 
54
- A("div", o.inner, () => {
60
+ A("div", o.contentAttrs, () => {
55
61
  if (o.content) o.content();
56
62
  });
57
63
 
58
64
  A(() => {
59
- if (o.footer != null) A("footer", o.footerInner, () => drawSlot(o.footer));
65
+ if (o.footer != null) A("footer.s-s.raised", o.footerAttrs, () => drawSlot(o.footer));
60
66
  });
61
67
  });
62
68
  }