staffa 0.4.1 → 0.4.3
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 +5 -7
- 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 +2 -0
- package/dist/components/buttonGroup.js +2 -0
- 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 +38 -4
- package/dist/components/menu.js +44 -4
- 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 +6 -6
- package/src/components/buttonGroup.ts +2 -0
- 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 -4
- package/src/components/select.ts +1 -0
- 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 +6 -2
- package/src/core.ts +11 -9
- package/src/index.ts +1 -1
package/README.md
CHANGED
|
@@ -52,8 +52,9 @@ All components get their options in a typed object. The object may be an Aberdee
|
|
|
52
52
|
```ts
|
|
53
53
|
const $btn = A.proxy({ content: "Save", disabled: false });
|
|
54
54
|
S.button($btn);
|
|
55
|
-
//
|
|
56
|
-
$btn.disabled = true; // button updates instantly
|
|
55
|
+
setTimeout(() => // Later..
|
|
56
|
+
$btn.disabled = true; // button updates instantly
|
|
57
|
+
}, 3000);
|
|
57
58
|
```
|
|
58
59
|
|
|
59
60
|
### Rich text slots
|
|
@@ -175,13 +176,10 @@ Components share naming conventions for options: `attrs` (outermost element), `c
|
|
|
175
176
|
|
|
176
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):
|
|
177
178
|
|
|
178
|
-
```ts
|
|
179
|
-
import { sparkles, bell } from "staffa/icons.js";
|
|
180
|
-
```
|
|
181
|
-
|
|
182
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()`:
|
|
183
180
|
|
|
184
181
|
```ts
|
|
182
|
+
import { sparkles, bell } from "staffa/icons.js";
|
|
185
183
|
S.button({ content: "Save", icon: bell });
|
|
186
184
|
sparkles({ size: "1.5em", color: "var(--s-primary)", strokeWidth: 1.5 });
|
|
187
185
|
```
|
|
@@ -190,7 +188,7 @@ Options: `size`, `color` (defaults to `currentColor`), `strokeWidth`, `cap`, `jo
|
|
|
190
188
|
|
|
191
189
|
### Other
|
|
192
190
|
|
|
193
|
-
- **`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.
|
|
194
192
|
- **`S.toast(opts)`**: transient notification at the bottom of the viewport.
|
|
195
193
|
- **`S.addTooltip(el, opts)`**: tooltip on hover, attached to an existing element.
|
|
196
194
|
|
|
@@ -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,6 +19,8 @@ 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: [
|
|
@@ -22,6 +22,8 @@ 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: [
|
|
@@ -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;
|
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;
|
|
@@ -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;
|
package/dist/components/toast.js
CHANGED
|
@@ -22,6 +22,9 @@ let toastCount = 0;
|
|
|
22
22
|
// Keyed by stable ID so A.onEach scopes are per-toast — removing one never re-renders others.
|
|
23
23
|
const toasts = A.proxy({});
|
|
24
24
|
mountPortal(() => {
|
|
25
|
+
// Only redraws when emptiness flips, keeping the DOM clean while no toasts show.
|
|
26
|
+
if (A.isEmpty(toasts))
|
|
27
|
+
return;
|
|
25
28
|
A("div.s-toasts aria-live=polite aria-atomic=false", () => {
|
|
26
29
|
A.onEach(toasts, (entry) => {
|
|
27
30
|
const { opts } = entry;
|
|
@@ -59,8 +62,7 @@ function dismiss(id) {
|
|
|
59
62
|
* S.toast({ message: "Saved!", type: "success" });
|
|
60
63
|
* S.toast({ title: "Error", message: "Upload failed.", type: "danger", duration: 0 });
|
|
61
64
|
* const off = S.toast({ message: "Uploading…", duration: 0, dismissible: false });
|
|
62
|
-
* //
|
|
63
|
-
* off();
|
|
65
|
+
* setTimeout(off, 3000); // Later..
|
|
64
66
|
* ```
|
|
65
67
|
*/
|
|
66
68
|
export function toast(opts) {
|
|
@@ -20,11 +20,13 @@ export interface TooltipOptions {
|
|
|
20
20
|
*
|
|
21
21
|
* @example
|
|
22
22
|
* ```ts
|
|
23
|
-
*
|
|
23
|
+
* S.button(() => {
|
|
24
|
+
* A("#Save");
|
|
24
25
|
* S.addTooltip({ tip: "Saves your work to the cloud" });
|
|
25
26
|
* });
|
|
26
27
|
*
|
|
27
|
-
*
|
|
28
|
+
* S.button(() => {
|
|
29
|
+
* A(".danger#Delete");
|
|
28
30
|
* S.addTooltip({ tip: "Dangerous — cannot be undone", placement: "bottom" });
|
|
29
31
|
* });
|
|
30
32
|
* ```
|
|
@@ -102,11 +102,13 @@ mountPortal(() => {
|
|
|
102
102
|
*
|
|
103
103
|
* @example
|
|
104
104
|
* ```ts
|
|
105
|
-
*
|
|
105
|
+
* S.button(() => {
|
|
106
|
+
* A("#Save");
|
|
106
107
|
* S.addTooltip({ tip: "Saves your work to the cloud" });
|
|
107
108
|
* });
|
|
108
109
|
*
|
|
109
|
-
*
|
|
110
|
+
* S.button(() => {
|
|
111
|
+
* A(".danger#Delete");
|
|
110
112
|
* S.addTooltip({ tip: "Dangerous — cannot be undone", placement: "bottom" });
|
|
111
113
|
* });
|
|
112
114
|
* ```
|
package/dist/core.d.ts
CHANGED
|
@@ -57,10 +57,15 @@ export declare function uniqueId(prefix?: string): string;
|
|
|
57
57
|
*/
|
|
58
58
|
export declare function drawSlot<Args extends unknown[] = []>(slot: Slot<Args> | undefined, ...args: Args): void;
|
|
59
59
|
/**
|
|
60
|
-
* Mount a portal (tooltip, toast, menu, dialog, …)
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
60
|
+
* Mount a portal (tooltip, toast, menu, dialog, …) directly into `<body>`.
|
|
61
|
+
* Must be called at module top level, where Aberdeen's root scope (whose
|
|
62
|
+
* element is `document.body`) is current. Separate `A.mount`s sharing a parent
|
|
63
|
+
* can't tell their nodes apart, but sibling scopes within the root scope track
|
|
64
|
+
* their positions, so portals coexist without wrapper elements and add nothing
|
|
65
|
+
* to the DOM while they draw nothing.
|
|
66
|
+
*
|
|
67
|
+
* Scope creation is deferred a microtask so that an app drawing into `<body>`
|
|
68
|
+
* at module top level gets its content *before* the portals, keeping overlays
|
|
69
|
+
* at the end of the document.
|
|
65
70
|
*/
|
|
66
71
|
export declare function mountPortal(draw: () => void): void;
|
package/dist/core.js
CHANGED
|
@@ -18,15 +18,17 @@ export function drawSlot(slot, ...args) {
|
|
|
18
18
|
A("rich=", slot);
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
-
* Mount a portal (tooltip, toast, menu, dialog, …)
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
21
|
+
* Mount a portal (tooltip, toast, menu, dialog, …) directly into `<body>`.
|
|
22
|
+
* Must be called at module top level, where Aberdeen's root scope (whose
|
|
23
|
+
* element is `document.body`) is current. Separate `A.mount`s sharing a parent
|
|
24
|
+
* can't tell their nodes apart, but sibling scopes within the root scope track
|
|
25
|
+
* their positions, so portals coexist without wrapper elements and add nothing
|
|
26
|
+
* to the DOM while they draw nothing.
|
|
27
|
+
*
|
|
28
|
+
* Scope creation is deferred a microtask so that an app drawing into `<body>`
|
|
29
|
+
* at module top level gets its content *before* the portals, keeping overlays
|
|
30
|
+
* at the end of the document.
|
|
26
31
|
*/
|
|
27
32
|
export function mountPortal(draw) {
|
|
28
|
-
|
|
29
|
-
container.style.display = "contents";
|
|
30
|
-
document.body.appendChild(container);
|
|
31
|
-
A.mount(container, draw);
|
|
33
|
+
queueMicrotask(() => A(draw));
|
|
32
34
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export { buttonGroup, type ButtonGroupOptions } from "./components/buttonGroup.j
|
|
|
38
38
|
export { checkbox, type CheckboxOptions } from "./components/checkbox.js";
|
|
39
39
|
export { form, type FormOptions } from "./components/form.js";
|
|
40
40
|
export { main, type MainOptions } from "./components/main.js";
|
|
41
|
-
export { menuButton, showFloatingMenu, type MenuOptions, type MenuEntry, type MenuItem, type MenuSeparator, type FloatingMenuOptions } from "./components/menu.js";
|
|
41
|
+
export { menuButton, showFloatingMenu, addContextMenu, type MenuOptions, type MenuEntry, type MenuItem, type MenuSeparator, type FloatingMenuOptions, type ContextMenuOptions } from "./components/menu.js";
|
|
42
42
|
export { dialog, alert, confirm, prompt, type DialogOptions } from "./components/dialog.js";
|
|
43
43
|
export { select, type SelectOptions, type SelectOptionInput } from "./components/select.js";
|
|
44
44
|
export { tabs, type Tab, type TabsOptions } from "./components/tabs.js";
|
package/dist/index.js
CHANGED
|
@@ -41,7 +41,7 @@ export { buttonGroup } from "./components/buttonGroup.js";
|
|
|
41
41
|
export { checkbox } from "./components/checkbox.js";
|
|
42
42
|
export { form } from "./components/form.js";
|
|
43
43
|
export { main } from "./components/main.js";
|
|
44
|
-
export { menuButton, showFloatingMenu } from "./components/menu.js";
|
|
44
|
+
export { menuButton, showFloatingMenu, addContextMenu } from "./components/menu.js";
|
|
45
45
|
export { dialog, alert, confirm, prompt } from "./components/dialog.js";
|
|
46
46
|
export { select } from "./components/select.js";
|
|
47
47
|
export { tabs } from "./components/tabs.js";
|