staffa 0.14.0 → 0.16.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 (82) hide show
  1. package/README.md +99 -271
  2. package/dist/components/autocomplete.js +4 -5
  3. package/dist/components/box.js +11 -21
  4. package/dist/components/button.d.ts +20 -5
  5. package/dist/components/button.js +55 -47
  6. package/dist/components/buttonChooser.js +1 -3
  7. package/dist/components/checkbox.js +1 -2
  8. package/dist/components/dialog.d.ts +9 -2
  9. package/dist/components/dialog.js +29 -35
  10. package/dist/components/field.d.ts +5 -8
  11. package/dist/components/field.js +4 -6
  12. package/dist/components/form.d.ts +5 -7
  13. package/dist/components/form.js +6 -9
  14. package/dist/components/keyhelp.d.ts +22 -0
  15. package/dist/components/keyhelp.js +91 -0
  16. package/dist/components/main.js +190 -318
  17. package/dist/components/menu.d.ts +36 -9
  18. package/dist/components/menu.js +193 -144
  19. package/dist/components/panels.d.ts +152 -232
  20. package/dist/components/panels.js +341 -556
  21. package/dist/components/select.js +1 -3
  22. package/dist/components/tabs.d.ts +10 -13
  23. package/dist/components/tabs.js +40 -63
  24. package/dist/components/textline.d.ts +3 -5
  25. package/dist/components/textline.js +3 -5
  26. package/dist/components/toast.d.ts +1 -3
  27. package/dist/components/toast.js +3 -6
  28. package/dist/components/tooltip.d.ts +4 -5
  29. package/dist/components/tooltip.js +13 -22
  30. package/dist/core.d.ts +17 -39
  31. package/dist/core.js +13 -35
  32. package/dist/icons-helpers.d.ts +3 -3
  33. package/dist/icons-helpers.js +6 -11
  34. package/dist/index.d.ts +3 -1
  35. package/dist/index.js +5 -4
  36. package/dist/keys.d.ts +92 -0
  37. package/dist/keys.js +279 -0
  38. package/dist/staffa.esm.js +1 -1
  39. package/dist/theme.d.ts +4 -10
  40. package/dist/theme.js +58 -123
  41. package/package.json +2 -2
  42. package/skill/ButtonOptions.md +12 -0
  43. package/skill/DialogOptions.md +11 -2
  44. package/skill/FieldOptions.md +3 -5
  45. package/skill/IconButtonOptions.md +8 -0
  46. package/skill/MenuItem.md +22 -3
  47. package/skill/Panel.md +8 -0
  48. package/skill/SKILL.md +161 -294
  49. package/skill/addTooltip.md +4 -5
  50. package/skill/bindKey.md +51 -0
  51. package/skill/box.md +1 -1
  52. package/skill/form.md +5 -7
  53. package/skill/formatKey.md +21 -0
  54. package/skill/iconButton.md +4 -5
  55. package/skill/scrollStrip.md +7 -9
  56. package/skill/showFloatingMenu.md +2 -2
  57. package/skill/showKeyHelp.md +17 -0
  58. package/skill/tabs.md +3 -4
  59. package/skill/textline.md +3 -5
  60. package/src/components/autocomplete.ts +4 -5
  61. package/src/components/box.ts +11 -21
  62. package/src/components/button.ts +70 -47
  63. package/src/components/buttonChooser.ts +1 -3
  64. package/src/components/checkbox.ts +1 -2
  65. package/src/components/dialog.ts +39 -37
  66. package/src/components/field.ts +7 -11
  67. package/src/components/form.ts +6 -9
  68. package/src/components/keyhelp.ts +96 -0
  69. package/src/components/main.ts +194 -318
  70. package/src/components/menu.ts +209 -146
  71. package/src/components/panels.ts +389 -618
  72. package/src/components/select.ts +1 -3
  73. package/src/components/tabs.ts +40 -63
  74. package/src/components/textline.ts +3 -5
  75. package/src/components/toast.ts +4 -9
  76. package/src/components/tooltip.ts +13 -22
  77. package/src/core.ts +17 -43
  78. package/src/icons-helpers.ts +6 -11
  79. package/src/index.ts +5 -4
  80. package/src/keys.ts +300 -0
  81. package/src/theme.ts +58 -123
  82. package/skill/Attributes.md +0 -10
@@ -3,31 +3,20 @@ import { type Slot } from "../core.js";
3
3
  /**
4
4
  * Routed, multi-column panel navigation for {@link main}.
5
5
  *
6
- * Each route draws one screen of the app, called a *panel*. The open panels
7
- * form a **stack**, whose last panel is the **current** one: the panel the URL
8
- * names, and the rightmost column on screen. As many panels as fit are shown,
9
- * ending at the current one — on a phone that is one at a time, on a wider
10
- * screen the panels that would have covered each other sit side by side
11
- * instead. The app's own code is the same either way.
6
+ * Each route draws one screen, called a *panel*. The open panels form a
7
+ * **stack** whose last panel is the **current** one: the panel the URL names,
8
+ * and the rightmost column on screen. As many as fit are shown, ending there.
12
9
  *
13
- * Whatever a navigation lands on becomes the top of that stack, and the same
14
- * path is never in it twice. Opening a new panel closes everything after the
15
- * panel it came from; a plain link to a panel that is already open — a
16
- * breadcrumb, a nav item for the section you are in — returns to its own
17
- * place, closing whatever was stacked on top, while a `replace` or `open`
18
- * applies its usual shape, the open panel moving into it alive.
19
- * Two kinds of panel survive that: the ones the user pinned, which ride along,
20
- * and the ones holding unsaved work, which no navigation ever tears down.
21
- * Those wait parked out of sight past the rightmost column, which is the only
22
- * way a panel ever sits *after* the current one. Escape closes the current
23
- * panel — or just steps left, when it holds unsaved work or panels sit parked
24
- * beyond it.
10
+ * A navigation's target becomes the top of the stack, and the same path is
11
+ * never in it twice: opening a panel closes everything after the one it came
12
+ * from, while a plain link to an already-open panel returns to its place. Two
13
+ * kinds survive that — pinned panels, which ride along, and unsaved ones, which
14
+ * nothing tears down. Both wait parked past the rightmost column, the only way
15
+ * a panel ever sits *after* the current one.
25
16
  *
26
- * Navigation runs through `aberdeen/route`: the URL holds the current panel,
27
- * and the rest of the arrangement — the panels before it, the ones parked
28
- * after it, and which are pinned — is stored beside it in the history entry.
29
- * So back and forward step through whole arrangements of columns, and a reload
30
- * (or a shared link) brings the same columns back.
17
+ * Navigation runs through `aberdeen/route`: the URL holds the current panel and
18
+ * the rest of the arrangement is stored beside it in the history entry, so back
19
+ * and forward step through whole arrangements of columns.
31
20
  */
32
21
  /** Flattens an intersection into a single object type, so hovers read nicely. */
33
22
  type Prettify<T> = {
@@ -111,6 +100,10 @@ export interface Panel<P = Record<string, string | number | string[]>> {
111
100
  * The params matched from this panel's path, typed per its route key:
112
101
  * `[x]` is a `string`, `[x=integer]` a `number`, `[...x]` a `string`.
113
102
  * Read-only.
103
+ *
104
+ * Single-segment params are percent-decoded; `[...x]` is not, since decoding
105
+ * it would make an encoded slash indistinguishable from a separator. Split it
106
+ * yourself: `x.split("/").map(decodeURIComponent)`.
114
107
  */
115
108
  readonly params: P;
116
109
  /**
@@ -184,6 +177,10 @@ export interface Panel<P = Record<string, string | number | string[]>> {
184
177
  * window, never on what else is open, so opening or closing a panel never
185
178
  * resizes another — the run of columns just recentres in the area.
186
179
  *
180
+ * The ask is a ceiling only; there is no matching floor, since the window can
181
+ * be any width. Aim your layout at 360px — about the narrowest phone still in
182
+ * common use — and let it degrade gracefully below that.
183
+ *
187
184
  * Ask only for what your content can actually use: a panel that would cap
188
185
  * its own content narrower than its ask is holding room that would have
189
186
  * let another column fit beside it.
@@ -302,10 +299,9 @@ export interface PanelStackOptions {
302
299
  /** The shell's own title, used as the suffix of `document.title`. */
303
300
  title?: unknown;
304
301
  /**
305
- * The shell's live narrow flag (see `main()`), which decides where a panel's
306
- * chrome goes: in its own column, or promoted into the top bar. Shared rather
307
- * than measured again here, so the bar and the columns can't disagree about
308
- * which regime they are in.
302
+ * The shell's live narrow flag (see `main()`), deciding whether a panel's
303
+ * chrome sits in its own column or is promoted into the top bar. Shared
304
+ * rather than measured again, so the two can't disagree about the regime.
309
305
  */
310
306
  $shell: {
311
307
  narrow: boolean;
@@ -414,10 +410,9 @@ export interface PanelStack {
414
410
  */
415
411
  export declare class PanelStackController implements PanelStack {
416
412
  /**
417
- * Kept out of Aberdeen's proxy wrapping: this is a class instance holding
418
- * DOM nodes, timers and route handlers, and it rides inside every
419
- * {@link Panel.stack}. Its reactivity doesn't need the wrapper — it comes
420
- * from `$state` and the panels, which are proxies in their own right.
413
+ * Kept out of Aberdeen's proxy wrapping: a class instance holding DOM nodes,
414
+ * timers and route handlers, riding inside every {@link Panel.stack}. Its
415
+ * reactivity comes from `$state` and the panels, which are proxies already.
421
416
  */
422
417
  readonly [OPAQUE] = true;
423
418
  private compiled;
@@ -425,35 +420,24 @@ export declare class PanelStackController implements PanelStack {
425
420
  private ancestors;
426
421
  private opts;
427
422
  /**
428
- * The live stack, oldest first (closing panels are no longer part of it),
429
- * and which of its panels is current — the one reactive fact about the
430
- * stack's *shape*. The getters, the crumbs and `document.title` subscribe
431
- * to it simply by reading it; each commit publishes the next shape by
432
- * assigning a fresh `live` array. The entries themselves are opaque (see
433
- * {@link PanelEntry}), so the array carries their comings, goings and
434
- * order — nothing deeper; a panel's own facts stay separately reactive on
435
- * its `$panel`, which is what lets a panel rename itself without the
436
- * stack redrawing.
437
- *
438
- * One rule makes this safe to touch from anywhere: **queries subscribe,
439
- * commands peek**. The getters below are the queries. Every navigation
440
- * entry point (`navigate`, `closePath`, `back`, …) wraps itself in
441
- * `A.peek`, so an app calling one from inside a reactive scope (a
442
- * redirect in a route handler, say) can't subscribe that scope to the
443
- * very stack it is changing — and everything those commands call through
444
- * to, `propose` and `commit` included, inherits the same guarantee and
445
- * reads the stack plainly.
423
+ * The live stack, oldest first (closing panels have left it), and which panel
424
+ * is current — the one reactive fact about the stack's *shape*. Each commit
425
+ * publishes the next shape by assigning a fresh `live` array; the entries are
426
+ * opaque, so a panel renaming itself doesn't redraw the stack.
427
+ *
428
+ * The invariant that makes this safe to touch from anywhere: **queries
429
+ * subscribe, commands peek**. Every navigation entry point (`navigate`,
430
+ * `closePath`, `back`, …) wraps itself in `A.peek`, so an app calling one
431
+ * from a reactive scope can't subscribe that scope to the stack it is
432
+ * changing; `propose` and `commit` inherit that and read the stack plainly.
446
433
  */
447
434
  private $state;
448
435
  /**
449
- * The open panels again, keyed by path — the shape as the DOM consumes it.
450
- * `drawColumns`' `onEach` mounts and unmounts panels by key, so a panel
451
- * spliced out of the middle of the stack touches exactly one key, and the
452
- * DOM of the retained columns — scroll positions, half-typed forms — is
453
- * left alone. (Iterating `live` itself would key panels by array index,
454
- * and a splice renumbers every index after it, redrawing them all.) A
455
- * key's value is its entry, by reference, and is never reassigned, so a
456
- * panel only ever redraws wholesale when its path closes.
436
+ * The open panels again, keyed by path — the shape the DOM consumes.
437
+ * `drawColumns`' `onEach` mounts by key, so splicing a panel out of the
438
+ * middle touches exactly one key and the retained columns keep their scroll
439
+ * positions and half-typed forms. (Iterating `live` would key by array index,
440
+ * which a splice renumbers, redrawing every panel after it.)
457
441
  */
458
442
  private $open;
459
443
  /** Feeds {@link PanelEntry.order}: one shared counter, so keys never tie. */
@@ -479,24 +463,18 @@ export declare class PanelStackController implements PanelStack {
479
463
  private matches;
480
464
  /**
481
465
  * The stack for origin-less navigation: a cold deep link, a nav item, a
482
- * `route.go()` — anything arriving without a panel to build on and without a
483
- * snapshot to restore.
484
- *
485
- * The app's {@link PanelStackOptions.ancestors} gets first say, since only it
486
- * can know what belongs under a path that doesn't spell its own context out
487
- * (a `/thread/[id]` reached from a notification). Failing that — or when it
488
- * has no opinion — every prefix of the path is probed against the route table
489
- * and the matching ones become the stack. Either way, a path with no route is
490
- * skipped rather than opened as a "not found" column, so an app that doesn't
491
- * want one screen stacked under another simply doesn't route it. The path
492
- * itself always ends the derived stack, matched or not.
466
+ * `route.go()` — anything with no panel to build on and no snapshot to
467
+ * restore. {@link PanelStackOptions.ancestors} gets first say; failing that,
468
+ * every prefix of the path is probed against the route table. A path with no
469
+ * route is skipped rather than opened as a "not found" column, so not routing
470
+ * a screen keeps it from appearing under another. The path itself always ends
471
+ * the derived stack, matched or not.
493
472
  */
494
473
  private deriveStack;
495
474
  /**
496
- * Ask the `ancestors` table what belongs beneath `path`. The first key that
497
- * matches answers — with its own matched params, so it never has to take the
498
- * path apart itself — and `undefined` from it means "no opinion", leaving the
499
- * path to the prefix derivation just as an unlisted one is.
475
+ * Ask the `ancestors` table what belongs beneath `path`. The first matching
476
+ * key answers, with its own matched params; `undefined` means "no opinion",
477
+ * leaving the path to the prefix derivation as an unlisted one would be.
500
478
  */
501
479
  private askAncestors;
502
480
  /** Every prefix of `path` that has a route, shallowest first. */
@@ -510,70 +488,57 @@ export declare class PanelStackController implements PanelStack {
510
488
  /** Whether the panel open at `path` (if any) holds unsaved work. */
511
489
  private unsavedAt;
512
490
  /**
513
- * The arrangement a route implies: its snapshot around its path, or —
514
- * without a snapshot — derived, with the new panel current at the end and
515
- * any pinned panels carried along beneath it.
491
+ * The arrangement a route implies: its snapshot around its path, or — without
492
+ * one — derived, the new panel current at the end with pinned panels beneath.
516
493
  *
517
- * The snapshot reads subscribe — they are the URL's, exactly what the
518
- * route observer is for. The derivation is peeked instead: it reads the
519
- * live stack for its pins, which is the very thing that observer rewrites,
520
- * and subscribing to it would re-run the observer once per commit.
494
+ * The snapshot reads subscribe (they are the URL's). The derivation is peeked:
495
+ * it reads the live stack for its pins, which the route observer rewrites, so
496
+ * subscribing would re-run that observer once per commit.
521
497
  */
522
498
  private targetFor;
523
499
  /** The arrangement the current history entry asks for. Subscribes to path + snapshot. */
524
500
  private computeTarget;
525
501
  private paths;
526
502
  /**
527
- * Adopt an arrangement proposed by the URL — after repairing it: panels
528
- * holding unsaved work are never torn down by a navigation, wherever it
529
- * came from — a link, a nav item, even a browser back to an entry from
530
- * before the panel existed. Whatever the target drops, they stay, parked
531
- * after the current panel and wearing the ● that says why. (They are
532
- * deliberately not written into history entries: the work they protect
533
- * lives in the page's DOM, which a reload clears anyway.)
503
+ * Adopt an arrangement proposed by the URL, after repairing it: a panel holding
504
+ * unsaved work is never torn down by any navigation — including a back to an
505
+ * entry from before it existed — so whatever the target drops, it stays, parked
506
+ * after the current panel. Parked panels are deliberately kept out of history
507
+ * entries: the work they protect lives in DOM a reload clears anyway.
534
508
  */
535
509
  private propose;
536
510
  /**
537
511
  * Apply a target arrangement: unmount what's gone, mount what's new, animate
538
512
  * the difference.
539
513
  *
540
- * Reconciliation is BY PATH (a stack can't hold the same path twice, so that's
541
- * well-defined): a panel present in both stacks stays mounted *even if its
542
- * index shifted*, which is what lets a panel be spliced out of the middle
543
- * (§7) without disturbing the columns above it. A common-prefix diff would
544
- * remount every one of them, throwing away exactly the scroll and form state
545
- * rule 5 promises to keep.
514
+ * Reconciliation is BY PATH, not by index: a panel in both stacks stays mounted
515
+ * even if its index shifted, which is what lets one be spliced out of the middle
516
+ * without disturbing the columns above it. A common-prefix diff would remount
517
+ * them all, throwing away their scroll and form state.
546
518
  */
547
519
  private commit;
548
520
  private createEntry;
549
521
  /**
550
- * Take a panel out of the shell. The *scope* goes now: its cleaners run this
551
- * tick, so whatever the panel registered with `A.clean` — subscriptions,
552
- * timers, an open portal — is torn down when the panel closes, not when its
553
- * animation is over. Only the element lingers, to play that animation, which
554
- * is what the `destroy=` hook in `drawPanel` is for: Aberdeen hands the
555
- * element to {@link playExit} instead of removing it.
522
+ * Take a panel out of the shell. The *scope* goes now — its `A.clean` hooks run
523
+ * this tick, so subscriptions, timers and portals stop at the close rather than
524
+ * at the end of the animation. Only the element lingers to play that animation,
525
+ * which is what `drawPanel`'s `destroy=` hook hands to {@link playExit}.
556
526
  */
557
527
  private beginClose;
558
528
  /**
559
- * A closed panel's send-off, run by Aberdeen once the panel's scope is gone (so
560
- * the content it shows is frozen, which is exactly what a departing column
561
- * should be): it fades where it stands, inert, and leaves the DOM when the fade
562
- * itself ends. Removing it on a fixed timer instead would race the transition —
563
- * pull the element a frame early and the panel appears to fade half-way and
564
- * then vanish. The timeout is just a fallback for when no `transitionend` is
565
- * coming at all (transitions off, or an element that never got placed).
529
+ * A closed panel's send-off, run by Aberdeen once its scope is gone: it fades
530
+ * where it stands, inert, and leaves the DOM on `transitionend`. A fixed timer
531
+ * would race the transition and pull the element a frame early, making the
532
+ * panel appear to fade half-way and vanish; the timeout is only a fallback for
533
+ * when no `transitionend` is coming (transitions off, element never placed).
566
534
  */
567
535
  private playExit;
568
536
  /**
569
537
  * The arrangement navigation works from: the one we're on the way to while a
570
- * change is still settling, and the one on screen otherwise.
571
- *
572
- * Settling takes a moment more often than it looks: every `route.back()`
573
- * travels through the browser's history and lands on a `popstate`, and an
574
- * app-registered route guard may be async. Working from the committed
575
- * arrangement in that window would make a second Escape aim at the panel the
576
- * first one is already taking away — so two quick Escapes would peel one panel.
538
+ * change is still settling, the one on screen otherwise. That window is common
539
+ * (every `route.back()` waits for a `popstate`), and working from the committed
540
+ * arrangement inside it would aim a second Escape at the panel the first is
541
+ * already taking away — two quick Escapes would peel one panel.
577
542
  */
578
543
  private intended;
579
544
  /** The history `state` describing `arr` — exactly what `targetFor` reads back. */
@@ -581,96 +546,67 @@ export declare class PanelStackController implements PanelStack {
581
546
  /** The paths of the pinned panels — the pin list history entries persist. */
582
547
  private pinnedPaths;
583
548
  /**
584
- * Put a navigation to the router, or — while one is still settling — behind
585
- * the one that is. Only the newest waits: each was worked out against
586
- * {@link intended}, so the newest is the one that means what the user last
587
- * asked for, and the one it displaces resolves `false`.
588
- *
589
- * A refusal empties the queue instead of running it: a navigation can still
590
- * fail to land — an app-registered route guard vetoes it, or another one
591
- * supersedes it — and what was queued behind it was worked out against the
592
- * arrangement it would have produced.
549
+ * Put a navigation to the router, or — while one is still settling — behind the
550
+ * one that is. Only the newest waits; the one it displaces resolves `false`.
551
+ * A refusal empties the queue rather than running it, since what was queued
552
+ * was worked out against the arrangement the refused one would have produced.
593
553
  */
594
554
  private issue;
595
555
  private start;
596
556
  /**
597
557
  * Make the stack's `index`th panel current without closing anything, leaving
598
- * the panels right of it parked out of sight.
599
- *
600
- * Only ever a step around a panel that refuses to close — nothing else is
601
- * left sitting after the current one — so this is Escape's way past an
602
- * unsaved panel, and the way back to one. It is a history entry, so the
603
- * browser's back button returns the focus to where it was.
558
+ * the panels right of it parked out of sight — Escape's way past an unsaved
559
+ * panel, and back to one. A history entry, so back returns the focus.
604
560
  */
605
561
  private focusAt;
606
562
  /**
607
- * One step back along the stack — what Escape does (`main()` calls this;
608
- * it is not {@link PanelStack} API). Normally that closes the current panel,
609
- * which is the stack's end. When it holds {@link Panel.unsaved} work — or
610
- * panels sit parked beyond it — it stays open instead, and the focus
611
- * simply moves to the panel on its left.
612
- * Resolves `false` at the stack's start, where there is no left to go.
563
+ * One step back along the stack — what Escape does (`main()` calls this; it is
564
+ * not {@link PanelStack} API). Normally that closes the current panel; when it
565
+ * holds {@link Panel.unsaved} work, or panels sit parked beyond it, the focus
566
+ * moves left instead. Resolves `false` at the stack's start.
613
567
  */
614
568
  back(): Promise<boolean>;
615
569
  /**
616
570
  * Close whichever panel is open at `path`, current or not — what
617
571
  * {@link Panel.close}, {@link PanelStack.closePanel} and the crumb menu's
618
- * Close come down to. `false` when that path isn't open, is the stack's
619
- * only panel, or holds {@link Panel.unsaved} work — nothing may close an
620
- * unsaved panel; the app clears the flag first, which is its explicit
621
- * "this is now discardable".
622
- *
623
- * Closing the current panel at the stack's very end pops back through the
624
- * browser's history to the entry beneath it, when it is there (restoring its
625
- * scroll and search state); the arrangement is part of the match, so an
626
- * entry where the closing panel was merely parked won't do. Every other
627
- * close is a *splice*: the columns around the closed one keep their place
628
- * and state (the commit reconciles by path). That still gets its own
629
- * history entry, so the browser's back button restores the closed column
630
- * like any other arrangement — which is why it goes through `route.go` here
631
- * rather than through `navigate()`, whose "link to an open panel" check
632
- * would turn it into a focus move.
572
+ * Close all come down to. `false` when that path isn't open, is the stack's
573
+ * only panel, or holds {@link Panel.unsaved} work.
574
+ *
575
+ * Closing the current panel at the stack's end pops back through history to
576
+ * the entry beneath it (restoring its scroll and search state); the
577
+ * arrangement is part of the match, so an entry where the closing panel was
578
+ * merely parked won't do. Every other close is a *splice*, which still earns
579
+ * its own history entry — hence `route.go` here rather than `navigate()`,
580
+ * whose "link to an open panel" check would turn it into a focus move.
633
581
  */
634
582
  private closePath;
635
583
  /**
636
584
  * Navigate to `href` — the one implementation behind a link click,
637
- * {@link Panel.open} and the stack's own methods, so none of them can
638
- * behave differently.
639
- *
640
- * `from` is the path of the panel the navigation starts from — the one
641
- * the link lives in — or absent when it has none: a nav item, or a call
642
- * that means the whole stack, which is then built instead (see
643
- * {@link deriveStack}), or taken outright from `beneath`, for callers
644
- * that know it.
645
- *
646
- * `how` is the link's `data-panel` attribute (or the caller's word for
647
- * it), picking how much of `from`'s context the target keeps: a push (the
648
- * default, and what unrecognised values fall back to) keeps `from` and
649
- * builds on it, `"replace"` keeps only what is beneath `from`, and
650
- * `"open"` keeps nothing — the target arrives with its own stack, the way
651
- * a nav item's link does. Absent, it is the shell's `linkNavigation`
652
- * default, like a link without the attribute. A target that is already
653
- * open is returned to by a push, and *moved* — alive, state intact — by
654
- * the other two: the stack never holds a path twice.
655
- *
656
- * Resolves the way every {@link PanelStack} method does: `true` once the
657
- * navigation lands, `false` when it doesn't (already there counts as
658
- * landed).
585
+ * {@link Panel.open} and the stack's own methods, so none can drift.
586
+ *
587
+ * `from` is the panel the navigation starts from, absent when it has none (a
588
+ * nav item), in which case the stack is derived or taken from `beneath`.
589
+ *
590
+ * `how` is the link's `data-panel` attribute, picking how much of `from`'s
591
+ * context the target keeps: a push (the default, and the fallback for
592
+ * unrecognised values) builds on `from`, `"replace"` keeps only what is
593
+ * beneath it, `"open"` keeps nothing. A target that is already open is
594
+ * returned to by a push and *moved* — alive — by the other two, since the
595
+ * stack never holds a path twice.
596
+ *
597
+ * Resolves `true` once the navigation lands (already being there counts).
659
598
  */
660
599
  private navigate;
661
600
  /** Programmatic push/replace, with the current panel as the implied origin. */
662
601
  private pushPath;
663
602
  /**
664
- * Link handling through `route.interceptLinks()`, whose handler hook hands us
665
- * the anchor so we can decide what the click *means*. The exclusion rules
666
- * (targets, downloads, modified clicks, external URLs) live in Aberdeen; the
667
- * close guards run in `checkChange` when our navigation reaches the router.
603
+ * Link handling through `route.interceptLinks()`, whose hook hands us the
604
+ * anchor so we can decide what the click means. The exclusion rules (targets,
605
+ * downloads, modified clicks, external URLs) live in Aberdeen.
668
606
  *
669
- * A link inside a panel is that panel's {@link Panel.open}, the `data-panel`
670
- * attribute as its `how` (see {@link navigate}, the shared implementation).
671
- * A link that isn't inside any panel — a nav item, one in a dialog — has no
672
- * panel to build on, so it replaces the stack as a whole, exactly as a cold
673
- * link to the same URL would open it.
607
+ * A link inside a panel is that panel's {@link Panel.open}, with `data-panel`
608
+ * as its `how`. A link outside every panel — a nav item, one in a dialog —
609
+ * has nothing to build on, so it replaces the stack as a cold link would.
674
610
  */
675
611
  private interceptLinks;
676
612
  get currentPanel(): Panel | undefined;
@@ -685,94 +621,78 @@ export declare class PanelStackController implements PanelStack {
685
621
  /** Adopt a changed `linkNavigation` default; the next click reads it. */
686
622
  setLinkNavigation(mode: "push" | "replace" | "open" | undefined): void;
687
623
  /**
688
- * The breadcrumb stack, drawn by `main()` into the top bar: every open
689
- * panel, oldest first, the ones on screen right now in bold, pinned ones
690
- * wearing their pin. Every crumb but the current panel's is a plain link to
691
- * that panel, and a link to an open panel returns to it (see `navigate`) —
692
- * so clicking a crumb goes back to that panel and closes what was stacked on
693
- * top of it, pinned and unsaved panels excepted. Right-click (or long-press)
694
- * offers pinning, and closing just that one panel — the close that splices
695
- * it out of the middle when it isn't last.
624
+ * The breadcrumb stack, drawn by `main()` into the top bar: every open panel,
625
+ * oldest first, the ones on screen in bold. Every crumb but the current one is
626
+ * a plain link, and a link to an open panel returns to it (see `navigate`),
627
+ * closing what was stacked on top. Right-click offers pinning and closing.
696
628
  */
697
629
  drawCrumbs(): void;
698
630
  private drawCrumb;
699
631
  /**
700
- * Flip a panel's pin (see {@link Panel.pinned}). The flag lives on the panel;
701
- * the current history entry's snapshot is rewritten too, so a reload keeps
702
- * the pin — a same-panel state tweak, which the router applies unguarded.
632
+ * Flip a panel's pin (see {@link Panel.pinned}). The current history entry's
633
+ * snapshot is rewritten too, so a reload keeps the pin.
703
634
  */
704
635
  private togglePin;
705
636
  /**
706
- * `"<panel title> · <app title>"`, kept in sync with the current panel — and
707
- * prefixed `"• "` while *any* open panel holds unsaved work, the way editors
708
- * mark a dirty document. Any panel, not just the current one: the risk of
709
- * losing the work is tab-wide, so the mark on the tab is too.
637
+ * `"<panel title> · <app title>"`, kept in sync with the current panel, and
638
+ * prefixed `"• "` while *any* open panel holds unsaved work — not just the
639
+ * current one, since the risk of losing it is tab-wide.
710
640
  */
711
641
  private watchTitle;
712
642
  /**
713
- * While any open panel holds unsaved work, closing the tab — or navigating
714
- * the whole browser away — runs into the browser's own are-you-sure, with
715
- * the unsaved panel brought on screen as the question is raised, so what is
716
- * holding the tab is in front of the user rather than parked out of sight.
643
+ * While any open panel holds unsaved work, closing the tab runs into the
644
+ * browser's own are-you-sure, with that panel brought on screen as the
645
+ * question is raised rather than left parked out of sight.
717
646
  */
718
647
  private guardTabClose;
719
648
  /**
720
649
  * Draw the column viewport into the current element. Called by `main()`.
721
650
  *
722
- * A column is the panel's own content, plus the one bit of chrome the shell
723
- * places for it: its {@link Panel.actions}, in a strip on wide shells and in
724
- * the top bar on narrow ones (see {@link drawActions}).
651
+ * A column is the panel's own content plus the one bit of chrome the shell
652
+ * places for it: its {@link Panel.actions} (see {@link drawActions}).
725
653
  */
726
654
  drawColumns(): void;
727
655
  private drawPanel;
728
656
  /**
729
657
  * The one bit of column chrome the shell draws: the panel's actions, in a
730
658
  * quiet strip above the scroll area — and only while the shell is wide, the
731
- * top bar carrying them otherwise. Everything else in a column is the panel's
732
- * own content: a screen that wants a heading or a card draws them itself.
733
- * Going back isn't here either — that is the breadcrumbs' job, in the bar.
659
+ * top bar carrying them otherwise. Everything else is the panel's own content.
734
660
  */
735
661
  private drawActions;
736
662
  scheduleLayout(): void;
663
+ /**
664
+ * Run the pending layout pass now instead of on the frame it waits for, for
665
+ * callers that must read what only the pass knows (`$panel.visible`,
666
+ * `$panel.width`) and can't wait. Does nothing when no pass is owed.
667
+ */
668
+ private flushLayout;
737
669
  /**
738
670
  * Measure the content area, and with it the width a panel of each size gets.
671
+ * The column region *is* that area (CSS's doing), so there is nothing to add
672
+ * up here that could drift from the bars above and below. Widths stay
673
+ * fractional: a rounded column edge would drift a pixel away from that chrome.
674
+ *
675
+ * The area divides into the narrowest whole number of columns of at least
676
+ * {@link SMALL_MIN_PX} — so 1080px is three of 360, 1520px four of 380 — each
677
+ * capped at {@link SMALL_MAX_PX}. A width is therefore a pure function of the
678
+ * window: a panel NEVER resizes because a neighbour came or went.
739
679
  *
740
- * The column region *is* the content area: it takes whatever the shell has
741
- * left beside the sidebar, capped by the shell's own `maxWidth` — all of it
742
- * CSS's doing, so there is nothing to add up here and nothing that could
743
- * drift from the width the bars above and below line up with. Fractional
744
- * widths throughout: a rounded column edge would drift a pixel away from that
745
- * chrome.
746
- *
747
- * The area divides into the narrowest whole number of columns that keeps each
748
- * at least {@link SMALL_MIN_PX} wide — the `"small"` unit every other size is
749
- * a multiple of, capped at the area itself. So 1080px is three columns of 360
750
- * and 1520px four of 380. An area too narrow for two is a single column,
751
- * itself capped at {@link SMALL_MAX_PX}: a small centres there instead of
752
- * stretching toward 720, so its ceiling holds, while the larger sizes still
753
- * take the whole area. A width is thus a pure function of the window: a panel
754
- * NEVER resizes because a neighbour came or went, and only a window resize
755
- * (the snap pass in `layout`) changes one.
756
- *
757
- * `undefined` while the shell has no width to speak of (it isn't in a document
758
- * yet, or it's `display:none`); the next pass tries again.
680
+ * `undefined` while the shell has no width yet (not in a document, or
681
+ * `display:none`); the next pass tries again.
759
682
  */
760
683
  private measure;
761
684
  /**
762
- * The measurements this pass runs on. Taken once per layout pass and per
763
- * commit, and shared with the panels drawn in between — they all size
764
- * themselves against the same shell, and a `getBoundingClientRect()` each
765
- * would be a forced reflow each, in the middle of building their DOM.
685
+ * The measurements this pass runs on, taken once per pass and per commit and
686
+ * shared with the panels drawn in between: they must all size against the
687
+ * same shell, and a `getBoundingClientRect()` each is a forced reflow each.
766
688
  */
767
689
  private geometry;
768
690
  /** How wide a panel asking for this is, right now; 0 while the shell can't be measured. */
769
691
  private roomFor;
770
692
  /**
771
- * Size and position every panel.
772
- *
773
- * This is everything CSS can't work out for itself: which panels exist, which
774
- * of them are visible, how wide each one is and where it sits. All the motion
775
- * between two of these arrangements is CSS's job.
693
+ * Size and position every panel — everything CSS can't work out for itself:
694
+ * which panels exist, which are visible, how wide each is and where it sits.
695
+ * The motion between two such arrangements is CSS's job.
776
696
  */
777
697
  private layout;
778
698
  /** Let a `loading` panel's enter animation wait — but not indefinitely. */