staffa 0.7.3 → 0.8.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 +113 -7
- package/dist/components/autocomplete.js +9 -3
- package/dist/components/box.d.ts +20 -0
- package/dist/components/box.js +43 -3
- package/dist/components/dialog.js +17 -10
- package/dist/components/layers.d.ts +330 -0
- package/dist/components/layers.js +888 -0
- package/dist/components/main.d.ts +98 -6
- package/dist/components/main.js +222 -37
- package/dist/components/menu.d.ts +14 -1
- package/dist/components/menu.js +32 -4
- package/dist/components/panels.d.ts +349 -0
- package/dist/components/panels.js +933 -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 +3 -2
- package/dist/index.js +3 -2
- package/dist/staffa.esm.js +1 -1
- package/package.json +7 -5
- package/skill/BoxOptions.md +18 -0
- package/skill/MainOptions.md +99 -3
- package/skill/Page.md +108 -0
- package/skill/PathParams.md +7 -0
- package/skill/SKILL.md +185 -7
- 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/autocomplete.ts +8 -2
- package/src/components/box.ts +57 -2
- package/src/components/dialog.ts +16 -10
- package/src/components/main.ts +314 -40
- package/src/components/menu.ts +33 -5
- package/src/components/panels.ts +1167 -0
- package/src/components/tabs.ts +126 -19
- package/src/core.ts +8 -0
- package/src/index.ts +3 -2
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routed, multi-column "layer stack" navigation for {@link main}.
|
|
3
|
+
*
|
|
4
|
+
* An app designs every screen ("layer") as a narrow column. On a phone exactly
|
|
5
|
+
* one layer is visible — classic push/pop stack navigation. On a wider screen as
|
|
6
|
+
* many *top-of-stack* layers as fit are shown side by side, left-to-right =
|
|
7
|
+
* shallow-to-deep. Same code, no media queries in the app.
|
|
8
|
+
*
|
|
9
|
+
* Navigation is URL-driven through `aberdeen/route`: the URL carries the *top*
|
|
10
|
+
* layer, while the layers beneath it live in the history entry's state — so the
|
|
11
|
+
* browser's back/forward buttons walk an undo history of whole stack snapshots,
|
|
12
|
+
* and a reload (or a shared link) reproduces the columns exactly.
|
|
13
|
+
*/
|
|
14
|
+
/** Flattens an intersection into a single object type, so hovers read nicely. */
|
|
15
|
+
type Prettify<T> = {
|
|
16
|
+
[K in keyof T]: T[K];
|
|
17
|
+
} & {};
|
|
18
|
+
/**
|
|
19
|
+
* The params contributed by a single path-template segment: `:x` a string,
|
|
20
|
+
* `:x(num)` a number, `*x` the remaining segments as `string[]`.
|
|
21
|
+
*/
|
|
22
|
+
export type SegParams<S extends string> = S extends `:${infer Name}(num)` ? {
|
|
23
|
+
[K in Name]: number;
|
|
24
|
+
} : S extends `:${infer Name}` ? {
|
|
25
|
+
[K in Name]: string;
|
|
26
|
+
} : S extends `*${infer Name}` ? {
|
|
27
|
+
[K in Name]: string[];
|
|
28
|
+
} : {};
|
|
29
|
+
/**
|
|
30
|
+
* The params object described by a path template, e.g.
|
|
31
|
+
* `PathParams<"/projects/:id/tasks/:taskId(num)">` is
|
|
32
|
+
* `{ id: string; taskId: number }`.
|
|
33
|
+
*/
|
|
34
|
+
export type PathParams<P extends string> = P extends `${infer Head}/${infer Rest}` ? SegParams<Head> & PathParams<Rest> : SegParams<P>;
|
|
35
|
+
/** A layer draw function: it receives the layer's {@link Page} and draws into the current scope. */
|
|
36
|
+
export type RouteHandler<P = any> = (page: Page<P>) => void;
|
|
37
|
+
/**
|
|
38
|
+
* A route table: path templates mapped to layer draw functions. Used as the
|
|
39
|
+
* loose (non-inferred) type; `S.main()` infers a more precise type from the
|
|
40
|
+
* literal you pass, so each handler's `$page.params` is typed per its key.
|
|
41
|
+
*/
|
|
42
|
+
export type Routes = Record<string, RouteHandler>;
|
|
43
|
+
/**
|
|
44
|
+
* The shape `S.main()`'s `routes` option is checked against: every key types its
|
|
45
|
+
* own handler's `params`. Used as a self-referential generic constraint, which
|
|
46
|
+
* is what makes `$page.params` infer from the route key.
|
|
47
|
+
*/
|
|
48
|
+
export type RouteTable<R> = {
|
|
49
|
+
[K in keyof R & string]: (page: Page<Prettify<PathParams<K>>>) => void;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* The per-layer state object, an Aberdeen proxy passed to the route handler as
|
|
53
|
+
* its only argument. The handler draws inside the layer's own reactive scope, so
|
|
54
|
+
* mutating the page (a `title` arriving with the data, `loading` flipping off)
|
|
55
|
+
* updates the shell in place.
|
|
56
|
+
*
|
|
57
|
+
* Search params and the hash belong to the **top** layer only — a layer that is
|
|
58
|
+
* pushed under another one keeps only its path, so anything a layer needs to
|
|
59
|
+
* redraw itself must live in that path.
|
|
60
|
+
*/
|
|
61
|
+
export interface Page<P = Record<string, string | number | string[]>> {
|
|
62
|
+
/**
|
|
63
|
+
* The params matched from this layer's path, typed per its route key:
|
|
64
|
+
* `:x` is a `string`, `:x(num)` a `number`, `*x` a `string[]`. Read-only.
|
|
65
|
+
*/
|
|
66
|
+
readonly params: P;
|
|
67
|
+
/** This layer's path, e.g. `"/projects/7"`. Read-only. */
|
|
68
|
+
readonly path: string;
|
|
69
|
+
/** Shown in `document.title` while this layer is top-most. */
|
|
70
|
+
title?: string;
|
|
71
|
+
/**
|
|
72
|
+
* How much room this layer asks for.
|
|
73
|
+
*
|
|
74
|
+
* - `"medium"` (the default) fills the standard content area exactly — the
|
|
75
|
+
* room a 1280px page leaves beside the sidebar.
|
|
76
|
+
* - `"small"` is half of that content area (minus a gutter) whenever the
|
|
77
|
+
* screen is wide enough for two columns. A lone small leaves its other
|
|
78
|
+
* half open — which is exactly where the next pushed small lands, without
|
|
79
|
+
* anything on screen moving. On a screen too narrow for two columns it
|
|
80
|
+
* fills the whole content area, like a medium.
|
|
81
|
+
* - `"large"` takes as much room as the window has: while it is the visible
|
|
82
|
+
* layer the whole shell (top bar, body, footer) stretches to the screen
|
|
83
|
+
* edges instead of the standard 1280px page.
|
|
84
|
+
*
|
|
85
|
+
* As many top-of-stack layers as the window fits are shown side by side; on
|
|
86
|
+
* a wide enough screen the page stretches beyond its standard 1280px —
|
|
87
|
+
* staying centred — to hold them (three smalls, a medium and a small, ...).
|
|
88
|
+
*
|
|
89
|
+
* Widths depend only on the window — never on what else is open — so a
|
|
90
|
+
* layer is never resized except when the window itself is. Read **once**,
|
|
91
|
+
* right after the handler's synchronous run: set it in the handler, because
|
|
92
|
+
* later changes are ignored.
|
|
93
|
+
*/
|
|
94
|
+
layout?: "small" | "medium" | "large";
|
|
95
|
+
/**
|
|
96
|
+
* Set `true` while the layer is still fetching what it needs, and back to
|
|
97
|
+
* `false` when done. A freshly pushed layer that is `loading` briefly holds
|
|
98
|
+
* its enter animation so it can slide in with real content; if the fetch
|
|
99
|
+
* drags on it slides in anyway and shows a built-in loading indicator until
|
|
100
|
+
* the flag clears. Presentation only — the stack, the URL and the close
|
|
101
|
+
* guards are never delayed by it.
|
|
102
|
+
*/
|
|
103
|
+
loading?: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Close guard. Called — and awaited — whenever anything would remove this
|
|
106
|
+
* layer: Escape, this page's own close affordances ({@link Page.close}, a box
|
|
107
|
+
* with `close: true`), browser back, a link that truncates past it, or
|
|
108
|
+
* {@link layers}.`close()`. Return `false` to veto. Typical use: a dirty check
|
|
109
|
+
* plus {@link confirm}.
|
|
110
|
+
*/
|
|
111
|
+
requestClose?: () => boolean | Promise<boolean>;
|
|
112
|
+
/**
|
|
113
|
+
* Guarded close of **this** layer, wherever it sits in the stack. The top
|
|
114
|
+
* layer pops (back to the snapshot beneath it); a layer that isn't on top is
|
|
115
|
+
* *spliced* out — the columns above it keep their place, their DOM and their
|
|
116
|
+
* state, and the URL doesn't change. Either way it is recorded as a history
|
|
117
|
+
* entry, so the browser's back button restores the closed column.
|
|
118
|
+
*
|
|
119
|
+
* Resolves `false` when {@link Page.requestClose} vetoed (or when the layer is
|
|
120
|
+
* the only one on the stack, and so has nothing to close back to). The shell
|
|
121
|
+
* draws no close chrome of its own, so this — or `S.box`'s `close` option — is
|
|
122
|
+
* how a page provides its way out.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```ts
|
|
126
|
+
* S.button({ content: "Cancel", attrs: ".neutral", click: () => void $page.close() });
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
close(): Promise<boolean>;
|
|
130
|
+
}
|
|
131
|
+
/** Options the layer stack needs from its shell. */
|
|
132
|
+
export interface LayerStackOptions {
|
|
133
|
+
routes: Routes;
|
|
134
|
+
notFound?: RouteHandler<{}>;
|
|
135
|
+
/** Set `false` to show only the top layer, however much room there is. */
|
|
136
|
+
stacking?: boolean;
|
|
137
|
+
/** The shell's own title, used as the suffix of `document.title`. */
|
|
138
|
+
title?: unknown;
|
|
139
|
+
}
|
|
140
|
+
export declare class LayerController {
|
|
141
|
+
private compiled;
|
|
142
|
+
private opts;
|
|
143
|
+
/** The live stack, shallow-to-deep. Closing layers are no longer part of it. */
|
|
144
|
+
private live;
|
|
145
|
+
private byId;
|
|
146
|
+
private nextId;
|
|
147
|
+
/** Drives rendering: layer id → its `order` (used only as the sort key). */
|
|
148
|
+
$ids: Record<string, number>;
|
|
149
|
+
/**
|
|
150
|
+
* The live stack's paths and its top layer, for reactive readers: the
|
|
151
|
+
* `document.title` watcher, `main()`'s Escape handling and `S.layers.stack`.
|
|
152
|
+
*/
|
|
153
|
+
$state: {
|
|
154
|
+
paths: string[];
|
|
155
|
+
topId: number;
|
|
156
|
+
};
|
|
157
|
+
private containerEl?;
|
|
158
|
+
/** The body width at the last layout; a change means a window resize → snap. */
|
|
159
|
+
private lastBodyW;
|
|
160
|
+
private layoutQueued;
|
|
161
|
+
private timers;
|
|
162
|
+
constructor(opts: LayerStackOptions);
|
|
163
|
+
/** Resolve a path to its route handler + params, falling back to `notFound`. */
|
|
164
|
+
private resolve;
|
|
165
|
+
private matches;
|
|
166
|
+
/**
|
|
167
|
+
* The one derivation rule for origin-less navigation (§2.8): probe every
|
|
168
|
+
* prefix of the path against the route table; the matching prefixes become
|
|
169
|
+
* the stack. Prefixes without a route are simply skipped, so an app that
|
|
170
|
+
* doesn't want one screen stacked under another just doesn't route that
|
|
171
|
+
* prefix. The path itself is always the top layer, matched or not.
|
|
172
|
+
*/
|
|
173
|
+
deriveStack(path: string): string[];
|
|
174
|
+
/** The stack a route implies: its snapshot topped by its path, or — without a snapshot — derived. */
|
|
175
|
+
private targetFor;
|
|
176
|
+
/** The stack the current history entry asks for. Subscribes to path + snapshot. */
|
|
177
|
+
private computeTarget;
|
|
178
|
+
/**
|
|
179
|
+
* The route guard (see `route.setGuard` in the constructor): asked before any
|
|
180
|
+
* route change lands, wherever it came from. Runs the {@link Page.requestClose}
|
|
181
|
+
* guard of every layer the new route's stack would remove — a set defined by
|
|
182
|
+
* the target (the commit reconciles by path), so a derived stack that shares
|
|
183
|
+
* nothing with the live one still asks exactly the layers that are closing.
|
|
184
|
+
*/
|
|
185
|
+
private checkChange;
|
|
186
|
+
private paths;
|
|
187
|
+
/** The live layers a target stack drops — by path, so a splice removes only its own column. */
|
|
188
|
+
private removedBy;
|
|
189
|
+
/**
|
|
190
|
+
* Adopt a stack proposed by the URL. Close guards have already been run (and
|
|
191
|
+
* have passed) by the time a route change is visible here — `checkChange` is
|
|
192
|
+
* consulted by the router itself, before anything is applied.
|
|
193
|
+
*/
|
|
194
|
+
private propose;
|
|
195
|
+
/**
|
|
196
|
+
* Apply a target stack: unmount what's gone, mount what's new, animate the
|
|
197
|
+
* difference.
|
|
198
|
+
*
|
|
199
|
+
* Reconciliation is BY PATH (a stack can't hold the same path twice, so that's
|
|
200
|
+
* well-defined): a layer present in both stacks stays mounted *even if its
|
|
201
|
+
* index shifted*, which is what lets a layer be spliced out of the middle
|
|
202
|
+
* (§7) without disturbing the columns above it. A common-prefix diff would
|
|
203
|
+
* remount every one of them, throwing away exactly the scroll and form state
|
|
204
|
+
* rule 5 promises to keep.
|
|
205
|
+
*/
|
|
206
|
+
private commit;
|
|
207
|
+
private createEntry;
|
|
208
|
+
/**
|
|
209
|
+
* Start a layer's exit: it lingers in the DOM, inert and fading, and is dropped
|
|
210
|
+
* only when the fade itself ends. Removing it on a fixed timer instead would
|
|
211
|
+
* race the transition — pull the element a frame early and the layer appears to
|
|
212
|
+
* fade half-way and then vanish. The timeout is just a fallback for when no
|
|
213
|
+
* `transitionend` is coming at all (transitions off, or an element that never
|
|
214
|
+
* got placed).
|
|
215
|
+
*/
|
|
216
|
+
private beginClose;
|
|
217
|
+
/**
|
|
218
|
+
* Navigate back to a stack that is a truncation of the current one — the shared
|
|
219
|
+
* implementation of Escape, a page closing itself, return-links and
|
|
220
|
+
* `S.layers.close()`. `route.back()` prefers the history entry where that
|
|
221
|
+
* layer was on top (with its scroll state intact); when there is no such entry
|
|
222
|
+
* it replaces the current one, carrying the snapshot passed as the fallback.
|
|
223
|
+
* Either way the route guard asks the closing layers first, and the returned
|
|
224
|
+
* promise reports its verdict.
|
|
225
|
+
*/
|
|
226
|
+
private goBackTo;
|
|
227
|
+
/** Close every layer above `index` (guarded). Resolves `false` when vetoed. */
|
|
228
|
+
closeDownTo(index: number): Promise<boolean>;
|
|
229
|
+
/** Guarded close of the top layer. */
|
|
230
|
+
closeTop(): Promise<boolean>;
|
|
231
|
+
/**
|
|
232
|
+
* Guarded close of the layer at `index`, top of the stack or not — what a
|
|
233
|
+
* page's own close affordances ({@link Page.close}, a box's ✕) come down to.
|
|
234
|
+
*
|
|
235
|
+
* The top layer pops back to the snapshot beneath it. Any other layer is
|
|
236
|
+
* *spliced* out: its guard runs, the columns above it keep their place and
|
|
237
|
+
* state (the commit reconciles by path), and the URL doesn't change, since the
|
|
238
|
+
* top layer didn't. That still gets its own history entry, so the browser's
|
|
239
|
+
* back button restores the closed column like any other snapshot — which is
|
|
240
|
+
* why it goes through `route.go` here rather than through `navigate()`, whose
|
|
241
|
+
* "link to the layer we're already on" check would see a no-op.
|
|
242
|
+
*/
|
|
243
|
+
closeLayerAt(index: number): Promise<boolean>;
|
|
244
|
+
/** Guarded close of whichever layer `path` is open as. False when it isn't open. */
|
|
245
|
+
closeByPath(path: string): Promise<boolean>;
|
|
246
|
+
/** Guarded close of the layer whose `.s-layer` element this is. */
|
|
247
|
+
closeLayerEl(el: HTMLElement): Promise<boolean>;
|
|
248
|
+
/**
|
|
249
|
+
* Navigate to `href`. `originIndex` is the depth of the layer the link lives
|
|
250
|
+
* in (−1 when it has none — a nav item or a programmatic call, which derives
|
|
251
|
+
* the whole stack instead). `replace` swaps the originating layer rather than
|
|
252
|
+
* stacking on top of it.
|
|
253
|
+
*/
|
|
254
|
+
navigate(href: string, originIndex: number, replace?: boolean): void;
|
|
255
|
+
/** Programmatic push/replace, with the top layer as the implied origin. */
|
|
256
|
+
pushPath(path: string, replace: boolean): void;
|
|
257
|
+
/**
|
|
258
|
+
* Link handling through `route.interceptLinks()`, whose handler hook hands us
|
|
259
|
+
* the anchor so we can decide what the click *means*: the originating
|
|
260
|
+
* `.s-layer` (which decides what the click truncates), `data-layer=replace`,
|
|
261
|
+
* and return-to-an-open-layer semantics. The exclusion rules (targets,
|
|
262
|
+
* downloads, modified clicks, external URLs) live in Aberdeen; the close
|
|
263
|
+
* guards run in `checkChange` when our navigation reaches the router.
|
|
264
|
+
*/
|
|
265
|
+
private interceptLinks;
|
|
266
|
+
/** `"<page title> · <app title>"`, kept in sync with the top layer. */
|
|
267
|
+
private watchTitle;
|
|
268
|
+
/**
|
|
269
|
+
* Draw the layer viewport into the current element. Called by `main()`.
|
|
270
|
+
*
|
|
271
|
+
* There is deliberately no close chrome here — no back rail, no ←: pages
|
|
272
|
+
* provide their own way out (see {@link Page.close} and `S.box`'s `close`
|
|
273
|
+
* option). The shell contributes Escape and the browser's own back button.
|
|
274
|
+
*/
|
|
275
|
+
drawStack(): void;
|
|
276
|
+
private drawLayer;
|
|
277
|
+
scheduleLayout(): void;
|
|
278
|
+
/**
|
|
279
|
+
* Size and position every layer, and publish the width of the whole ensemble
|
|
280
|
+
* (sidebar + separator + columns) for the shell to centre itself on.
|
|
281
|
+
*
|
|
282
|
+
* This is everything CSS can't work out for itself: which layers exist, which
|
|
283
|
+
* of them are visible, how wide each one is and where it sits. All the motion
|
|
284
|
+
* between two of these arrangements is CSS's job.
|
|
285
|
+
*/
|
|
286
|
+
private layout;
|
|
287
|
+
/** Let a `loading` layer's enter animation wait — but not indefinitely. */
|
|
288
|
+
private holdEnter;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Programmatic navigation for the routed `S.main()` shell — for acts that aren't
|
|
292
|
+
* link clicks, such as pushing the screen for a record you just created.
|
|
293
|
+
*
|
|
294
|
+
* The same rules as a link click apply: pushing a path that is already an open
|
|
295
|
+
* layer *returns* to it instead of duplicating it, and anything that would
|
|
296
|
+
* remove a layer asks its {@link Page.requestClose} guard first.
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* ```ts
|
|
300
|
+
* S.button({ content: "New task", click: async () => {
|
|
301
|
+
* const task = await createTask();
|
|
302
|
+
* S.layers.push(`/tasks/${task.id}`);
|
|
303
|
+
* }});
|
|
304
|
+
* ```
|
|
305
|
+
*/
|
|
306
|
+
export declare const layers: {
|
|
307
|
+
/** Push `path` on top of the current top layer. */
|
|
308
|
+
push(path: string): void;
|
|
309
|
+
/** Replace the current top layer with `path`. */
|
|
310
|
+
replace(path: string): void;
|
|
311
|
+
/**
|
|
312
|
+
* Guarded close: of the top layer, or — given a `path` — of whichever layer is
|
|
313
|
+
* open at it, which is *spliced* out when it isn't on top (the columns above it
|
|
314
|
+
* stay exactly as they are). Resolves `false` when a guard vetoed, or when
|
|
315
|
+
* `path` isn't an open layer.
|
|
316
|
+
*/
|
|
317
|
+
close(path?: string): Promise<boolean>;
|
|
318
|
+
/** The current stack of paths, shallow-to-deep. Reactive: safe to read in a scope. */
|
|
319
|
+
readonly stack: readonly string[];
|
|
320
|
+
};
|
|
321
|
+
/**
|
|
322
|
+
* Guarded close of the layer `el` sits in, resolved from the DOM — which is what
|
|
323
|
+
* lets a close affordance work without any page context, from any column,
|
|
324
|
+
* whether or not it is on top. Used by `S.box`'s `close: true`.
|
|
325
|
+
*
|
|
326
|
+
* Outside a routed shell (or outside any layer — a box in a dialog, say) there is
|
|
327
|
+
* nothing to close: it warns and resolves `false`.
|
|
328
|
+
*/
|
|
329
|
+
export declare function closeContainingLayer(el: Element | null | undefined): Promise<boolean>;
|
|
330
|
+
export {};
|