staffa 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -16
- package/dist/components/autocomplete.js +12 -9
- package/dist/components/box.js +15 -10
- package/dist/components/button.d.ts +3 -1
- package/dist/components/button.js +21 -28
- package/dist/components/buttonChooser.js +1 -1
- package/dist/components/buttonGroup.d.ts +3 -3
- package/dist/components/buttonGroup.js +3 -3
- package/dist/components/dialog.d.ts +3 -1
- package/dist/components/dialog.js +20 -10
- package/dist/components/field.js +7 -4
- package/dist/components/main.js +52 -15
- package/dist/components/menu.d.ts +23 -2
- package/dist/components/menu.js +54 -29
- package/dist/components/select.js +1 -1
- package/dist/components/tabs.js +3 -3
- package/dist/components/toast.js +8 -8
- package/dist/components/tooltip.js +4 -3
- package/dist/core.d.ts +14 -2
- package/dist/core.js +23 -0
- package/dist/staffa.esm.js +1 -1
- package/dist/theme.js +138 -224
- package/package.json +1 -1
- package/skill/ButtonOptions.md +2 -1
- package/skill/SKILL.md +108 -47
- package/skill/ToastOptions.md +1 -1
- package/skill/addContextMenu.md +28 -0
- package/skill/addTooltip.md +6 -2
- package/skill/autocomplete.md +9 -2
- package/skill/box.md +1 -0
- package/skill/button.md +5 -3
- package/skill/buttonGroup.md +5 -3
- package/skill/checkbox.md +1 -0
- package/skill/confirm.md +1 -1
- package/skill/dialog.md +2 -2
- package/skill/form.md +3 -2
- package/skill/main.md +5 -1
- package/skill/menuButton.md +1 -1
- package/skill/prompt.md +1 -1
- package/skill/select.md +1 -0
- package/skill/showFloatingMenu.md +5 -6
- package/skill/tabs.md +2 -2
- package/skill/textarea.md +3 -2
- package/skill/textline.md +1 -0
- package/skill/toast.md +2 -3
- package/src/components/autocomplete.ts +12 -9
- package/src/components/box.ts +15 -10
- package/src/components/button.ts +23 -32
- package/src/components/buttonChooser.ts +1 -1
- package/src/components/buttonGroup.ts +3 -3
- package/src/components/dialog.ts +22 -11
- package/src/components/field.ts +7 -4
- package/src/components/main.ts +47 -16
- package/src/components/menu.ts +73 -40
- package/src/components/select.ts +1 -1
- package/src/components/tabs.ts +3 -3
- package/src/components/toast.ts +8 -8
- package/src/components/tooltip.ts +4 -3
- package/src/core.ts +30 -2
- package/src/theme.ts +152 -234
package/skill/SKILL.md
CHANGED
|
@@ -13,26 +13,31 @@ import * as S from "staffa";
|
|
|
13
13
|
|
|
14
14
|
const $user = A.proxy({ name: "", email: "" });
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
S.
|
|
22
|
-
|
|
23
|
-
content: () =>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
16
|
+
S.main({
|
|
17
|
+
title: "Sign up",
|
|
18
|
+
maxWidth: "40rem",
|
|
19
|
+
content: () => {
|
|
20
|
+
S.form({
|
|
21
|
+
submit: () => S.dialog({
|
|
22
|
+
header: "Submitted",
|
|
23
|
+
content: () => A.dump($user)
|
|
24
|
+
}),
|
|
25
|
+
content: () => {
|
|
26
|
+
S.textline({ label: "Name", required: true, bind: A.ref($user, "name") });
|
|
27
|
+
S.textline({ label: "Email", type: "email", bind: A.ref($user, "email") });
|
|
28
|
+
},
|
|
29
|
+
actions: () => S.button({ content: "Create account", type: "submit" }),
|
|
30
|
+
});
|
|
31
|
+
},
|
|
31
32
|
});
|
|
32
33
|
```
|
|
33
34
|
|
|
34
35
|
Staffa is made to look decent out of the box, but easily customizable at runtime.
|
|
35
36
|
|
|
37
|
+
## Screenshot
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
36
41
|
## Install
|
|
37
42
|
|
|
38
43
|
```sh
|
|
@@ -48,7 +53,7 @@ Aberdeen is a peer dependency. Staffa is published as ESM with TypeScript types.
|
|
|
48
53
|
Every component takes a single typed options object and draws DOM via Aberdeen. No classes, no web components. The `S` object collects all component functions:
|
|
49
54
|
|
|
50
55
|
```ts
|
|
51
|
-
S.button({
|
|
56
|
+
S.button({ content: "Save", disabled: false });
|
|
52
57
|
S.box({ header: "Settings", content: () => { ... } });
|
|
53
58
|
```
|
|
54
59
|
|
|
@@ -57,10 +62,11 @@ S.box({ header: "Settings", content: () => { ... } });
|
|
|
57
62
|
All components get their options in a typed object. The object may be an Aberdeen proxy, if you want to update the component in-place.
|
|
58
63
|
|
|
59
64
|
```ts
|
|
60
|
-
const $btn = A.proxy({
|
|
65
|
+
const $btn = A.proxy({ content: "Save", disabled: false });
|
|
61
66
|
S.button($btn);
|
|
62
|
-
//
|
|
63
|
-
$btn.disabled = true; // button updates instantly
|
|
67
|
+
setTimeout(() => // Later..
|
|
68
|
+
$btn.disabled = true; // button updates instantly
|
|
69
|
+
}, 3000);
|
|
64
70
|
```
|
|
65
71
|
|
|
66
72
|
### Rich text slots
|
|
@@ -68,26 +74,35 @@ $btn.disabled = true; // button updates instantly
|
|
|
68
74
|
Anywhere a component takes content, a `label`, `header`, button `text`, dialog body, etc, you can pass either a string or a `() => void` draw function. Strings render as **rich text**: `*italic*`, `**bold**`, `` `code` ``, `[link](/path)`. All text is safely escaped.
|
|
69
75
|
|
|
70
76
|
```ts
|
|
71
|
-
S.button({
|
|
77
|
+
S.button({ content: "Save **now**" });
|
|
72
78
|
S.box({ header: "See the [docs](/docs)", content: () => { ... } });
|
|
73
79
|
```
|
|
74
80
|
|
|
75
81
|
### Surfaces
|
|
76
82
|
|
|
77
|
-
Staffa builds on **surfaces**: elements marked with `.s-s` that have their own background and derived text/border tokens.
|
|
83
|
+
Staffa builds on **surfaces**: elements marked with `.s-s` that have their own background and derived text/border tokens. There are two families:
|
|
78
84
|
|
|
79
|
-
- **
|
|
80
|
-
- **
|
|
81
|
-
- **variant**: `.filled`, `.tonal`, `.outlined`
|
|
85
|
+
- **Nesting surfaces** — `.nest` (and the implicit page at `:root`). A calm neutral whose shade steps automatically with nesting depth (page → panel → raised, capped). Use them for cards, bars, popovers — anything that just holds content. No variants.
|
|
86
|
+
- **Solid surfaces** — `.primary`, `.danger`, `.success`, `.warning`, `.link` (a bare `.s-s` defaults to primary). A bright fill with white ink, painted as a subtle single-colour gradient. They take a **variant**: `.filled` (default), `.tonal`, or `.outlined`. A surface nested *inside* a solid surface is always rendered filled, so it can't bleed into the vivid parent.
|
|
82
87
|
|
|
83
|
-
Components are built from these (`S.button` is a `.s-s.primary
|
|
88
|
+
Components are built from these (`S.button` is a `.s-s.primary`, `S.box` a `.s-s.nest`, etc.). Because component options include an optional `attrs` string, which has Aberdeen `A()` string semantics, you can easily override it:
|
|
84
89
|
|
|
85
90
|
```ts
|
|
86
|
-
S.button({
|
|
87
|
-
S.
|
|
91
|
+
S.button({ content: "Delete", attrs: ".danger" });
|
|
92
|
+
S.button({ content: "Cancel", attrs: ".nest" }); // neutral button
|
|
93
|
+
S.box({ attrs: ".primary", content: () => { ... } });
|
|
88
94
|
```
|
|
89
95
|
|
|
90
|
-
Inside any surface, CSS variables are defined for
|
|
96
|
+
Inside any surface (including `:root`), CSS variables are defined for the background and a set of safe foreground colors: `$s-bg`, `$s-text` (also applied as `color`), `$s-muted`, `$s-accent` (the surface's "pop" — the brand primary on nesting surfaces, the ink on solid surfaces), and `$s-faint`. By using these, components adapt to wherever they're nested.
|
|
97
|
+
|
|
98
|
+
The colour tokens are mode-independent and settable: `$s-primary` (the one brand colour — it tints the neutrals and defines `.s-s.primary`), `$s-danger`, `$s-success`, `$s-warning`, and `$s-link` (the link colour, which is also the fill of the `.s-s.link` surface). Links render in `$s-link` on nesting surfaces and in the ink on solid surfaces.
|
|
99
|
+
|
|
100
|
+
**Borders & shadows.** Nesting surfaces carry a subtle hairline border on their own (so a card looks like a card without any component help). Any surface can be lifted with `.shadow` or `.extra-shadow`: on a nesting surface that's a neutral drop shadow, on a solid surface it's a self-coloured glow (a lit button is just a `.primary` surface with `.shadow`), and on `.tonal`/`.outlined` it's ignored. `.no-shadow` removes a component's built-in shadow:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
S.box({ attrs: ".extra-shadow", content: () => { ... } }); // a more raised card
|
|
104
|
+
S.button({ content: "Quiet", attrs: ".no-shadow" }); // drop the button glow
|
|
105
|
+
```
|
|
91
106
|
|
|
92
107
|
### Dark and light modes
|
|
93
108
|
|
|
@@ -107,30 +122,30 @@ Staffa includes a lightweight CSS reset that makes bare semantic HTML look a bit
|
|
|
107
122
|
|
|
108
123
|
### Theming
|
|
109
124
|
|
|
110
|
-
The first step in theming is just setting some CSS variables
|
|
125
|
+
The first step in theming is just setting some CSS variables. Everything derives from a single brand colour, `s-primary` (the neutral surface shades are tinted toward it too), so often that's all you need. This can be done through CSS directly, or using Aberdeen:
|
|
111
126
|
|
|
112
127
|
```ts
|
|
113
128
|
A.cssVars["s-primary"] = "#fdda58";
|
|
114
|
-
A.cssVars["s-secondary"] = "#cc5624";
|
|
115
129
|
A.cssVars["s-danger"] = "#ee4422";
|
|
116
130
|
A.cssVars["s-radius"] = "4px";
|
|
117
131
|
```
|
|
118
132
|
|
|
119
133
|
See `src/theme.ts` for what other CSS variables are being used.
|
|
120
134
|
|
|
121
|
-
If you need further customization, just add some CSS to override the default styling. For instance, to add your own surface
|
|
135
|
+
If you need further customization, just add some CSS to override the default styling. For instance, to add your own solid surface, set its background (and, if needed, its ink) — the subtle gradient and the rest of the tokens follow automatically:
|
|
122
136
|
|
|
123
137
|
```ts
|
|
124
|
-
|
|
125
|
-
A.insertGlobalCss({".s-s.my-surface": "--s-a:white --s-b:#ef6b00"});
|
|
138
|
+
A.insertGlobalCss({".s-s.my-surface": "--s-bg:#ef6b00 --s-text:#fff"});
|
|
126
139
|
|
|
127
140
|
S.button({
|
|
128
|
-
|
|
141
|
+
content: "You'll want to click me",
|
|
129
142
|
attrs: ".my-surface",
|
|
130
143
|
click: () => S.alert("Good work!", {attrs: ".my-surface"})
|
|
131
144
|
});
|
|
132
145
|
```
|
|
133
146
|
|
|
147
|
+
Custom surface class names may be anything (other than the built-in modifiers `.tonal`, `.outlined`, `.small`, `.large`). The `.tonal` and `.outlined` variants work on your surface for free.
|
|
148
|
+
|
|
134
149
|
Note that when changing CSS like this, things *may* break if you upgrade Staffa. The recommended update strategy is therefore: don't!
|
|
135
150
|
|
|
136
151
|
If you want to make changes that are dependent upon the current light/dark mode setting, rely on Aberdeen reactivity:
|
|
@@ -139,10 +154,10 @@ If you want to make changes that are dependent upon the current light/dark mode
|
|
|
139
154
|
A(() => {
|
|
140
155
|
if (S.getDarkMode()) {
|
|
141
156
|
A.cssVars["s-primary"] = "#aa9944";
|
|
142
|
-
A.insertGlobalCss({".s-s.my-surface": "--s-
|
|
157
|
+
A.insertGlobalCss({".s-s.my-surface": "--s-bg:#444444 --s-text:#fff"});
|
|
143
158
|
} else {
|
|
144
159
|
A.cssVars["s-primary"] = "#fdda58";
|
|
145
|
-
A.insertGlobalCss({".s-s.my-surface": "--s-
|
|
160
|
+
A.insertGlobalCss({".s-s.my-surface": "--s-bg:#cccccc --s-text:#000"});
|
|
146
161
|
}
|
|
147
162
|
});
|
|
148
163
|
```
|
|
@@ -182,14 +197,12 @@ Components share naming conventions for options: `attrs` (outermost element), `c
|
|
|
182
197
|
|
|
183
198
|
Staffa ships the full [Lucide icon set](https://lucide.dev/icons/) as named exports. Import only the ones you use, so a bundler tree-shakes the rest (the whole set is ~82 kB gzipped):
|
|
184
199
|
|
|
185
|
-
```ts
|
|
186
|
-
import { sparkles, bell } from "staffa/icons.js";
|
|
187
|
-
```
|
|
188
|
-
|
|
189
200
|
Each icon is a draw function usable anywhere a slot is accepted (e.g. a button `icon`), or called directly. Customize per call, or globally via `setDefaults()`:
|
|
190
201
|
|
|
191
202
|
```ts
|
|
192
|
-
S
|
|
203
|
+
import * as S from "staffa";
|
|
204
|
+
import { sparkles, bell } from "staffa/icons";
|
|
205
|
+
S.button({ content: "Save", icon: bell });
|
|
193
206
|
sparkles({ size: "1.5em", color: "var(--s-primary)", strokeWidth: 1.5 });
|
|
194
207
|
```
|
|
195
208
|
|
|
@@ -197,7 +210,7 @@ Options: `size`, `color` (defaults to `currentColor`), `strokeWidth`, `cap`, `jo
|
|
|
197
210
|
|
|
198
211
|
### Other
|
|
199
212
|
|
|
200
|
-
- **`S.menuButton(opts)` / `S.showFloatingMenu(opts)`**:
|
|
213
|
+
- **`S.menuButton(opts)` / `S.addContextMenu(opts)` / `S.showFloatingMenu(opts)`**: dropdown menus from a button, right-click/long-press context menus, and the underlying floating menu primitive — with keyboard navigation.
|
|
201
214
|
- **`S.toast(opts)`**: transient notification at the bottom of the viewport.
|
|
202
215
|
- **`S.addTooltip(el, opts)`**: tooltip on hover, attached to an existing element.
|
|
203
216
|
|
|
@@ -237,7 +250,7 @@ Staffa is designed for extension. A component is simply a plain function taking
|
|
|
237
250
|
|
|
238
251
|
3. **Reach for reactivity deliberately.** Pass option strings straight to `A` as positional args (the caller's scope). Only wrap a dedicated `A(() => ...)` scope where it matters: input elements (recreation loses focus), or large subtrees you don't want to redraw. Use `A.peek(() => ...)` when you need a value but must not subscribe.
|
|
239
252
|
|
|
240
|
-
4. **Build on surfaces.** Mark elements `.s-s` and add
|
|
253
|
+
4. **Build on surfaces.** Mark elements `.s-s` and add `.nest` (neutral) or a solid role (`.primary`, `.danger`, …) plus an optional variant. Inside them, use the contextual CSS variables (`$s-text`, `$s-bg`, `$s-muted`, `$s-accent`, `$s-faint`, ...) so components adapt to wherever they're nested. Hard-coding colors in components shouldn't be needed, but if you must, make sure you set *both* foreground and background.
|
|
241
254
|
|
|
242
255
|
5. **No outer margins.** Components don't margin themselves; spacing is the parent's job. Content components set default `padding` on the content element; `contentAttrs` overrides it.
|
|
243
256
|
|
|
@@ -276,6 +289,40 @@ npx shotest review # review/accept the visual changes against the baseline
|
|
|
276
289
|
|
|
277
290
|
The visual tests (`tests/*.spec.ts`) need a build first (`npm run build`); they serve the repo root themselves and click through every demo page. Accepted baselines live in `test-accepted/`.
|
|
278
291
|
|
|
292
|
+
## AI skill
|
|
293
|
+
|
|
294
|
+
If you use Claude Code, GitHub Copilot or another AI agents that supports Skills, Staffa includes a `skill/` directory that provides specialized knowledge to the AI about how to use the library effectively.
|
|
295
|
+
|
|
296
|
+
To use this, it is recommended to symlink the skill into your project's `.claude/skills` directory:
|
|
297
|
+
|
|
298
|
+
```sh
|
|
299
|
+
mkdir -p .claude/skills
|
|
300
|
+
ln -s ../../node_modules/staffa/skill .claude/skills/staffa
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Breaking changes
|
|
304
|
+
|
|
305
|
+
- **0.7** — the surface model was simplified to two families: **nesting** (`.nest`) and **solid** (`.primary`/`.danger`/`.success`/`.warning`/`.link`).
|
|
306
|
+
- Surface levels `.base`/`.panel`/`.raised`/`.neutral` are replaced by a single `.nest` class whose shade steps with nesting depth. Replace them all with `.nest` (a neutral *button* is `.nest` too, in place of `.neutral .outlined`).
|
|
307
|
+
- `.secondary` and `.gradient` are gone: there is no more `s-secondary` colour, and the default button is `.primary` (which now carries a subtle auto-gradient). Drop `s-secondary` overrides.
|
|
308
|
+
- Solid surfaces now use **white** ink (was near-black), and the brand/semantic colours (`s-primary`/`s-danger`/`s-success`/`s-warning`) are now a single mode-independent value each.
|
|
309
|
+
- New: a `s-link` colour and a matching `.s-s.link` surface. Links use `s-link` on nesting surfaces, the ink on solid ones.
|
|
310
|
+
- Nesting surfaces now carry their own hairline border, and `.shadow`/`.extra-shadow`/`.no-shadow` utilities work on any surface. `S.box` no longer draws its own border/shadow (they come from the surface); pass `.no-shadow` to `S.box`/`S.button` to drop the default elevation.
|
|
311
|
+
- Contextual tokens were renamed/trimmed: `--s-fg`→`--s-text`, `--s-fg-muted`→`--s-muted`, `--s-border`→`--s-faint`; `--s-strong`, `--s-fg-faint`, `--s-border-strong`, `--s-on-accent`, `--s-ink`, `--s-page`/`--s-panel`/`--s-raised`, `--s-tint`, `--s-glow`, `--s-shadow`, `--s-gradient-surface`, and the `--s-a`/`--s-b` anchors were removed. Custom solid surfaces now set `--s-bg`/`--s-text`.
|
|
312
|
+
|
|
313
|
+
- **0.6**: None.
|
|
314
|
+
|
|
315
|
+
- **0.5**
|
|
316
|
+
- Surfaces (`.s-s`) now apply `border-radius` and — for `.tonal` and `.outlined` variants — `border` automatically. Custom surfaces or components that previously set these manually may see doubled or conflicting styles; remove the manual declarations.
|
|
317
|
+
- `border:0` is now applied to `.s-btn` by default (overriding the browser's 2px button border). Custom button-like components built on `.s-btn` that relied on the browser default border should add an explicit border.
|
|
318
|
+
|
|
319
|
+
- **0.4**
|
|
320
|
+
- There is no default export anymore: replace `import S from "staffa"` with `import * as S from "staffa"`.
|
|
321
|
+
- `S.button` no longer has a `text` option: use `content` instead (it accepts a string or a draw function).
|
|
322
|
+
- The `Content` type is gone: use `Slot` instead. The `Styling` type alias is now exported as `Attributes`.
|
|
323
|
+
- `S.buttonChooser` uses `undefined` instead of `null` for "nothing selected" (in `bind` and with `allowDeselect`).
|
|
324
|
+
|
|
325
|
+
|
|
279
326
|
# API Reference
|
|
280
327
|
|
|
281
328
|
## setDarkMode · function
|
|
@@ -321,8 +368,8 @@ Options for `box`.
|
|
|
321
368
|
|
|
322
369
|
## [button](button.md) · function
|
|
323
370
|
|
|
324
|
-
A button.
|
|
325
|
-
|
|
371
|
+
A button. Tonal and outlined variants show a border; filled variants rely on
|
|
372
|
+
their solid background for affordance.
|
|
326
373
|
|
|
327
374
|
## [ButtonOptions](ButtonOptions.md) · interface
|
|
328
375
|
|
|
@@ -391,6 +438,13 @@ Open a floating dropdown menu anchored to an element. Portals to
|
|
|
391
438
|
no room below), and closes on Escape, Tab, item selection, or any click
|
|
392
439
|
outside the panel and anchor. Returns a `close()` function.
|
|
393
440
|
|
|
441
|
+
## [addContextMenu](addContextMenu.md) · function
|
|
442
|
+
|
|
443
|
+
Attaches a context menu to the current element: adds a `contextmenu` handler
|
|
444
|
+
via `A` so a | floating menu opens (instead of
|
|
445
|
+
the browser's own menu) on right-click or long-press. The menu is anchored to
|
|
446
|
+
the element and closes on Escape, Tab, item selection, or any click outside.
|
|
447
|
+
|
|
394
448
|
## [MenuOptions](MenuOptions.md) · interface
|
|
395
449
|
|
|
396
450
|
Options for `menuButton` and `MainOptions.nav`.
|
|
@@ -421,6 +475,13 @@ A visual divider between groups of items.
|
|
|
421
475
|
|
|
422
476
|
Options for `showFloatingMenu`.
|
|
423
477
|
|
|
478
|
+
## ContextMenuOptions · type
|
|
479
|
+
|
|
480
|
+
Options for `addContextMenu` — like `FloatingMenuOptions`, but
|
|
481
|
+
the anchor is the element the handler is attached to.
|
|
482
|
+
|
|
483
|
+
**Type:** `Omit<FloatingMenuOptions, "anchor">`
|
|
484
|
+
|
|
424
485
|
## [dialog](dialog.md) · function
|
|
425
486
|
|
|
426
487
|
A dialog rendered into `document.body` via `A.mount`, with a dimming backdrop
|
|
@@ -479,7 +540,7 @@ Options for `tabs`.
|
|
|
479
540
|
## [textarea](textarea.md) · function
|
|
480
541
|
|
|
481
542
|
A multi-line text input. Shares the field chrome and styling of
|
|
482
|
-
`textline
|
|
543
|
+
`textline`.
|
|
483
544
|
|
|
484
545
|
## [TextareaOptions](TextareaOptions.md) · interface
|
|
485
546
|
|
|
@@ -503,7 +564,7 @@ that need their own widget (`checkbox`, `radio`, `color`, `range`, `file`,
|
|
|
503
564
|
## [toast](toast.md) · function
|
|
504
565
|
|
|
505
566
|
Show a toast notification. Returns a `dismiss()` function to remove it
|
|
506
|
-
programmatically. Auto-dismisses after `duration` ms (default
|
|
567
|
+
programmatically. Auto-dismisses after `duration` ms (default 6 000).
|
|
507
568
|
|
|
508
569
|
## [ToastOptions](ToastOptions.md) · interface
|
|
509
570
|
|
package/skill/ToastOptions.md
CHANGED
|
@@ -22,7 +22,7 @@ Colour role. Defaults to `"neutral"`.
|
|
|
22
22
|
|
|
23
23
|
### toastOptions.duration · member
|
|
24
24
|
|
|
25
|
-
Auto-dismiss delay in milliseconds. Defaults to `
|
|
25
|
+
Auto-dismiss delay in milliseconds. Defaults to `6000`.
|
|
26
26
|
Pass `0` to make the toast persistent until dismissed manually.
|
|
27
27
|
|
|
28
28
|
**Type:** `number`
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
## addContextMenu · function
|
|
2
|
+
|
|
3
|
+
Attaches a context menu to the current element: adds a `contextmenu` handler
|
|
4
|
+
via `A` so a | floating menu opens (instead of
|
|
5
|
+
the browser's own menu) on right-click or long-press. The menu is anchored to
|
|
6
|
+
the element and closes on Escape, Tab, item selection, or any click outside.
|
|
7
|
+
|
|
8
|
+
**Signature:** `(opts: ContextMenuOptions) => void`
|
|
9
|
+
|
|
10
|
+
**Parameters:**
|
|
11
|
+
|
|
12
|
+
- `opts: ContextMenuOptions`
|
|
13
|
+
|
|
14
|
+
**Examples:**
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import * as icons from "staffa/icons";
|
|
18
|
+
|
|
19
|
+
S.box(() => {
|
|
20
|
+
A("#Right-click / long-tap me!");
|
|
21
|
+
S.addContextMenu({
|
|
22
|
+
items: [
|
|
23
|
+
{ label: "AI something", icon: icons.sparkles, click: () => ai() },
|
|
24
|
+
{ label: "Launch missiles", icon: icons.rocket, click: () => launch() },
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
```
|
package/skill/addTooltip.md
CHANGED
|
@@ -15,11 +15,15 @@ element's bounding rect and automatically flips when near the viewport edge.
|
|
|
15
15
|
**Examples:**
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
|
-
|
|
18
|
+
S.button(() => {
|
|
19
|
+
A("#Save");
|
|
19
20
|
S.addTooltip({ tip: "Saves your work to the cloud" });
|
|
20
21
|
});
|
|
21
22
|
|
|
22
|
-
A("
|
|
23
|
+
A(" ");
|
|
24
|
+
|
|
25
|
+
S.button(() => {
|
|
26
|
+
A(".danger #Delete");
|
|
23
27
|
S.addTooltip({ tip: "Dangerous — cannot be undone", placement: "bottom" });
|
|
24
28
|
});
|
|
25
29
|
```
|
package/skill/autocomplete.md
CHANGED
|
@@ -14,9 +14,16 @@ backspace-to-remove). Implements the ARIA combobox/listbox pattern.
|
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
16
|
// Single select from a fixed list
|
|
17
|
+
const $sel = A.proxy("Netherlands");
|
|
17
18
|
S.autocomplete({ label: "Country", options: ["Belgium", "Netherlands"], bind: $sel });
|
|
18
19
|
|
|
19
20
|
// Multi-select, disallowing custom items
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
const $tags = A.proxy({value: [] as string[]});
|
|
22
|
+
S.autocomplete({
|
|
23
|
+
label: "Tags",
|
|
24
|
+
multi: true,
|
|
25
|
+
allowCustom: false,
|
|
26
|
+
options: ["Rust", "JS", "C++", "Klingon", "Go"],
|
|
27
|
+
bind: A.ref($tags)
|
|
28
|
+
});
|
|
22
29
|
```
|
package/skill/box.md
CHANGED
|
@@ -18,6 +18,7 @@ Shortcut: pass a function to use it directly as the body content.
|
|
|
18
18
|
**Examples:**
|
|
19
19
|
|
|
20
20
|
```ts
|
|
21
|
+
const $user = A.proxy({name: "Kvothe"});
|
|
21
22
|
S.box({ header: "Profile", contentAttrs: "display:flex flex-direction:column", content: () => {
|
|
22
23
|
S.textline({ label: "Name", bind: A.ref($user, "name") });
|
|
23
24
|
}});
|
package/skill/button.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
## button · function
|
|
2
2
|
|
|
3
|
-
A button.
|
|
4
|
-
|
|
3
|
+
A button. Tonal and outlined variants show a border; filled variants rely on
|
|
4
|
+
their solid background for affordance.
|
|
5
5
|
|
|
6
6
|
Shortcut: pass a string to use it as the label, or a function for custom
|
|
7
7
|
content.
|
|
@@ -9,6 +9,7 @@ content.
|
|
|
9
9
|
**Tip:** pair `href` with Aberdeen's `interceptLinks()` (called once at app
|
|
10
10
|
startup) for SPA-style navigation without manual click handlers:
|
|
11
11
|
```ts
|
|
12
|
+
import {interceptLinks} from from "aberdeen/route";
|
|
12
13
|
interceptLinks(); // once at root
|
|
13
14
|
S.button({ href: "/dashboard", content: "Dashboard" }); // navigates via router
|
|
14
15
|
```
|
|
@@ -22,7 +23,8 @@ S.button({ href: "/dashboard", content: "Dashboard" }); // navigates via router
|
|
|
22
23
|
**Examples:**
|
|
23
24
|
|
|
24
25
|
```ts
|
|
25
|
-
S.button({ content: "Save", click:
|
|
26
|
+
S.button({ content: "Save", click: S.alert("Saved.") });
|
|
27
|
+
S.button({ content: "Cancel", attrs: ".nest", click: cancel }); // neutral button
|
|
26
28
|
S.button({ content: "Delete", attrs: ".danger .outlined", click: del });
|
|
27
29
|
S.button("Cancel"); // shorthand for { content: "Cancel" }
|
|
28
30
|
S.button({ href: "/docs", content: "Docs" }); // renders an <a role=button>
|
package/skill/buttonGroup.md
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
Groups related buttons, either as a joined segmented control (`attached`) or
|
|
4
4
|
spaced out. A `role=group` is applied for assistive tech.
|
|
5
5
|
|
|
6
|
+
If you want a single button to be *selected*, use `buttonChooser`.
|
|
7
|
+
|
|
6
8
|
**Signature:** `(opts?: ButtonGroupOptions) => void`
|
|
7
9
|
|
|
8
10
|
**Parameters:**
|
|
@@ -13,8 +15,8 @@ spaced out. A `role=group` is applied for assistive tech.
|
|
|
13
15
|
|
|
14
16
|
```ts
|
|
15
17
|
S.buttonGroup({ buttons: [
|
|
16
|
-
{
|
|
17
|
-
{
|
|
18
|
-
{
|
|
18
|
+
{ content: "Day", attrs: ".nest" },
|
|
19
|
+
{ content: "Week", attrs: ".nest" },
|
|
20
|
+
{ content: "Month", attrs: ".nest" },
|
|
19
21
|
]});
|
|
20
22
|
```
|
package/skill/checkbox.md
CHANGED
package/skill/confirm.md
CHANGED
package/skill/dialog.md
CHANGED
|
@@ -21,8 +21,8 @@ S.dialog({
|
|
|
21
21
|
header: "Confirm",
|
|
22
22
|
content: (close) => {
|
|
23
23
|
A("p #Are you sure?");
|
|
24
|
-
S.button({ content: "Yes", click: () => {
|
|
25
|
-
S.button({ content: "Cancel", attrs: ".
|
|
24
|
+
S.button({ content: "Yes", click: () => { S.alert("Nice!"); close(); } });
|
|
25
|
+
S.button({ content: "Cancel", attrs: ".nest", click: close });
|
|
26
26
|
},
|
|
27
27
|
});
|
|
28
28
|
```
|
package/skill/form.md
CHANGED
|
@@ -17,11 +17,12 @@ native validation runs, but the page never reloads.
|
|
|
17
17
|
**Examples:**
|
|
18
18
|
|
|
19
19
|
```ts
|
|
20
|
+
const $user = A.proxy({name: "Darth", email: "d.vader@example.com"});
|
|
20
21
|
S.form({
|
|
21
22
|
submit: () => save(),
|
|
22
23
|
content: () => {
|
|
23
|
-
S.textline({ label: "Name", required: true, bind: A.ref($
|
|
24
|
-
S.textline({ label: "Email", type: "email", bind: A.ref($
|
|
24
|
+
S.textline({ label: "Name", required: true, bind: A.ref($user, "name") });
|
|
25
|
+
S.textline({ label: "Email", type: "email", bind: A.ref($user, "email") });
|
|
25
26
|
},
|
|
26
27
|
actions: () => S.button({ content: "Save", type: "submit" }),
|
|
27
28
|
});
|
package/skill/main.md
CHANGED
|
@@ -27,7 +27,11 @@ S.main({
|
|
|
27
27
|
},
|
|
28
28
|
navPosition: "left",
|
|
29
29
|
menu: () => S.button({ content: "New", attrs: ".small" }),
|
|
30
|
-
content:
|
|
30
|
+
content: drawPage,
|
|
31
31
|
footer: "© 2026",
|
|
32
32
|
});
|
|
33
|
+
|
|
34
|
+
function drawPage() {
|
|
35
|
+
S.box({title: "Hello world", content: "Here's you app.."});
|
|
36
|
+
}
|
|
33
37
|
```
|
package/skill/menuButton.md
CHANGED
|
@@ -17,7 +17,7 @@ for SPA navigation — active items are highlighted automatically.
|
|
|
17
17
|
|
|
18
18
|
```ts
|
|
19
19
|
S.menuButton({
|
|
20
|
-
button: {
|
|
20
|
+
button: { content: "Actions", attrs: ".nest" },
|
|
21
21
|
items: [
|
|
22
22
|
{ label: "Edit", icon: () => A("#✎"), click: () => edit() },
|
|
23
23
|
{ separator: true },
|
package/skill/prompt.md
CHANGED
package/skill/select.md
CHANGED
|
@@ -5,6 +5,10 @@ Open a floating dropdown menu anchored to an element. Portals to
|
|
|
5
5
|
no room below), and closes on Escape, Tab, item selection, or any click
|
|
6
6
|
outside the panel and anchor. Returns a `close()` function.
|
|
7
7
|
|
|
8
|
+
Menus are usually opened through `menuButton` or
|
|
9
|
+
`addContextMenu`; reach for this primitive when you need to trigger a
|
|
10
|
+
menu from some other event, anchored to an arbitrary element.
|
|
11
|
+
|
|
8
12
|
**Signature:** `(opts: FloatingMenuOptions) => () => void`
|
|
9
13
|
|
|
10
14
|
**Parameters:**
|
|
@@ -14,9 +18,4 @@ outside the panel and anchor. Returns a `close()` function.
|
|
|
14
18
|
**Examples:**
|
|
15
19
|
|
|
16
20
|
```ts
|
|
17
|
-
//
|
|
18
|
-
el.addEventListener("contextmenu", (e) => {
|
|
19
|
-
e.preventDefault();
|
|
20
|
-
S.showFloatingMenu({ items, anchor: el });
|
|
21
|
-
});
|
|
22
|
-
```
|
|
21
|
+
// An
|
package/skill/tabs.md
CHANGED
|
@@ -13,7 +13,7 @@ for the selected tab. Supports keyboard navigation (left/right/home/end).
|
|
|
13
13
|
|
|
14
14
|
```ts
|
|
15
15
|
S.tabs({ tabs: [
|
|
16
|
-
{ label: "Overview", content: () => A("p
|
|
17
|
-
{ label: "Settings", content: () =>
|
|
16
|
+
{ label: "Overview", content: () => A("p#Let me give you an overview..") },
|
|
17
|
+
{ label: "Settings", content: () => S.checkbox({label: "I agree to anything", checked: true}) },
|
|
18
18
|
]});
|
|
19
19
|
```
|
package/skill/textarea.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
## textarea · function
|
|
2
2
|
|
|
3
3
|
A multi-line text input. Shares the field chrome and styling of
|
|
4
|
-
`textline
|
|
4
|
+
`textline`.
|
|
5
5
|
|
|
6
6
|
**Signature:** `(opts?: TextareaOptions) => void`
|
|
7
7
|
|
|
@@ -12,5 +12,6 @@ A multi-line text input. Shares the field chrome and styling of
|
|
|
12
12
|
**Examples:**
|
|
13
13
|
|
|
14
14
|
```ts
|
|
15
|
-
|
|
15
|
+
const $user = A.proxy({bio: ""});
|
|
16
|
+
S.textarea({ label: "Bio", bind: A.ref($user, "bio") });
|
|
16
17
|
```
|
package/skill/textline.md
CHANGED
package/skill/toast.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
## toast · function
|
|
2
2
|
|
|
3
3
|
Show a toast notification. Returns a `dismiss()` function to remove it
|
|
4
|
-
programmatically. Auto-dismisses after `duration` ms (default
|
|
4
|
+
programmatically. Auto-dismisses after `duration` ms (default 6 000).
|
|
5
5
|
|
|
6
6
|
**Signature:** `(opts: ToastOptions) => () => void`
|
|
7
7
|
|
|
@@ -15,6 +15,5 @@ programmatically. Auto-dismisses after `duration` ms (default 4 000).
|
|
|
15
15
|
S.toast({ message: "Saved!", type: "success" });
|
|
16
16
|
S.toast({ title: "Error", message: "Upload failed.", type: "danger", duration: 0 });
|
|
17
17
|
const off = S.toast({ message: "Uploading…", duration: 0, dismissible: false });
|
|
18
|
-
//
|
|
19
|
-
off();
|
|
18
|
+
setTimeout(off, 3000); // Later..
|
|
20
19
|
```
|
|
@@ -35,20 +35,23 @@ export interface AutocompleteOptions extends FieldOptions {
|
|
|
35
35
|
A.insertGlobalCss({
|
|
36
36
|
".s-ac": {
|
|
37
37
|
"&": "position:relative",
|
|
38
|
-
|
|
39
|
-
"> .s-control:
|
|
38
|
+
// Same light inset field as `.s-input` (see field.ts), derived from the surface.
|
|
39
|
+
"> .s-control": "display:flex flex-wrap:wrap align-items:center gap:$1 background: color-mix(in oklab, $s-bg, $s-text 4%); color:$s-text border: 1px solid $s-faint; r:$s-radius padding: 0.3em 0.4em; cursor:text; transition: border-color 0.15s, box-shadow 0.15s;",
|
|
40
|
+
"> .s-control:hover": "border-color: color-mix(in oklab, $s-text, $s-bg 55%);",
|
|
40
41
|
"> .s-control:focus-within": "border-color:$s-accent box-shadow: 0 0 0 3px $s-focus;",
|
|
41
42
|
"&[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
|
|
43
|
-
".s-chip > button": "cursor:pointer border:0 background:transparent fg:$s-
|
|
44
|
-
".s-chip > button:hover": "fg:$s-
|
|
43
|
+
".s-chip": "display:inline-flex align-items:center gap:$1 font-size:0.85em background: color-mix(in oklab, $s-bg, $s-text 10%); border: 1px solid $s-faint; r:$s-radius padding: 0.1em 0.2em 0.1em 0.5em;",
|
|
44
|
+
".s-chip > button": "cursor:pointer border:0 background:transparent fg:$s-muted font-size:1.1em line-height:1 padding: 0 0.2em; r:4px",
|
|
45
|
+
".s-chip > button:hover": "fg:$s-text background:$s-faint",
|
|
45
46
|
"input": "flex:1 min-width:6ch border:0 background:transparent color:inherit outline:none padding:0.25em",
|
|
46
|
-
|
|
47
|
+
// The popup is a `.s-s.neutral.shadow` surface (see below): background, border,
|
|
48
|
+
// radius and elevation all come from the surface.
|
|
49
|
+
"> .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",
|
|
47
50
|
"> .s-menu li": "margin:0",
|
|
48
51
|
".s-option": "padding: 0.45em 0.6em; r:6px cursor:pointer transition: background 0.1s;",
|
|
49
|
-
".s-option[aria-selected=true]": "background: color-mix(in srgb, $s-
|
|
52
|
+
".s-option[aria-selected=true]": "background: color-mix(in srgb, $s-text 10%, transparent);",
|
|
50
53
|
".s-add": "fg:$s-accent font-style:italic",
|
|
51
|
-
".s-empty": "padding: 0.45em 0.6em; fg:$s-
|
|
54
|
+
".s-empty": "padding: 0.45em 0.6em; fg:$s-muted",
|
|
52
55
|
},
|
|
53
56
|
});
|
|
54
57
|
|
|
@@ -191,7 +194,7 @@ export function autocomplete(opts: AutocompleteOptions): void {
|
|
|
191
194
|
const q = $st.query.trim();
|
|
192
195
|
const showAdd = opts.allowCustom !== false && q !== "" && !list.some((o) => o.label.toLowerCase() === q.toLowerCase());
|
|
193
196
|
|
|
194
|
-
A("ul.s-menu role=listbox", `id=${menuId}`, () => {
|
|
197
|
+
A("ul.s-menu.s-s.neutral.shadow role=listbox", `id=${menuId}`, () => {
|
|
195
198
|
list.forEach((option, i) => {
|
|
196
199
|
A("li.s-option role=option", `id=${menuId}-opt-${i}`, () => {
|
|
197
200
|
A(() => A("aria-selected=", $st.active === i ? "true" : "false"));
|