staffa 0.13.0 → 0.14.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.
@@ -4,20 +4,24 @@ import { type Slot } from "../core.js";
4
4
  * Routed, multi-column panel navigation for {@link main}.
5
5
  *
6
6
  * Each route draws one screen of the app, called a *panel*. The open panels
7
- * form a **stack**, and one of them is the **current** panel: the one the URL
7
+ * form a **stack**, whose last panel is the **current** one: the panel the URL
8
8
  * names, and the rightmost column on screen. As many panels as fit are shown,
9
9
  * ending at the current one — on a phone that is one at a time, on a wider
10
10
  * screen the panels that would have covered each other sit side by side
11
11
  * instead. The app's own code is the same either way.
12
12
  *
13
- * Going to a panel that is already open a breadcrumb, or any link to it —
14
- * just moves the current-panel cursor along the stack: panels right of it stay
15
- * open, parked past the right edge of the viewport, and nothing closes.
16
- * Opening a *new* panel is what prunes: everything after the panel it came
17
- * from closes, except panels the user pinned which ride along beneath the
18
- * new panel and panels holding unsaved work, which no navigation ever tears
19
- * down. Escape steps one panel left, closing the panel it leaves only when
20
- * that panel is the stack's discardable end.
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.
21
25
  *
22
26
  * Navigation runs through `aberdeen/route`: the URL holds the current panel,
23
27
  * and the rest of the arrangement — the panels before it, the ones parked
@@ -83,6 +87,14 @@ export type AncestorsHandler<P = any> = (params: P, path: string) => readonly st
83
87
  export type AncestorTable<R> = {
84
88
  [K in keyof R & string]?: (params: Prettify<PathParams<K>>, path: string) => readonly string[] | undefined | void;
85
89
  };
90
+ /**
91
+ * How wide a panel asks to be — a ceiling the shell never exceeds; see
92
+ * {@link Panel.maxWidth}. `"small"` is the column the content area is divided
93
+ * into; `"medium"` and `"large"` are two and three of those, and `"none"` is
94
+ * the whole area. Each is capped at the content area, so on a narrow window
95
+ * they all come to the same thing.
96
+ */
97
+ export type PanelSize = "small" | "medium" | "large" | "none";
86
98
  /**
87
99
  * What a route handler gets: the params from its route, plus everything the
88
100
  * shell needs to know about the panel it is drawing. It's an Aberdeen proxy, so
@@ -153,35 +165,35 @@ export interface Panel<P = Record<string, string | number | string[]>> {
153
165
  */
154
166
  readonly visible: boolean;
155
167
  /**
156
- * The widest this panel can usefully be. Every panel must work at 360–540px,
157
- * because that is what it gets when two columns fit; this says how much
158
- * *more* it can take.
159
- *
160
- * - `"half"` nothing more. Half the content area (360px up to half of
161
- * {@link MainOptions.fullWidth}), so a second column fits beside it. For
162
- * lists and detail forms.
163
- * - `"full"` (the default) — the whole content area, which is exactly
164
- * {@link MainOptions.fullWidth}: 1080px unless the app says otherwise.
165
- * - `"screen"` the whole window, unbounded: boards, wide tables, dense
166
- * dashboards. While one is open the columns stretch to the screen edges
167
- * instead of stopping at the standard page; the top bar and footer hold
168
- * the standard width throughout.
169
- *
170
- * Below the width two columns need, everything takes the content area
171
- * whatever it asked for. Widths depend only on the window, never on what
172
- * else is open, so opening or closing a panel never resizes another.
173
- *
174
- * This is a *layout regime*, not a width guarantee: handle whatever width
175
- * the bucket yields, and ask only for what your content can actually use
176
- * a screen that would cap its own content narrower than its ask is holding
177
- * room that would have let another column fit beside it.
168
+ * The widest this panel can usefully be a ceiling the shell never
169
+ * exceeds, so the draw function never has to look right past it. It is
170
+ * counted in the shell's *columns*: the content area divides into the
171
+ * narrowest whole number of columns of at least 360px each (three columns
172
+ * of 360 in a 1080px area, four of 380 in 1520px), and a column is never
173
+ * wider than 540 — where the area holds just one, a small centres in it
174
+ * rather than stretching. So an ask never exceeds its column count × 540:
175
+ *
176
+ * - `"small"` one column, never above 540px: lists, detail forms.
177
+ * - `"medium"` (the default) two columns, never above 1080px.
178
+ * - `"large"` three columns, never above 1620px: wide tables.
179
+ * - `"none"` the whole content area, unbounded: boards, dashboards.
180
+ * Bound it with the shell's own `maxWidth` where that matters.
181
+ *
182
+ * Every size is capped at the content area, so on a phone they all come to
183
+ * the same thing: one screen at a time. And a width depends only on the
184
+ * window, never on what else is open, so opening or closing a panel never
185
+ * resizes another — the run of columns just recentres in the area.
186
+ *
187
+ * Ask only for what your content can actually use: a panel that would cap
188
+ * its own content narrower than its ask is holding room that would have
189
+ * let another column fit beside it.
178
190
  *
179
191
  * Set it at the top of your handler and the panel is already that wide when
180
192
  * you draw (see {@link Panel.width}); set it later — when your data tells you
181
193
  * — and the panel reflows without being redrawn, keeping its state, while
182
194
  * the columns beside it move over.
183
195
  */
184
- maxWidth?: "half" | "full" | "screen";
196
+ maxWidth?: PanelSize;
185
197
  /**
186
198
  * Set this while you're fetching what the panel needs, and back to `false`
187
199
  * when you're done. A new panel waits a moment before sliding in, so it can
@@ -192,10 +204,10 @@ export interface Panel<P = Record<string, string | number | string[]>> {
192
204
  loading?: boolean;
193
205
  /**
194
206
  * Keeps this panel from being closed by navigation happening *elsewhere*.
195
- * Opening a new panel normally closes everything after the panel it came
196
- * from; a pinned panel survives that, staying in the stack — parked past the
197
- * right edge of the viewport slotted in beneath the new panel, one crumb
198
- * click away. The user toggles it from the crumb's context menu
207
+ * A navigation normally closes everything after the panel it came from (or
208
+ * returned to); a pinned panel survives that, staying in the stack — slotted
209
+ * in beneath the new panel, or parked out of sight when the new panel was
210
+ * already beneath it. Either way it is one crumb click away. The user toggles it from the crumb's context menu
199
211
  * (right-click or long-press), which is also where the pin shows; setting
200
212
  * it from code does the same thing.
201
213
  *
@@ -207,12 +219,11 @@ export interface Panel<P = Record<string, string | number | string[]>> {
207
219
  /**
208
220
  * Set this while the panel holds work that must not be lost — a dirty form,
209
221
  * an upload in flight. An unsaved panel cannot be closed, by anything:
210
- * navigation that would prune it parks it instead, past the viewport's
211
- * right edge, wearing a ● in its crumb — even the browser's back button
212
- * only parks it. {@link Panel.close} and the crumb menu's Close refuse,
222
+ * navigation that would prune it parks it out of sight instead, wearing a
223
+ * ● in its crumb — even the browser's back button only parks it. {@link Panel.close} and the crumb menu's Close refuse,
213
224
  * Escape on it steps left along the stack rather than closing, and closing
214
- * the browser tab runs into the browser's own are-you-sure (after which the
215
- * shell brings the unsaved panel back on screen).
225
+ * the browser tab runs into the browser's own are-you-sure, the unsaved
226
+ * panel brought on screen as the question is raised.
216
227
  *
217
228
  * Only the app clears it; the user has no toggle. A Save or Discard button
218
229
  * clears it and then closes:
@@ -277,6 +288,7 @@ export interface Panel<P = Record<string, string | number | string[]>> {
277
288
  */
278
289
  open(href: string, how?: "push" | "replace" | "open"): Promise<boolean>;
279
290
  }
291
+ export declare const SMALL_MAX_PX = 540;
280
292
  /** Options the stack needs from its shell. */
281
293
  export interface PanelStackOptions {
282
294
  routes: Routes;
@@ -287,8 +299,6 @@ export interface PanelStackOptions {
287
299
  columns?: "auto" | "single";
288
300
  /** What a bare link does. See {@link MainOptions.linkNavigation}. */
289
301
  linkNavigation?: "push" | "replace" | "open";
290
- /** How wide a `"full"` panel gets, in px. See {@link MainOptions.fullWidth}. */
291
- fullWidth: number;
292
302
  /** The shell's own title, used as the suffix of `document.title`. */
293
303
  title?: unknown;
294
304
  /**
@@ -346,10 +356,10 @@ export interface PanelStack {
346
356
  * the new panel).
347
357
  *
348
358
  * The same rules as a link click apply: pushing a path that is already open
349
- * goes back to it — a focus move along the stack, closing nothing rather
350
- * than opening it twice, and a panel holding {@link Panel.unsaved} work is
351
- * never closed, only parked. That's what a plain link does, and what
352
- * `data-panel=push` says outright.
359
+ * returns to it — closing whatever was stacked on top rather than opening
360
+ * it twice, and a panel holding {@link Panel.unsaved} work is never closed,
361
+ * only parked. That's what a plain link does, and what `data-panel=push`
362
+ * says outright.
353
363
  *
354
364
  * Note that a link builds on the panel it is *drawn in*, which is the
355
365
  * current panel only while no column beside it has the focus. Code
@@ -584,19 +594,21 @@ export declare class PanelStackController implements PanelStack {
584
594
  private issue;
585
595
  private start;
586
596
  /**
587
- * Make the stack's `index`th panel current: the URL and the visible run move
588
- * to it, while the panels right of it stay open, parked past the right edge
589
- * of the viewport. Nothing closes; it is a history entry, so the browser's
590
- * back button returns the focus to where it was. What a click on a
591
- * breadcrumb any link to an open panel comes down to.
597
+ * 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.
592
604
  */
593
605
  private focusAt;
594
606
  /**
595
607
  * One step back along the stack — what Escape does (`main()` calls this;
596
- * it is not {@link PanelStack} API). At the stack's end this closes the
597
- * current panel; mid-stack with panels parked to the right — or when the
598
- * panel holds {@link Panel.unsaved} work, the panel stays open and the
599
- * focus just moves to the panel on its left, parking the one it leaves.
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.
600
612
  * Resolves `false` at the stack's start, where there is no left to go.
601
613
  */
602
614
  back(): Promise<boolean>;
@@ -632,11 +644,14 @@ export declare class PanelStackController implements PanelStack {
632
644
  * that know it.
633
645
  *
634
646
  * `how` is the link's `data-panel` attribute (or the caller's word for
635
- * it): absent like a link without the attribute it is the shell's
636
- * `linkNavigation` default, an unrecognised value is a push on top of
637
- * `from`, `"replace"` swaps `from` out rather than stacking on it, and
638
- * `"open"` drops `from` altogether so the target arrives with its own
639
- * stack, the way a nav item's link does.
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.
640
655
  *
641
656
  * Resolves the way every {@link PanelStack} method does: `true` once the
642
657
  * navigation lands, `false` when it doesn't (already there counts as
@@ -669,21 +684,15 @@ export declare class PanelStackController implements PanelStack {
669
684
  setColumns(columns: "auto" | "single" | undefined): void;
670
685
  /** Adopt a changed `linkNavigation` default; the next click reads it. */
671
686
  setLinkNavigation(mode: "push" | "replace" | "open" | undefined): void;
672
- /**
673
- * Adopt a changed `fullWidth`: one layout pass, nothing redrawn. A changed
674
- * `navWidth` needs no counterpart — resizing the sidebar resizes the column
675
- * region, which the layout engine is already observing.
676
- */
677
- setFullWidth(px: number): void;
678
687
  /**
679
688
  * The breadcrumb stack, drawn by `main()` into the top bar: every open
680
689
  * panel, oldest first, the ones on screen right now in bold, pinned ones
681
690
  * wearing their pin. Every crumb but the current panel's is a plain link to
682
- * that panel, and a link to an open panel is a focus move (see `navigate`) —
683
- * so clicking along the stack closes nothing, in either direction, and the
684
- * panels right of the current one wait just past the viewport's edge.
685
- * Right-click (or long-press) offers pinning, and closing just that one
686
- * panel — the close that splices it out of the middle when it isn't last.
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.
687
696
  */
688
697
  drawCrumbs(): void;
689
698
  private drawCrumb;
@@ -702,9 +711,9 @@ export declare class PanelStackController implements PanelStack {
702
711
  private watchTitle;
703
712
  /**
704
713
  * While any open panel holds unsaved work, closing the tab — or navigating
705
- * the whole browser away — runs into the browser's own are-you-sure. When
706
- * the user stays, the unsaved panel is brought back on screen if it wasn't,
707
- * so what held the tab is in front of them rather than parked out of sight.
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.
708
717
  */
709
718
  private guardTabClose;
710
719
  /**
@@ -726,11 +735,24 @@ export declare class PanelStackController implements PanelStack {
726
735
  private drawActions;
727
736
  scheduleLayout(): void;
728
737
  /**
729
- * Measure the shell, and with it the width the window gives a panel of each
730
- * layout. Measured on the *shell*, not on the column region: the region's width
731
- * is the layout engine's own output, so reading it back would nail the layout
732
- * to whatever it happened to be a frame ago. Fractional widths throughout a
733
- * rounded column edge would drift a pixel away from the chrome above it.
738
+ * Measure the content area, and with it the width a panel of each size gets.
739
+ *
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.
734
756
  *
735
757
  * `undefined` while the shell has no width to speak of (it isn't in a document
736
758
  * yet, or it's `display:none`); the next pass tries again.
@@ -746,8 +768,7 @@ export declare class PanelStackController implements PanelStack {
746
768
  /** How wide a panel asking for this is, right now; 0 while the shell can't be measured. */
747
769
  private roomFor;
748
770
  /**
749
- * Size and position every panel, and publish the width of the whole ensemble
750
- * (sidebar + separator + columns) for the shell to centre itself on.
771
+ * Size and position every panel.
751
772
  *
752
773
  * This is everything CSS can't work out for itself: which panels exist, which
753
774
  * of them are visible, how wide each one is and where it sits. All the motion