staffa 0.4.0 → 0.4.2
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 -29
- package/dist/components/autocomplete.d.ts +9 -2
- package/dist/components/autocomplete.js +9 -2
- package/dist/components/box.d.ts +1 -0
- package/dist/components/box.js +1 -0
- package/dist/components/button.d.ts +1 -0
- package/dist/components/button.js +5 -5
- package/dist/components/buttonGroup.d.ts +5 -3
- package/dist/components/buttonGroup.js +5 -3
- package/dist/components/checkbox.d.ts +1 -0
- package/dist/components/checkbox.js +1 -0
- package/dist/components/dialog.d.ts +3 -3
- package/dist/components/dialog.js +3 -3
- package/dist/components/form.d.ts +3 -2
- package/dist/components/form.js +3 -2
- package/dist/components/main.d.ts +5 -1
- package/dist/components/main.js +5 -1
- package/dist/components/menu.d.ts +39 -5
- package/dist/components/menu.js +45 -5
- package/dist/components/tabs.d.ts +2 -2
- package/dist/components/tabs.js +2 -2
- package/dist/components/textarea.d.ts +3 -2
- package/dist/components/textarea.js +3 -2
- package/dist/components/textline.d.ts +1 -0
- package/dist/components/textline.js +1 -0
- package/dist/components/toast.d.ts +1 -2
- package/dist/components/toast.js +4 -2
- package/dist/components/tooltip.d.ts +4 -2
- package/dist/components/tooltip.js +4 -2
- package/dist/core.d.ts +10 -5
- package/dist/core.js +11 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/staffa.esm.js +1 -1
- package/package.json +1 -1
- package/src/components/autocomplete.ts +9 -2
- package/src/components/box.ts +1 -0
- package/src/components/button.ts +5 -5
- package/src/components/buttonGroup.ts +5 -3
- package/src/components/checkbox.ts +1 -0
- package/src/components/dialog.ts +3 -3
- package/src/components/form.ts +3 -2
- package/src/components/main.ts +5 -1
- package/src/components/menu.ts +50 -5
- package/src/components/tabs.ts +2 -2
- package/src/components/textarea.ts +3 -2
- package/src/components/textline.ts +1 -0
- package/src/components/toast.ts +3 -2
- package/src/components/tooltip.ts +4 -2
- package/src/core.ts +11 -9
- package/src/index.ts +1 -1
package/README.md
CHANGED
|
@@ -8,21 +8,19 @@ import * as S from "staffa";
|
|
|
8
8
|
|
|
9
9
|
const $user = A.proxy({ name: "", email: "" });
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
},
|
|
25
|
-
});
|
|
11
|
+
S.main({
|
|
12
|
+
title: "Sign up",
|
|
13
|
+
maxWidth: "40rem",
|
|
14
|
+
content: () => {
|
|
15
|
+
S.form({
|
|
16
|
+
submit: () => console.log(A.unproxy($user)),
|
|
17
|
+
content: () => {
|
|
18
|
+
S.textline({ label: "Name", required: true, bind: A.ref($user, "name") });
|
|
19
|
+
S.textline({ label: "Email", type: "email", bind: A.ref($user, "email") });
|
|
20
|
+
},
|
|
21
|
+
actions: () => S.button({ content: "Create account", type: "submit" }),
|
|
22
|
+
});
|
|
23
|
+
},
|
|
26
24
|
});
|
|
27
25
|
```
|
|
28
26
|
|
|
@@ -43,7 +41,7 @@ Aberdeen is a peer dependency. Staffa is published as ESM with TypeScript types.
|
|
|
43
41
|
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:
|
|
44
42
|
|
|
45
43
|
```ts
|
|
46
|
-
S.button({
|
|
44
|
+
S.button({ content: "Save", disabled: false });
|
|
47
45
|
S.box({ header: "Settings", content: () => { ... } });
|
|
48
46
|
```
|
|
49
47
|
|
|
@@ -52,10 +50,11 @@ S.box({ header: "Settings", content: () => { ... } });
|
|
|
52
50
|
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.
|
|
53
51
|
|
|
54
52
|
```ts
|
|
55
|
-
const $btn = A.proxy({
|
|
53
|
+
const $btn = A.proxy({ content: "Save", disabled: false });
|
|
56
54
|
S.button($btn);
|
|
57
|
-
//
|
|
58
|
-
$btn.disabled = true; // button updates instantly
|
|
55
|
+
setTimeout(() => // Later..
|
|
56
|
+
$btn.disabled = true; // button updates instantly
|
|
57
|
+
}, 3000);
|
|
59
58
|
```
|
|
60
59
|
|
|
61
60
|
### Rich text slots
|
|
@@ -63,7 +62,7 @@ $btn.disabled = true; // button updates instantly
|
|
|
63
62
|
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.
|
|
64
63
|
|
|
65
64
|
```ts
|
|
66
|
-
S.button({
|
|
65
|
+
S.button({ content: "Save **now**" });
|
|
67
66
|
S.box({ header: "See the [docs](/docs)", content: () => { ... } });
|
|
68
67
|
```
|
|
69
68
|
|
|
@@ -78,7 +77,7 @@ Staffa builds on **surfaces**: elements marked with `.s-s` that have their own b
|
|
|
78
77
|
Components are built from these (`S.button` is a `.s-s.primary.filled`, `S.box` a `.s-s.panel`, etc.). Because component options include an optional `attrs` string, which has Aberdeen `A()` string semantics, you can easily override it:
|
|
79
78
|
|
|
80
79
|
```ts
|
|
81
|
-
S.button({
|
|
80
|
+
S.button({ content: "Delete", attrs: ".danger" });
|
|
82
81
|
S.box({ attrs: ".raised.outlined", content: () => { ... } });
|
|
83
82
|
```
|
|
84
83
|
|
|
@@ -120,7 +119,7 @@ If you need further customization, just add some CSS to override the default sty
|
|
|
120
119
|
A.insertGlobalCss({".s-s.my-surface": "--s-a:white --s-b:#ef6b00"});
|
|
121
120
|
|
|
122
121
|
S.button({
|
|
123
|
-
|
|
122
|
+
content: "You'll want to click me",
|
|
124
123
|
attrs: ".my-surface",
|
|
125
124
|
click: () => S.alert("Good work!", {attrs: ".my-surface"})
|
|
126
125
|
});
|
|
@@ -177,14 +176,11 @@ Components share naming conventions for options: `attrs` (outermost element), `c
|
|
|
177
176
|
|
|
178
177
|
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):
|
|
179
178
|
|
|
180
|
-
```ts
|
|
181
|
-
import { sparkles, bell } from "staffa/icons.js";
|
|
182
|
-
```
|
|
183
|
-
|
|
184
179
|
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()`:
|
|
185
180
|
|
|
186
181
|
```ts
|
|
187
|
-
|
|
182
|
+
import { sparkles, bell } from "staffa/icons.js";
|
|
183
|
+
S.button({ content: "Save", icon: bell });
|
|
188
184
|
sparkles({ size: "1.5em", color: "var(--s-primary)", strokeWidth: 1.5 });
|
|
189
185
|
```
|
|
190
186
|
|
|
@@ -192,7 +188,7 @@ Options: `size`, `color` (defaults to `currentColor`), `strokeWidth`, `cap`, `jo
|
|
|
192
188
|
|
|
193
189
|
### Other
|
|
194
190
|
|
|
195
|
-
- **`S.menuButton(opts)` / `S.showFloatingMenu(opts)`**:
|
|
191
|
+
- **`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.
|
|
196
192
|
- **`S.toast(opts)`**: transient notification at the bottom of the viewport.
|
|
197
193
|
- **`S.addTooltip(el, opts)`**: tooltip on hover, attached to an existing element.
|
|
198
194
|
|
|
@@ -280,4 +276,13 @@ To use this, it is recommended to symlink the skill into your project's `.claude
|
|
|
280
276
|
```sh
|
|
281
277
|
mkdir -p .claude/skills
|
|
282
278
|
ln -s ../../node_modules/staffa/skill .claude/skills/staffa
|
|
283
|
-
```
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Breaking changes
|
|
282
|
+
|
|
283
|
+
- **0.4**
|
|
284
|
+
- There is no default export anymore: replace `import S from "staffa"` with `import * as S from "staffa"`.
|
|
285
|
+
- `S.button` no longer has a `text` option: use `content` instead (it accepts a string or a draw function).
|
|
286
|
+
- The `Content` type is gone: use `Slot` instead. The `Styling` type alias is now exported as `Attributes`.
|
|
287
|
+
- `S.buttonChooser` uses `undefined` instead of `null` for "nothing selected" (in `bind` and with `allowDeselect`).
|
|
288
|
+
|
|
@@ -34,11 +34,18 @@ export interface AutocompleteOptions extends FieldOptions {
|
|
|
34
34
|
* @example
|
|
35
35
|
* ```ts
|
|
36
36
|
* // Single select from a fixed list
|
|
37
|
+
* const $sel = A.proxy("Netherlands");
|
|
37
38
|
* S.autocomplete({ label: "Country", options: ["Belgium", "Netherlands"], bind: $sel });
|
|
38
39
|
*
|
|
39
40
|
* // Multi-select, disallowing custom items
|
|
40
|
-
*
|
|
41
|
-
*
|
|
41
|
+
* const $tags = A.proxy([] as string[]);
|
|
42
|
+
* S.autocomplete({
|
|
43
|
+
* label: "Tags",
|
|
44
|
+
* multi: true,
|
|
45
|
+
* allowCustom: true,
|
|
46
|
+
* options: ["Rust", "JS", "C++", "Klingon", "Go"],
|
|
47
|
+
* bind: A.ref($tags)
|
|
48
|
+
* });
|
|
42
49
|
* ```
|
|
43
50
|
*/
|
|
44
51
|
export declare function autocomplete(opts: AutocompleteOptions): void;
|
|
@@ -31,11 +31,18 @@ function normOption(o) {
|
|
|
31
31
|
* @example
|
|
32
32
|
* ```ts
|
|
33
33
|
* // Single select from a fixed list
|
|
34
|
+
* const $sel = A.proxy("Netherlands");
|
|
34
35
|
* S.autocomplete({ label: "Country", options: ["Belgium", "Netherlands"], bind: $sel });
|
|
35
36
|
*
|
|
36
37
|
* // Multi-select, disallowing custom items
|
|
37
|
-
*
|
|
38
|
-
*
|
|
38
|
+
* const $tags = A.proxy([] as string[]);
|
|
39
|
+
* S.autocomplete({
|
|
40
|
+
* label: "Tags",
|
|
41
|
+
* multi: true,
|
|
42
|
+
* allowCustom: true,
|
|
43
|
+
* options: ["Rust", "JS", "C++", "Klingon", "Go"],
|
|
44
|
+
* bind: A.ref($tags)
|
|
45
|
+
* });
|
|
39
46
|
* ```
|
|
40
47
|
*/
|
|
41
48
|
export function autocomplete(opts) {
|
package/dist/components/box.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface BoxOptions extends ContentOptions {
|
|
|
24
24
|
*
|
|
25
25
|
* @example
|
|
26
26
|
* ```ts
|
|
27
|
+
* const $user = A.proxy({name: "Kvothe"});
|
|
27
28
|
* S.box({ header: "Profile", contentAttrs: "display:flex flex-direction:column", content: () => {
|
|
28
29
|
* S.textline({ label: "Name", bind: A.ref($user, "name") });
|
|
29
30
|
* }});
|
package/dist/components/box.js
CHANGED
|
@@ -25,6 +25,7 @@ A.insertGlobalCss({
|
|
|
25
25
|
*
|
|
26
26
|
* @example
|
|
27
27
|
* ```ts
|
|
28
|
+
* const $user = A.proxy({name: "Kvothe"});
|
|
28
29
|
* S.box({ header: "Profile", contentAttrs: "display:flex flex-direction:column", content: () => {
|
|
29
30
|
* S.textline({ label: "Name", bind: A.ref($user, "name") });
|
|
30
31
|
* }});
|
|
@@ -36,6 +36,7 @@ export interface ButtonOptions {
|
|
|
36
36
|
* **Tip:** pair `href` with Aberdeen's `interceptLinks()` (called once at app
|
|
37
37
|
* startup) for SPA-style navigation without manual click handlers:
|
|
38
38
|
* ```ts
|
|
39
|
+
* import {interceptLinks} from from "aberdeen/route";
|
|
39
40
|
* interceptLinks(); // once at root
|
|
40
41
|
* S.button({ href: "/dashboard", content: "Dashboard" }); // navigates via router
|
|
41
42
|
* ```
|
|
@@ -11,10 +11,9 @@ A.insertGlobalCss({
|
|
|
11
11
|
"transition: background 0.15s, border-color 0.15s, color 0.15s, filter 0.15s, box-shadow 0.15s, transform 0.08s;",
|
|
12
12
|
"&:focus-visible": "outline:none box-shadow: 0 0 0 3px $s-focus;",
|
|
13
13
|
"&:disabled, &[aria-disabled=true]": "opacity:0.45 cursor:not-allowed pointer-events:none filter:saturate(0.6)",
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
"&:hover": "filter: brightness(1.08); transform: translateY(-1px)",
|
|
14
|
+
// Hover feedback is colour-only (no movement). The filled `.gradient` CTA
|
|
15
|
+
// below layers a deeper shadow on top, so it still reads as the signature action.
|
|
16
|
+
"&:hover": "filter: brightness(1.08)",
|
|
18
17
|
"&.tonal:hover, &.outlined:hover": "background: color-mix(in srgb, $s-b 26%, transparent);",
|
|
19
18
|
// A filled `.gradient` button (the default) is the app's signature call to
|
|
20
19
|
// action: a borderless gradient with a hairline top highlight (a hint of
|
|
@@ -25,7 +24,7 @@ A.insertGlobalCss({
|
|
|
25
24
|
// artifact where a gradient clipped to a transparent rounded border fringes
|
|
26
25
|
// the edge with the gradient's far colour.
|
|
27
26
|
"&.gradient:not(.tonal):not(.outlined)": "border:0 box-shadow: inset 0 1px 0 color-mix(in srgb, white 25%, transparent), $s-glow;",
|
|
28
|
-
"&.gradient:not(.tonal):not(.outlined):hover": "filter: brightness(1.05); box-shadow: inset 0 1px 0 color-mix(in srgb, white 25%, transparent), 0 7px 18px color-mix(in srgb, $s-primary 34%, transparent);
|
|
27
|
+
"&.gradient:not(.tonal):not(.outlined):hover": "filter: brightness(1.05); box-shadow: inset 0 1px 0 color-mix(in srgb, white 25%, transparent), 0 7px 18px color-mix(in srgb, $s-primary 34%, transparent);",
|
|
29
28
|
// Subtle press feedback.
|
|
30
29
|
"&:active:not(:disabled):not([aria-disabled=true])": "transform: translateY(1px)",
|
|
31
30
|
// Size: set on the button itself, or inherited from a `.small`/`.large`
|
|
@@ -47,6 +46,7 @@ const ROLE_CLASS = /\.(gradient|primary|secondary|neutral|danger|success|warning
|
|
|
47
46
|
* **Tip:** pair `href` with Aberdeen's `interceptLinks()` (called once at app
|
|
48
47
|
* startup) for SPA-style navigation without manual click handlers:
|
|
49
48
|
* ```ts
|
|
49
|
+
* import {interceptLinks} from from "aberdeen/route";
|
|
50
50
|
* interceptLinks(); // once at root
|
|
51
51
|
* S.button({ href: "/dashboard", content: "Dashboard" }); // navigates via router
|
|
52
52
|
* ```
|
|
@@ -19,12 +19,14 @@ export interface ButtonGroupOptions extends ContentOptions {
|
|
|
19
19
|
* Groups related buttons, either as a joined segmented control (`attached`) or
|
|
20
20
|
* spaced out. A `role=group` is applied for assistive tech.
|
|
21
21
|
*
|
|
22
|
+
* If you want a single button to be *selected*, use {@link buttonChooser}.
|
|
23
|
+
*
|
|
22
24
|
* @example
|
|
23
25
|
* ```ts
|
|
24
26
|
* S.buttonGroup({ buttons: [
|
|
25
|
-
* {
|
|
26
|
-
* {
|
|
27
|
-
* {
|
|
27
|
+
* { content: "Day", attrs: ".neutral .outlined" },
|
|
28
|
+
* { content: "Week", attrs: ".neutral .outlined" },
|
|
29
|
+
* { content: "Month", attrs: ".neutral .outlined" },
|
|
28
30
|
* ]});
|
|
29
31
|
* ```
|
|
30
32
|
*/
|
|
@@ -22,12 +22,14 @@ A.insertGlobalCss({
|
|
|
22
22
|
* Groups related buttons, either as a joined segmented control (`attached`) or
|
|
23
23
|
* spaced out. A `role=group` is applied for assistive tech.
|
|
24
24
|
*
|
|
25
|
+
* If you want a single button to be *selected*, use {@link buttonChooser}.
|
|
26
|
+
*
|
|
25
27
|
* @example
|
|
26
28
|
* ```ts
|
|
27
29
|
* S.buttonGroup({ buttons: [
|
|
28
|
-
* {
|
|
29
|
-
* {
|
|
30
|
-
* {
|
|
30
|
+
* { content: "Day", attrs: ".neutral .outlined" },
|
|
31
|
+
* { content: "Week", attrs: ".neutral .outlined" },
|
|
32
|
+
* { content: "Month", attrs: ".neutral .outlined" },
|
|
31
33
|
* ]});
|
|
32
34
|
* ```
|
|
33
35
|
*/
|
|
@@ -52,7 +52,7 @@ export interface DialogOptions {
|
|
|
52
52
|
* header: "Confirm",
|
|
53
53
|
* content: (close) => {
|
|
54
54
|
* A("p #Are you sure?");
|
|
55
|
-
* S.button({ content: "Yes", click: () => {
|
|
55
|
+
* S.button({ content: "Yes", click: () => { S.alert("Nice!"); close(); } });
|
|
56
56
|
* S.button({ content: "Cancel", attrs: ".neutral .outlined", click: close });
|
|
57
57
|
* },
|
|
58
58
|
* });
|
|
@@ -75,7 +75,7 @@ export declare function alert(message: string, opts?: Partial<DialogOptions>): P
|
|
|
75
75
|
*
|
|
76
76
|
* @example
|
|
77
77
|
* ```ts
|
|
78
|
-
* if (await S.confirm("Delete this item?"))
|
|
78
|
+
* if (await S.confirm("Delete this item?")) S.alert("Gone!");
|
|
79
79
|
* ```
|
|
80
80
|
*/
|
|
81
81
|
export declare function confirm(message: string, opts?: Partial<DialogOptions>): Promise<boolean>;
|
|
@@ -86,7 +86,7 @@ export declare function confirm(message: string, opts?: Partial<DialogOptions>):
|
|
|
86
86
|
* @example
|
|
87
87
|
* ```ts
|
|
88
88
|
* const name = await S.prompt("Enter your name:", "Alice");
|
|
89
|
-
* if (name !== null)
|
|
89
|
+
* if (name !== null) S.alert(`Hi ${name}!`);
|
|
90
90
|
* ```
|
|
91
91
|
*/
|
|
92
92
|
export declare function prompt(message: string, defaultValue?: string, opts?: Partial<DialogOptions>): Promise<string | null>;
|
|
@@ -78,7 +78,7 @@ mountPortal(() => {
|
|
|
78
78
|
* header: "Confirm",
|
|
79
79
|
* content: (close) => {
|
|
80
80
|
* A("p #Are you sure?");
|
|
81
|
-
* S.button({ content: "Yes", click: () => {
|
|
81
|
+
* S.button({ content: "Yes", click: () => { S.alert("Nice!"); close(); } });
|
|
82
82
|
* S.button({ content: "Cancel", attrs: ".neutral .outlined", click: close });
|
|
83
83
|
* },
|
|
84
84
|
* });
|
|
@@ -138,7 +138,7 @@ export function alert(message, opts = {}) {
|
|
|
138
138
|
*
|
|
139
139
|
* @example
|
|
140
140
|
* ```ts
|
|
141
|
-
* if (await S.confirm("Delete this item?"))
|
|
141
|
+
* if (await S.confirm("Delete this item?")) S.alert("Gone!");
|
|
142
142
|
* ```
|
|
143
143
|
*/
|
|
144
144
|
export function confirm(message, opts = {}) {
|
|
@@ -169,7 +169,7 @@ export function confirm(message, opts = {}) {
|
|
|
169
169
|
* @example
|
|
170
170
|
* ```ts
|
|
171
171
|
* const name = await S.prompt("Enter your name:", "Alice");
|
|
172
|
-
* if (name !== null)
|
|
172
|
+
* if (name !== null) S.alert(`Hi ${name}!`);
|
|
173
173
|
* ```
|
|
174
174
|
*/
|
|
175
175
|
export function prompt(message, defaultValue = "", opts = {}) {
|
|
@@ -29,11 +29,12 @@ export interface FormOptions extends ContentOptions {
|
|
|
29
29
|
*
|
|
30
30
|
* @example
|
|
31
31
|
* ```ts
|
|
32
|
+
* const $user = A.proxy({name: "Darth", email: "d.vader@example.com"});
|
|
32
33
|
* S.form({
|
|
33
34
|
* submit: () => save(),
|
|
34
35
|
* content: () => {
|
|
35
|
-
* S.textline({ label: "Name", required: true, bind: A.ref($
|
|
36
|
-
* S.textline({ label: "Email", type: "email", bind: A.ref($
|
|
36
|
+
* S.textline({ label: "Name", required: true, bind: A.ref($user, "name") });
|
|
37
|
+
* S.textline({ label: "Email", type: "email", bind: A.ref($user, "email") });
|
|
37
38
|
* },
|
|
38
39
|
* actions: () => S.button({ content: "Save", type: "submit" }),
|
|
39
40
|
* });
|
package/dist/components/form.js
CHANGED
|
@@ -19,11 +19,12 @@ A.insertGlobalCss({
|
|
|
19
19
|
*
|
|
20
20
|
* @example
|
|
21
21
|
* ```ts
|
|
22
|
+
* const $user = A.proxy({name: "Darth", email: "d.vader@example.com"});
|
|
22
23
|
* S.form({
|
|
23
24
|
* submit: () => save(),
|
|
24
25
|
* content: () => {
|
|
25
|
-
* S.textline({ label: "Name", required: true, bind: A.ref($
|
|
26
|
-
* S.textline({ label: "Email", type: "email", bind: A.ref($
|
|
26
|
+
* S.textline({ label: "Name", required: true, bind: A.ref($user, "name") });
|
|
27
|
+
* S.textline({ label: "Email", type: "email", bind: A.ref($user, "email") });
|
|
27
28
|
* },
|
|
28
29
|
* actions: () => S.button({ content: "Save", type: "submit" }),
|
|
29
30
|
* });
|
|
@@ -66,9 +66,13 @@ export interface MainOptions {
|
|
|
66
66
|
* },
|
|
67
67
|
* navPosition: "left",
|
|
68
68
|
* menu: () => S.button({ content: "New", attrs: ".small" }),
|
|
69
|
-
* content:
|
|
69
|
+
* content: drawPage,
|
|
70
70
|
* footer: "© 2026",
|
|
71
71
|
* });
|
|
72
|
+
*
|
|
73
|
+
* function drawPage() {
|
|
74
|
+
* S.box({title: "Hello world", content: "Here's you app.."});
|
|
75
|
+
* }
|
|
72
76
|
* ```
|
|
73
77
|
*/
|
|
74
78
|
export declare function main(opts?: MainOptions): void;
|
package/dist/components/main.js
CHANGED
|
@@ -89,9 +89,13 @@ A.insertGlobalCss({
|
|
|
89
89
|
* },
|
|
90
90
|
* navPosition: "left",
|
|
91
91
|
* menu: () => S.button({ content: "New", attrs: ".small" }),
|
|
92
|
-
* content:
|
|
92
|
+
* content: drawPage,
|
|
93
93
|
* footer: "© 2026",
|
|
94
94
|
* });
|
|
95
|
+
*
|
|
96
|
+
* function drawPage() {
|
|
97
|
+
* S.box({title: "Hello world", content: "Here's you app.."});
|
|
98
|
+
* }
|
|
95
99
|
* ```
|
|
96
100
|
*/
|
|
97
101
|
export function main(opts = {}) {
|
|
@@ -64,6 +64,9 @@ export interface FloatingMenuOptions {
|
|
|
64
64
|
/** Aberdeen attr/style string on the floating panel. */
|
|
65
65
|
dropdownAttrs?: Attributes;
|
|
66
66
|
}
|
|
67
|
+
/** Options for {@link addContextMenu} — like {@link FloatingMenuOptions}, but
|
|
68
|
+
* the anchor is the element the handler is attached to. */
|
|
69
|
+
export type ContextMenuOptions = Omit<FloatingMenuOptions, "anchor">;
|
|
67
70
|
/**
|
|
68
71
|
* Draw a list of {@link MenuEntry} items into the *current* element, with
|
|
69
72
|
* arrow-key / Home / End navigation between the focusable items. The single
|
|
@@ -85,16 +88,47 @@ export declare function drawMenu(items: MenuEntry[], onActivate?: () => void): v
|
|
|
85
88
|
* no room below), and closes on Escape, Tab, item selection, or any click
|
|
86
89
|
* outside the panel and anchor. Returns a `close()` function.
|
|
87
90
|
*
|
|
91
|
+
* Menus are usually opened through {@link menuButton} or
|
|
92
|
+
* {@link addContextMenu}; reach for this primitive when you need to trigger a
|
|
93
|
+
* menu from some other event, anchored to an arbitrary element.
|
|
94
|
+
*
|
|
88
95
|
* @example
|
|
89
96
|
* ```ts
|
|
90
|
-
* //
|
|
91
|
-
*
|
|
92
|
-
* e
|
|
93
|
-
*
|
|
97
|
+
* // An @-mention picker: typing "@" in the input pops up a user menu.
|
|
98
|
+
* A("input placeholder=Comment…", () => {
|
|
99
|
+
* A("keydown=", (e: KeyboardEvent) => {
|
|
100
|
+
* if (e.key !== "@") return;
|
|
101
|
+
* S.showFloatingMenu({
|
|
102
|
+
* anchor: e.currentTarget as HTMLElement,
|
|
103
|
+
* items: users.map((u) => ({ label: u.name, click: () => mention(u) })),
|
|
104
|
+
* });
|
|
105
|
+
* });
|
|
94
106
|
* });
|
|
95
107
|
* ```
|
|
96
108
|
*/
|
|
97
109
|
export declare function showFloatingMenu(opts: FloatingMenuOptions): () => void;
|
|
110
|
+
/**
|
|
111
|
+
* Attaches a context menu to the current element: adds a `contextmenu` handler
|
|
112
|
+
* via {@link A} so a {@link showFloatingMenu | floating menu} opens (instead of
|
|
113
|
+
* the browser's own menu) on right-click or long-press. The menu is anchored to
|
|
114
|
+
* the element and closes on Escape, Tab, item selection, or any click outside.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```ts
|
|
118
|
+
* import * as icons from "staffa/icons";
|
|
119
|
+
*
|
|
120
|
+
* S.box(() => {
|
|
121
|
+
* A("#Right-click / long-tap me!");
|
|
122
|
+
* S.addContextMenu({
|
|
123
|
+
* items: [
|
|
124
|
+
* { label: "AI something", icon: icons.sparkles, click: () => ai() },
|
|
125
|
+
* { label: "Launch missiles", icon: icons.rocket, click: () => launch() },
|
|
126
|
+
* ],
|
|
127
|
+
* });
|
|
128
|
+
* });
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export declare function addContextMenu(opts: ContextMenuOptions): void;
|
|
98
132
|
/**
|
|
99
133
|
* A button that opens a {@link showFloatingMenu | floating dropdown menu} on
|
|
100
134
|
* click. Keyboard navigation: Arrow Up/Down, Home, End; Escape/Tab to close;
|
|
@@ -106,7 +140,7 @@ export declare function showFloatingMenu(opts: FloatingMenuOptions): () => void;
|
|
|
106
140
|
* @example
|
|
107
141
|
* ```ts
|
|
108
142
|
* S.menuButton({
|
|
109
|
-
* button: {
|
|
143
|
+
* button: { content: "Actions", attrs: ".neutral .outlined" },
|
|
110
144
|
* items: [
|
|
111
145
|
* { label: "Edit", icon: () => A("#✎"), click: () => edit() },
|
|
112
146
|
* { separator: true },
|
package/dist/components/menu.js
CHANGED
|
@@ -163,12 +163,21 @@ mountPortal(() => {
|
|
|
163
163
|
* no room below), and closes on Escape, Tab, item selection, or any click
|
|
164
164
|
* outside the panel and anchor. Returns a `close()` function.
|
|
165
165
|
*
|
|
166
|
+
* Menus are usually opened through {@link menuButton} or
|
|
167
|
+
* {@link addContextMenu}; reach for this primitive when you need to trigger a
|
|
168
|
+
* menu from some other event, anchored to an arbitrary element.
|
|
169
|
+
*
|
|
166
170
|
* @example
|
|
167
171
|
* ```ts
|
|
168
|
-
* //
|
|
169
|
-
*
|
|
170
|
-
* e
|
|
171
|
-
*
|
|
172
|
+
* // An @-mention picker: typing "@" in the input pops up a user menu.
|
|
173
|
+
* A("input placeholder=Comment…", () => {
|
|
174
|
+
* A("keydown=", (e: KeyboardEvent) => {
|
|
175
|
+
* if (e.key !== "@") return;
|
|
176
|
+
* S.showFloatingMenu({
|
|
177
|
+
* anchor: e.currentTarget as HTMLElement,
|
|
178
|
+
* items: users.map((u) => ({ label: u.name, click: () => mention(u) })),
|
|
179
|
+
* });
|
|
180
|
+
* });
|
|
172
181
|
* });
|
|
173
182
|
* ```
|
|
174
183
|
*/
|
|
@@ -176,6 +185,37 @@ export function showFloatingMenu(opts) {
|
|
|
176
185
|
$floating.opts = opts;
|
|
177
186
|
return closeFloating;
|
|
178
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Attaches a context menu to the current element: adds a `contextmenu` handler
|
|
190
|
+
* via {@link A} so a {@link showFloatingMenu | floating menu} opens (instead of
|
|
191
|
+
* the browser's own menu) on right-click or long-press. The menu is anchored to
|
|
192
|
+
* the element and closes on Escape, Tab, item selection, or any click outside.
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```ts
|
|
196
|
+
* import * as icons from "staffa/icons";
|
|
197
|
+
*
|
|
198
|
+
* S.box(() => {
|
|
199
|
+
* A("#Right-click / long-tap me!");
|
|
200
|
+
* S.addContextMenu({
|
|
201
|
+
* items: [
|
|
202
|
+
* { label: "AI something", icon: icons.sparkles, click: () => ai() },
|
|
203
|
+
* { label: "Launch missiles", icon: icons.rocket, click: () => launch() },
|
|
204
|
+
* ],
|
|
205
|
+
* });
|
|
206
|
+
* });
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
export function addContextMenu(opts) {
|
|
210
|
+
let myEl = null;
|
|
211
|
+
A.clean(() => { if ($floating.opts?.anchor === myEl)
|
|
212
|
+
closeFloating(); });
|
|
213
|
+
A("contextmenu=", (e) => {
|
|
214
|
+
e.preventDefault();
|
|
215
|
+
myEl = e.currentTarget;
|
|
216
|
+
showFloatingMenu({ items: opts.items, anchor: myEl, dropdownAttrs: opts.dropdownAttrs });
|
|
217
|
+
});
|
|
218
|
+
}
|
|
179
219
|
/**
|
|
180
220
|
* A button that opens a {@link showFloatingMenu | floating dropdown menu} on
|
|
181
221
|
* click. Keyboard navigation: Arrow Up/Down, Home, End; Escape/Tab to close;
|
|
@@ -187,7 +227,7 @@ export function showFloatingMenu(opts) {
|
|
|
187
227
|
* @example
|
|
188
228
|
* ```ts
|
|
189
229
|
* S.menuButton({
|
|
190
|
-
* button: {
|
|
230
|
+
* button: { content: "Actions", attrs: ".neutral .outlined" },
|
|
191
231
|
* items: [
|
|
192
232
|
* { label: "Edit", icon: () => A("#✎"), click: () => edit() },
|
|
193
233
|
* { separator: true },
|
|
@@ -33,8 +33,8 @@ export interface TabsOptions {
|
|
|
33
33
|
* @example
|
|
34
34
|
* ```ts
|
|
35
35
|
* S.tabs({ tabs: [
|
|
36
|
-
* { label: "Overview", content: () => A("p
|
|
37
|
-
* { label: "Settings", content: () =>
|
|
36
|
+
* { label: "Overview", content: () => A("p#Let me give you an overview..") },
|
|
37
|
+
* { label: "Settings", content: () => S.checkbox({label: "I agree to anything", checked: true}) },
|
|
38
38
|
* ]});
|
|
39
39
|
* ```
|
|
40
40
|
*/
|
package/dist/components/tabs.js
CHANGED
|
@@ -23,8 +23,8 @@ A.insertGlobalCss({
|
|
|
23
23
|
* @example
|
|
24
24
|
* ```ts
|
|
25
25
|
* S.tabs({ tabs: [
|
|
26
|
-
* { label: "Overview", content: () => A("p
|
|
27
|
-
* { label: "Settings", content: () =>
|
|
26
|
+
* { label: "Overview", content: () => A("p#Let me give you an overview..") },
|
|
27
|
+
* { label: "Settings", content: () => S.checkbox({label: "I agree to anything", checked: true}) },
|
|
28
28
|
* ]});
|
|
29
29
|
* ```
|
|
30
30
|
*/
|
|
@@ -21,11 +21,12 @@ export interface TextareaOptions extends FieldOptions {
|
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
23
|
* A multi-line text input. Shares the field chrome and styling of
|
|
24
|
-
* {@link textline}
|
|
24
|
+
* {@link textline}.
|
|
25
25
|
*
|
|
26
26
|
* @example
|
|
27
27
|
* ```ts
|
|
28
|
-
*
|
|
28
|
+
* const $user = A.proxy({bio: ""});
|
|
29
|
+
* S.textarea({ label: "Bio", bind: A.ref($user, "bio") });
|
|
29
30
|
* ```
|
|
30
31
|
*/
|
|
31
32
|
export declare function textarea(opts?: TextareaOptions): void;
|
|
@@ -6,11 +6,12 @@ A.insertGlobalCss({
|
|
|
6
6
|
});
|
|
7
7
|
/**
|
|
8
8
|
* A multi-line text input. Shares the field chrome and styling of
|
|
9
|
-
* {@link textline}
|
|
9
|
+
* {@link textline}.
|
|
10
10
|
*
|
|
11
11
|
* @example
|
|
12
12
|
* ```ts
|
|
13
|
-
*
|
|
13
|
+
* const $user = A.proxy({bio: ""});
|
|
14
|
+
* S.textarea({ label: "Bio", bind: A.ref($user, "bio") });
|
|
14
15
|
* ```
|
|
15
16
|
*/
|
|
16
17
|
export function textarea(opts = {}) {
|
|
@@ -28,8 +28,7 @@ export interface ToastOptions {
|
|
|
28
28
|
* S.toast({ message: "Saved!", type: "success" });
|
|
29
29
|
* S.toast({ title: "Error", message: "Upload failed.", type: "danger", duration: 0 });
|
|
30
30
|
* const off = S.toast({ message: "Uploading…", duration: 0, dismissible: false });
|
|
31
|
-
* //
|
|
32
|
-
* off();
|
|
31
|
+
* setTimeout(off, 3000); // Later..
|
|
33
32
|
* ```
|
|
34
33
|
*/
|
|
35
34
|
export declare function toast(opts: ToastOptions): () => void;
|