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
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
import { type Slot } from "../core.js";
|
|
2
|
+
/**
|
|
3
|
+
* Routed, multi-column page navigation for {@link main}.
|
|
4
|
+
*
|
|
5
|
+
* Each route draws one screen of the app, called a page. The open pages form
|
|
6
|
+
* a *trail*, and one of them is the **current** page: the one the URL names,
|
|
7
|
+
* and the rightmost column on screen. As many pages as fit are shown, ending
|
|
8
|
+
* at the current one — on a phone that is one at a time, on a wider screen the
|
|
9
|
+
* pages that would have covered each other sit side by side instead. The
|
|
10
|
+
* app's own code is the same either way.
|
|
11
|
+
*
|
|
12
|
+
* Going to a page that is already open — a breadcrumb, or any link to it —
|
|
13
|
+
* just moves the current-page cursor along the trail: pages right of it stay
|
|
14
|
+
* open, parked past the right edge of the viewport, and nothing closes.
|
|
15
|
+
* Opening a *new* page is what prunes: everything after the page it came from
|
|
16
|
+
* closes, except pages the user pinned — which ride along beneath the new page
|
|
17
|
+
* — and pages holding unsaved work, which no navigation ever tears down.
|
|
18
|
+
* Escape steps one page left, closing the page it leaves only when that page
|
|
19
|
+
* is the trail's discardable end.
|
|
20
|
+
*
|
|
21
|
+
* Navigation runs through `aberdeen/route`: the URL holds the current page,
|
|
22
|
+
* and the rest of the arrangement — the pages before it, the ones parked after
|
|
23
|
+
* it, and which are pinned — is stored beside it in the history entry. So back
|
|
24
|
+
* and forward step through whole arrangements of columns, and a reload (or a
|
|
25
|
+
* shared link) brings the same columns back.
|
|
26
|
+
*/
|
|
27
|
+
/** Flattens an intersection into a single object type, so hovers read nicely. */
|
|
28
|
+
type Prettify<T> = {
|
|
29
|
+
[K in keyof T]: T[K];
|
|
30
|
+
} & {};
|
|
31
|
+
/**
|
|
32
|
+
* What a `[name=matcher]` matcher name yields. An unrecognised name resolves to
|
|
33
|
+
* `never`, which shows up as an unusable param at the handler rather than
|
|
34
|
+
* quietly typing as `string` (the route key itself throws at mount time).
|
|
35
|
+
*/
|
|
36
|
+
export type MatcherType<M extends string> = M extends "integer" ? number : never;
|
|
37
|
+
/**
|
|
38
|
+
* The params contributed by a single path-template segment: `[x]` a string,
|
|
39
|
+
* `[x=integer]` a number, `[...x]` the rest of the path as one raw string.
|
|
40
|
+
*/
|
|
41
|
+
export type SegParams<S extends string> = S extends `[...${infer Name}]` ? {
|
|
42
|
+
[K in Name]: string;
|
|
43
|
+
} : S extends `[${infer Name}=${infer Matcher}]` ? {
|
|
44
|
+
[K in Name]: MatcherType<Matcher>;
|
|
45
|
+
} : S extends `[${infer Name}]` ? {
|
|
46
|
+
[K in Name]: string;
|
|
47
|
+
} : {};
|
|
48
|
+
/**
|
|
49
|
+
* The params object described by a path template, e.g.
|
|
50
|
+
* `PathParams<"/projects/[id]/tasks/[taskId=integer]">` is
|
|
51
|
+
* `{ id: string; taskId: number }`.
|
|
52
|
+
*/
|
|
53
|
+
export type PathParams<P extends string> = P extends `${infer Head}/${infer Rest}` ? SegParams<Head> & PathParams<Rest> : SegParams<P>;
|
|
54
|
+
/** A page draw function: it receives the page's {@link Page} and draws into the current scope. */
|
|
55
|
+
export type RouteHandler<P = any> = (page: Page<P>) => void;
|
|
56
|
+
/**
|
|
57
|
+
* A route table: path templates mapped to page draw functions. Used as the
|
|
58
|
+
* loose (non-inferred) type; `S.main()` infers a more precise type from the
|
|
59
|
+
* literal you pass, so each handler's `$page.params` is typed per its key.
|
|
60
|
+
*/
|
|
61
|
+
export type Routes = Record<string, RouteHandler>;
|
|
62
|
+
/**
|
|
63
|
+
* The shape `S.main()`'s `routes` option is checked against: every key types its
|
|
64
|
+
* own handler's `params`. Used as a self-referential generic constraint, which
|
|
65
|
+
* is what makes `$page.params` infer from the route key.
|
|
66
|
+
*/
|
|
67
|
+
export type RouteTable<R> = {
|
|
68
|
+
[K in keyof R & string]: (page: Page<Prettify<PathParams<K>>>) => void;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* What belongs beneath a path that arrives cold, worked out from the params of
|
|
72
|
+
* the path itself. Return the paths shallowest first, or nothing to leave this
|
|
73
|
+
* one to the parent-path derivation.
|
|
74
|
+
*/
|
|
75
|
+
export type AncestorsHandler<P = any> = (params: P, path: string) => readonly string[] | undefined | void;
|
|
76
|
+
/**
|
|
77
|
+
* A table of {@link AncestorsHandler}s keyed by path template, the same way
|
|
78
|
+
* `routes` is — so each one's `params` are matched and typed from its own key
|
|
79
|
+
* rather than parsed out of the path a second time. The keys are checked
|
|
80
|
+
* against the route table, so a stale one is a type error.
|
|
81
|
+
*/
|
|
82
|
+
export type AncestorTable<R> = {
|
|
83
|
+
[K in keyof R & string]?: (params: Prettify<PathParams<K>>, path: string) => readonly string[] | undefined | void;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* What a route handler gets: the params from its route, plus everything the
|
|
87
|
+
* shell needs to know about the page it is drawing. It's an Aberdeen proxy, so
|
|
88
|
+
* you can set things later, such as a `title` that arrives with your data or
|
|
89
|
+
* `loading` going back to `false`, and the shell keeps up.
|
|
90
|
+
*
|
|
91
|
+
* Search params and the `#hash` belong to the current page only. Any other
|
|
92
|
+
* page keeps just its path, so anything a page needs in order to redraw
|
|
93
|
+
* itself has to live in that path. (A page browsed away from does get its
|
|
94
|
+
* search and hash back when a crumb makes it current again.)
|
|
95
|
+
*/
|
|
96
|
+
export interface Page<P = Record<string, string | number | string[]>> {
|
|
97
|
+
/**
|
|
98
|
+
* The params matched from this page's path, typed per its route key:
|
|
99
|
+
* `[x]` is a `string`, `[x=integer]` a `number`, `[...x]` a `string`.
|
|
100
|
+
* Read-only.
|
|
101
|
+
*/
|
|
102
|
+
readonly params: P;
|
|
103
|
+
/** This page's path, e.g. `"/projects/7"`. Read-only. */
|
|
104
|
+
readonly path: string;
|
|
105
|
+
/**
|
|
106
|
+
* Names this screen, in the top bar's breadcrumb trail and in
|
|
107
|
+
* `document.title` while the page is current. A page that doesn't set one
|
|
108
|
+
* borrows the first line of text in its own body, so the trail never shows a
|
|
109
|
+
* blank — but a borrowed paragraph makes a poor name, so say it yourself.
|
|
110
|
+
*
|
|
111
|
+
* It does **not** conjure a heading: naming a screen and heading its content
|
|
112
|
+
* are different jobs, and a screen that wants its name in its own body
|
|
113
|
+
* writes it there, where it owns the typography.
|
|
114
|
+
*/
|
|
115
|
+
title?: string;
|
|
116
|
+
/**
|
|
117
|
+
* This screen's own actions: a couple of buttons, a menu. The shell draws
|
|
118
|
+
* them — never the page — and *where* depends on facts only the shell has:
|
|
119
|
+
* a quiet strip at the top of this page's column while several columns are
|
|
120
|
+
* up, and the top bar (where they take the app's own `menu` slot) once the
|
|
121
|
+
* shell is narrow and this page is the screen.
|
|
122
|
+
*
|
|
123
|
+
* They are drawn in exactly one of those places at a time, so crossing the
|
|
124
|
+
* threshold redraws them; anything stateful inside (the focus in a search
|
|
125
|
+
* box) is lost. Buttons and menus are fine.
|
|
126
|
+
*/
|
|
127
|
+
actions?: Slot;
|
|
128
|
+
/**
|
|
129
|
+
* How wide this page's column actually is, in pixels — what
|
|
130
|
+
* {@link Page.maxWidth} asked for, resolved against the window. Reactive and
|
|
131
|
+
* read-only, and correct *before* your handler draws, so content that sizes
|
|
132
|
+
* itself can read it instead of measuring.
|
|
133
|
+
*
|
|
134
|
+
* You rarely need it: the shell places the chrome for you. It's for content
|
|
135
|
+
* that genuinely differs by width, such as a table that becomes a list.
|
|
136
|
+
*/
|
|
137
|
+
readonly width: number;
|
|
138
|
+
/**
|
|
139
|
+
* Whether this page is on screen right now: not crowded out from under the
|
|
140
|
+
* visible run, not parked past its right end, and not on its way out.
|
|
141
|
+
* Reactive and read-only.
|
|
142
|
+
*
|
|
143
|
+
* The one to hang per-page floating UI on (a FAB, a "3 selected" bar), for
|
|
144
|
+
* which "am I the current page?" is the wrong question — two columns can be
|
|
145
|
+
* visible at once, and both of them are really there.
|
|
146
|
+
*/
|
|
147
|
+
readonly visible: boolean;
|
|
148
|
+
/**
|
|
149
|
+
* The widest this page can usefully be. Every page must work at 360–540px,
|
|
150
|
+
* because that is what it gets when two columns fit; this says how much
|
|
151
|
+
* *more* it can take.
|
|
152
|
+
*
|
|
153
|
+
* - `"half"` — nothing more. Half the content area (360–540px), so a second
|
|
154
|
+
* column fits beside it. For lists and detail forms.
|
|
155
|
+
* - `"full"` (the default) — the whole content area, up to ~1100px.
|
|
156
|
+
* - `"screen"` — the whole window, unbounded: boards, wide tables, dense
|
|
157
|
+
* dashboards. While one is open the shell itself stretches to the screen
|
|
158
|
+
* edges instead of stopping at the standard 1280px page.
|
|
159
|
+
*
|
|
160
|
+
* Below the width two columns need, everything takes the content area
|
|
161
|
+
* whatever it asked for. Widths depend only on the window, never on what
|
|
162
|
+
* else is open, so opening or closing a page never resizes another.
|
|
163
|
+
*
|
|
164
|
+
* Set it at the top of your handler and the page is already that wide when
|
|
165
|
+
* you draw (see {@link Page.width}); set it later — when your data tells you
|
|
166
|
+
* — and the page reflows without being redrawn, keeping its state, while
|
|
167
|
+
* the columns beside it move over.
|
|
168
|
+
*/
|
|
169
|
+
maxWidth?: "half" | "full" | "screen";
|
|
170
|
+
/**
|
|
171
|
+
* Set this while you're fetching what the page needs, and back to `false`
|
|
172
|
+
* when you're done. A new page waits a moment before sliding in, so it can
|
|
173
|
+
* arrive with real content instead of empty; if the wait drags on it slides
|
|
174
|
+
* in anyway and shows a loading indicator until the flag clears. It only
|
|
175
|
+
* affects the animation; the stack and the URL never wait for it.
|
|
176
|
+
*/
|
|
177
|
+
loading?: boolean;
|
|
178
|
+
/**
|
|
179
|
+
* Keeps this page from being closed by navigation happening *elsewhere*.
|
|
180
|
+
* Opening a new page normally closes everything after the page it came
|
|
181
|
+
* from; a pinned page survives that, staying in the trail — parked past the
|
|
182
|
+
* right edge of the viewport — slotted in beneath the new page, one crumb
|
|
183
|
+
* click away. The user toggles it from the crumb's context menu
|
|
184
|
+
* (right-click or long-press), which is also where the pin shows; setting
|
|
185
|
+
* it from code does the same thing.
|
|
186
|
+
*
|
|
187
|
+
* A pin never blocks an *explicit* close: Escape at the trail's end,
|
|
188
|
+
* {@link Page.close}, the crumb menu's Close and `data-page=replace` all
|
|
189
|
+
* still close the page.
|
|
190
|
+
*/
|
|
191
|
+
pinned?: boolean;
|
|
192
|
+
/**
|
|
193
|
+
* Set this while the page holds work that must not be lost — a dirty form,
|
|
194
|
+
* an upload in flight. An unsaved page cannot be closed, by anything:
|
|
195
|
+
* navigation that would prune it parks it instead, past the viewport's
|
|
196
|
+
* right edge, wearing a ● in its crumb — even the browser's back button
|
|
197
|
+
* only parks it. {@link Page.close} and the crumb menu's Close refuse,
|
|
198
|
+
* Escape on it steps left along the trail rather than closing, and closing
|
|
199
|
+
* the browser tab runs into the browser's own are-you-sure (after which the
|
|
200
|
+
* shell brings the unsaved page back on screen).
|
|
201
|
+
*
|
|
202
|
+
* Only the app clears it; the user has no toggle. A Save or Discard button
|
|
203
|
+
* clears it and then closes:
|
|
204
|
+
*
|
|
205
|
+
* ```ts
|
|
206
|
+
* A(() => { $page.unsaved = $form.dirty || undefined; });
|
|
207
|
+
* S.button({ content: "Discard", attrs: ".neutral", click: () => {
|
|
208
|
+
* $page.unsaved = false; // explicitly — see below
|
|
209
|
+
* void $page.close();
|
|
210
|
+
* }});
|
|
211
|
+
* ```
|
|
212
|
+
*
|
|
213
|
+
* The explicit `unsaved = false` before `close()` matters when the flag is
|
|
214
|
+
* kept by a reactive scope, as above: resetting the form marks that scope
|
|
215
|
+
* dirty, but it reruns *after* the running handler — after `close()` has
|
|
216
|
+
* already been refused.
|
|
217
|
+
*/
|
|
218
|
+
unsaved?: boolean;
|
|
219
|
+
/**
|
|
220
|
+
* Closes **this** page, wherever it sits in the trail. Closing the current
|
|
221
|
+
* page hands the focus to the page on its left; closing any other page
|
|
222
|
+
* takes just it away, leaving the columns around it where they are, with
|
|
223
|
+
* their state. Either way it becomes a history entry, so the browser's
|
|
224
|
+
* back button brings the page back.
|
|
225
|
+
*
|
|
226
|
+
* Resolves `false` if the page didn't close: it holds
|
|
227
|
+
* {@link Page.unsaved} work, it was the only page on the trail (so
|
|
228
|
+
* there's nothing to show instead), or another navigation got there first.
|
|
229
|
+
* The shell's breadcrumbs already travel back, so reach for this when a
|
|
230
|
+
* screen wants a more explicit way out: a Cancel button, or a Save that
|
|
231
|
+
* closes.
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* ```ts
|
|
235
|
+
* S.button({ content: "Cancel", attrs: ".neutral", click: () => void $page.close() });
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
close(): Promise<boolean>;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* A {@link Page} as the controller holds it: the same object the handler gets,
|
|
242
|
+
* minus the `readonly`s. `width` and `visible` are read-only *to the app* —
|
|
243
|
+
* they are facts about the page, not requests — but the shell keeps them up to
|
|
244
|
+
* date by writing them, which is what makes reading them reactive.
|
|
245
|
+
*/
|
|
246
|
+
type PageState = {
|
|
247
|
+
-readonly [K in keyof Page<any>]: Page<any>[K];
|
|
248
|
+
};
|
|
249
|
+
/** Options the trail needs from its shell. */
|
|
250
|
+
export interface PageStackOptions {
|
|
251
|
+
routes: Routes;
|
|
252
|
+
notFound?: RouteHandler<{}>;
|
|
253
|
+
/** What to open beneath a path that arrives cold. See {@link MainOptions.ancestors}. */
|
|
254
|
+
ancestors?: Record<string, AncestorsHandler | undefined>;
|
|
255
|
+
/** Set `false` to show only the current page, however much room there is. */
|
|
256
|
+
stacking?: boolean;
|
|
257
|
+
/** The shell's own title, used as the suffix of `document.title`. */
|
|
258
|
+
title?: unknown;
|
|
259
|
+
/**
|
|
260
|
+
* The shell's live narrow flag (see `main()`), which decides where a page's
|
|
261
|
+
* chrome goes: in its own column, or promoted into the top bar. Shared rather
|
|
262
|
+
* than measured again here, so the bar and the columns can't disagree about
|
|
263
|
+
* which regime they are in.
|
|
264
|
+
*/
|
|
265
|
+
$shell: {
|
|
266
|
+
narrow: boolean;
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
export declare class PageController {
|
|
270
|
+
private compiled;
|
|
271
|
+
/** The `ancestors` table, compiled like the routes it is keyed by. */
|
|
272
|
+
private ancestors;
|
|
273
|
+
private opts;
|
|
274
|
+
/** The live trail, oldest first. Closing pages are no longer part of it. */
|
|
275
|
+
private live;
|
|
276
|
+
/** Index into `live` of the current page — the one the URL names. */
|
|
277
|
+
private focus;
|
|
278
|
+
private byId;
|
|
279
|
+
private nextId;
|
|
280
|
+
/** Drives rendering: page id → its `order` (used only as the sort key). */
|
|
281
|
+
$ids: Record<string, number>;
|
|
282
|
+
/**
|
|
283
|
+
* The live trail's paths and its current page, for reactive readers: the
|
|
284
|
+
* `document.title` watcher, `main()`'s Escape handling, the breadcrumbs,
|
|
285
|
+
* and `S.pages.stack`.
|
|
286
|
+
*/
|
|
287
|
+
$state: {
|
|
288
|
+
paths: string[];
|
|
289
|
+
focus: number;
|
|
290
|
+
currentId: number;
|
|
291
|
+
};
|
|
292
|
+
private containerEl?;
|
|
293
|
+
/** The shell's measurements, shared by everything drawn since they were taken. */
|
|
294
|
+
private geom?;
|
|
295
|
+
/** The body width at the last layout; a change means a window resize → snap. */
|
|
296
|
+
private lastBodyW;
|
|
297
|
+
private layoutQueued;
|
|
298
|
+
private timers;
|
|
299
|
+
/** The arrangement the navigation in flight is heading for; see {@link intended}. */
|
|
300
|
+
private intent;
|
|
301
|
+
/** The navigation the router hasn't settled yet, if any. */
|
|
302
|
+
private settling;
|
|
303
|
+
/** The one navigation waiting behind it; see {@link issue}. */
|
|
304
|
+
private queued;
|
|
305
|
+
constructor(opts: PageStackOptions);
|
|
306
|
+
/** Resolve a path to its route handler + params, falling back to `notFound`. */
|
|
307
|
+
private resolve;
|
|
308
|
+
private matches;
|
|
309
|
+
/**
|
|
310
|
+
* The stack for origin-less navigation: a cold deep link, a nav item, a
|
|
311
|
+
* `route.go()` — anything arriving without a page to build on and without a
|
|
312
|
+
* snapshot to restore.
|
|
313
|
+
*
|
|
314
|
+
* The app's {@link PageStackOptions.ancestors} gets first say, since only it
|
|
315
|
+
* can know what belongs under a path that doesn't spell its own context out
|
|
316
|
+
* (a `/thread/[id]` reached from a notification). Failing that — or when it
|
|
317
|
+
* has no opinion — every prefix of the path is probed against the route table
|
|
318
|
+
* and the matching ones become the stack. Either way, a path with no route is
|
|
319
|
+
* skipped rather than opened as a "not found" column, so an app that doesn't
|
|
320
|
+
* want one screen stacked under another simply doesn't route it. The path
|
|
321
|
+
* itself always ends the derived stack, matched or not.
|
|
322
|
+
*/
|
|
323
|
+
deriveTrail(path: string): string[];
|
|
324
|
+
/**
|
|
325
|
+
* Ask the `ancestors` table what belongs beneath `path`. The first key that
|
|
326
|
+
* matches answers — with its own matched params, so it never has to take the
|
|
327
|
+
* path apart itself — and `undefined` from it means "no opinion", leaving the
|
|
328
|
+
* path to the prefix derivation just as an unlisted one is.
|
|
329
|
+
*/
|
|
330
|
+
private askAncestors;
|
|
331
|
+
/** Every prefix of `path` that has a route, shallowest first. */
|
|
332
|
+
private prefixesOf;
|
|
333
|
+
/**
|
|
334
|
+
* The pinned pages among `trail`, in order, minus `omit` — the ones a
|
|
335
|
+
* navigation must carry along rather than close. Pin flags live on the
|
|
336
|
+
* pages themselves, so a path without a live page can't be pinned.
|
|
337
|
+
*/
|
|
338
|
+
private pinnedIn;
|
|
339
|
+
/** Whether the page open at `path` (if any) holds unsaved work. Peeked. */
|
|
340
|
+
private unsavedAt;
|
|
341
|
+
/**
|
|
342
|
+
* The arrangement a route implies: its snapshot around its path, or —
|
|
343
|
+
* without a snapshot — derived, with the new page current at the end and
|
|
344
|
+
* any pinned pages carried along beneath it.
|
|
345
|
+
*/
|
|
346
|
+
private targetFor;
|
|
347
|
+
/** The arrangement the current history entry asks for. Subscribes to path + snapshot. */
|
|
348
|
+
private computeTarget;
|
|
349
|
+
private paths;
|
|
350
|
+
/**
|
|
351
|
+
* Adopt an arrangement proposed by the URL — after repairing it: pages
|
|
352
|
+
* holding unsaved work are never torn down by a navigation, wherever it
|
|
353
|
+
* came from — a link, a nav item, even a browser back to an entry from
|
|
354
|
+
* before the page existed. Whatever the target drops, they stay, parked
|
|
355
|
+
* after the current page and wearing the ● that says why. (They are
|
|
356
|
+
* deliberately not written into history entries: the work they protect
|
|
357
|
+
* lives in the page's DOM, which a reload clears anyway.)
|
|
358
|
+
*/
|
|
359
|
+
private propose;
|
|
360
|
+
/**
|
|
361
|
+
* Apply a target arrangement: unmount what's gone, mount what's new, animate
|
|
362
|
+
* the difference.
|
|
363
|
+
*
|
|
364
|
+
* Reconciliation is BY PATH (a trail can't hold the same path twice, so that's
|
|
365
|
+
* well-defined): a page present in both stacks stays mounted *even if its
|
|
366
|
+
* index shifted*, which is what lets a page be spliced out of the middle
|
|
367
|
+
* (§7) without disturbing the columns above it. A common-prefix diff would
|
|
368
|
+
* remount every one of them, throwing away exactly the scroll and form state
|
|
369
|
+
* rule 5 promises to keep.
|
|
370
|
+
*/
|
|
371
|
+
private commit;
|
|
372
|
+
private createEntry;
|
|
373
|
+
/**
|
|
374
|
+
* Take a page out of the shell. The *scope* goes now: its cleaners run this
|
|
375
|
+
* tick, so whatever the page registered with `A.clean` — subscriptions,
|
|
376
|
+
* timers, an open portal — is torn down when the page closes, not when its
|
|
377
|
+
* animation is over. Only the element lingers, to play that animation, which
|
|
378
|
+
* is what the `destroy=` hook in `drawPage` is for: Aberdeen hands the
|
|
379
|
+
* element to {@link playExit} instead of removing it.
|
|
380
|
+
*/
|
|
381
|
+
private beginClose;
|
|
382
|
+
/**
|
|
383
|
+
* A closed page's send-off, run by Aberdeen once the page's scope is gone (so
|
|
384
|
+
* the content it shows is frozen, which is exactly what a departing column
|
|
385
|
+
* should be): it fades where it stands, inert, and leaves the DOM when the fade
|
|
386
|
+
* itself ends. Removing it on a fixed timer instead would race the transition —
|
|
387
|
+
* pull the element a frame early and the page appears to fade half-way and
|
|
388
|
+
* then vanish. The timeout is just a fallback for when no `transitionend` is
|
|
389
|
+
* coming at all (transitions off, or an element that never got placed).
|
|
390
|
+
*/
|
|
391
|
+
private playExit;
|
|
392
|
+
/**
|
|
393
|
+
* The arrangement navigation works from: the one we're on the way to while a
|
|
394
|
+
* change is still settling, and the one on screen otherwise.
|
|
395
|
+
*
|
|
396
|
+
* Settling takes a moment more often than it looks: every `route.back()`
|
|
397
|
+
* travels through the browser's history and lands on a `popstate`, and an
|
|
398
|
+
* app-registered route guard may be async. Working from the committed
|
|
399
|
+
* arrangement in that window would make a second Escape aim at the page the
|
|
400
|
+
* first one is already taking away — so two quick Escapes would peel one page.
|
|
401
|
+
*/
|
|
402
|
+
private intended;
|
|
403
|
+
/** The history `state` describing `arr` — exactly what `targetFor` reads back. */
|
|
404
|
+
private stateFor;
|
|
405
|
+
/**
|
|
406
|
+
* Stash the URL's search params and hash on the page they belong to — the
|
|
407
|
+
* current one — for when a crumb brings it back (see PageEntry.search).
|
|
408
|
+
* Called as a navigation is issued, just before the URL moves off it.
|
|
409
|
+
*/
|
|
410
|
+
private stashQuery;
|
|
411
|
+
/**
|
|
412
|
+
* Put a navigation to the router, or — while one is still settling — behind
|
|
413
|
+
* the one that is. Only the newest waits: each was worked out against
|
|
414
|
+
* {@link intended}, so the newest is the one that means what the user last
|
|
415
|
+
* asked for, and the one it displaces resolves `false`.
|
|
416
|
+
*
|
|
417
|
+
* A refusal empties the queue instead of running it: a navigation can still
|
|
418
|
+
* fail to land — an app-registered route guard vetoes it, or another one
|
|
419
|
+
* supersedes it — and what was queued behind it was worked out against the
|
|
420
|
+
* arrangement it would have produced.
|
|
421
|
+
*/
|
|
422
|
+
private issue;
|
|
423
|
+
private start;
|
|
424
|
+
/**
|
|
425
|
+
* Make the trail's `index`th page current: the URL and the visible run move
|
|
426
|
+
* to it, while the pages right of it stay open, parked past the right edge
|
|
427
|
+
* of the viewport. Nothing closes; it is a history entry, so the browser's
|
|
428
|
+
* back button returns the focus to where it was. What a click on a
|
|
429
|
+
* breadcrumb — any link to an open page — comes down to.
|
|
430
|
+
*/
|
|
431
|
+
private focusAt;
|
|
432
|
+
/**
|
|
433
|
+
* One step back along the trail — what Escape does. At the trail's
|
|
434
|
+
* end this closes the current page; mid-trail — with pages parked to the
|
|
435
|
+
* right — or when the page holds {@link Page.unsaved} work, the page stays
|
|
436
|
+
* open and the focus just moves to the page on its left, parking the one
|
|
437
|
+
* it leaves. Resolves `false` at the trail's start, where there is no left
|
|
438
|
+
* to go.
|
|
439
|
+
*/
|
|
440
|
+
back(): Promise<boolean>;
|
|
441
|
+
/** Close the current page (refused, like any close, while it is unsaved). */
|
|
442
|
+
closeCurrent(): Promise<boolean>;
|
|
443
|
+
/**
|
|
444
|
+
* Close whichever page is open at `path`, current or not — what
|
|
445
|
+
* {@link Page.close} and the crumb menu's Close come down to. `false` when
|
|
446
|
+
* that path isn't open, is the trail's only page, or holds
|
|
447
|
+
* {@link Page.unsaved} work — nothing may close an unsaved page; the app
|
|
448
|
+
* clears the flag first, which is its explicit "this is now discardable".
|
|
449
|
+
*
|
|
450
|
+
* Closing the current page at the trail's very end pops back through the
|
|
451
|
+
* browser's history to the entry beneath it, when it is there (restoring its
|
|
452
|
+
* scroll and search state); the arrangement is part of the match, so an
|
|
453
|
+
* entry where the closing page was merely parked won't do. Every other
|
|
454
|
+
* close is a *splice*: the columns around the closed one keep their place
|
|
455
|
+
* and state (the commit reconciles by path). That still gets its own
|
|
456
|
+
* history entry, so the browser's back button restores the closed column
|
|
457
|
+
* like any other arrangement — which is why it goes through `route.go` here
|
|
458
|
+
* rather than through `navigate()`, whose "link to an open page" check
|
|
459
|
+
* would turn it into a focus move.
|
|
460
|
+
*/
|
|
461
|
+
closePath(path: string): Promise<boolean>;
|
|
462
|
+
/**
|
|
463
|
+
* Navigate to `href`. `origin` is the path of the page the link lives in, or
|
|
464
|
+
* `null` when it has none — a nav item, or a programmatic call, which builds
|
|
465
|
+
* the whole stack instead (see {@link deriveTrail}). `replace` swaps the
|
|
466
|
+
* originating page rather than stacking on top of it, and `beneath` says what
|
|
467
|
+
* the stack under the target is outright, for callers that know.
|
|
468
|
+
*/
|
|
469
|
+
navigate(href: string, origin: string | null, replace?: boolean, beneath?: readonly string[]): void;
|
|
470
|
+
/** Programmatic push/replace, with the current page as the implied origin. */
|
|
471
|
+
pushPath(path: string, replace: boolean): void;
|
|
472
|
+
/** Programmatic open-as-a-whole-trail: `beneath` as given, or derived. */
|
|
473
|
+
openPath(path: string, beneath?: readonly string[]): void;
|
|
474
|
+
/**
|
|
475
|
+
* Link handling through `route.interceptLinks()`, whose handler hook hands us
|
|
476
|
+
* the anchor so we can decide what the click *means*: the originating
|
|
477
|
+
* `.s-page` (which decides what the click truncates), `data-page=replace`,
|
|
478
|
+
* and return-to-an-open-page semantics. The exclusion rules (targets,
|
|
479
|
+
* downloads, modified clicks, external URLs) live in Aberdeen; the close
|
|
480
|
+
* guards run in `checkChange` when our navigation reaches the router.
|
|
481
|
+
*/
|
|
482
|
+
private interceptLinks;
|
|
483
|
+
/**
|
|
484
|
+
* The current page's page, or `undefined` while there is none. Reactive on
|
|
485
|
+
* *which* page is current, so the bar follows every move along the trail;
|
|
486
|
+
* the fields it then reads (`title`, `actions`) are reactive in their own
|
|
487
|
+
* right, so a page that names itself when its data lands updates the bar in
|
|
488
|
+
* place.
|
|
489
|
+
*/
|
|
490
|
+
currentPage(): PageState | undefined;
|
|
491
|
+
/**
|
|
492
|
+
* The breadcrumb trail, drawn by `main()` into the top bar: every open
|
|
493
|
+
* page, oldest first, the ones on screen right now in bold, pinned ones
|
|
494
|
+
* wearing their pin. Every crumb but the current page's is a plain link to
|
|
495
|
+
* that page, and a link to an open page is a focus move (see `navigate`) —
|
|
496
|
+
* so clicking along the trail closes nothing, in either direction, and the
|
|
497
|
+
* pages right of the current one wait just past the viewport's edge.
|
|
498
|
+
* Right-click (or long-press) offers pinning, and closing just that one
|
|
499
|
+
* page — the close that splices it out of the middle when it isn't last.
|
|
500
|
+
*/
|
|
501
|
+
drawCrumbs(): void;
|
|
502
|
+
private drawCrumb;
|
|
503
|
+
/**
|
|
504
|
+
* Flip a page's pin (see {@link Page.pinned}). The flag lives on the page;
|
|
505
|
+
* the current history entry's snapshot is rewritten too, so a reload keeps
|
|
506
|
+
* the pin — a same-page state tweak, which the router applies unguarded.
|
|
507
|
+
*/
|
|
508
|
+
private togglePin;
|
|
509
|
+
/**
|
|
510
|
+
* `"<page title> · <app title>"`, kept in sync with the current page — and
|
|
511
|
+
* prefixed `"• "` while *any* open page holds unsaved work, the way editors
|
|
512
|
+
* mark a dirty document. Any page, not just the current one: the risk of
|
|
513
|
+
* losing the work is tab-wide, so the mark on the tab is too.
|
|
514
|
+
*/
|
|
515
|
+
private watchTitle;
|
|
516
|
+
/**
|
|
517
|
+
* While any open page holds unsaved work, closing the tab — or navigating
|
|
518
|
+
* the whole browser away — runs into the browser's own are-you-sure. When
|
|
519
|
+
* the user stays, the unsaved page is brought back on screen if it wasn't,
|
|
520
|
+
* so what held the tab is in front of them rather than parked out of sight.
|
|
521
|
+
*/
|
|
522
|
+
private guardTabClose;
|
|
523
|
+
/**
|
|
524
|
+
* Draw the column viewport into the current element. Called by `main()`.
|
|
525
|
+
*
|
|
526
|
+
* A column is the page's own content, plus the one bit of chrome the shell
|
|
527
|
+
* places for it: its {@link Page.actions}, in a strip on wide shells and in
|
|
528
|
+
* the top bar on narrow ones (see {@link drawActions}).
|
|
529
|
+
*/
|
|
530
|
+
drawColumns(): void;
|
|
531
|
+
private drawPage;
|
|
532
|
+
/**
|
|
533
|
+
* The one bit of column chrome the shell draws: the page's actions, in a
|
|
534
|
+
* quiet strip above the scroll area — and only while the shell is wide, the
|
|
535
|
+
* top bar carrying them otherwise. Everything else in a column is the page's
|
|
536
|
+
* own content: a screen that wants a heading or a card draws them itself.
|
|
537
|
+
* Going back isn't here either — that is the breadcrumbs' job, in the bar.
|
|
538
|
+
*/
|
|
539
|
+
private drawActions;
|
|
540
|
+
scheduleLayout(): void;
|
|
541
|
+
/**
|
|
542
|
+
* Measure the shell, and with it the width the window gives a page of each
|
|
543
|
+
* layout. Measured on the *shell*, not on the column region: the region's width
|
|
544
|
+
* is the layout engine's own output, so reading it back would nail the layout
|
|
545
|
+
* to whatever it happened to be a frame ago. Fractional widths throughout — a
|
|
546
|
+
* rounded column edge would drift a pixel away from the chrome above it.
|
|
547
|
+
*
|
|
548
|
+
* `undefined` while the shell has no width to speak of (it isn't in a document
|
|
549
|
+
* yet, or it's `display:none`); the next pass tries again.
|
|
550
|
+
*/
|
|
551
|
+
private measure;
|
|
552
|
+
/**
|
|
553
|
+
* The measurements this pass runs on. Taken once per layout pass and per
|
|
554
|
+
* commit, and shared with the pages drawn in between — they all size
|
|
555
|
+
* themselves against the same shell, and a `getBoundingClientRect()` each
|
|
556
|
+
* would be a forced reflow each, in the middle of building their DOM.
|
|
557
|
+
*/
|
|
558
|
+
private geometry;
|
|
559
|
+
/** How wide a page asking for this is, right now; 0 while the shell can't be measured. */
|
|
560
|
+
private roomFor;
|
|
561
|
+
/**
|
|
562
|
+
* Size and position every page, and publish the width of the whole ensemble
|
|
563
|
+
* (sidebar + separator + columns) for the shell to centre itself on.
|
|
564
|
+
*
|
|
565
|
+
* This is everything CSS can't work out for itself: which pages exist, which
|
|
566
|
+
* of them are visible, how wide each one is and where it sits. All the motion
|
|
567
|
+
* between two of these arrangements is CSS's job.
|
|
568
|
+
*/
|
|
569
|
+
private layout;
|
|
570
|
+
/** Let a `loading` page's enter animation wait — but not indefinitely. */
|
|
571
|
+
private holdEnter;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Navigating the routed `S.main()` shell from code, for the times it isn't a
|
|
575
|
+
* link click, such as opening the screen for a record you just created.
|
|
576
|
+
*
|
|
577
|
+
* The same rules as a link click apply: pushing a path that is already open
|
|
578
|
+
* goes back to it — a focus move along the trail, closing nothing — rather
|
|
579
|
+
* than opening it twice, and a page holding {@link Page.unsaved} work is
|
|
580
|
+
* never closed, only parked.
|
|
581
|
+
*
|
|
582
|
+
* @example
|
|
583
|
+
* ```ts
|
|
584
|
+
* S.button({ content: "New task", click: async () => {
|
|
585
|
+
* const task = await createTask();
|
|
586
|
+
* S.pages.push(`/tasks/${task.id}`);
|
|
587
|
+
* }});
|
|
588
|
+
* ```
|
|
589
|
+
*/
|
|
590
|
+
export declare const pages: {
|
|
591
|
+
/**
|
|
592
|
+
* Opens `path` in a new page on top of the current one, closing the
|
|
593
|
+
* unpinned pages that were after it (pinned ones stay, sliding in beneath
|
|
594
|
+
* the new page).
|
|
595
|
+
*/
|
|
596
|
+
push(path: string): void;
|
|
597
|
+
/**
|
|
598
|
+
* Opens `path` in place of the current page, which closes. The pages
|
|
599
|
+
* beneath it stay as they are.
|
|
600
|
+
*/
|
|
601
|
+
replace(path: string): void;
|
|
602
|
+
/**
|
|
603
|
+
* Opens `path` as a whole arrangement rather than on top of what's there: the
|
|
604
|
+
* same thing a nav item or a fresh tab does. Without `beneath`, the stack under
|
|
605
|
+
* it is worked out the way a cold link's is (see `S.main()`'s `ancestors`);
|
|
606
|
+
* with it, the paths you give are opened underneath, shallowest first.
|
|
607
|
+
*
|
|
608
|
+
* That's the one for a screen whose URL doesn't say where it belongs — the
|
|
609
|
+
* thread a notification opens — and for seeding a stack from code in general.
|
|
610
|
+
* Pages the new arrangement also holds stay as they are; ones it drops
|
|
611
|
+
* close, except pages with {@link Page.unsaved} work, which stay, parked.
|
|
612
|
+
*
|
|
613
|
+
* @example
|
|
614
|
+
* ```ts
|
|
615
|
+
* S.pages.open(`/thread/${id}`, [`/mailbox/${mailboxId}`]);
|
|
616
|
+
* ```
|
|
617
|
+
*/
|
|
618
|
+
open(path: string, beneath?: readonly string[]): void;
|
|
619
|
+
/**
|
|
620
|
+
* Closes the current page, or, given a `path`, whichever page is open at
|
|
621
|
+
* it. A page that isn't current is taken out on its own, leaving the
|
|
622
|
+
* columns around it exactly as they are.
|
|
623
|
+
*
|
|
624
|
+
* Resolves `false` if the page didn't close: it holds
|
|
625
|
+
* {@link Page.unsaved} work, `path` isn't open, or another navigation got
|
|
626
|
+
* there first.
|
|
627
|
+
*/
|
|
628
|
+
close(path?: string): Promise<boolean>;
|
|
629
|
+
/** The paths of the open pages, oldest first. Reactive: safe to read in a scope. */
|
|
630
|
+
readonly trail: readonly string[];
|
|
631
|
+
/**
|
|
632
|
+
* The path of the current page: the one the URL names, and the rightmost
|
|
633
|
+
* column on screen. The pages after it in {@link pages.trail} are the
|
|
634
|
+
* ones parked past the viewport's right edge. Reactive.
|
|
635
|
+
*/
|
|
636
|
+
readonly current: string | undefined;
|
|
637
|
+
};
|
|
638
|
+
export {};
|