staffa 0.8.0 → 0.9.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 +34 -22
- package/dist/components/main.d.ts +66 -1
- package/dist/components/main.js +92 -15
- package/dist/components/menu.js +8 -1
- package/dist/components/panels.d.ts +149 -23
- package/dist/components/panels.js +361 -135
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/staffa.esm.js +1 -1
- package/package.json +2 -2
- package/skill/AncestorTable.md +10 -0
- package/skill/MainOptions.md +46 -0
- package/skill/Page.md +13 -2
- package/skill/SKILL.md +55 -22
- package/skill/closeNav.md +23 -0
- package/skill/panels.md +1 -1
- package/src/components/main.ts +141 -15
- package/src/components/menu.ts +7 -1
- package/src/components/panels.ts +413 -141
- package/src/index.ts +2 -2
|
@@ -55,6 +55,21 @@ export type Routes = Record<string, RouteHandler>;
|
|
|
55
55
|
export type RouteTable<R> = {
|
|
56
56
|
[K in keyof R & string]: (page: Page<Prettify<PathParams<K>>>) => void;
|
|
57
57
|
};
|
|
58
|
+
/**
|
|
59
|
+
* What belongs beneath a path that arrives cold, worked out from the params of
|
|
60
|
+
* the path itself. Return the paths shallowest first, or nothing to leave this
|
|
61
|
+
* one to the parent-path derivation.
|
|
62
|
+
*/
|
|
63
|
+
export type AncestorsHandler<P = any> = (params: P, path: string) => readonly string[] | undefined | void;
|
|
64
|
+
/**
|
|
65
|
+
* A table of {@link AncestorsHandler}s keyed by path template, the same way
|
|
66
|
+
* `routes` is — so each one's `params` are matched and typed from its own key
|
|
67
|
+
* rather than parsed out of the path a second time. The keys are checked
|
|
68
|
+
* against the route table, so a stale one is a type error.
|
|
69
|
+
*/
|
|
70
|
+
export type AncestorTable<R> = {
|
|
71
|
+
[K in keyof R & string]?: (params: Prettify<PathParams<K>>, path: string) => readonly string[] | undefined | void;
|
|
72
|
+
};
|
|
58
73
|
/**
|
|
59
74
|
* What a route handler gets: the params from its route, plus everything the
|
|
60
75
|
* shell needs to know about the panel it is drawing. It's an Aberdeen proxy, so
|
|
@@ -101,8 +116,19 @@ export interface Page<P = Record<string, string | number | string[]>> {
|
|
|
101
116
|
*
|
|
102
117
|
* A panel's width depends only on the size of the window, never on what else
|
|
103
118
|
* is open, so opening or closing a panel never resizes the ones already on
|
|
104
|
-
* screen.
|
|
105
|
-
*
|
|
119
|
+
* screen.
|
|
120
|
+
*
|
|
121
|
+
* The panel is sized from this **before** your handler runs, so anything that
|
|
122
|
+
* measures its own box has a real one from the first frame. What it is sized
|
|
123
|
+
* at is whatever this says at that moment, which for a brand-new panel is the
|
|
124
|
+
* default: a handler that *assigns* `layout` is drawn at the medium width and
|
|
125
|
+
* reflowed immediately after — in time for the frame, but not for a
|
|
126
|
+
* measurement taken in the same breath.
|
|
127
|
+
*
|
|
128
|
+
* Assigning it later works just as well. When your data arrives and you find
|
|
129
|
+
* you want the wide one, the panel reflows to its new width without being
|
|
130
|
+
* redrawn — so nothing in it is rebuilt or loses its state — and the columns
|
|
131
|
+
* beside it move over.
|
|
106
132
|
*/
|
|
107
133
|
layout?: "small" | "medium" | "large";
|
|
108
134
|
/**
|
|
@@ -146,6 +172,8 @@ export interface Page<P = Record<string, string | number | string[]>> {
|
|
|
146
172
|
export interface PanelStackOptions {
|
|
147
173
|
routes: Routes;
|
|
148
174
|
notFound?: RouteHandler<{}>;
|
|
175
|
+
/** What to open beneath a path that arrives cold. See {@link MainOptions.ancestors}. */
|
|
176
|
+
ancestors?: Record<string, AncestorsHandler | undefined>;
|
|
149
177
|
/** Set `false` to show only the top panel, however much room there is. */
|
|
150
178
|
stacking?: boolean;
|
|
151
179
|
/** The shell's own title, used as the suffix of `document.title`. */
|
|
@@ -153,6 +181,8 @@ export interface PanelStackOptions {
|
|
|
153
181
|
}
|
|
154
182
|
export declare class PanelController {
|
|
155
183
|
private compiled;
|
|
184
|
+
/** The `ancestors` table, compiled like the routes it is keyed by. */
|
|
185
|
+
private ancestors;
|
|
156
186
|
private opts;
|
|
157
187
|
/** The live stack, shallow-to-deep. Closing panels are no longer part of it. */
|
|
158
188
|
private live;
|
|
@@ -169,22 +199,46 @@ export declare class PanelController {
|
|
|
169
199
|
topId: number;
|
|
170
200
|
};
|
|
171
201
|
private containerEl?;
|
|
202
|
+
/** The shell's measurements, shared by everything drawn since they were taken. */
|
|
203
|
+
private geom?;
|
|
172
204
|
/** The body width at the last layout; a change means a window resize → snap. */
|
|
173
205
|
private lastBodyW;
|
|
174
206
|
private layoutQueued;
|
|
175
207
|
private timers;
|
|
208
|
+
/** The stack the navigation in flight is heading for; see {@link intended}. */
|
|
209
|
+
private intent;
|
|
210
|
+
/** The navigation the router hasn't settled yet, if any. */
|
|
211
|
+
private settling;
|
|
212
|
+
/** The one navigation waiting behind it; see {@link issue}. */
|
|
213
|
+
private queued;
|
|
176
214
|
constructor(opts: PanelStackOptions);
|
|
177
215
|
/** Resolve a path to its route handler + params, falling back to `notFound`. */
|
|
178
216
|
private resolve;
|
|
179
217
|
private matches;
|
|
180
218
|
/**
|
|
181
|
-
* The
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
219
|
+
* The stack for origin-less navigation: a cold deep link, a nav item, a
|
|
220
|
+
* `route.go()` — anything arriving without a panel to build on and without a
|
|
221
|
+
* snapshot to restore.
|
|
222
|
+
*
|
|
223
|
+
* The app's {@link PanelStackOptions.ancestors} gets first say, since only it
|
|
224
|
+
* can know what belongs under a path that doesn't spell its own context out
|
|
225
|
+
* (a `/thread/[id]` reached from a notification). Failing that — or when it
|
|
226
|
+
* has no opinion — every prefix of the path is probed against the route table
|
|
227
|
+
* and the matching ones become the stack. Either way, a path with no route is
|
|
228
|
+
* skipped rather than opened as a "not found" column, so an app that doesn't
|
|
229
|
+
* want one screen stacked under another simply doesn't route it. The path
|
|
230
|
+
* itself is always the top panel, matched or not.
|
|
186
231
|
*/
|
|
187
232
|
deriveStack(path: string): string[];
|
|
233
|
+
/**
|
|
234
|
+
* Ask the `ancestors` table what belongs beneath `path`. The first key that
|
|
235
|
+
* matches answers — with its own matched params, so it never has to take the
|
|
236
|
+
* path apart itself — and `undefined` from it means "no opinion", leaving the
|
|
237
|
+
* path to the prefix derivation just as an unlisted one is.
|
|
238
|
+
*/
|
|
239
|
+
private askAncestors;
|
|
240
|
+
/** Every prefix of `path` that has a route, shallowest first. */
|
|
241
|
+
private prefixesOf;
|
|
188
242
|
/** The stack a route implies: its snapshot topped by its path, or — without a snapshot — derived. */
|
|
189
243
|
private targetFor;
|
|
190
244
|
/** The stack the current history entry asks for. Subscribes to path + snapshot. */
|
|
@@ -220,14 +274,47 @@ export declare class PanelController {
|
|
|
220
274
|
private commit;
|
|
221
275
|
private createEntry;
|
|
222
276
|
/**
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
277
|
+
* Take a panel out of the shell. The *scope* goes now: its cleaners run this
|
|
278
|
+
* tick, so whatever the panel registered with `A.clean` — subscriptions,
|
|
279
|
+
* timers, an open portal — is torn down when the panel closes, not when its
|
|
280
|
+
* animation is over. Only the element lingers, to play that animation, which
|
|
281
|
+
* is what the `destroy=` hook in `drawPanel` is for: Aberdeen hands the
|
|
282
|
+
* element to {@link playExit} instead of removing it.
|
|
229
283
|
*/
|
|
230
284
|
private beginClose;
|
|
285
|
+
/**
|
|
286
|
+
* A closed panel's send-off, run by Aberdeen once the panel's scope is gone (so
|
|
287
|
+
* the content it shows is frozen, which is exactly what a departing column
|
|
288
|
+
* should be): it fades where it stands, inert, and leaves the DOM when the fade
|
|
289
|
+
* itself ends. Removing it on a fixed timer instead would race the transition —
|
|
290
|
+
* pull the element a frame early and the panel appears to fade half-way and
|
|
291
|
+
* then vanish. The timeout is just a fallback for when no `transitionend` is
|
|
292
|
+
* coming at all (transitions off, or an element that never got placed).
|
|
293
|
+
*/
|
|
294
|
+
private playExit;
|
|
295
|
+
/**
|
|
296
|
+
* The stack navigation works from: the one we're on the way to while a change
|
|
297
|
+
* is still settling, and the one on screen otherwise.
|
|
298
|
+
*
|
|
299
|
+
* Settling takes a moment more often than it looks: an async
|
|
300
|
+
* {@link Page.requestClose}, and every `route.back()`, which travels through
|
|
301
|
+
* the browser's history and lands on a `popstate`. Working from the committed
|
|
302
|
+
* stack in that window would make a second Escape ask for the panel the first
|
|
303
|
+
* one is already taking away — so two quick Escapes would peel one panel.
|
|
304
|
+
*/
|
|
305
|
+
private intended;
|
|
306
|
+
/**
|
|
307
|
+
* Put a navigation to the router, or — while one is still settling — behind
|
|
308
|
+
* the one that is. Only the newest waits: each was worked out against
|
|
309
|
+
* {@link intended}, so the newest is the one that means what the user last
|
|
310
|
+
* asked for, and the one it displaces resolves `false`.
|
|
311
|
+
*
|
|
312
|
+
* A refusal empties the queue instead of running it. A veto is a "no, keep
|
|
313
|
+
* this open", and the Escape queued behind it was aimed a panel deeper — with
|
|
314
|
+
* the veto standing, running it would close the very panel that just said no.
|
|
315
|
+
*/
|
|
316
|
+
private issue;
|
|
317
|
+
private start;
|
|
231
318
|
/**
|
|
232
319
|
* Navigate back to a stack that is a truncation of the current one — the shared
|
|
233
320
|
* implementation of Escape, a page closing itself, return-links and
|
|
@@ -243,8 +330,9 @@ export declare class PanelController {
|
|
|
243
330
|
/** Guarded close of the top panel. */
|
|
244
331
|
closeTop(): Promise<boolean>;
|
|
245
332
|
/**
|
|
246
|
-
* Guarded close of
|
|
247
|
-
* page's own close affordances ({@link Page.close}, a box's ✕) come
|
|
333
|
+
* Guarded close of whichever panel is open at `path`, top of the stack or not
|
|
334
|
+
* — what a page's own close affordances ({@link Page.close}, a box's ✕) come
|
|
335
|
+
* down to. `false` when that path isn't open.
|
|
248
336
|
*
|
|
249
337
|
* The top panel pops back to the snapshot beneath it. Any other panel is
|
|
250
338
|
* *spliced* out: its guard runs, the columns above it keep their place and
|
|
@@ -254,20 +342,21 @@ export declare class PanelController {
|
|
|
254
342
|
* why it goes through `route.go` here rather than through `navigate()`, whose
|
|
255
343
|
* "link to the panel we're already on" check would see a no-op.
|
|
256
344
|
*/
|
|
257
|
-
|
|
258
|
-
/** Guarded close of whichever panel `path` is open as. False when it isn't open. */
|
|
259
|
-
closeByPath(path: string): Promise<boolean>;
|
|
345
|
+
closePath(path: string): Promise<boolean>;
|
|
260
346
|
/** Guarded close of the panel whose `.s-panel` element this is. */
|
|
261
347
|
closePanelEl(el: HTMLElement): Promise<boolean>;
|
|
262
348
|
/**
|
|
263
|
-
* Navigate to `href`. `
|
|
264
|
-
*
|
|
265
|
-
* the whole stack instead). `replace` swaps the
|
|
266
|
-
* stacking on top of it
|
|
349
|
+
* Navigate to `href`. `origin` is the path of the panel the link lives in, or
|
|
350
|
+
* `null` when it has none — a nav item, or a programmatic call, which builds
|
|
351
|
+
* the whole stack instead (see {@link deriveStack}). `replace` swaps the
|
|
352
|
+
* originating panel rather than stacking on top of it, and `beneath` says what
|
|
353
|
+
* the stack under the target is outright, for callers that know.
|
|
267
354
|
*/
|
|
268
|
-
navigate(href: string,
|
|
355
|
+
navigate(href: string, origin: string | null, replace?: boolean, beneath?: readonly string[]): void;
|
|
269
356
|
/** Programmatic push/replace, with the top panel as the implied origin. */
|
|
270
357
|
pushPath(path: string, replace: boolean): void;
|
|
358
|
+
/** Programmatic open-as-a-whole-stack: `beneath` as given, or derived. */
|
|
359
|
+
openPath(path: string, beneath?: readonly string[]): void;
|
|
271
360
|
/**
|
|
272
361
|
* Link handling through `route.interceptLinks()`, whose handler hook hands us
|
|
273
362
|
* the anchor so we can decide what the click *means*: the originating
|
|
@@ -289,6 +378,26 @@ export declare class PanelController {
|
|
|
289
378
|
drawStack(): void;
|
|
290
379
|
private drawPanel;
|
|
291
380
|
scheduleLayout(): void;
|
|
381
|
+
/**
|
|
382
|
+
* Measure the shell, and with it the width the window gives a panel of each
|
|
383
|
+
* layout. Measured on the *shell*, not on the panel region: the region's width
|
|
384
|
+
* is the layout engine's own output, so reading it back would nail the layout
|
|
385
|
+
* to whatever it happened to be a frame ago. Fractional widths throughout — a
|
|
386
|
+
* rounded column edge would drift a pixel away from the chrome above it.
|
|
387
|
+
*
|
|
388
|
+
* `undefined` while the shell has no width to speak of (it isn't in a document
|
|
389
|
+
* yet, or it's `display:none`); the next pass tries again.
|
|
390
|
+
*/
|
|
391
|
+
private measure;
|
|
392
|
+
/**
|
|
393
|
+
* The measurements this pass runs on. Taken once per layout pass and per
|
|
394
|
+
* commit, and shared with the panels drawn in between — they all size
|
|
395
|
+
* themselves against the same shell, and a `getBoundingClientRect()` each
|
|
396
|
+
* would be a forced reflow each, in the middle of building their DOM.
|
|
397
|
+
*/
|
|
398
|
+
private geometry;
|
|
399
|
+
/** How wide a panel of this layout is, right now; 0 while the shell can't be measured. */
|
|
400
|
+
private roomFor;
|
|
292
401
|
/**
|
|
293
402
|
* Size and position every panel, and publish the width of the whole ensemble
|
|
294
403
|
* (sidebar + separator + columns) for the shell to centre itself on.
|
|
@@ -325,6 +434,23 @@ export declare const panels: {
|
|
|
325
434
|
* {@link Page.requestClose} first). The panels beneath it stay as they are.
|
|
326
435
|
*/
|
|
327
436
|
replace(path: string): void;
|
|
437
|
+
/**
|
|
438
|
+
* Opens `path` as a whole arrangement rather than on top of what's there: the
|
|
439
|
+
* same thing a nav item or a fresh tab does. Without `beneath`, the stack under
|
|
440
|
+
* it is worked out the way a cold link's is (see `S.main()`'s `ancestors`);
|
|
441
|
+
* with it, the paths you give are opened underneath, shallowest first.
|
|
442
|
+
*
|
|
443
|
+
* That's the one for a screen whose URL doesn't say where it belongs — the
|
|
444
|
+
* thread a notification opens — and for seeding a stack from code in general.
|
|
445
|
+
* Panels the new arrangement also holds stay as they are, and any it drops are
|
|
446
|
+
* asked their {@link Page.requestClose} first.
|
|
447
|
+
*
|
|
448
|
+
* @example
|
|
449
|
+
* ```ts
|
|
450
|
+
* S.panels.open(`/thread/${id}`, [`/mailbox/${mailboxId}`]);
|
|
451
|
+
* ```
|
|
452
|
+
*/
|
|
453
|
+
open(path: string, beneath?: readonly string[]): void;
|
|
328
454
|
/**
|
|
329
455
|
* Closes the top panel, or, given a `path`, whichever panel is open at it,
|
|
330
456
|
* asking {@link Page.requestClose} first. A panel that isn't on top is taken
|