staffa 0.7.4 → 0.8.1
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 +117 -7
- package/dist/components/box.d.ts +20 -0
- package/dist/components/box.js +43 -3
- package/dist/components/layers.d.ts +330 -0
- package/dist/components/layers.js +888 -0
- package/dist/components/main.d.ts +103 -6
- package/dist/components/main.js +250 -43
- package/dist/components/menu.d.ts +14 -1
- package/dist/components/menu.js +32 -4
- package/dist/components/panels.d.ts +392 -0
- package/dist/components/panels.js +1031 -0
- package/dist/components/tabs.d.ts +5 -0
- package/dist/components/tabs.js +125 -19
- package/dist/core.d.ts +7 -0
- package/dist/core.js +7 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/staffa.esm.js +1 -1
- package/package.json +7 -5
- package/skill/BoxOptions.md +18 -0
- package/skill/MainOptions.md +104 -3
- package/skill/Page.md +119 -0
- package/skill/PathParams.md +7 -0
- package/skill/SKILL.md +177 -9
- package/skill/SegParams.md +8 -0
- package/skill/box.md +4 -0
- package/skill/isFloatingMenuOpen.md +12 -0
- package/skill/main.md +14 -2
- package/skill/panels.md +10 -0
- package/skill/tabs.md +5 -0
- package/src/components/box.ts +57 -2
- package/src/components/main.ts +347 -46
- package/src/components/menu.ts +33 -5
- package/src/components/panels.ts +1292 -0
- package/src/components/tabs.ts +126 -19
- package/src/core.ts +8 -0
- package/src/index.ts +2 -1
package/dist/components/menu.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import A from "aberdeen";
|
|
2
2
|
import { matchCurrent } from "aberdeen/route";
|
|
3
3
|
import { drawSlot, mountPortal, focusFirst } from "../core.js";
|
|
4
|
+
import { mk } from "../icons-helpers.js";
|
|
4
5
|
import { button } from "./button.js";
|
|
6
|
+
// The two glyphs the shell draws for itself. As inline SVG (built with the icon
|
|
7
|
+
// set's own helper, so no icon data is pulled in) rather than the `☰`/`✕`
|
|
8
|
+
// characters: a text glyph is at the mercy of the system font, and next to a real
|
|
9
|
+
// icon it lands thin and undersized. These match Lucide's `menu` and `x` exactly,
|
|
10
|
+
// so a nav trigger sits beside app icons as an equal.
|
|
11
|
+
/** `☰` — opens a menu or the nav. */
|
|
12
|
+
export const menuGlyph = mk('<path d="M4 6h16"/><path d="M4 12h16"/><path d="M4 18h16"/>');
|
|
13
|
+
/** `✕` — dismisses what the {@link menuGlyph} opened. */
|
|
14
|
+
export const closeGlyph = mk('<path d="M18 6 6 18"/><path d="m6 6 12 12"/>');
|
|
5
15
|
// Styles shared by the floating dropdown and the sidebar nav, so both look
|
|
6
16
|
// identical. The item styles aren't scoped to a container, so `drawMenu` can
|
|
7
17
|
// render its items into either one.
|
|
@@ -20,7 +30,12 @@ A.insertGlobalCss({
|
|
|
20
30
|
"font-size:0.9em border:0 background:transparent fg:$s-text text-decoration:none " +
|
|
21
31
|
"transition: color 0.12s, transform 0.12s, text-shadow 0.12s;",
|
|
22
32
|
".s-menu-item:focus-visible:not([aria-current=page]), .s-menu-item:hover:not([aria-disabled=true]):not([aria-current=page])": "filter:none color: color-mix(in lab, $s-primary 33%, $s-text);",
|
|
23
|
-
|
|
33
|
+
// The active row is simply drawn in the surface's accent — the brand colour on a
|
|
34
|
+
// neutral surface, the ink on an accent one. No glow and no brightening: those
|
|
35
|
+
// pushed it off the brand colour, so it read as a lit-up variant of it rather
|
|
36
|
+
// than as the colour itself. `filter:none` keeps the global `a:hover` brighten
|
|
37
|
+
// off it too, since the hover rule above deliberately skips the active row.
|
|
38
|
+
".s-menu-item[aria-current=page]": "color:$s-accent filter:none",
|
|
24
39
|
".s-menu-item[aria-disabled=true]": "opacity:0.45 cursor:not-allowed pointer-events:none",
|
|
25
40
|
".s-menu-icon": "flex-shrink:0",
|
|
26
41
|
// A soft hairline that fades out at both ends, rather than a hard full-width
|
|
@@ -116,9 +131,22 @@ function closeFloating() {
|
|
|
116
131
|
* Whether a floating menu is currently open. Reflects live state (cleared the
|
|
117
132
|
* instant it closes), unlike the DOM — the panel lingers briefly while its
|
|
118
133
|
* `destroy=` transition plays out.
|
|
134
|
+
*
|
|
135
|
+
* @param anchor When given, only reports `true` for a menu opened from *this*
|
|
136
|
+
* anchor — so a component can ask about its own menu rather than any menu.
|
|
137
|
+
*/
|
|
138
|
+
export function isFloatingMenuOpen(anchor) {
|
|
139
|
+
const opts = $floating.opts;
|
|
140
|
+
return opts != null && (anchor == null || opts.anchor === anchor);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Close the open floating menu (if any), returning focus to its anchor. With an
|
|
144
|
+
* `anchor`, only closes when the open menu belongs to it, so dismissing your own
|
|
145
|
+
* menu can't steal someone else's.
|
|
119
146
|
*/
|
|
120
|
-
export function
|
|
121
|
-
|
|
147
|
+
export function closeFloatingMenu(anchor) {
|
|
148
|
+
if (isFloatingMenuOpen(anchor))
|
|
149
|
+
closeFloating();
|
|
122
150
|
}
|
|
123
151
|
function positionMenu(menuEl, rect) {
|
|
124
152
|
const mw = menuEl.offsetWidth, mh = menuEl.offsetHeight;
|
|
@@ -267,7 +295,7 @@ export function menuButton(opts) {
|
|
|
267
295
|
A.clean(() => { if ($floating.opts?.anchor === myEl)
|
|
268
296
|
closeFloating(); });
|
|
269
297
|
button({
|
|
270
|
-
icon: () =>
|
|
298
|
+
icon: () => menuGlyph({ size: "1.4em" }),
|
|
271
299
|
// Only label the trigger "Open menu" when it has no visible text of its
|
|
272
300
|
// own — an aria-label would otherwise *hide* that text from AT.
|
|
273
301
|
...(opts.button?.content == null ? { ariaLabel: "Open menu" } : null),
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routed, multi-column panel navigation for {@link main}.
|
|
3
|
+
*
|
|
4
|
+
* Each route draws one screen of the app, called a panel, and as many panels as
|
|
5
|
+
* fit are shown at a time. On a phone that is one, so a link opens a new panel
|
|
6
|
+
* on top and closing it brings the previous one back. On a wider screen the
|
|
7
|
+
* panels that would have covered each other sit side by side instead, oldest on
|
|
8
|
+
* the left. The app's own code is the same either way.
|
|
9
|
+
*
|
|
10
|
+
* Navigation runs through `aberdeen/route`: the URL holds the top panel, and
|
|
11
|
+
* the ones beneath it are stored beside it in the history entry. So back and
|
|
12
|
+
* forward step through whole arrangements of columns, and a reload (or a shared
|
|
13
|
+
* link) brings the same columns back.
|
|
14
|
+
*/
|
|
15
|
+
/** Flattens an intersection into a single object type, so hovers read nicely. */
|
|
16
|
+
type Prettify<T> = {
|
|
17
|
+
[K in keyof T]: T[K];
|
|
18
|
+
} & {};
|
|
19
|
+
/**
|
|
20
|
+
* What a `[name=matcher]` matcher name yields. An unrecognised name resolves to
|
|
21
|
+
* `never`, which shows up as an unusable param at the handler rather than
|
|
22
|
+
* quietly typing as `string` (the route key itself throws at mount time).
|
|
23
|
+
*/
|
|
24
|
+
export type MatcherType<M extends string> = M extends "integer" ? number : never;
|
|
25
|
+
/**
|
|
26
|
+
* The params contributed by a single path-template segment: `[x]` a string,
|
|
27
|
+
* `[x=integer]` a number, `[...x]` the rest of the path as one raw string.
|
|
28
|
+
*/
|
|
29
|
+
export type SegParams<S extends string> = S extends `[...${infer Name}]` ? {
|
|
30
|
+
[K in Name]: string;
|
|
31
|
+
} : S extends `[${infer Name}=${infer Matcher}]` ? {
|
|
32
|
+
[K in Name]: MatcherType<Matcher>;
|
|
33
|
+
} : S extends `[${infer Name}]` ? {
|
|
34
|
+
[K in Name]: string;
|
|
35
|
+
} : {};
|
|
36
|
+
/**
|
|
37
|
+
* The params object described by a path template, e.g.
|
|
38
|
+
* `PathParams<"/projects/[id]/tasks/[taskId=integer]">` is
|
|
39
|
+
* `{ id: string; taskId: number }`.
|
|
40
|
+
*/
|
|
41
|
+
export type PathParams<P extends string> = P extends `${infer Head}/${infer Rest}` ? SegParams<Head> & PathParams<Rest> : SegParams<P>;
|
|
42
|
+
/** A panel draw function: it receives the panel's {@link Page} and draws into the current scope. */
|
|
43
|
+
export type RouteHandler<P = any> = (page: Page<P>) => void;
|
|
44
|
+
/**
|
|
45
|
+
* A route table: path templates mapped to panel draw functions. Used as the
|
|
46
|
+
* loose (non-inferred) type; `S.main()` infers a more precise type from the
|
|
47
|
+
* literal you pass, so each handler's `$page.params` is typed per its key.
|
|
48
|
+
*/
|
|
49
|
+
export type Routes = Record<string, RouteHandler>;
|
|
50
|
+
/**
|
|
51
|
+
* The shape `S.main()`'s `routes` option is checked against: every key types its
|
|
52
|
+
* own handler's `params`. Used as a self-referential generic constraint, which
|
|
53
|
+
* is what makes `$page.params` infer from the route key.
|
|
54
|
+
*/
|
|
55
|
+
export type RouteTable<R> = {
|
|
56
|
+
[K in keyof R & string]: (page: Page<Prettify<PathParams<K>>>) => void;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* What a route handler gets: the params from its route, plus everything the
|
|
60
|
+
* shell needs to know about the panel it is drawing. It's an Aberdeen proxy, so
|
|
61
|
+
* you can set things later, such as a `title` that arrives with your data or
|
|
62
|
+
* `loading` going back to `false`, and the shell keeps up.
|
|
63
|
+
*
|
|
64
|
+
* Search params and the `#hash` belong to the top panel only. A panel with
|
|
65
|
+
* another one on top of it keeps just its path, so anything a panel needs in
|
|
66
|
+
* order to redraw itself has to live in that path.
|
|
67
|
+
*/
|
|
68
|
+
export interface Page<P = Record<string, string | number | string[]>> {
|
|
69
|
+
/**
|
|
70
|
+
* The params matched from this panel's path, typed per its route key:
|
|
71
|
+
* `[x]` is a `string`, `[x=integer]` a `number`, `[...x]` a `string`.
|
|
72
|
+
* Read-only.
|
|
73
|
+
*/
|
|
74
|
+
readonly params: P;
|
|
75
|
+
/** This panel's path, e.g. `"/projects/7"`. Read-only. */
|
|
76
|
+
readonly path: string;
|
|
77
|
+
/** Shown in `document.title` while this panel is top-most. */
|
|
78
|
+
title?: string;
|
|
79
|
+
/**
|
|
80
|
+
* How much room this panel takes. The content area is the page, at most
|
|
81
|
+
* 1280px wide, minus the nav sidebar; the widths below assume a sidebar of
|
|
82
|
+
* around 170px, so without one add that back.
|
|
83
|
+
*
|
|
84
|
+
* - `"small"` is 360 to 540px once two panels fit side by side, which is
|
|
85
|
+
* what makes it right for lists, detail forms, and anything else that
|
|
86
|
+
* reads well at phone width. Below that it takes the whole content area
|
|
87
|
+
* (so up to ~730px), like a medium does. A lone small leaves its other
|
|
88
|
+
* half empty, and that is exactly where the next small lands, without
|
|
89
|
+
* anything on screen moving.
|
|
90
|
+
* - `"medium"` (the default) takes the whole content area: up to ~1100px,
|
|
91
|
+
* and the screen width on a phone. The safe default for ordinary screens.
|
|
92
|
+
* Nothing fits beside a medium on a standard 1280px page, though on a wide
|
|
93
|
+
* enough window a small still can.
|
|
94
|
+
* - `"large"` takes the whole window, with no upper limit (~1750px on a
|
|
95
|
+
* 1920px screen): for boards, wide tables and dense dashboards. While it's
|
|
96
|
+
* open the whole shell (top bar, content and footer) stretches to the
|
|
97
|
+
* screen edges rather than stopping at 1280px.
|
|
98
|
+
*
|
|
99
|
+
* When more columns fit than the standard page holds (three smalls, or a
|
|
100
|
+
* medium and a small) the page itself grows, staying centred, to hold them.
|
|
101
|
+
*
|
|
102
|
+
* A panel's width depends only on the size of the window, never on what else
|
|
103
|
+
* is open, so opening or closing a panel never resizes the ones already on
|
|
104
|
+
* screen.
|
|
105
|
+
*
|
|
106
|
+
* The panel is sized from this **before** your handler runs, so anything that
|
|
107
|
+
* measures its own box has a real one from the first frame. What it is sized
|
|
108
|
+
* at is whatever this says at that moment, which for a brand-new panel is the
|
|
109
|
+
* default: a handler that *assigns* `layout` is drawn at the medium width and
|
|
110
|
+
* reflowed immediately after — in time for the frame, but not for a
|
|
111
|
+
* measurement taken in the same breath.
|
|
112
|
+
*
|
|
113
|
+
* Assigning it later works just as well. When your data arrives and you find
|
|
114
|
+
* you want the wide one, the panel reflows to its new width without being
|
|
115
|
+
* redrawn — so nothing in it is rebuilt or loses its state — and the columns
|
|
116
|
+
* beside it move over.
|
|
117
|
+
*/
|
|
118
|
+
layout?: "small" | "medium" | "large";
|
|
119
|
+
/**
|
|
120
|
+
* Set this while you're fetching what the panel needs, and back to `false`
|
|
121
|
+
* when you're done. A new panel waits a moment before sliding in, so it can
|
|
122
|
+
* arrive with real content instead of empty; if the wait drags on it slides
|
|
123
|
+
* in anyway and shows a loading indicator until the flag clears. It only
|
|
124
|
+
* affects the animation; the stack, the URL and `requestClose` never wait
|
|
125
|
+
* for it.
|
|
126
|
+
*/
|
|
127
|
+
loading?: boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Your chance to say no. Everything that would close this panel waits for
|
|
130
|
+
* it: Escape, the panel's own ✕ or Cancel button ({@link Page.close}, or a
|
|
131
|
+
* box with `close: true`), the browser's back button, a link that would
|
|
132
|
+
* close it, and {@link panels}.`close()`. Return `false` to keep the panel
|
|
133
|
+
* open, usually after a dirty check and a {@link confirm}.
|
|
134
|
+
*/
|
|
135
|
+
requestClose?: () => boolean | Promise<boolean>;
|
|
136
|
+
/**
|
|
137
|
+
* Closes **this** panel, wherever it sits in the stack. The top panel goes
|
|
138
|
+
* back to whatever was underneath it; any other panel is taken out on its
|
|
139
|
+
* own, leaving the columns to its right where they are, with their state,
|
|
140
|
+
* and the URL alone, since the top panel didn't move. Either way it
|
|
141
|
+
* becomes a history entry, so the browser's back button brings it back.
|
|
142
|
+
*
|
|
143
|
+
* Resolves `false` if the panel didn't close: {@link Page.requestClose} said
|
|
144
|
+
* no, it was the only panel on the stack (so there's nothing to go back to),
|
|
145
|
+
* or another navigation got there first. The shell draws no back arrows or
|
|
146
|
+
* ✕ of its own, so this (or `S.box`'s `close` option) is how a panel gives
|
|
147
|
+
* the user a way out.
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```ts
|
|
151
|
+
* S.button({ content: "Cancel", attrs: ".neutral", click: () => void $page.close() });
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
close(): Promise<boolean>;
|
|
155
|
+
}
|
|
156
|
+
/** Options the panel stack needs from its shell. */
|
|
157
|
+
export interface PanelStackOptions {
|
|
158
|
+
routes: Routes;
|
|
159
|
+
notFound?: RouteHandler<{}>;
|
|
160
|
+
/** Set `false` to show only the top panel, however much room there is. */
|
|
161
|
+
stacking?: boolean;
|
|
162
|
+
/** The shell's own title, used as the suffix of `document.title`. */
|
|
163
|
+
title?: unknown;
|
|
164
|
+
}
|
|
165
|
+
export declare class PanelController {
|
|
166
|
+
private compiled;
|
|
167
|
+
private opts;
|
|
168
|
+
/** The live stack, shallow-to-deep. Closing panels are no longer part of it. */
|
|
169
|
+
private live;
|
|
170
|
+
private byId;
|
|
171
|
+
private nextId;
|
|
172
|
+
/** Drives rendering: panel id → its `order` (used only as the sort key). */
|
|
173
|
+
$ids: Record<string, number>;
|
|
174
|
+
/**
|
|
175
|
+
* The live stack's paths and its top panel, for reactive readers: the
|
|
176
|
+
* `document.title` watcher, `main()`'s Escape handling and `S.panels.stack`.
|
|
177
|
+
*/
|
|
178
|
+
$state: {
|
|
179
|
+
paths: string[];
|
|
180
|
+
topId: number;
|
|
181
|
+
};
|
|
182
|
+
private containerEl?;
|
|
183
|
+
/** The shell's measurements, shared by everything drawn since they were taken. */
|
|
184
|
+
private geom?;
|
|
185
|
+
/** The body width at the last layout; a change means a window resize → snap. */
|
|
186
|
+
private lastBodyW;
|
|
187
|
+
private layoutQueued;
|
|
188
|
+
private timers;
|
|
189
|
+
constructor(opts: PanelStackOptions);
|
|
190
|
+
/** Resolve a path to its route handler + params, falling back to `notFound`. */
|
|
191
|
+
private resolve;
|
|
192
|
+
private matches;
|
|
193
|
+
/**
|
|
194
|
+
* The one derivation rule for origin-less navigation (§2.8): probe every
|
|
195
|
+
* prefix of the path against the route table; the matching prefixes become
|
|
196
|
+
* the stack. Prefixes without a route are simply skipped, so an app that
|
|
197
|
+
* doesn't want one screen stacked under another just doesn't route that
|
|
198
|
+
* prefix. The path itself is always the top panel, matched or not.
|
|
199
|
+
*/
|
|
200
|
+
deriveStack(path: string): string[];
|
|
201
|
+
/** The stack a route implies: its snapshot topped by its path, or — without a snapshot — derived. */
|
|
202
|
+
private targetFor;
|
|
203
|
+
/** The stack the current history entry asks for. Subscribes to path + snapshot. */
|
|
204
|
+
private computeTarget;
|
|
205
|
+
/**
|
|
206
|
+
* The route guard (see `route.setGuard` in the constructor): asked before any
|
|
207
|
+
* route change lands, wherever it came from. Runs the {@link Page.requestClose}
|
|
208
|
+
* guard of every panel the new route's stack would remove — a set defined by
|
|
209
|
+
* the target (the commit reconciles by path), so a derived stack that shares
|
|
210
|
+
* nothing with the live one still asks exactly the panels that are closing.
|
|
211
|
+
*/
|
|
212
|
+
private checkChange;
|
|
213
|
+
private paths;
|
|
214
|
+
/** The live panels a target stack drops — by path, so a splice removes only its own column. */
|
|
215
|
+
private removedBy;
|
|
216
|
+
/**
|
|
217
|
+
* Adopt a stack proposed by the URL. Close guards have already been run (and
|
|
218
|
+
* have passed) by the time a route change is visible here — `checkChange` is
|
|
219
|
+
* consulted by the router itself, before anything is applied.
|
|
220
|
+
*/
|
|
221
|
+
private propose;
|
|
222
|
+
/**
|
|
223
|
+
* Apply a target stack: unmount what's gone, mount what's new, animate the
|
|
224
|
+
* difference.
|
|
225
|
+
*
|
|
226
|
+
* Reconciliation is BY PATH (a stack can't hold the same path twice, so that's
|
|
227
|
+
* well-defined): a panel present in both stacks stays mounted *even if its
|
|
228
|
+
* index shifted*, which is what lets a panel be spliced out of the middle
|
|
229
|
+
* (§7) without disturbing the columns above it. A common-prefix diff would
|
|
230
|
+
* remount every one of them, throwing away exactly the scroll and form state
|
|
231
|
+
* rule 5 promises to keep.
|
|
232
|
+
*/
|
|
233
|
+
private commit;
|
|
234
|
+
private createEntry;
|
|
235
|
+
/**
|
|
236
|
+
* Take a panel out of the shell. The *scope* goes now: its cleaners run this
|
|
237
|
+
* tick, so whatever the panel registered with `A.clean` — subscriptions,
|
|
238
|
+
* timers, an open portal — is torn down when the panel closes, not when its
|
|
239
|
+
* animation is over. Only the element lingers, to play that animation, which
|
|
240
|
+
* is what the `destroy=` hook in `drawPanel` is for: Aberdeen hands the
|
|
241
|
+
* element to {@link playExit} instead of removing it.
|
|
242
|
+
*/
|
|
243
|
+
private beginClose;
|
|
244
|
+
/**
|
|
245
|
+
* A closed panel's send-off, run by Aberdeen once the panel's scope is gone (so
|
|
246
|
+
* the content it shows is frozen, which is exactly what a departing column
|
|
247
|
+
* should be): it fades where it stands, inert, and leaves the DOM when the fade
|
|
248
|
+
* itself ends. Removing it on a fixed timer instead would race the transition —
|
|
249
|
+
* pull the element a frame early and the panel appears to fade half-way and
|
|
250
|
+
* then vanish. The timeout is just a fallback for when no `transitionend` is
|
|
251
|
+
* coming at all (transitions off, or an element that never got placed).
|
|
252
|
+
*/
|
|
253
|
+
private playExit;
|
|
254
|
+
/**
|
|
255
|
+
* Navigate back to a stack that is a truncation of the current one — the shared
|
|
256
|
+
* implementation of Escape, a page closing itself, return-links and
|
|
257
|
+
* `S.panels.close()`. `route.back()` prefers the history entry where that
|
|
258
|
+
* panel was on top (with its scroll state intact); when there is no such entry
|
|
259
|
+
* it replaces the current one, carrying the snapshot passed as the fallback.
|
|
260
|
+
* Either way the route guard asks the closing panels first, and the returned
|
|
261
|
+
* promise reports its verdict.
|
|
262
|
+
*/
|
|
263
|
+
private goBackTo;
|
|
264
|
+
/** Close every panel above `index` (guarded). Resolves `false` when vetoed. */
|
|
265
|
+
closeDownTo(index: number): Promise<boolean>;
|
|
266
|
+
/** Guarded close of the top panel. */
|
|
267
|
+
closeTop(): Promise<boolean>;
|
|
268
|
+
/**
|
|
269
|
+
* Guarded close of the panel at `index`, top of the stack or not — what a
|
|
270
|
+
* page's own close affordances ({@link Page.close}, a box's ✕) come down to.
|
|
271
|
+
*
|
|
272
|
+
* The top panel pops back to the snapshot beneath it. Any other panel is
|
|
273
|
+
* *spliced* out: its guard runs, the columns above it keep their place and
|
|
274
|
+
* state (the commit reconciles by path), and the URL doesn't change, since the
|
|
275
|
+
* top panel didn't. That still gets its own history entry, so the browser's
|
|
276
|
+
* back button restores the closed column like any other snapshot — which is
|
|
277
|
+
* why it goes through `route.go` here rather than through `navigate()`, whose
|
|
278
|
+
* "link to the panel we're already on" check would see a no-op.
|
|
279
|
+
*/
|
|
280
|
+
closePanelAt(index: number): Promise<boolean>;
|
|
281
|
+
/** Guarded close of whichever panel `path` is open as. False when it isn't open. */
|
|
282
|
+
closeByPath(path: string): Promise<boolean>;
|
|
283
|
+
/** Guarded close of the panel whose `.s-panel` element this is. */
|
|
284
|
+
closePanelEl(el: HTMLElement): Promise<boolean>;
|
|
285
|
+
/**
|
|
286
|
+
* Navigate to `href`. `originIndex` is the depth of the panel the link lives
|
|
287
|
+
* in (−1 when it has none — a nav item or a programmatic call, which derives
|
|
288
|
+
* the whole stack instead). `replace` swaps the originating panel rather than
|
|
289
|
+
* stacking on top of it.
|
|
290
|
+
*/
|
|
291
|
+
navigate(href: string, originIndex: number, replace?: boolean): void;
|
|
292
|
+
/** Programmatic push/replace, with the top panel as the implied origin. */
|
|
293
|
+
pushPath(path: string, replace: boolean): void;
|
|
294
|
+
/**
|
|
295
|
+
* Link handling through `route.interceptLinks()`, whose handler hook hands us
|
|
296
|
+
* the anchor so we can decide what the click *means*: the originating
|
|
297
|
+
* `.s-panel` (which decides what the click truncates), `data-panel=replace`,
|
|
298
|
+
* and return-to-an-open-panel semantics. The exclusion rules (targets,
|
|
299
|
+
* downloads, modified clicks, external URLs) live in Aberdeen; the close
|
|
300
|
+
* guards run in `checkChange` when our navigation reaches the router.
|
|
301
|
+
*/
|
|
302
|
+
private interceptLinks;
|
|
303
|
+
/** `"<page title> · <app title>"`, kept in sync with the top panel. */
|
|
304
|
+
private watchTitle;
|
|
305
|
+
/**
|
|
306
|
+
* Draw the panel viewport into the current element. Called by `main()`.
|
|
307
|
+
*
|
|
308
|
+
* There is deliberately no close chrome here — no back rail, no ←: pages
|
|
309
|
+
* provide their own way out (see {@link Page.close} and `S.box`'s `close`
|
|
310
|
+
* option). The shell contributes Escape and the browser's own back button.
|
|
311
|
+
*/
|
|
312
|
+
drawStack(): void;
|
|
313
|
+
private drawPanel;
|
|
314
|
+
scheduleLayout(): void;
|
|
315
|
+
/**
|
|
316
|
+
* Measure the shell, and with it the width the window gives a panel of each
|
|
317
|
+
* layout. Measured on the *shell*, not on the panel region: the region's width
|
|
318
|
+
* is the layout engine's own output, so reading it back would nail the layout
|
|
319
|
+
* to whatever it happened to be a frame ago. Fractional widths throughout — a
|
|
320
|
+
* rounded column edge would drift a pixel away from the chrome above it.
|
|
321
|
+
*
|
|
322
|
+
* `undefined` while the shell has no width to speak of (it isn't in a document
|
|
323
|
+
* yet, or it's `display:none`); the next pass tries again.
|
|
324
|
+
*/
|
|
325
|
+
private measure;
|
|
326
|
+
/**
|
|
327
|
+
* The measurements this pass runs on. Taken once per layout pass and per
|
|
328
|
+
* commit, and shared with the panels drawn in between — they all size
|
|
329
|
+
* themselves against the same shell, and a `getBoundingClientRect()` each
|
|
330
|
+
* would be a forced reflow each, in the middle of building their DOM.
|
|
331
|
+
*/
|
|
332
|
+
private geometry;
|
|
333
|
+
/** How wide a panel of this layout is, right now; 0 while the shell can't be measured. */
|
|
334
|
+
private roomFor;
|
|
335
|
+
/**
|
|
336
|
+
* Size and position every panel, and publish the width of the whole ensemble
|
|
337
|
+
* (sidebar + separator + columns) for the shell to centre itself on.
|
|
338
|
+
*
|
|
339
|
+
* This is everything CSS can't work out for itself: which panels exist, which
|
|
340
|
+
* of them are visible, how wide each one is and where it sits. All the motion
|
|
341
|
+
* between two of these arrangements is CSS's job.
|
|
342
|
+
*/
|
|
343
|
+
private layout;
|
|
344
|
+
/** Let a `loading` panel's enter animation wait — but not indefinitely. */
|
|
345
|
+
private holdEnter;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Navigating the routed `S.main()` shell from code, for the times it isn't a
|
|
349
|
+
* link click, such as opening the screen for a record you just created.
|
|
350
|
+
*
|
|
351
|
+
* The same rules as a link click apply: pushing a path that is already open
|
|
352
|
+
* goes back to it rather than opening it twice, and anything that would close a
|
|
353
|
+
* panel asks its {@link Page.requestClose} first.
|
|
354
|
+
*
|
|
355
|
+
* @example
|
|
356
|
+
* ```ts
|
|
357
|
+
* S.button({ content: "New task", click: async () => {
|
|
358
|
+
* const task = await createTask();
|
|
359
|
+
* S.panels.push(`/tasks/${task.id}`);
|
|
360
|
+
* }});
|
|
361
|
+
* ```
|
|
362
|
+
*/
|
|
363
|
+
export declare const panels: {
|
|
364
|
+
/** Opens `path` in a new panel on top of the top one. */
|
|
365
|
+
push(path: string): void;
|
|
366
|
+
/**
|
|
367
|
+
* Opens `path` in place of the top panel, which closes (asking its
|
|
368
|
+
* {@link Page.requestClose} first). The panels beneath it stay as they are.
|
|
369
|
+
*/
|
|
370
|
+
replace(path: string): void;
|
|
371
|
+
/**
|
|
372
|
+
* Closes the top panel, or, given a `path`, whichever panel is open at it,
|
|
373
|
+
* asking {@link Page.requestClose} first. A panel that isn't on top is taken
|
|
374
|
+
* out on its own, leaving the columns to its right exactly as they are.
|
|
375
|
+
*
|
|
376
|
+
* Resolves `false` if the panel didn't close: `requestClose` said no, `path`
|
|
377
|
+
* isn't open, or another navigation got there first.
|
|
378
|
+
*/
|
|
379
|
+
close(path?: string): Promise<boolean>;
|
|
380
|
+
/** The paths of the open panels, oldest first. Reactive: safe to read in a scope. */
|
|
381
|
+
readonly stack: readonly string[];
|
|
382
|
+
};
|
|
383
|
+
/**
|
|
384
|
+
* Closes the panel `el` sits in, working out which one that is from the DOM.
|
|
385
|
+
* That is what lets a close button work without being handed a `$page`, from
|
|
386
|
+
* any column, whether or not it is on top. Used by `S.box`'s `close: true`.
|
|
387
|
+
*
|
|
388
|
+
* Outside a routed shell (or outside any panel, such as a box in a dialog) there is
|
|
389
|
+
* nothing to close: it warns and resolves `false`.
|
|
390
|
+
*/
|
|
391
|
+
export declare function closeContainingPanel(el: Element | null | undefined): Promise<boolean>;
|
|
392
|
+
export {};
|