staffa 0.15.0 → 0.16.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 +99 -271
- package/dist/components/autocomplete.js +4 -5
- package/dist/components/box.js +11 -21
- package/dist/components/button.d.ts +20 -5
- package/dist/components/button.js +55 -47
- package/dist/components/buttonChooser.js +1 -3
- package/dist/components/checkbox.js +1 -2
- package/dist/components/dialog.d.ts +9 -2
- package/dist/components/dialog.js +28 -32
- package/dist/components/field.d.ts +5 -8
- package/dist/components/field.js +4 -6
- package/dist/components/form.d.ts +5 -7
- package/dist/components/form.js +6 -9
- package/dist/components/keyhelp.d.ts +22 -0
- package/dist/components/keyhelp.js +91 -0
- package/dist/components/main.js +188 -271
- package/dist/components/menu.d.ts +36 -9
- package/dist/components/menu.js +185 -135
- package/dist/components/panels.d.ts +145 -236
- package/dist/components/panels.js +327 -557
- package/dist/components/select.js +1 -3
- package/dist/components/tabs.d.ts +10 -13
- package/dist/components/tabs.js +38 -58
- package/dist/components/textline.d.ts +3 -5
- package/dist/components/textline.js +3 -5
- package/dist/components/toast.d.ts +1 -3
- package/dist/components/toast.js +2 -3
- package/dist/components/tooltip.d.ts +4 -5
- package/dist/components/tooltip.js +7 -10
- package/dist/core.d.ts +17 -24
- package/dist/core.js +13 -18
- package/dist/icons-helpers.d.ts +3 -3
- package/dist/icons-helpers.js +6 -11
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -4
- package/dist/keys.d.ts +92 -0
- package/dist/keys.js +279 -0
- package/dist/staffa.esm.js +1 -1
- package/dist/theme.d.ts +4 -10
- package/dist/theme.js +58 -123
- package/package.json +2 -2
- package/skill/ButtonOptions.md +12 -0
- package/skill/DialogOptions.md +11 -2
- package/skill/FieldOptions.md +3 -5
- package/skill/IconButtonOptions.md +8 -0
- package/skill/MenuItem.md +22 -3
- package/skill/Panel.md +8 -0
- package/skill/SKILL.md +161 -294
- package/skill/addTooltip.md +4 -5
- package/skill/bindKey.md +51 -0
- package/skill/box.md +1 -1
- package/skill/form.md +5 -7
- package/skill/formatKey.md +21 -0
- package/skill/iconButton.md +4 -5
- package/skill/scrollStrip.md +7 -9
- package/skill/showFloatingMenu.md +2 -2
- package/skill/showKeyHelp.md +17 -0
- package/skill/tabs.md +3 -4
- package/skill/textline.md +3 -5
- package/src/components/autocomplete.ts +4 -5
- package/src/components/box.ts +11 -21
- package/src/components/button.ts +70 -47
- package/src/components/buttonChooser.ts +1 -3
- package/src/components/checkbox.ts +1 -2
- package/src/components/dialog.ts +38 -34
- package/src/components/field.ts +7 -11
- package/src/components/form.ts +6 -9
- package/src/components/keyhelp.ts +96 -0
- package/src/components/main.ts +191 -270
- package/src/components/menu.ts +202 -138
- package/src/components/panels.ts +371 -619
- package/src/components/select.ts +1 -3
- package/src/components/tabs.ts +38 -58
- package/src/components/textline.ts +3 -5
- package/src/components/toast.ts +3 -6
- package/src/components/tooltip.ts +7 -10
- package/src/core.ts +17 -24
- package/src/icons-helpers.ts +6 -11
- package/src/index.ts +5 -4
- package/src/keys.ts +300 -0
- package/src/theme.ts +58 -123
- package/skill/Attributes.md +0 -10
|
@@ -22,15 +22,13 @@ A.insertGlobalCss({
|
|
|
22
22
|
export function select(opts) {
|
|
23
23
|
drawField(opts, (id, isInvalid) => {
|
|
24
24
|
A("div.s-select_wrap", () => {
|
|
25
|
-
//
|
|
26
|
-
// aria-label on the wrapper div would label nothing.
|
|
25
|
+
// Control attrs go on the <select>: on the wrapper div they'd label nothing.
|
|
27
26
|
A("select.s-input", opts.inputAttrs, () => {
|
|
28
27
|
applyControlAttrs(opts, id, isInvalid);
|
|
29
28
|
A("change=", (e) => {
|
|
30
29
|
if (opts.bind)
|
|
31
30
|
opts.bind.value = e.target.value;
|
|
32
31
|
});
|
|
33
|
-
// Render options reactively; re-runs when options list or selected value changes.
|
|
34
32
|
A(() => {
|
|
35
33
|
const raw = typeof opts.options === "function" ? opts.options() : opts.options;
|
|
36
34
|
const current = (opts.bind?.value ?? "");
|
|
@@ -37,16 +37,14 @@ export interface TabsOptions {
|
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
39
|
* A horizontal row that scrolls when its content outgrows it, with a ‹ / ›
|
|
40
|
-
* button appearing over whichever end still has something left to reach —
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* the buttons scroll it by most of a width at a time.
|
|
40
|
+
* button appearing over whichever end still has something left to reach — so it
|
|
41
|
+
* isn't just a swipe target. The row's own scrollbar is hidden, and the buttons
|
|
42
|
+
* scroll it by most of a width at a time.
|
|
44
43
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* say — call {@link revealInStrip} with that child.
|
|
44
|
+
* {@link tabs} puts its tab strip in one, and the routed {@link main} shell its
|
|
45
|
+
* breadcrumb stack. Reach for it for any row of chrome that can outgrow its
|
|
46
|
+
* space: a filter bar, a row of chips, a toolbar. {@link revealInStrip} brings
|
|
47
|
+
* one of its children into view.
|
|
50
48
|
*
|
|
51
49
|
* @example
|
|
52
50
|
* ```ts
|
|
@@ -67,10 +65,9 @@ export declare function revealInStrip(el: HTMLElement): void;
|
|
|
67
65
|
* A tabbed view. Renders an ARIA `tablist` of buttons and a single live panel
|
|
68
66
|
* for the selected tab. Supports keyboard navigation (left/right/home/end).
|
|
69
67
|
*
|
|
70
|
-
* More tabs than fit make the strip scroll sideways
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* (arrow keys, or a `bind` written from elsewhere) scrolls it back in.
|
|
68
|
+
* More tabs than fit make the strip scroll sideways (see {@link scrollStrip});
|
|
69
|
+
* selecting a tab that's out of view — with the arrow keys, or a `bind` written
|
|
70
|
+
* from elsewhere — scrolls it back in.
|
|
74
71
|
*
|
|
75
72
|
* @example
|
|
76
73
|
* ```ts
|
package/dist/components/tabs.js
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
import A from "aberdeen";
|
|
2
2
|
import { drawSlot, uniqueId } from "../core.js";
|
|
3
3
|
import { mk } from "../icons-helpers.js";
|
|
4
|
-
//
|
|
5
|
-
// no icon data comes along) rather than `‹`/`›` characters — matching Lucide's
|
|
6
|
-
// `chevron-left`/`chevron-right`.
|
|
4
|
+
// Lucide's chevrons, inlined via the icon set's helper so no icon data tags along.
|
|
7
5
|
const chevronLeft = mk('<path d="m15 18-6-6 6-6"/>');
|
|
8
6
|
const chevronRight = mk('<path d="m9 18 6-6-6-6"/>');
|
|
9
7
|
A.insertGlobalCss({
|
|
10
|
-
//
|
|
11
|
-
// button appearing over whichever end still has something to reach. Its own
|
|
12
|
-
// scrollbar is hidden — a raw scrollbar under a row of chrome reads as a
|
|
13
|
-
// mistake — so those buttons, and the fade they sit in, are the affordance.
|
|
14
|
-
// Shared by `tabs()` and the shell's breadcrumb stack; see `scrollStrip()`.
|
|
8
|
+
// See `scrollStrip()`. Shared by `tabs()` and the shell's breadcrumb stack.
|
|
15
9
|
".s-strip": {
|
|
16
10
|
// The positioning context the buttons overlay from.
|
|
17
11
|
"&": "position:relative display:flex min-width:0",
|
|
@@ -22,35 +16,33 @@ A.insertGlobalCss({
|
|
|
22
16
|
"overflow-x:auto overflow-y:hidden scrollbar-width:none scroll-behavior:smooth",
|
|
23
17
|
"> .s-strip-row::-webkit-scrollbar": "display:none",
|
|
24
18
|
// The buttons overlay the row's ends rather than sitting beside it, so no
|
|
25
|
-
// width is reserved when there's nothing to scroll
|
|
26
|
-
// out from under a fade instead of stopping at a hard edge.
|
|
19
|
+
// width is reserved when there's nothing to scroll.
|
|
27
20
|
"> .s-strip-btn": "position:absolute top:0 bottom:0 z-index:1 display:none align-items:center justify-content:center " +
|
|
28
21
|
"width:2.4em border:0 padding:0 cursor:pointer fg:$s-muted " +
|
|
29
22
|
"transition: color 0.15s;",
|
|
30
23
|
"> .s-strip-btn:hover": "fg:$s-text",
|
|
31
24
|
"> .s-strip-btn-left": "left:0 justify-content:flex-start background: linear-gradient(to right, $s-bg 45%, transparent)",
|
|
32
25
|
"> .s-strip-btn-right": "right:0 justify-content:flex-end background: linear-gradient(to left, $s-bg 45%, transparent)",
|
|
33
|
-
// Shown only
|
|
34
|
-
//
|
|
26
|
+
// Shown only where there is something to scroll towards, so the pair doubles
|
|
27
|
+
// as a position indicator.
|
|
35
28
|
"&.s-can-left > .s-strip-btn-left, &.s-can-right > .s-strip-btn-right": "display:flex",
|
|
36
29
|
},
|
|
37
30
|
".s-tabs": {
|
|
38
31
|
"&": "display:flex flex-direction:column gap:$3",
|
|
39
|
-
// The bar owns the hairline, so it runs the full width
|
|
40
|
-
// buttons too.
|
|
32
|
+
// The bar, not the strip, owns the hairline, so it runs the full width.
|
|
41
33
|
".s-tabbar": "border-bottom: 1px solid $s-faint;",
|
|
42
34
|
".s-tablist": "gap:$1 align-items:stretch " +
|
|
43
|
-
// Pulls the strip
|
|
44
|
-
//
|
|
45
|
-
//
|
|
35
|
+
// Pulls the strip down over the bar's hairline, so the active tab's underline
|
|
36
|
+
// lands *on* it. On the strip, not the tabs: a negative margin inside a
|
|
37
|
+
// scroll container is overflow.
|
|
46
38
|
"margin-bottom:-1px",
|
|
47
39
|
".s-tab": "display:inline-flex align-items:center gap:$2 cursor:pointer background:transparent " +
|
|
48
40
|
"border:0 color: $s-muted; font-weight:600 padding: 0.6em 0.9em; white-space:nowrap " +
|
|
49
41
|
"border-bottom: 3px solid transparent; " +
|
|
50
42
|
"transition: color 0.15s, background 0.15s, border-color 0.15s;",
|
|
51
43
|
".s-tab:hover:not(:disabled), .s-tab[aria-selected=true]": "color: $s-text;",
|
|
52
|
-
// An inset ring: the strip
|
|
53
|
-
//
|
|
44
|
+
// An inset ring: the strip clips its own painting, so an outset ring on the
|
|
45
|
+
// first/last tab would be shaved off.
|
|
54
46
|
".s-tab:focus-visible": "outline:none box-shadow: inset 0 0 0 2px $s-focus; r: $s-radius;",
|
|
55
47
|
".s-tab[aria-selected=true]": "border-image: $s-gradient 1;",
|
|
56
48
|
".s-tabpanel": "display:block",
|
|
@@ -58,16 +50,14 @@ A.insertGlobalCss({
|
|
|
58
50
|
});
|
|
59
51
|
/**
|
|
60
52
|
* A horizontal row that scrolls when its content outgrows it, with a ‹ / ›
|
|
61
|
-
* button appearing over whichever end still has something left to reach —
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* the buttons scroll it by most of a width at a time.
|
|
53
|
+
* button appearing over whichever end still has something left to reach — so it
|
|
54
|
+
* isn't just a swipe target. The row's own scrollbar is hidden, and the buttons
|
|
55
|
+
* scroll it by most of a width at a time.
|
|
65
56
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* say — call {@link revealInStrip} with that child.
|
|
57
|
+
* {@link tabs} puts its tab strip in one, and the routed {@link main} shell its
|
|
58
|
+
* breadcrumb stack. Reach for it for any row of chrome that can outgrow its
|
|
59
|
+
* space: a filter bar, a row of chips, a toolbar. {@link revealInStrip} brings
|
|
60
|
+
* one of its children into view.
|
|
71
61
|
*
|
|
72
62
|
* @example
|
|
73
63
|
* ```ts
|
|
@@ -94,8 +84,8 @@ export function revealInStrip(el) {
|
|
|
94
84
|
const row = el.parentElement;
|
|
95
85
|
if (!row || !el.isConnected)
|
|
96
86
|
return;
|
|
97
|
-
// The overlays are 2.4em wide;
|
|
98
|
-
//
|
|
87
|
+
// The overlays are 2.4em wide; clear a little more, so the revealed item
|
|
88
|
+
// doesn't sit right against one.
|
|
99
89
|
const pad = parseFloat(getComputedStyle(row).fontSize) * 2.6;
|
|
100
90
|
const box = el.getBoundingClientRect(), strip = row.getBoundingClientRect();
|
|
101
91
|
const left = box.left - strip.left, right = box.right - strip.right;
|
|
@@ -108,10 +98,9 @@ export function revealInStrip(el) {
|
|
|
108
98
|
* A tabbed view. Renders an ARIA `tablist` of buttons and a single live panel
|
|
109
99
|
* for the selected tab. Supports keyboard navigation (left/right/home/end).
|
|
110
100
|
*
|
|
111
|
-
* More tabs than fit make the strip scroll sideways
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* (arrow keys, or a `bind` written from elsewhere) scrolls it back in.
|
|
101
|
+
* More tabs than fit make the strip scroll sideways (see {@link scrollStrip});
|
|
102
|
+
* selecting a tab that's out of view — with the arrow keys, or a `bind` written
|
|
103
|
+
* from elsewhere — scrolls it back in.
|
|
115
104
|
*
|
|
116
105
|
* @example
|
|
117
106
|
* ```ts
|
|
@@ -123,11 +112,9 @@ export function revealInStrip(el) {
|
|
|
123
112
|
*/
|
|
124
113
|
export function tabs(opts) {
|
|
125
114
|
const groupId = uniqueId("tabs");
|
|
126
|
-
// Resolve a tab's selection key (its id, or its index as a string).
|
|
127
115
|
const keyOf = (tab, index) => tab.id ?? String(index);
|
|
128
|
-
// Selection state: caller-provided binding, or internal.
|
|
129
116
|
const $sel = opts.bind ?? A.proxy(keyOf(opts.tabs[0] ?? { label: "" }, 0));
|
|
130
|
-
//
|
|
117
|
+
// A bound value that names no tab falls back to the first one.
|
|
131
118
|
if (opts.tabs.length > 0 && !opts.tabs.some((t, i) => keyOf(t, i) === A.peek(() => $sel.value))) {
|
|
132
119
|
$sel.value = keyOf(opts.tabs[0], 0);
|
|
133
120
|
}
|
|
@@ -149,9 +136,7 @@ export function tabs(opts) {
|
|
|
149
136
|
const selected = $sel.value === key;
|
|
150
137
|
A("aria-selected=", selected ? "true" : "false");
|
|
151
138
|
A("tabindex=", selected ? "0" : "-1");
|
|
152
|
-
//
|
|
153
|
-
// view, so the strip follows the selection however it was made:
|
|
154
|
-
// a click, the arrow keys, or a `bind` written from elsewhere.
|
|
139
|
+
// So the strip follows the selection however it was made.
|
|
155
140
|
if (selected)
|
|
156
141
|
requestAnimationFrame(() => revealInStrip(tabEl));
|
|
157
142
|
});
|
|
@@ -179,34 +164,31 @@ export function tabs(opts) {
|
|
|
179
164
|
});
|
|
180
165
|
}
|
|
181
166
|
/**
|
|
182
|
-
* One of the two scroll buttons overlaying the ends of the row
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
* would read as a twitch.
|
|
167
|
+
* One of the two scroll buttons overlaying the ends of the row (`dir` is -1 for
|
|
168
|
+
* left, 1 for right). It scrolls most of a width at a time: a nudge of a single
|
|
169
|
+
* tab or crumb would read as a twitch.
|
|
186
170
|
*/
|
|
187
171
|
function drawScrollButton(row, dir) {
|
|
188
172
|
A(`button.s-strip-btn.s-strip-btn-${dir < 0 ? "left" : "right"} type=button`, () => {
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
// row straight into whatever follows.
|
|
173
|
+
// A mouse convenience only — out of the tab order, so Tab still steps from
|
|
174
|
+
// the row straight into whatever follows.
|
|
192
175
|
A("tabindex=-1 aria-hidden=true");
|
|
193
176
|
A("click=", () => row.scrollBy({ left: dir * row.clientWidth * 0.8, behavior: "smooth" }));
|
|
194
177
|
(dir < 0 ? chevronLeft : chevronRight)({ size: "1.1em" });
|
|
195
178
|
});
|
|
196
179
|
}
|
|
197
180
|
/**
|
|
198
|
-
* Keep the `.s-can-left` / `.s-can-right` classes
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* changes the answer without any scrolling at all.
|
|
181
|
+
* Keep the strip's `.s-can-left` / `.s-can-right` classes in step with what there
|
|
182
|
+
* is left to scroll towards. Watches the row's scrolling and its size — and its
|
|
183
|
+
* children's, since items arriving or leaving change the answer without any
|
|
184
|
+
* scrolling at all.
|
|
203
185
|
*/
|
|
204
186
|
function watchScroll(row) {
|
|
205
187
|
const strip = row.parentElement;
|
|
206
188
|
if (!strip || typeof ResizeObserver === "undefined")
|
|
207
189
|
return; // No-op outside the browser.
|
|
208
190
|
const update = () => {
|
|
209
|
-
//
|
|
191
|
+
// Sub-pixel slack: fractional layout widths otherwise leave a permanent
|
|
210
192
|
// half-pixel of "scrollable" at an end that is plainly already reached.
|
|
211
193
|
const max = row.scrollWidth - row.clientWidth;
|
|
212
194
|
strip.classList.toggle("s-can-left", row.scrollLeft > 1);
|
|
@@ -215,11 +197,9 @@ function watchScroll(row) {
|
|
|
215
197
|
row.addEventListener("scroll", update, { passive: true });
|
|
216
198
|
const ro = new ResizeObserver(update);
|
|
217
199
|
ro.observe(row);
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
// row redraws on navigation) would otherwise grow the observation list — and
|
|
222
|
-
// retain the detached elements — without bound.
|
|
200
|
+
// One observer follows the row's live children. Removed ones must be
|
|
201
|
+
// unobserved: a strip whose items churn (the breadcrumbs, on navigation) would
|
|
202
|
+
// otherwise retain every detached element for as long as it lives.
|
|
223
203
|
const mo = typeof MutationObserver === "undefined" ? undefined : new MutationObserver((records) => {
|
|
224
204
|
for (const record of records) {
|
|
225
205
|
for (const el of record.addedNodes)
|
|
@@ -24,11 +24,9 @@ export interface TextlineOptions extends FieldOptions {
|
|
|
24
24
|
change?: (event: Event) => void;
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
* A single-line text input —
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* Renders inside the standard {@link drawField} chrome (label, control,
|
|
31
|
-
* help/error), so it aligns cleanly inside a {@link form}.
|
|
27
|
+
* A single-line text input — text, passwords, numbers, email, dates and the other
|
|
28
|
+
* line-oriented `<input>` types. Renders inside the standard {@link drawField}
|
|
29
|
+
* chrome (label, control, help/error), so it aligns cleanly inside a {@link form}.
|
|
32
30
|
*
|
|
33
31
|
* @example
|
|
34
32
|
* ```ts
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import A from "aberdeen";
|
|
2
2
|
import { applyControlAttrs, drawField } from "./field.js";
|
|
3
3
|
/**
|
|
4
|
-
* A single-line text input —
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Renders inside the standard {@link drawField} chrome (label, control,
|
|
8
|
-
* help/error), so it aligns cleanly inside a {@link form}.
|
|
4
|
+
* A single-line text input — text, passwords, numbers, email, dates and the other
|
|
5
|
+
* line-oriented `<input>` types. Renders inside the standard {@link drawField}
|
|
6
|
+
* chrome (label, control, help/error), so it aligns cleanly inside a {@link form}.
|
|
9
7
|
*
|
|
10
8
|
* @example
|
|
11
9
|
* ```ts
|
|
@@ -5,9 +5,7 @@ export interface ToastOptions {
|
|
|
5
5
|
message: Slot;
|
|
6
6
|
/** Optional bold title above the message. */
|
|
7
7
|
title?: Slot;
|
|
8
|
-
/**
|
|
9
|
-
* Colour role. Defaults to `"neutral"`.
|
|
10
|
-
*/
|
|
8
|
+
/** Colour role. Defaults to `"neutral"`. */
|
|
11
9
|
type?: "success" | "danger" | "warning" | "neutral";
|
|
12
10
|
/**
|
|
13
11
|
* Auto-dismiss delay in milliseconds. Defaults to `6000`.
|
package/dist/components/toast.js
CHANGED
|
@@ -22,15 +22,14 @@ 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
|
-
//
|
|
26
|
-
//
|
|
25
|
+
// The peek keeps this scope from resubscribing once `toasts` is non-empty, so the
|
|
26
|
+
// container stays in the DOM afterwards and exit animations aren't cut off.
|
|
27
27
|
if (A.peek(() => A.isEmpty(toasts)) && A.isEmpty(toasts))
|
|
28
28
|
return;
|
|
29
29
|
A("div.s-toasts", () => {
|
|
30
30
|
A.onEach(toasts, (entry) => {
|
|
31
31
|
const { opts, id } = entry;
|
|
32
32
|
const role = opts.type === "danger" || opts.type === "warning" ? "alert" : "status";
|
|
33
|
-
// "neutral" (the default) is a neutral surface; the rest are accent surfaces.
|
|
34
33
|
const surface = opts.type == null || opts.type === "neutral" ? "neutral" : opts.type;
|
|
35
34
|
const duration = opts.duration ?? 6000;
|
|
36
35
|
let timer;
|
|
@@ -12,11 +12,10 @@ export interface TooltipOptions {
|
|
|
12
12
|
attrs?: Attributes;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
* Attaches a tooltip to the current element
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* element's bounding rect and automatically flips when near the viewport edge.
|
|
15
|
+
* Attaches a tooltip to the current element, shown on hover or keyboard focus.
|
|
16
|
+
* The tip panel is portalled into `document.body`, so `overflow:hidden` ancestors
|
|
17
|
+
* never clip it; it is placed from the element's bounding rect, flipping to the
|
|
18
|
+
* opposite side when near the viewport edge.
|
|
20
19
|
*
|
|
21
20
|
* @example
|
|
22
21
|
* ```ts
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import A from "aberdeen";
|
|
2
2
|
import { drawSlot, mountPortal } from "../core.js";
|
|
3
|
-
//
|
|
4
|
-
// the page's next neutral shade
|
|
5
|
-
// radius and elevation.
|
|
3
|
+
// Background, ink, border, radius and elevation come from the `.s-s.neutral.shadow`
|
|
4
|
+
// surface; portalled to <body>, it renders at the page's next neutral shade.
|
|
6
5
|
A.insertGlobalCss({
|
|
7
6
|
".s-tt-tip": {
|
|
8
7
|
"&": "position:fixed z-index:500 " +
|
|
@@ -12,8 +11,7 @@ A.insertGlobalCss({
|
|
|
12
11
|
},
|
|
13
12
|
});
|
|
14
13
|
// ─── Global portal state ────────────────────────────────────────────────────
|
|
15
|
-
// At most one tooltip
|
|
16
|
-
// handlers were attached to (its bounding rect drives positioning).
|
|
14
|
+
// At most one tooltip at a time; the anchor is the element whose rect positions it.
|
|
17
15
|
const $ttActive = A.proxy(undefined);
|
|
18
16
|
let hideTimer = null;
|
|
19
17
|
// Hide tooltip when the page scrolls (anchor has moved).
|
|
@@ -96,11 +94,10 @@ mountPortal(() => {
|
|
|
96
94
|
});
|
|
97
95
|
// ─── Public component ────────────────────────────────────────────────────────
|
|
98
96
|
/**
|
|
99
|
-
* Attaches a tooltip to the current element
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* element's bounding rect and automatically flips when near the viewport edge.
|
|
97
|
+
* Attaches a tooltip to the current element, shown on hover or keyboard focus.
|
|
98
|
+
* The tip panel is portalled into `document.body`, so `overflow:hidden` ancestors
|
|
99
|
+
* never clip it; it is placed from the element's bounding rect, flipping to the
|
|
100
|
+
* opposite side when near the viewport edge.
|
|
104
101
|
*
|
|
105
102
|
* @example
|
|
106
103
|
* ```ts
|
package/dist/core.d.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared building blocks for the Staffa component library
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Aberdeen's {@link A} function. This module defines the option-type hierarchy
|
|
7
|
-
* that all components build on, plus a couple of tiny helpers.
|
|
2
|
+
* Shared building blocks for the Staffa component library: the option-type
|
|
3
|
+
* hierarchy every component builds on, plus a couple of tiny helpers. A
|
|
4
|
+
* component is just an Aberdeen draw function — one typed options object in,
|
|
5
|
+
* DOM out through {@link A}.
|
|
8
6
|
*/
|
|
9
7
|
/**
|
|
10
8
|
* An Aberdeen attribute/style/class string, e.g. `"display:flex gap:$3 .my-class"`.
|
|
@@ -51,9 +49,9 @@ export interface ContentOptions {
|
|
|
51
49
|
}
|
|
52
50
|
/**
|
|
53
51
|
* Shell width — not viewport width — at or below which the app shell goes
|
|
54
|
-
* "narrow": the nav sidebar collapses to a hamburger, and a routed shell
|
|
55
|
-
*
|
|
56
|
-
*
|
|
52
|
+
* "narrow": the nav sidebar collapses to a hamburger, and a routed shell has room
|
|
53
|
+
* for exactly one full-bleed column. Shared by the `@container` queries that do
|
|
54
|
+
* the switching and by the JS that has to agree with them.
|
|
57
55
|
*/
|
|
58
56
|
export declare const NARROW_PX = 640;
|
|
59
57
|
/** Generates a process-unique id, used to wire `<label for>` to its control. */
|
|
@@ -66,25 +64,20 @@ export declare function uniqueId(prefix?: string): string;
|
|
|
66
64
|
export declare function drawSlot<Args extends unknown[] = []>(slot: Slot<Args> | undefined, ...args: Args): void;
|
|
67
65
|
/**
|
|
68
66
|
* Move keyboard focus to the first focusable element inside `container`, skipping
|
|
69
|
-
* disabled, `aria-disabled`, `tabindex=-1` and hidden ones.
|
|
70
|
-
*
|
|
71
|
-
*
|
|
67
|
+
* disabled, `aria-disabled`, `tabindex=-1` and hidden ones. A `prefer` selector,
|
|
68
|
+
* where it matches a focusable element, wins — e.g. a menu's current item.
|
|
69
|
+
* Returns whether anything was focused.
|
|
72
70
|
*
|
|
73
|
-
* Shared by overlays (
|
|
74
|
-
*
|
|
75
|
-
* and laid out (typically inside a `requestAnimationFrame`).
|
|
71
|
+
* Shared by the overlays (floating menu, dialogs). Call it once the element is in
|
|
72
|
+
* the DOM and laid out — typically inside a `requestAnimationFrame`.
|
|
76
73
|
*/
|
|
77
74
|
export declare function focusFirst(container: HTMLElement, prefer?: string): boolean;
|
|
78
75
|
/**
|
|
79
76
|
* Mount a portal (tooltip, toast, menu, dialog, …) directly into `<body>`.
|
|
80
|
-
* Must be called at module top level, where Aberdeen's root scope (whose
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* Scope creation is deferred a microtask so that an app drawing into `<body>`
|
|
87
|
-
* at module top level gets its content *before* the portals, keeping overlays
|
|
88
|
-
* at the end of the document.
|
|
77
|
+
* Must be called at module top level, where Aberdeen's root scope (whose element
|
|
78
|
+
* is `document.body`) is current: sibling scopes there track their positions, so
|
|
79
|
+
* portals coexist without wrapper elements — separate `A.mount`s sharing a parent
|
|
80
|
+
* could not. Scope creation is deferred a microtask, so an app drawing into
|
|
81
|
+
* `<body>` gets its content first and the overlays stay at the end.
|
|
89
82
|
*/
|
|
90
83
|
export declare function mountPortal(draw: () => void): void;
|
package/dist/core.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import A from "aberdeen";
|
|
2
2
|
/**
|
|
3
3
|
* Shell width — not viewport width — at or below which the app shell goes
|
|
4
|
-
* "narrow": the nav sidebar collapses to a hamburger, and a routed shell
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* "narrow": the nav sidebar collapses to a hamburger, and a routed shell has room
|
|
5
|
+
* for exactly one full-bleed column. Shared by the `@container` queries that do
|
|
6
|
+
* the switching and by the JS that has to agree with them.
|
|
7
7
|
*/
|
|
8
8
|
export const NARROW_PX = 640;
|
|
9
9
|
let idCounter = 0;
|
|
@@ -28,13 +28,12 @@ export function drawSlot(slot, ...args) {
|
|
|
28
28
|
const FOCUSABLE_SELECTOR = "a[href], button, input, select, textarea, [tabindex]";
|
|
29
29
|
/**
|
|
30
30
|
* Move keyboard focus to the first focusable element inside `container`, skipping
|
|
31
|
-
* disabled, `aria-disabled`, `tabindex=-1` and hidden ones.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* disabled, `aria-disabled`, `tabindex=-1` and hidden ones. A `prefer` selector,
|
|
32
|
+
* where it matches a focusable element, wins — e.g. a menu's current item.
|
|
33
|
+
* Returns whether anything was focused.
|
|
34
34
|
*
|
|
35
|
-
* Shared by overlays (
|
|
36
|
-
*
|
|
37
|
-
* and laid out (typically inside a `requestAnimationFrame`).
|
|
35
|
+
* Shared by the overlays (floating menu, dialogs). Call it once the element is in
|
|
36
|
+
* the DOM and laid out — typically inside a `requestAnimationFrame`.
|
|
38
37
|
*/
|
|
39
38
|
export function focusFirst(container, prefer) {
|
|
40
39
|
const ok = (el) => el instanceof HTMLElement &&
|
|
@@ -49,15 +48,11 @@ export function focusFirst(container, prefer) {
|
|
|
49
48
|
}
|
|
50
49
|
/**
|
|
51
50
|
* Mount a portal (tooltip, toast, menu, dialog, …) directly into `<body>`.
|
|
52
|
-
* Must be called at module top level, where Aberdeen's root scope (whose
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* Scope creation is deferred a microtask so that an app drawing into `<body>`
|
|
59
|
-
* at module top level gets its content *before* the portals, keeping overlays
|
|
60
|
-
* at the end of the document.
|
|
51
|
+
* Must be called at module top level, where Aberdeen's root scope (whose element
|
|
52
|
+
* is `document.body`) is current: sibling scopes there track their positions, so
|
|
53
|
+
* portals coexist without wrapper elements — separate `A.mount`s sharing a parent
|
|
54
|
+
* could not. Scope creation is deferred a microtask, so an app drawing into
|
|
55
|
+
* `<body>` gets its content first and the overlays stay at the end.
|
|
61
56
|
*/
|
|
62
57
|
export function mountPortal(draw) {
|
|
63
58
|
queueMicrotask(() => A(draw));
|
package/dist/icons-helpers.d.ts
CHANGED
|
@@ -39,8 +39,8 @@ export interface IconDefaults {
|
|
|
39
39
|
*/
|
|
40
40
|
export declare function setDefaults(opts: Partial<IconDefaults>): void;
|
|
41
41
|
/**
|
|
42
|
-
* Turn a piece of inner-SVG markup into an icon draw-function
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* Turn a piece of inner-SVG markup into an icon draw-function: it emits a freshly
|
|
43
|
+
* built `<svg>` into the current Aberdeen scope, applying the {@link IconOptions}
|
|
44
|
+
* (or the module defaults).
|
|
45
45
|
*/
|
|
46
46
|
export declare function mk(inner: string): (opts?: IconOptions) => void;
|
package/dist/icons-helpers.js
CHANGED
|
@@ -19,25 +19,20 @@ export function setDefaults(opts) {
|
|
|
19
19
|
Object.assign(defaults, opts);
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
22
|
+
* The shared body behind every icon: build one `<svg>` through Aberdeen (from the
|
|
23
23
|
* {@link IconOptions} or the module defaults) and fill in its inner markup.
|
|
24
|
-
*
|
|
25
|
-
* This is the shared body behind every icon. {@link mk} hands it the icon's
|
|
26
|
-
* `inner` markup, so the per-icon closures stay tiny instead of each carrying
|
|
27
|
-
* a copy of this logic.
|
|
28
24
|
*/
|
|
29
25
|
function drawIcon(inner, opts) {
|
|
30
26
|
const size = opts.size ?? defaults.size;
|
|
31
27
|
const el = A('svg.s-icon aria-hidden=true viewBox="0 0 24 24" fill=none', "width=", size, "height=", size, "stroke=", opts.color ?? defaults.color, "stroke-width=", opts.strokeWidth ?? defaults.strokeWidth, "stroke-linecap=", opts.cap ?? defaults.cap, "stroke-linejoin=", opts.join ?? defaults.join, opts.attrs);
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
// `html=` builds them in the HTML namespace, leaving them non-rendering.)
|
|
28
|
+
// innerHTML on the `<svg>` itself puts the children in the SVG namespace;
|
|
29
|
+
// Aberdeen's `html=` would build them in the HTML namespace, non-rendering.
|
|
35
30
|
el.innerHTML = inner;
|
|
36
31
|
}
|
|
37
32
|
/**
|
|
38
|
-
* Turn a piece of inner-SVG markup into an icon draw-function
|
|
39
|
-
*
|
|
40
|
-
*
|
|
33
|
+
* Turn a piece of inner-SVG markup into an icon draw-function: it emits a freshly
|
|
34
|
+
* built `<svg>` into the current Aberdeen scope, applying the {@link IconOptions}
|
|
35
|
+
* (or the module defaults).
|
|
41
36
|
*/
|
|
42
37
|
export function mk(inner) {
|
|
43
38
|
return (opts = {}) => drawIcon(inner, opts);
|
package/dist/index.d.ts
CHANGED
|
@@ -27,9 +27,11 @@
|
|
|
27
27
|
* Every component takes a single typed options object (see each function's
|
|
28
28
|
* docs). The options object — or parts of it — may be an Aberdeen proxy, in
|
|
29
29
|
* which case the component re-renders the affected parts in place when you
|
|
30
|
-
* mutate it. See `
|
|
30
|
+
* mutate it. See `README.md` for the design philosophy.
|
|
31
31
|
*/
|
|
32
32
|
export { setDarkMode, getDarkMode } from "./theme.js";
|
|
33
|
+
export { formatKey, bindKey } from "./keys.js";
|
|
34
|
+
export { showKeyHelp, setKeyHelp } from "./components/keyhelp.js";
|
|
33
35
|
export { autocomplete, type AutocompleteOptions, type AutocompleteOptionInput } from "./components/autocomplete.js";
|
|
34
36
|
export { box, type BoxOptions } from "./components/box.js";
|
|
35
37
|
export { button, iconButton, type ButtonOptions, type IconButtonOptions } from "./components/button.js";
|
package/dist/index.js
CHANGED
|
@@ -27,12 +27,13 @@
|
|
|
27
27
|
* Every component takes a single typed options object (see each function's
|
|
28
28
|
* docs). The options object — or parts of it — may be an Aberdeen proxy, in
|
|
29
29
|
* which case the component re-renders the affected parts in place when you
|
|
30
|
-
* mutate it. See `
|
|
30
|
+
* mutate it. See `README.md` for the design philosophy.
|
|
31
31
|
*/
|
|
32
|
-
// Importing
|
|
33
|
-
//
|
|
34
|
-
// theme.ts); toggle modes with setDarkMode / getDarkMode.
|
|
32
|
+
// Importing theme.js installs the spacing vars, the reactive theme and the base
|
|
33
|
+
// stylesheet.
|
|
35
34
|
export { setDarkMode, getDarkMode } from "./theme.js";
|
|
35
|
+
export { formatKey, bindKey } from "./keys.js";
|
|
36
|
+
export { showKeyHelp, setKeyHelp } from "./components/keyhelp.js";
|
|
36
37
|
export { autocomplete } from "./components/autocomplete.js";
|
|
37
38
|
export { box } from "./components/box.js";
|
|
38
39
|
export { button, iconButton } from "./components/button.js";
|