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.
- package/README.md +36 -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 +3 -2
- package/skill/Attributes.md +10 -0
- package/skill/AutocompleteOptions.md +37 -0
- package/skill/BoxOptions.md +33 -0
- package/skill/ButtonChooserOptions.md +37 -0
- package/skill/ButtonGroupOptions.md +23 -0
- package/skill/ButtonOptions.md +58 -0
- package/skill/CheckboxOptions.md +27 -0
- package/skill/ContentOptions.md +16 -0
- package/skill/DialogOptions.md +70 -0
- package/skill/FieldOptions.md +63 -0
- package/skill/FloatingMenuOptions.md +21 -0
- package/skill/FormOptions.md +31 -0
- package/skill/MainOptions.md +91 -0
- package/skill/MenuItem.md +52 -0
- package/skill/MenuOptions.md +25 -0
- package/skill/SKILL.md +609 -0
- package/skill/SelectOptions.md +21 -0
- package/skill/Slot.md +13 -0
- package/skill/Tab.md +33 -0
- package/skill/TabsOptions.md +28 -0
- package/skill/TextareaOptions.md +51 -0
- package/skill/TextlineOptions.md +45 -0
- package/skill/TextlineType.md +18 -0
- package/skill/ToastOptions.md +40 -0
- package/skill/TooltipOptions.md +22 -0
- package/skill/addContextMenu.md +28 -0
- package/skill/addTooltip.md +29 -0
- package/skill/alert.md +17 -0
- package/skill/autocomplete.md +29 -0
- package/skill/box.md +26 -0
- package/skill/button.md +31 -0
- package/skill/buttonChooser.md +23 -0
- package/skill/buttonGroup.md +22 -0
- package/skill/checkbox.md +18 -0
- package/skill/confirm.md +17 -0
- package/skill/dialog.md +28 -0
- package/skill/form.md +29 -0
- package/skill/getDarkMode.md +12 -0
- package/skill/main.md +37 -0
- package/skill/menuButton.md +27 -0
- package/skill/prompt.md +19 -0
- package/skill/select.md +18 -0
- package/skill/showFloatingMenu.md +21 -0
- package/skill/tabs.md +19 -0
- package/skill/textarea.md +17 -0
- package/skill/textline.md +20 -0
- package/skill/toast.md +19 -0
- 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/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
|
|
9
|
-
*
|
|
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
|
-
* -
|
|
13
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
21
|
-
*
|
|
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
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* default
|
|
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
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
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
|
|
84
|
-
*
|
|
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
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
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
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
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
|
-
|
|
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-
|
|
289
|
-
pre: "background: color-mix(in oklab, $s-
|
|
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-
|
|
293
|
-
"::placeholder": "color: $s-
|
|
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-
|
|
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
|
|
306
|
-
//
|
|
307
|
-
//
|
|
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.
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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.
|
|
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`
|