staffa 0.9.0 → 0.10.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 +106 -48
- package/dist/components/autocomplete.js +1 -1
- package/dist/components/box.d.ts +8 -16
- package/dist/components/box.js +21 -27
- package/dist/components/button.d.ts +40 -0
- package/dist/components/button.js +85 -12
- package/dist/components/buttonChooser.js +1 -1
- package/dist/components/checkbox.js +3 -3
- package/dist/components/field.js +3 -3
- package/dist/components/main.d.ts +134 -71
- package/dist/components/main.js +245 -174
- package/dist/components/menu.d.ts +72 -14
- package/dist/components/menu.js +231 -34
- package/dist/components/pages.d.ts +638 -0
- package/dist/components/pages.js +1510 -0
- package/dist/components/panels.d.ts +448 -225
- package/dist/components/panels.js +819 -435
- package/dist/components/tabs.d.ts +37 -0
- package/dist/components/tabs.js +128 -69
- package/dist/core.d.ts +1 -1
- package/dist/core.js +1 -1
- package/dist/glyphs.d.ts +24 -0
- package/dist/glyphs.js +25 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -4
- package/dist/staffa.esm.js +1 -1
- package/dist/theme.d.ts +67 -0
- package/dist/theme.js +12 -2
- package/package.json +2 -2
- package/skill/BoxOptions.md +7 -12
- package/skill/IconButtonOptions.md +41 -0
- package/skill/MainOptions.md +106 -58
- package/skill/MenuItem.md +16 -1
- package/skill/MenuListOptions.md +24 -0
- package/skill/MenuOptions.md +3 -2
- package/skill/Panel.md +190 -0
- package/skill/PanelStack.md +106 -0
- package/skill/SKILL.md +172 -64
- package/skill/ScrollStripOptions.md +21 -0
- package/skill/box.md +1 -4
- package/skill/closeNav.md +3 -3
- package/skill/iconButton.md +27 -0
- package/skill/main.md +13 -9
- package/skill/menu.md +29 -0
- package/skill/scrollStrip.md +28 -0
- package/src/components/autocomplete.ts +1 -1
- package/src/components/box.ts +29 -39
- package/src/components/button.ts +109 -8
- package/src/components/buttonChooser.ts +1 -1
- package/src/components/checkbox.ts +3 -3
- package/src/components/field.ts +3 -3
- package/src/components/main.ts +381 -188
- package/src/components/menu.ts +265 -37
- package/src/components/panels.ts +1136 -526
- package/src/components/tabs.ts +134 -68
- package/src/core.ts +1 -1
- package/src/index.ts +4 -4
- package/src/theme.ts +14 -3
- package/skill/Page.md +0 -119
- package/skill/panels.md +0 -10
package/src/components/tabs.ts
CHANGED
|
@@ -16,6 +16,16 @@ export interface Tab {
|
|
|
16
16
|
disabled?: boolean;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/** Options for {@link scrollStrip}. */
|
|
20
|
+
export interface ScrollStripOptions {
|
|
21
|
+
/** The row's content, laid out left to right. */
|
|
22
|
+
content: Slot;
|
|
23
|
+
/** Aberdeen attr/style string applied to the outer element. */
|
|
24
|
+
attrs?: Attributes;
|
|
25
|
+
/** Aberdeen attr/style string applied to the scrolling row itself. */
|
|
26
|
+
stripAttrs?: Attributes;
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
/** Options for {@link tabs}. */
|
|
20
30
|
export interface TabsOptions {
|
|
21
31
|
/** Aberdeen attr/style string applied to the outermost element. */
|
|
@@ -38,25 +48,46 @@ const chevronLeft = mk('<path d="m15 18-6-6 6-6"/>');
|
|
|
38
48
|
const chevronRight = mk('<path d="m9 18 6-6-6-6"/>');
|
|
39
49
|
|
|
40
50
|
A.insertGlobalCss({
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
|
|
51
|
+
// A row that scrolls sideways once its content outgrows it, with a ‹ / ›
|
|
52
|
+
// button appearing over whichever end still has something to reach. Its own
|
|
53
|
+
// scrollbar is hidden — a raw scrollbar under a row of chrome reads as a
|
|
54
|
+
// mistake — so those buttons, and the fade they sit in, are the affordance.
|
|
55
|
+
// Shared by `tabs()` and the shell's breadcrumb stack; see `scrollStrip()`.
|
|
56
|
+
".s-strip": {
|
|
57
|
+
// The positioning context the buttons overlay from.
|
|
58
|
+
"&": "position:relative display:flex min-width:0",
|
|
49
59
|
// `overflow-y:hidden` is load-bearing: `overflow-x:auto` alone forces the
|
|
50
60
|
// computed `overflow-y` off `visible`, which turned the active tab's 1px
|
|
51
61
|
// overhang into a stray couple of pixels of *vertical* scroll.
|
|
62
|
+
"> .s-strip-row":
|
|
63
|
+
"display:flex align-items:center flex:1 min-width:0 " +
|
|
64
|
+
"overflow-x:auto overflow-y:hidden scrollbar-width:none scroll-behavior:smooth",
|
|
65
|
+
"> .s-strip-row::-webkit-scrollbar": "display:none",
|
|
66
|
+
// The buttons overlay the row's ends rather than sitting beside it, so no
|
|
67
|
+
// width is reserved when there's nothing to scroll — and the content slides
|
|
68
|
+
// out from under a fade instead of stopping at a hard edge.
|
|
69
|
+
"> .s-strip-btn":
|
|
70
|
+
"position:absolute top:0 bottom:0 z-index:1 display:none align-items:center justify-content:center " +
|
|
71
|
+
"width:2.4em border:0 padding:0 cursor:pointer fg:$s-muted " +
|
|
72
|
+
"transition: color 0.15s;",
|
|
73
|
+
"> .s-strip-btn:hover": "fg:$s-text",
|
|
74
|
+
"> .s-strip-btn-left": "left:0 justify-content:flex-start background: linear-gradient(to right, $s-bg 45%, transparent)",
|
|
75
|
+
"> .s-strip-btn-right": "right:0 justify-content:flex-end background: linear-gradient(to left, $s-bg 45%, transparent)",
|
|
76
|
+
// Shown only for the direction there is actually something to scroll towards,
|
|
77
|
+
// so the pair doubles as a position indicator.
|
|
78
|
+
"&.s-can-left > .s-strip-btn-left, &.s-can-right > .s-strip-btn-right": "display:flex",
|
|
79
|
+
},
|
|
80
|
+
".s-tabs": {
|
|
81
|
+
"&": "display:flex flex-direction:column gap:$3",
|
|
82
|
+
// The bar owns the hairline, so it runs the full width — under the scroll
|
|
83
|
+
// buttons too.
|
|
84
|
+
".s-tabbar": "border-bottom: 1px solid $s-faint;",
|
|
52
85
|
".s-tablist":
|
|
53
|
-
"
|
|
54
|
-
"overflow-x:auto overflow-y:hidden scrollbar-width:none scroll-behavior:smooth " +
|
|
86
|
+
"gap:$1 align-items:stretch " +
|
|
55
87
|
// Pulls the strip 1px down over the bar's hairline, so the active tab's
|
|
56
88
|
// underline lands *on* it rather than stacking above it. On the strip, not
|
|
57
89
|
// the tabs: a negative margin inside a scroll container is overflow.
|
|
58
90
|
"margin-bottom:-1px",
|
|
59
|
-
".s-tablist::-webkit-scrollbar": "display:none",
|
|
60
91
|
".s-tab":
|
|
61
92
|
"display:inline-flex align-items:center gap:$2 cursor:pointer background:transparent " +
|
|
62
93
|
"border:0 color: $s-muted; font-weight:600 padding: 0.6em 0.9em; white-space:nowrap " +
|
|
@@ -67,23 +98,56 @@ A.insertGlobalCss({
|
|
|
67
98
|
// painting, and an outset ring on the first/last tab would be shaved off.
|
|
68
99
|
".s-tab:focus-visible": "outline:none box-shadow: inset 0 0 0 2px $s-focus; r: $s-radius;",
|
|
69
100
|
".s-tab[aria-selected=true]": "border-image: $s-gradient 1;",
|
|
70
|
-
// The scroll buttons overlay the strip's ends rather than sitting beside it,
|
|
71
|
-
// so no width is reserved when there's nothing to scroll — and the tabs slide
|
|
72
|
-
// out from under a fade instead of stopping at a hard edge.
|
|
73
|
-
".s-tabscroll":
|
|
74
|
-
"position:absolute top:0 bottom:0 z-index:1 display:none align-items:center justify-content:center " +
|
|
75
|
-
"width:2.4em border:0 padding:0 cursor:pointer fg:$s-muted " +
|
|
76
|
-
"transition: color 0.15s;",
|
|
77
|
-
".s-tabscroll:hover": "fg:$s-text",
|
|
78
|
-
".s-tabscroll-left": "left:0 justify-content:flex-start background: linear-gradient(to right, $s-bg 45%, transparent)",
|
|
79
|
-
".s-tabscroll-right": "right:0 justify-content:flex-end background: linear-gradient(to left, $s-bg 45%, transparent)",
|
|
80
|
-
// Shown only for the direction there is actually something to scroll towards,
|
|
81
|
-
// so the pair doubles as a position indicator.
|
|
82
|
-
".s-tabbar.s-can-left > .s-tabscroll-left, .s-tabbar.s-can-right > .s-tabscroll-right": "display:flex",
|
|
83
101
|
".s-tabpanel": "display:block",
|
|
84
102
|
},
|
|
85
103
|
});
|
|
86
104
|
|
|
105
|
+
/**
|
|
106
|
+
* A horizontal row that scrolls when its content outgrows it, with a ‹ / ›
|
|
107
|
+
* button appearing over whichever end still has something left to reach — a
|
|
108
|
+
* bare scroll area says nothing about itself to a mouse, and a scrollbar under
|
|
109
|
+
* a row of chrome reads as a mistake. The row's own scrollbar is hidden, and
|
|
110
|
+
* the buttons scroll it by most of a width at a time.
|
|
111
|
+
*
|
|
112
|
+
* This is what {@link tabs} puts its tab strip in, and what the routed
|
|
113
|
+
* {@link main} shell puts its breadcrumb stack in. Reach for it for any row of
|
|
114
|
+
* chrome that can outgrow its space: a filter bar, a row of chips, a toolbar.
|
|
115
|
+
* To bring one of its children into view — after selecting it from elsewhere,
|
|
116
|
+
* say — call {@link revealInStrip} with that child.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* S.scrollStrip({
|
|
121
|
+
* attrs: "gap:$1",
|
|
122
|
+
* content: () => { for (const tag of tags) S.button({ content: tag, attrs: ".small" }); },
|
|
123
|
+
* });
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export function scrollStrip(opts: ScrollStripOptions): void {
|
|
127
|
+
A("div.s-strip", opts.attrs, () => {
|
|
128
|
+
const row = A("div.s-strip-row", opts.stripAttrs, () => drawSlot(opts.content)) as HTMLElement;
|
|
129
|
+
drawScrollButton(row, -1);
|
|
130
|
+
drawScrollButton(row, 1);
|
|
131
|
+
watchScroll(row);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Scroll `el`'s {@link scrollStrip} just far enough to bring it into view,
|
|
137
|
+
* clearing the buttons that overlay the row's ends. Does nothing when `el`
|
|
138
|
+
* isn't in a strip, or is already comfortably visible.
|
|
139
|
+
*/
|
|
140
|
+
export function revealInStrip(el: HTMLElement): void {
|
|
141
|
+
const row = el.parentElement;
|
|
142
|
+
if (!row || !el.isConnected) return;
|
|
143
|
+
// The overlays are 2.4em wide; clearing a little more than that keeps the
|
|
144
|
+
// revealed item from sitting right against one.
|
|
145
|
+
const pad = parseFloat(getComputedStyle(row).fontSize) * 2.6;
|
|
146
|
+
const box = el.getBoundingClientRect(), strip = row.getBoundingClientRect();
|
|
147
|
+
if (box.left < strip.left + pad) row.scrollBy({ left: box.left - strip.left - pad, behavior: "smooth" });
|
|
148
|
+
else if (box.right > strip.right - pad) row.scrollBy({ left: box.right - strip.right + pad, behavior: "smooth" });
|
|
149
|
+
}
|
|
150
|
+
|
|
87
151
|
/**
|
|
88
152
|
* A tabbed view. Renders an ARIA `tablist` of buttons and a single live panel
|
|
89
153
|
* for the selected tab. Supports keyboard navigation (left/right/home/end).
|
|
@@ -121,12 +185,14 @@ export function tabs(opts: TabsOptions): void {
|
|
|
121
185
|
};
|
|
122
186
|
|
|
123
187
|
A("div.s-tabs", opts.attrs, () => {
|
|
124
|
-
|
|
125
|
-
|
|
188
|
+
scrollStrip({
|
|
189
|
+
attrs: ".s-tabbar",
|
|
190
|
+
stripAttrs: ".s-tablist role=tablist",
|
|
191
|
+
content: () => {
|
|
126
192
|
opts.tabs.forEach((tab, index) => {
|
|
127
193
|
const key = keyOf(tab, index);
|
|
128
194
|
const tabEl = A("button.s-tab type=button role=tab", () => {
|
|
129
|
-
A(
|
|
195
|
+
A("id=", `${groupId}-tab-${key}`, "aria-controls=", `${groupId}-panel-${key}`);
|
|
130
196
|
A(() => {
|
|
131
197
|
const selected = $sel.value === key;
|
|
132
198
|
A("aria-selected=", selected ? "true" : "false");
|
|
@@ -134,7 +200,7 @@ export function tabs(opts: TabsOptions): void {
|
|
|
134
200
|
// Selecting a tab that's (partly) scrolled out brings it into
|
|
135
201
|
// view, so the strip follows the selection however it was made:
|
|
136
202
|
// a click, the arrow keys, or a `bind` written from elsewhere.
|
|
137
|
-
if (selected) requestAnimationFrame(() =>
|
|
203
|
+
if (selected) requestAnimationFrame(() => revealInStrip(tabEl as HTMLElement));
|
|
138
204
|
});
|
|
139
205
|
if (tab.disabled) A("disabled=true");
|
|
140
206
|
A("click=", () => select(tab, index));
|
|
@@ -143,11 +209,7 @@ export function tabs(opts: TabsOptions): void {
|
|
|
143
209
|
drawSlot(tab.label);
|
|
144
210
|
});
|
|
145
211
|
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
drawScrollButton(listEl, -1);
|
|
149
|
-
drawScrollButton(listEl, 1);
|
|
150
|
-
watchScroll(listEl);
|
|
212
|
+
},
|
|
151
213
|
});
|
|
152
214
|
|
|
153
215
|
A("div.s-tabpanel role=tabpanel", opts.contentAttrs, () => {
|
|
@@ -156,7 +218,7 @@ export function tabs(opts: TabsOptions): void {
|
|
|
156
218
|
const index = opts.tabs.findIndex((t, i) => keyOf(t, i) === selKey);
|
|
157
219
|
const tab = opts.tabs[index] ?? opts.tabs[0];
|
|
158
220
|
if (!tab) return;
|
|
159
|
-
A(
|
|
221
|
+
A("id=", `${groupId}-panel-${keyOf(tab, index)}`, "aria-labelledby=", `${groupId}-tab-${keyOf(tab, index)}`);
|
|
160
222
|
drawSlot(tab.content);
|
|
161
223
|
});
|
|
162
224
|
});
|
|
@@ -164,60 +226,64 @@ export function tabs(opts: TabsOptions): void {
|
|
|
164
226
|
}
|
|
165
227
|
|
|
166
228
|
/**
|
|
167
|
-
* One of the two scroll buttons overlaying the ends of the
|
|
168
|
-
* the left one and 1 for the right.
|
|
169
|
-
*
|
|
229
|
+
* One of the two scroll buttons overlaying the ends of the row. `dir` is -1 for
|
|
230
|
+
* the left one and 1 for the right. Most of a width at a time (not one item at a
|
|
231
|
+
* time): the row scrolls smoothly, so a nudge that moved a single tab or crumb
|
|
232
|
+
* would read as a twitch.
|
|
170
233
|
*/
|
|
171
|
-
function drawScrollButton(
|
|
172
|
-
A(`button.s-
|
|
173
|
-
// The
|
|
174
|
-
//
|
|
175
|
-
//
|
|
234
|
+
function drawScrollButton(row: HTMLElement, dir: -1 | 1): void {
|
|
235
|
+
A(`button.s-strip-btn.s-strip-btn-${dir < 0 ? "left" : "right"} type=button`, () => {
|
|
236
|
+
// The row's own items are the real control; this is a convenience it offers
|
|
237
|
+
// a mouse. Keeping it out of the tab order means Tab still steps from the
|
|
238
|
+
// row straight into whatever follows.
|
|
176
239
|
A("tabindex=-1 aria-hidden=true");
|
|
177
|
-
A("click=", () =>
|
|
240
|
+
A("click=", () => row.scrollBy({ left: dir * row.clientWidth * 0.8, behavior: "smooth" }));
|
|
178
241
|
(dir < 0 ? chevronLeft : chevronRight)({ size: "1.1em" });
|
|
179
242
|
});
|
|
180
243
|
}
|
|
181
244
|
|
|
182
245
|
/**
|
|
183
|
-
* Keep the `.s-can-left` / `.s-can-right` classes on the
|
|
246
|
+
* Keep the `.s-can-left` / `.s-can-right` classes on the strip in step with what
|
|
184
247
|
* there is left to scroll towards, so each button appears exactly when it has
|
|
185
|
-
* somewhere to go. Watches the
|
|
186
|
-
*
|
|
248
|
+
* somewhere to go. Watches the row's own scrolling *and* its size — and its
|
|
249
|
+
* children's, since items arriving or leaving (a pushed page's crumb, a tab)
|
|
250
|
+
* changes the answer without any scrolling at all.
|
|
187
251
|
*/
|
|
188
|
-
function watchScroll(
|
|
189
|
-
const
|
|
190
|
-
if (!
|
|
252
|
+
function watchScroll(row: HTMLElement): void {
|
|
253
|
+
const strip = row.parentElement;
|
|
254
|
+
if (!strip || typeof ResizeObserver === "undefined") return; // No-op outside the browser.
|
|
191
255
|
const update = () => {
|
|
192
256
|
// A sub-pixel slack: fractional layout widths otherwise leave a permanent
|
|
193
257
|
// half-pixel of "scrollable" at an end that is plainly already reached.
|
|
194
|
-
const max =
|
|
195
|
-
|
|
196
|
-
|
|
258
|
+
const max = row.scrollWidth - row.clientWidth;
|
|
259
|
+
strip.classList.toggle("s-can-left", row.scrollLeft > 1);
|
|
260
|
+
strip.classList.toggle("s-can-right", row.scrollLeft < max - 1);
|
|
197
261
|
};
|
|
198
|
-
|
|
262
|
+
row.addEventListener("scroll", update, { passive: true });
|
|
199
263
|
const ro = new ResizeObserver(update);
|
|
200
|
-
ro.observe(
|
|
201
|
-
|
|
264
|
+
ro.observe(row);
|
|
265
|
+
// The children are watched through one observer that follows the row's live
|
|
266
|
+
// contents, so a strip whose items come and go keeps answering correctly.
|
|
267
|
+
// Removed children are unobserved: a strip whose items churn (the breadcrumb
|
|
268
|
+
// row redraws on navigation) would otherwise grow the observation list — and
|
|
269
|
+
// retain the detached elements — without bound.
|
|
270
|
+
const mo = typeof MutationObserver === "undefined" ? undefined : new MutationObserver((records) => {
|
|
271
|
+
for (const record of records) {
|
|
272
|
+
for (const el of record.addedNodes) if (el instanceof Element) ro.observe(el);
|
|
273
|
+
for (const el of record.removedNodes) if (el instanceof Element) ro.unobserve(el);
|
|
274
|
+
}
|
|
275
|
+
update();
|
|
276
|
+
});
|
|
277
|
+
mo?.observe(row, { childList: true });
|
|
278
|
+
for (const kid of Array.from(row.children)) ro.observe(kid);
|
|
202
279
|
update();
|
|
203
280
|
A.clean(() => {
|
|
204
|
-
|
|
281
|
+
row.removeEventListener("scroll", update);
|
|
205
282
|
ro.disconnect();
|
|
283
|
+
mo?.disconnect();
|
|
206
284
|
});
|
|
207
285
|
}
|
|
208
286
|
|
|
209
|
-
/** Scroll `el`'s strip just far enough to clear the buttons overlaying its ends. */
|
|
210
|
-
function reveal(el: HTMLElement): void {
|
|
211
|
-
const list = el.parentElement;
|
|
212
|
-
if (!list || !el.isConnected) return;
|
|
213
|
-
// The overlays are 2.4em wide; clearing a little more than that keeps the tab
|
|
214
|
-
// from sitting right against one.
|
|
215
|
-
const pad = parseFloat(getComputedStyle(list).fontSize) * 2.6;
|
|
216
|
-
const tab = el.getBoundingClientRect(), strip = list.getBoundingClientRect();
|
|
217
|
-
if (tab.left < strip.left + pad) list.scrollBy({ left: tab.left - strip.left - pad, behavior: "smooth" });
|
|
218
|
-
else if (tab.right > strip.right - pad) list.scrollBy({ left: tab.right - strip.right + pad, behavior: "smooth" });
|
|
219
|
-
}
|
|
220
|
-
|
|
221
287
|
/** Roving-tabindex keyboard handling for the tab strip. */
|
|
222
288
|
function onKey(
|
|
223
289
|
e: KeyboardEvent,
|
package/src/core.ts
CHANGED
|
@@ -56,7 +56,7 @@ export interface ContentOptions {
|
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* Shell width — not viewport width — at or below which the app shell goes
|
|
59
|
-
* "narrow": the nav sidebar collapses to a hamburger, and a routed
|
|
59
|
+
* "narrow": the nav sidebar collapses to a hamburger, and a routed shell
|
|
60
60
|
* has room for exactly one full-bleed column. Shared by the `@container` queries
|
|
61
61
|
* that do the switching and by the JS that has to agree with them.
|
|
62
62
|
*/
|
package/src/index.ts
CHANGED
|
@@ -35,17 +35,17 @@
|
|
|
35
35
|
export { setDarkMode, getDarkMode } from "./theme.js";
|
|
36
36
|
export { autocomplete, type AutocompleteOptions, type AutocompleteOptionInput } from "./components/autocomplete.js";
|
|
37
37
|
export { box, type BoxOptions } from "./components/box.js";
|
|
38
|
-
export { button, type ButtonOptions } from "./components/button.js";
|
|
38
|
+
export { button, iconButton, type ButtonOptions, type IconButtonOptions } from "./components/button.js";
|
|
39
39
|
export { buttonChooser, type ButtonChooserOptions } from "./components/buttonChooser.js";
|
|
40
40
|
export { buttonGroup, type ButtonGroupOptions } from "./components/buttonGroup.js";
|
|
41
41
|
export { checkbox, type CheckboxOptions } from "./components/checkbox.js";
|
|
42
42
|
export { form, type FormOptions } from "./components/form.js";
|
|
43
43
|
export { main, closeNav, type MainOptions } from "./components/main.js";
|
|
44
|
-
export {
|
|
45
|
-
export { menuButton, showFloatingMenu, addContextMenu, isFloatingMenuOpen, closeFloatingMenu, type MenuOptions, type MenuEntry, type MenuItem, type MenuSeparator, type FloatingMenuOptions, type ContextMenuOptions } from "./components/menu.js";
|
|
44
|
+
export { type PanelStack, type Panel, type Routes, type RouteHandler, type RouteTable, type AncestorsHandler, type AncestorTable, type PathParams, type SegParams } from "./components/panels.js";
|
|
45
|
+
export { menu, menuButton, showFloatingMenu, addContextMenu, isFloatingMenuOpen, closeFloatingMenu, type MenuListOptions, type MenuOptions, type MenuEntry, type MenuItem, type MenuSeparator, type FloatingMenuOptions, type ContextMenuOptions } from "./components/menu.js";
|
|
46
46
|
export { dialog, alert, confirm, prompt, isDialogOpen, type DialogOptions } from "./components/dialog.js";
|
|
47
47
|
export { select, type SelectOptions, type SelectOptionInput } from "./components/select.js";
|
|
48
|
-
export { tabs, type Tab, type TabsOptions } from "./components/tabs.js";
|
|
48
|
+
export { tabs, scrollStrip, revealInStrip, type Tab, type TabsOptions, type ScrollStripOptions } from "./components/tabs.js";
|
|
49
49
|
export { textarea, type TextareaOptions } from "./components/textarea.js";
|
|
50
50
|
export { textline, type TextlineOptions, type TextlineType } from "./components/textline.js";
|
|
51
51
|
export { toast, type ToastOptions } from "./components/toast.js";
|
package/src/theme.ts
CHANGED
|
@@ -58,6 +58,18 @@ import A from "aberdeen";
|
|
|
58
58
|
* ```
|
|
59
59
|
*/
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* The surface sheen: the subtle single-colour wash every surface (and the page)
|
|
63
|
+
* is painted with, as a `background:` declaration. A shared constant rather
|
|
64
|
+
* than a CSS custom property, deliberately: `var()`s inside a custom property
|
|
65
|
+
* resolve where the property is *defined*, so a `--s-sheen` at `:root` would
|
|
66
|
+
* paint every surface with the page's wash instead of its own `$s-bg`'s. Also
|
|
67
|
+
* used by panels.ts, where the routed columns have to visually *be* the
|
|
68
|
+
* surface around them.
|
|
69
|
+
*/
|
|
70
|
+
export const SURFACE_SHEEN =
|
|
71
|
+
"background: linear-gradient(170deg, color-mix(in oklab, $s-bg, white 9%), color-mix(in oklab, $s-bg, black 9%));";
|
|
72
|
+
|
|
61
73
|
const STORAGE_KEY = "staffa:darkMode";
|
|
62
74
|
|
|
63
75
|
/** The explicit dark-mode choice; `undefined` follows the OS via {@link A.darkMode}. */
|
|
@@ -221,8 +233,7 @@ A.insertGlobalCss({
|
|
|
221
233
|
"--s-faint: color-mix(in oklab, $s-text, $s-bg 80%); " +
|
|
222
234
|
"color:$s-text accent-color:$s-accent scrollbar-width:thin scrollbar-color: $s-faint transparent;",
|
|
223
235
|
// Subtle single-colour gradient sheen, painted on every surface (and the page).
|
|
224
|
-
".s-s, body":
|
|
225
|
-
"background: linear-gradient(170deg, color-mix(in oklab, $s-bg, white 9%), color-mix(in oklab, $s-bg, black 9%));",
|
|
236
|
+
".s-s, body": SURFACE_SHEEN,
|
|
226
237
|
".s-s": "r:$s-radius",
|
|
227
238
|
// Neutral surfaces own a subtle hairline border (a card reads as a card without
|
|
228
239
|
// any component help). `:where()` keeps it zero-specificity, so a bar/panel that
|
|
@@ -263,7 +274,7 @@ A.insertGlobalCss({
|
|
|
263
274
|
// Specificity (4 classes) beats the 2-class variant rules — no !important.
|
|
264
275
|
".s-s:not(.neutral) .s-s.tonal, .s-s:not(.neutral) .s-s.outlined":
|
|
265
276
|
"--s-text:#fff --s-accent:#fff --s-link-fg:#fff " +
|
|
266
|
-
|
|
277
|
+
SURFACE_SHEEN + " border-color: transparent;",
|
|
267
278
|
});
|
|
268
279
|
|
|
269
280
|
// ── Suppress transitions during the initial load ─────────────────────────────
|
package/skill/Page.md
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
## Page · interface
|
|
2
|
-
|
|
3
|
-
What a route handler gets: the params from its route, plus everything the
|
|
4
|
-
shell needs to know about the panel it is drawing. It's an Aberdeen proxy, so
|
|
5
|
-
you can set things later, such as a `title` that arrives with your data or
|
|
6
|
-
`loading` going back to `false`, and the shell keeps up.
|
|
7
|
-
|
|
8
|
-
Search params and the `#hash` belong to the top panel only. A panel with
|
|
9
|
-
another one on top of it keeps just its path, so anything a panel needs in
|
|
10
|
-
order to redraw itself has to live in that path.
|
|
11
|
-
|
|
12
|
-
**Type Parameters:**
|
|
13
|
-
|
|
14
|
-
- `P = Record<string, string | number | string[]>`
|
|
15
|
-
|
|
16
|
-
### page.params · member
|
|
17
|
-
|
|
18
|
-
The params matched from this panel's path, typed per its route key:
|
|
19
|
-
`[x]` is a `string`, `[x=integer]` a `number`, `[...x]` a `string`.
|
|
20
|
-
Read-only.
|
|
21
|
-
|
|
22
|
-
**Type:** `P`
|
|
23
|
-
|
|
24
|
-
### page.path · member
|
|
25
|
-
|
|
26
|
-
This panel's path, e.g. `"/projects/7"`. Read-only.
|
|
27
|
-
|
|
28
|
-
**Type:** `string`
|
|
29
|
-
|
|
30
|
-
### page.title · member
|
|
31
|
-
|
|
32
|
-
Shown in `document.title` while this panel is top-most.
|
|
33
|
-
|
|
34
|
-
**Type:** `string`
|
|
35
|
-
|
|
36
|
-
### page.layout · member
|
|
37
|
-
|
|
38
|
-
How much room this panel takes. The content area is the page, at most
|
|
39
|
-
1280px wide, minus the nav sidebar; the widths below assume a sidebar of
|
|
40
|
-
around 170px, so without one add that back.
|
|
41
|
-
|
|
42
|
-
- `"small"` is 360 to 540px once two panels fit side by side, which is
|
|
43
|
-
what makes it right for lists, detail forms, and anything else that
|
|
44
|
-
reads well at phone width. Below that it takes the whole content area
|
|
45
|
-
(so up to ~730px), like a medium does. A lone small leaves its other
|
|
46
|
-
half empty, and that is exactly where the next small lands, without
|
|
47
|
-
anything on screen moving.
|
|
48
|
-
- `"medium"` (the default) takes the whole content area: up to ~1100px,
|
|
49
|
-
and the screen width on a phone. The safe default for ordinary screens.
|
|
50
|
-
Nothing fits beside a medium on a standard 1280px page, though on a wide
|
|
51
|
-
enough window a small still can.
|
|
52
|
-
- `"large"` takes the whole window, with no upper limit (~1750px on a
|
|
53
|
-
1920px screen): for boards, wide tables and dense dashboards. While it's
|
|
54
|
-
open the whole shell (top bar, content and footer) stretches to the
|
|
55
|
-
screen edges rather than stopping at 1280px.
|
|
56
|
-
|
|
57
|
-
When more columns fit than the standard page holds (three smalls, or a
|
|
58
|
-
medium and a small) the page itself grows, staying centred, to hold them.
|
|
59
|
-
|
|
60
|
-
A panel's width depends only on the size of the window, never on what else
|
|
61
|
-
is open, so opening or closing a panel never resizes the ones already on
|
|
62
|
-
screen.
|
|
63
|
-
|
|
64
|
-
The panel is sized from this **before** your handler runs, so anything that
|
|
65
|
-
measures its own box has a real one from the first frame. What it is sized
|
|
66
|
-
at is whatever this says at that moment, which for a brand-new panel is the
|
|
67
|
-
default: a handler that *assigns* `layout` is drawn at the medium width and
|
|
68
|
-
reflowed immediately after — in time for the frame, but not for a
|
|
69
|
-
measurement taken in the same breath.
|
|
70
|
-
|
|
71
|
-
Assigning it later works just as well. When your data arrives and you find
|
|
72
|
-
you want the wide one, the panel reflows to its new width without being
|
|
73
|
-
redrawn — so nothing in it is rebuilt or loses its state — and the columns
|
|
74
|
-
beside it move over.
|
|
75
|
-
|
|
76
|
-
**Type:** `"small" | "medium" | "large"`
|
|
77
|
-
|
|
78
|
-
### page.loading · member
|
|
79
|
-
|
|
80
|
-
Set this while you're fetching what the panel needs, and back to `false`
|
|
81
|
-
when you're done. A new panel waits a moment before sliding in, so it can
|
|
82
|
-
arrive with real content instead of empty; if the wait drags on it slides
|
|
83
|
-
in anyway and shows a loading indicator until the flag clears. It only
|
|
84
|
-
affects the animation; the stack, the URL and `requestClose` never wait
|
|
85
|
-
for it.
|
|
86
|
-
|
|
87
|
-
**Type:** `boolean`
|
|
88
|
-
|
|
89
|
-
### page.requestClose · member
|
|
90
|
-
|
|
91
|
-
Your chance to say no. Everything that would close this panel waits for
|
|
92
|
-
it: Escape, the panel's own ✕ or Cancel button (`Page.close`, or a
|
|
93
|
-
box with `close: true`), the browser's back button, a link that would
|
|
94
|
-
close it, and `panels`.`close()`. Return `false` to keep the panel
|
|
95
|
-
open, usually after a dirty check and a `confirm`.
|
|
96
|
-
|
|
97
|
-
**Type:** `() => boolean | Promise<boolean>`
|
|
98
|
-
|
|
99
|
-
### page.close · member
|
|
100
|
-
|
|
101
|
-
Closes **this** panel, wherever it sits in the stack. The top panel goes
|
|
102
|
-
back to whatever was underneath it; any other panel is taken out on its
|
|
103
|
-
own, leaving the columns to its right where they are, with their state,
|
|
104
|
-
and the URL alone, since the top panel didn't move. Either way it
|
|
105
|
-
becomes a history entry, so the browser's back button brings it back.
|
|
106
|
-
|
|
107
|
-
Resolves `false` if the panel didn't close: `Page.requestClose` said
|
|
108
|
-
no, it was the only panel on the stack (so there's nothing to go back to),
|
|
109
|
-
or another navigation got there first. The shell draws no back arrows or
|
|
110
|
-
✕ of its own, so this (or `S.box`'s `close` option) is how a panel gives
|
|
111
|
-
the user a way out.
|
|
112
|
-
|
|
113
|
-
**Type:** `() => Promise<boolean>`
|
|
114
|
-
|
|
115
|
-
**Examples:**
|
|
116
|
-
|
|
117
|
-
```ts
|
|
118
|
-
S.button({ content: "Cancel", attrs: ".neutral", click: () => void $page.close() });
|
|
119
|
-
```
|
package/skill/panels.md
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
## panels · constant
|
|
2
|
-
|
|
3
|
-
Navigating the routed `S.main()` shell from code, for the times it isn't a
|
|
4
|
-
link click, such as opening the screen for a record you just created.
|
|
5
|
-
|
|
6
|
-
The same rules as a link click apply: pushing a path that is already open
|
|
7
|
-
goes back to it rather than opening it twice, and anything that would close a
|
|
8
|
-
panel asks its `Page.requestClose` first.
|
|
9
|
-
|
|
10
|
-
**Value:** `{ push(path: string): void; replace(path: string): void; open(path: string, beneath?: readonly string[]): void; close(path?: string): Promise<boolean>; readonly stack: readonly string[]; }`
|