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.
Files changed (41) hide show
  1. package/README.md +113 -7
  2. package/dist/components/autocomplete.js +9 -3
  3. package/dist/components/box.d.ts +20 -0
  4. package/dist/components/box.js +43 -3
  5. package/dist/components/dialog.js +17 -10
  6. package/dist/components/layers.d.ts +330 -0
  7. package/dist/components/layers.js +888 -0
  8. package/dist/components/main.d.ts +98 -6
  9. package/dist/components/main.js +222 -37
  10. package/dist/components/menu.d.ts +14 -1
  11. package/dist/components/menu.js +32 -4
  12. package/dist/components/panels.d.ts +349 -0
  13. package/dist/components/panels.js +933 -0
  14. package/dist/components/tabs.d.ts +5 -0
  15. package/dist/components/tabs.js +125 -19
  16. package/dist/core.d.ts +7 -0
  17. package/dist/core.js +7 -0
  18. package/dist/index.d.ts +3 -2
  19. package/dist/index.js +3 -2
  20. package/dist/staffa.esm.js +1 -1
  21. package/package.json +7 -5
  22. package/skill/BoxOptions.md +18 -0
  23. package/skill/MainOptions.md +99 -3
  24. package/skill/Page.md +108 -0
  25. package/skill/PathParams.md +7 -0
  26. package/skill/SKILL.md +185 -7
  27. package/skill/SegParams.md +8 -0
  28. package/skill/box.md +4 -0
  29. package/skill/isFloatingMenuOpen.md +12 -0
  30. package/skill/main.md +14 -2
  31. package/skill/panels.md +10 -0
  32. package/skill/tabs.md +5 -0
  33. package/src/components/autocomplete.ts +8 -2
  34. package/src/components/box.ts +57 -2
  35. package/src/components/dialog.ts +16 -10
  36. package/src/components/main.ts +314 -40
  37. package/src/components/menu.ts +33 -5
  38. package/src/components/panels.ts +1167 -0
  39. package/src/components/tabs.ts +126 -19
  40. package/src/core.ts +8 -0
  41. 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 {};