staffa 0.12.0 → 0.12.1
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 +1 -1
- package/dist/components/main.js +29 -20
- package/dist/components/panels.d.ts +68 -16
- package/dist/components/panels.js +75 -52
- package/dist/staffa.esm.js +1 -1
- package/package.json +3 -3
- package/skill/Panel.md +36 -2
- package/skill/PanelStack.md +5 -0
- package/skill/SKILL.md +1 -1
- package/src/components/main.ts +30 -22
- package/src/components/panels.ts +113 -53
package/README.md
CHANGED
|
@@ -190,7 +190,7 @@ Navigating faster than the shell can settle is fine: closing travels through the
|
|
|
190
190
|
|
|
191
191
|
Below the width two columns need, everything takes the whole content area whatever it asked for. Those numbers assume a nav sidebar of around 170px; without one, add that back. Nothing fits beside a `"full"` on a standard 1280px page, but on a wide enough window a `"half"` still can, and the page grows past 1280px to hold both.
|
|
192
192
|
|
|
193
|
-
A column's width depends only on the size of the window, never on what else is open. So opening or closing a panel never resizes the ones already on screen, and never reflows what someone was reading. A lone `"half"` leaves its other half empty, and that is exactly where the next one lands. When more columns fit than the standard 1280px page holds (three halves, say), the page itself grows, staying centred, to hold them.
|
|
193
|
+
A column's width depends only on the size of the window, never on what else is open. So opening or closing a panel never resizes the ones already on screen, and never reflows what someone was reading. A lone `"half"` leaves its other half empty, and that is exactly where the next one lands. When more columns fit than the standard 1280px page holds (three halves, say), the page itself grows, staying centred, to hold them — though the top bar and footer keep to the standard width, so the chrome holds still while the columns come and go.
|
|
194
194
|
|
|
195
195
|
Columns tile that area, separated by a hairline and no gutter — a column brings its own padding, so their contents stay comfortably apart regardless.
|
|
196
196
|
|
package/dist/components/main.js
CHANGED
|
@@ -8,7 +8,7 @@ import { drawMenu, isFloatingMenuOpen, consumeBranchNav, anyCurrent } from "./me
|
|
|
8
8
|
import { menu as menuIcon, x as closeIcon } from "../icons.js";
|
|
9
9
|
import { iconButton } from "./button.js";
|
|
10
10
|
import { isDialogOpen } from "./dialog.js";
|
|
11
|
-
import { PanelStackController } from "./panels.js";
|
|
11
|
+
import { PanelStackController, SHELL_PX } from "./panels.js";
|
|
12
12
|
A.insertGlobalCss({
|
|
13
13
|
".s-main": {
|
|
14
14
|
// container-type so @container queries below can respond to shell width.
|
|
@@ -26,8 +26,10 @@ A.insertGlobalCss({
|
|
|
26
26
|
// The bar reads `[leading] [title] …spacer… [trailing]`. The spacer is the
|
|
27
27
|
// trailing slot's own growth: it takes the free space and right-aligns
|
|
28
28
|
// itself in it, which is what lets a search box live there. When the two
|
|
29
|
-
// compete, the
|
|
30
|
-
//
|
|
29
|
+
// compete, the titles give way first: the trailing slot's near-zero
|
|
30
|
+
// shrink factor keeps a row of actions at its natural width while the
|
|
31
|
+
// crumbs absorb the squeeze — but only down to the titles' floor, past
|
|
32
|
+
// which the trailing slot shrinks after all: a wide search box must not
|
|
31
33
|
// starve the titles to nothing (the crumb strip's overlay buttons would
|
|
32
34
|
// escape their zero-width strip, over the ☰ beside it).
|
|
33
35
|
"> header > .s-bar, > footer > .s-bar": "display:flex align-items:center width:100% margin-inline:auto gap:$3 padding: $2 $3;",
|
|
@@ -48,13 +50,16 @@ A.insertGlobalCss({
|
|
|
48
50
|
// which their classes then provide. (`filter:none` keeps the global
|
|
49
51
|
// `a:hover` brighten off the gradient text.)
|
|
50
52
|
"> header a.s-logo, > header a.s-title": "text-decoration:none filter:none cursor:pointer",
|
|
51
|
-
"> header .s-menu": "display:flex align-items:center justify-content:flex-end gap:$2 flex: 1 1 auto; min-width:0",
|
|
53
|
+
"> header .s-menu": "display:flex align-items:center justify-content:flex-end gap:$2 flex: 1 0.1 auto; min-width:0",
|
|
52
54
|
// Body always wraps <main> (with or without a sidebar) so max-width centering
|
|
53
55
|
// and scrollbar alignment work identically in both cases.
|
|
54
56
|
// .s-body centres .s-body-inner; .s-body-inner caps the content to maxWidth.
|
|
55
57
|
// It's also the positioning + clipping context for the narrow-screen nav panel,
|
|
56
|
-
// which slides in and out across its left edge.
|
|
57
|
-
|
|
58
|
+
// which slides in and out across its left edge. `overflow:clip` rather than
|
|
59
|
+
// `hidden` for the same reason as `.s-panels`: a hidden box can still be
|
|
60
|
+
// scrolled (find-in-page, an anchor, an extension), and a stray scroll here
|
|
61
|
+
// would shove the whole row — sidebar and columns — out of place for good.
|
|
62
|
+
".s-body": "flex:1 overflow:clip display:flex flex-direction:row min-height:0 justify-content:center position:relative",
|
|
58
63
|
".s-body-inner": "flex:1 min-width:0 display:flex flex-direction:row min-height:0",
|
|
59
64
|
// Put the sidebar on the right (content fills the left) for right-hand navs.
|
|
60
65
|
"&.s-nav-right .s-body-inner": "flex-direction:row-reverse",
|
|
@@ -85,20 +90,24 @@ A.insertGlobalCss({
|
|
|
85
90
|
// Routed mode takes its width from the stack instead of from
|
|
86
91
|
// `maxWidth`: the layout engine publishes the ensemble width (sidebar +
|
|
87
92
|
// separator + content area) as --s-shell-w — the standard 1280px page
|
|
88
|
-
// normally, the
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
// the
|
|
100
|
-
|
|
101
|
-
|
|
93
|
+
// normally, wider while the columns outgrow it (a "screen" page, or
|
|
94
|
+
// extra columns fitting a wide window) — and the body row caps itself
|
|
95
|
+
// to it, staying centred around the columns. Changing the custom
|
|
96
|
+
// property animates the max-width consuming it, with no JS in the loop:
|
|
97
|
+
// the body recentres in step with the panel whose arrival or departure
|
|
98
|
+
// moved it, over the same --s-panel-ms (see panels.ts). During a window
|
|
99
|
+
// resize (and the very first pass) the layout engine raises
|
|
100
|
+
// `.s-shell-snap` so the new width is adopted instantly instead of
|
|
101
|
+
// chasing the window through a transition.
|
|
102
|
+
"&.s-routed > .s-body > .s-body-inner": "max-width: var(--s-shell-w, 100%); transition: max-width var(--s-panel-ms) ease;",
|
|
103
|
+
"&.s-routed.s-shell-snap > .s-body > .s-body-inner": "transition:none",
|
|
104
|
+
// The bars don't follow the ensemble past the standard page: a header
|
|
105
|
+
// stretching to the window's edges and back with every "screen" panel
|
|
106
|
+
// reads as the whole app flexing, so the chrome holds still and only
|
|
107
|
+
// the columns grow. (Below the standard width the ensemble is simply
|
|
108
|
+
// the window, which only a resize changes — so the bars never animate,
|
|
109
|
+
// and take no part in the transition above.)
|
|
110
|
+
[`&.s-routed > header > .s-bar, &.s-routed > footer > .s-bar`]: `max-width:${SHELL_PX}px`,
|
|
102
111
|
},
|
|
103
112
|
// Sidebar nav panel. Items reuse the shared `.s-menu-item` /
|
|
104
113
|
// `.s-menu-sep` styles from menu.ts, so the sidebar and the floating
|
|
@@ -161,13 +161,19 @@ export interface Panel<P = Record<string, string | number | string[]>> {
|
|
|
161
161
|
* column fits beside it. For lists and detail forms.
|
|
162
162
|
* - `"full"` (the default) — the whole content area, up to ~1100px.
|
|
163
163
|
* - `"screen"` — the whole window, unbounded: boards, wide tables, dense
|
|
164
|
-
* dashboards. While one is open the
|
|
165
|
-
*
|
|
164
|
+
* dashboards. While one is open the columns stretch to the screen edges
|
|
165
|
+
* instead of stopping at the standard 1280px page; the top bar and
|
|
166
|
+
* footer hold the standard width throughout.
|
|
166
167
|
*
|
|
167
168
|
* Below the width two columns need, everything takes the content area
|
|
168
169
|
* whatever it asked for. Widths depend only on the window, never on what
|
|
169
170
|
* else is open, so opening or closing a panel never resizes another.
|
|
170
171
|
*
|
|
172
|
+
* This is a *layout regime*, not a width guarantee: handle whatever width
|
|
173
|
+
* the bucket yields, and ask only for what your content can actually use —
|
|
174
|
+
* a screen that would cap its own content narrower than its ask is holding
|
|
175
|
+
* room that would have let another column fit beside it.
|
|
176
|
+
*
|
|
171
177
|
* Set it at the top of your handler and the panel is already that wide when
|
|
172
178
|
* you draw (see {@link Panel.width}); set it later — when your data tells you
|
|
173
179
|
* — and the panel reflows without being redrawn, keeping its state, while
|
|
@@ -243,7 +249,40 @@ export interface Panel<P = Record<string, string | number | string[]>> {
|
|
|
243
249
|
* ```
|
|
244
250
|
*/
|
|
245
251
|
close(): Promise<boolean>;
|
|
252
|
+
/**
|
|
253
|
+
* Opens `href` exactly as a click on a link inside this panel does — the
|
|
254
|
+
* shell's own link handling runs through this very call, so the two can't
|
|
255
|
+
* drift apart. By default that is a push: the target opens on top of this
|
|
256
|
+
* panel, closing the panels after it first (pinned ones ride along
|
|
257
|
+
* beneath the new panel, unsaved ones park), and a path that is already
|
|
258
|
+
* open is returned to rather than opened twice. `how` plays the part of a
|
|
259
|
+
* link's `data-panel` attribute: `"replace"` puts the target in this
|
|
260
|
+
* panel's place, `"open"` leaves the panel behind and gives the target
|
|
261
|
+
* its own stack, and omitting it follows the shell's
|
|
262
|
+
* {@link MainOptions.linkNavigation}, like a link without the attribute.
|
|
263
|
+
*
|
|
264
|
+
* This is the one for navigation that can't be a link: a row's click
|
|
265
|
+
* handler, a keyboard shortcut acting on this screen. The stack's
|
|
266
|
+
* {@link PanelStack.pushPanel} builds on the *current* panel instead — a
|
|
267
|
+
* different panel exactly when the interaction happened in a column
|
|
268
|
+
* beside it, where it would pile the new panel on top of the open detail
|
|
269
|
+
* rather than pruning back to this one.
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```ts
|
|
273
|
+
* A("div.row click=", () => void $panel.open(`/contacts/${id}`), ...);
|
|
274
|
+
* ```
|
|
275
|
+
*/
|
|
276
|
+
open(href: string, how?: "push" | "replace" | "open"): Promise<boolean>;
|
|
246
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* The standard page width: sidebar plus content area, capped by the window.
|
|
280
|
+
* `"full"` fills the content-area part of this exactly; only a `"screen"`
|
|
281
|
+
* page makes the shell grow past it. The top bar and footer keep to this
|
|
282
|
+
* width even then (see main.ts), so the chrome holds still while the
|
|
283
|
+
* columns stretch.
|
|
284
|
+
*/
|
|
285
|
+
export declare const SHELL_PX = 1280;
|
|
247
286
|
/** Options the stack needs from its shell. */
|
|
248
287
|
export interface PanelStackOptions {
|
|
249
288
|
routes: Routes;
|
|
@@ -315,6 +354,11 @@ export interface PanelStack {
|
|
|
315
354
|
* than opening it twice, and a panel holding {@link Panel.unsaved} work is
|
|
316
355
|
* never closed, only parked. That's what a plain link does, and what
|
|
317
356
|
* `data-panel=push` says outright.
|
|
357
|
+
*
|
|
358
|
+
* Note that a link builds on the panel it is *drawn in*, which is the
|
|
359
|
+
* current panel only while no column beside it has the focus. Code
|
|
360
|
+
* navigating on behalf of a particular screen — a row's click handler —
|
|
361
|
+
* wants that panel's own {@link Panel.open} instead.
|
|
318
362
|
*/
|
|
319
363
|
pushPanel(path: string): Promise<boolean>;
|
|
320
364
|
/**
|
|
@@ -581,11 +625,22 @@ export declare class PanelStackController implements PanelStack {
|
|
|
581
625
|
*/
|
|
582
626
|
private closePath;
|
|
583
627
|
/**
|
|
584
|
-
* Navigate to `href
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
* the
|
|
628
|
+
* Navigate to `href` — the one implementation behind a link click,
|
|
629
|
+
* {@link Panel.open} and the stack's own methods, so none of them can
|
|
630
|
+
* behave differently.
|
|
631
|
+
*
|
|
632
|
+
* `from` is the path of the panel the navigation starts from — the one
|
|
633
|
+
* the link lives in — or absent when it has none: a nav item, or a call
|
|
634
|
+
* that means the whole stack, which is then built instead (see
|
|
635
|
+
* {@link deriveStack}), or taken outright from `beneath`, for callers
|
|
636
|
+
* that know it.
|
|
637
|
+
*
|
|
638
|
+
* `how` is the link's `data-panel` attribute (or the caller's word for
|
|
639
|
+
* it): absent — like a link without the attribute — it is the shell's
|
|
640
|
+
* `linkNavigation` default, an unrecognised value is a push on top of
|
|
641
|
+
* `from`, `"replace"` swaps `from` out rather than stacking on it, and
|
|
642
|
+
* `"open"` drops `from` altogether so the target arrives with its own
|
|
643
|
+
* stack, the way a nav item's link does.
|
|
589
644
|
*
|
|
590
645
|
* Resolves the way every {@link PanelStack} method does: `true` once the
|
|
591
646
|
* navigation lands, `false` when it doesn't (already there counts as
|
|
@@ -596,18 +651,15 @@ export declare class PanelStackController implements PanelStack {
|
|
|
596
651
|
private pushPath;
|
|
597
652
|
/**
|
|
598
653
|
* Link handling through `route.interceptLinks()`, whose handler hook hands us
|
|
599
|
-
* the anchor so we can decide what the click *means
|
|
600
|
-
* `.s-panel` (which decides what the click truncates), the `data-panel`
|
|
601
|
-
* attribute, and return-to-an-open-panel semantics. The exclusion rules
|
|
654
|
+
* the anchor so we can decide what the click *means*. The exclusion rules
|
|
602
655
|
* (targets, downloads, modified clicks, external URLs) live in Aberdeen; the
|
|
603
656
|
* close guards run in `checkChange` when our navigation reaches the router.
|
|
604
657
|
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
* an unrecognised value is a `push`.
|
|
658
|
+
* A link inside a panel is that panel's {@link Panel.open}, the `data-panel`
|
|
659
|
+
* attribute as its `how` (see {@link navigate}, the shared implementation).
|
|
660
|
+
* A link that isn't inside any panel — a nav item, one in a dialog — has no
|
|
661
|
+
* panel to build on, so it replaces the stack as a whole, exactly as a cold
|
|
662
|
+
* link to the same URL would open it.
|
|
611
663
|
*/
|
|
612
664
|
private interceptLinks;
|
|
613
665
|
get currentPanel(): Panel | undefined;
|
|
@@ -112,7 +112,7 @@ function matchRoute(r, segments) {
|
|
|
112
112
|
/**
|
|
113
113
|
* The one duration every bit of shell motion shares: the enter/exit fades, the
|
|
114
114
|
* `left` moves of columns shifting sideways, the ensemble-width transition the
|
|
115
|
-
*
|
|
115
|
+
* body row follows (see `--s-shell-w` in main.ts), and the narrow-screen nav
|
|
116
116
|
* panel's slide. Published as the `--s-panel-ms` custom property below, so CSS
|
|
117
117
|
* and JS can't drift apart.
|
|
118
118
|
*
|
|
@@ -126,9 +126,11 @@ const LOADING_HOLD_MS = 300;
|
|
|
126
126
|
/**
|
|
127
127
|
* The standard page width: sidebar plus content area, capped by the window.
|
|
128
128
|
* `"full"` fills the content-area part of this exactly; only a `"screen"`
|
|
129
|
-
* page makes the shell grow past it.
|
|
129
|
+
* page makes the shell grow past it. The top bar and footer keep to this
|
|
130
|
+
* width even then (see main.ts), so the chrome holds still while the
|
|
131
|
+
* columns stretch.
|
|
130
132
|
*/
|
|
131
|
-
const SHELL_PX = 1280;
|
|
133
|
+
export const SHELL_PX = 1280;
|
|
132
134
|
/** Don't pair smalls when half the content area would be narrower than this. */
|
|
133
135
|
const PAIR_MIN_PX = 360;
|
|
134
136
|
/**
|
|
@@ -151,7 +153,12 @@ A.insertGlobalCss({
|
|
|
151
153
|
// The region paints the panel's sheen over its own box, and every panel shows
|
|
152
154
|
// a slice of that same gradient (see `.s-panel` below), so the columns and
|
|
153
155
|
// the ground beside them are one continuous surface.
|
|
154
|
-
|
|
156
|
+
// `overflow:clip`, not `hidden`: a hidden box is still a scroll container,
|
|
157
|
+
// and anything that ever scrolls it — find-in-page reaching for text in a
|
|
158
|
+
// parked column, an in-page anchor, an extension — shifts every column
|
|
159
|
+
// sideways, permanently, because nothing here would ever scroll it back.
|
|
160
|
+
// `clip` clips without being scrollable at all, closing the whole class.
|
|
161
|
+
".s-panels": "flex:1 min-width:0 min-height:0 position:relative overflow:clip isolation:isolate " +
|
|
155
162
|
SURFACE_SHEEN,
|
|
156
163
|
".s-panel": {
|
|
157
164
|
// A panel rests at a plain `left` offset and carries no transform: a
|
|
@@ -245,13 +252,18 @@ A.insertGlobalCss({
|
|
|
245
252
|
// the weight change alone is ambiguous in a short crumb, the colour alone
|
|
246
253
|
// too subtle. No padding of its own — the first crumb has to start on the
|
|
247
254
|
// same pixel as the app's name above it, and the gap below spaces the row.
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
+
// The flex is how a tight row is shared out. Every crumb grows from the
|
|
256
|
+
// same 4rem basis in equal shares, freezing at its own text
|
|
257
|
+
// (`max-width:max-content`) — so with room to spare every title shows in
|
|
258
|
+
// full, and under pressure it is the *longest* crumbs that give way
|
|
259
|
+
// first, equalising downward while short ones keep every character. No
|
|
260
|
+
// crumb drops below min(its text, 4rem) though: `flex-shrink:0`, so past
|
|
261
|
+
// that point the row overflows and the strip scrolls — which is what
|
|
262
|
+
// keeps a deep stack on a phone readable. (Crumbs allowed to shrink
|
|
263
|
+
// would ellipsise to a row of stubs instead, and the stack would never
|
|
264
|
+
// scroll.)
|
|
265
|
+
"&": "flex: 1 0 4rem; font-size:0.85em line-height:1.5 fg:$s-muted text-decoration:none " +
|
|
266
|
+
"white-space:nowrap max-width:max-content overflow:hidden text-overflow:ellipsis " +
|
|
255
267
|
"transition: color 0.12s;",
|
|
256
268
|
"&.s-crumb-on": "font-weight:600 fg:$s-text",
|
|
257
269
|
// The same hover treatment as a menu item. The panel you are on is a plain
|
|
@@ -620,9 +632,12 @@ export class PanelStackController {
|
|
|
620
632
|
maxWidth: "full",
|
|
621
633
|
width: 0,
|
|
622
634
|
};
|
|
623
|
-
// `close` closes *this* panel, current or not
|
|
624
|
-
//
|
|
625
|
-
//
|
|
635
|
+
// `close` closes *this* panel, current or not, and `open` navigates
|
|
636
|
+
// *from* it, through the very implementation a link click uses (see
|
|
637
|
+
// `navigate`). Both resolve the panel's place in the stack at call
|
|
638
|
+
// time, so they keep working after a splice has moved it; an `open`
|
|
639
|
+
// from a panel that has since closed falls back to a derived stack,
|
|
640
|
+
// like a link from nowhere.
|
|
626
641
|
//
|
|
627
642
|
// `visible` starts at what the panel's place implies: shown when it sits
|
|
628
643
|
// at or before the current panel (a pushed panel always does), hidden when
|
|
@@ -636,6 +651,7 @@ export class PanelStackController {
|
|
|
636
651
|
visible,
|
|
637
652
|
pinned: pinned || undefined,
|
|
638
653
|
close: () => this.closePath(entry.path),
|
|
654
|
+
open: (href, how) => this.navigate(href, { from: entry.path, how }),
|
|
639
655
|
});
|
|
640
656
|
return entry;
|
|
641
657
|
}
|
|
@@ -865,17 +881,31 @@ export class PanelStackController {
|
|
|
865
881
|
});
|
|
866
882
|
}
|
|
867
883
|
/**
|
|
868
|
-
* Navigate to `href
|
|
869
|
-
*
|
|
870
|
-
*
|
|
871
|
-
*
|
|
872
|
-
* the
|
|
884
|
+
* Navigate to `href` — the one implementation behind a link click,
|
|
885
|
+
* {@link Panel.open} and the stack's own methods, so none of them can
|
|
886
|
+
* behave differently.
|
|
887
|
+
*
|
|
888
|
+
* `from` is the path of the panel the navigation starts from — the one
|
|
889
|
+
* the link lives in — or absent when it has none: a nav item, or a call
|
|
890
|
+
* that means the whole stack, which is then built instead (see
|
|
891
|
+
* {@link deriveStack}), or taken outright from `beneath`, for callers
|
|
892
|
+
* that know it.
|
|
893
|
+
*
|
|
894
|
+
* `how` is the link's `data-panel` attribute (or the caller's word for
|
|
895
|
+
* it): absent — like a link without the attribute — it is the shell's
|
|
896
|
+
* `linkNavigation` default, an unrecognised value is a push on top of
|
|
897
|
+
* `from`, `"replace"` swaps `from` out rather than stacking on it, and
|
|
898
|
+
* `"open"` drops `from` altogether so the target arrives with its own
|
|
899
|
+
* stack, the way a nav item's link does.
|
|
873
900
|
*
|
|
874
901
|
* Resolves the way every {@link PanelStack} method does: `true` once the
|
|
875
902
|
* navigation lands, `false` when it doesn't (already there counts as
|
|
876
903
|
* landed).
|
|
877
904
|
*/
|
|
878
|
-
navigate(href,
|
|
905
|
+
navigate(href, { from, how, beneath } = {}) {
|
|
906
|
+
const mode = how ?? this.opts.linkNavigation;
|
|
907
|
+
const origin = mode === "open" ? null : from ?? null;
|
|
908
|
+
const replace = mode === "replace";
|
|
879
909
|
return A.peek(() => {
|
|
880
910
|
let url;
|
|
881
911
|
try {
|
|
@@ -933,43 +963,36 @@ export class PanelStackController {
|
|
|
933
963
|
pushPath(path, replace) {
|
|
934
964
|
return A.peek(() => {
|
|
935
965
|
const arr = this.intended();
|
|
936
|
-
return this.navigate(path, arr.stack[arr.focus]
|
|
966
|
+
return this.navigate(path, { from: arr.stack[arr.focus], how: replace ? "replace" : "push" });
|
|
937
967
|
});
|
|
938
968
|
}
|
|
939
969
|
// ── Link interception ──────────────────────────────────────────────────
|
|
940
970
|
/**
|
|
941
971
|
* Link handling through `route.interceptLinks()`, whose handler hook hands us
|
|
942
|
-
* the anchor so we can decide what the click *means
|
|
943
|
-
* `.s-panel` (which decides what the click truncates), the `data-panel`
|
|
944
|
-
* attribute, and return-to-an-open-panel semantics. The exclusion rules
|
|
972
|
+
* the anchor so we can decide what the click *means*. The exclusion rules
|
|
945
973
|
* (targets, downloads, modified clicks, external URLs) live in Aberdeen; the
|
|
946
974
|
* close guards run in `checkChange` when our navigation reaches the router.
|
|
947
975
|
*
|
|
948
|
-
*
|
|
949
|
-
*
|
|
950
|
-
*
|
|
951
|
-
*
|
|
952
|
-
*
|
|
953
|
-
* an unrecognised value is a `push`.
|
|
976
|
+
* A link inside a panel is that panel's {@link Panel.open}, the `data-panel`
|
|
977
|
+
* attribute as its `how` (see {@link navigate}, the shared implementation).
|
|
978
|
+
* A link that isn't inside any panel — a nav item, one in a dialog — has no
|
|
979
|
+
* panel to build on, so it replaces the stack as a whole, exactly as a cold
|
|
980
|
+
* link to the same URL would open it.
|
|
954
981
|
*/
|
|
955
982
|
interceptLinks() {
|
|
956
983
|
route.interceptLinks((url, anchor) => {
|
|
957
|
-
const
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
origin = this.$state.live[this.$state.focus]?.path ?? null;
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
void this.navigate(url.href, origin, mode === "replace");
|
|
984
|
+
const how = anchor.getAttribute("data-panel") ?? undefined;
|
|
985
|
+
// The panel the link lives in: the enclosing `.s-panel`, or — for the
|
|
986
|
+
// current panel's actions, promoted into the top bar on a narrow shell
|
|
987
|
+
// (see main.ts), outside every `.s-panel` — the current panel, whose
|
|
988
|
+
// own chrome they remain at every width.
|
|
989
|
+
const panelEl = anchor.closest(".s-panel");
|
|
990
|
+
const entry = panelEl
|
|
991
|
+
? this.$state.live.find((e) => e.el === panelEl)
|
|
992
|
+
: anchor.closest(".s-panel-origin")
|
|
993
|
+
? this.$state.live[this.$state.focus]
|
|
994
|
+
: undefined;
|
|
995
|
+
void this.navigate(url.href, { from: entry?.path, how });
|
|
973
996
|
return true;
|
|
974
997
|
});
|
|
975
998
|
}
|
|
@@ -993,7 +1016,7 @@ export class PanelStackController {
|
|
|
993
1016
|
return this.pushPath(path, true);
|
|
994
1017
|
}
|
|
995
1018
|
openPanelStack(path, beneath) {
|
|
996
|
-
return this.navigate(path,
|
|
1019
|
+
return this.navigate(path, { how: "open", beneath });
|
|
997
1020
|
}
|
|
998
1021
|
closePanel(path) {
|
|
999
1022
|
return A.peek(() => {
|
|
@@ -1447,11 +1470,11 @@ export class PanelStackController {
|
|
|
1447
1470
|
if (!entry.width)
|
|
1448
1471
|
entry.width = width(entry);
|
|
1449
1472
|
}
|
|
1450
|
-
// The
|
|
1451
|
-
//
|
|
1452
|
-
//
|
|
1453
|
-
//
|
|
1454
|
-
//
|
|
1473
|
+
// The body row caps itself to the ensemble width, keeping the columns
|
|
1474
|
+
// centred however far the area stretches, and transitions its max-width
|
|
1475
|
+
// (see main.ts) so the recentring plays along with the panel that caused
|
|
1476
|
+
// it. The bars above and below don't follow — they hold at the standard
|
|
1477
|
+
// page width (also main.ts).
|
|
1455
1478
|
shell.style.setProperty("--s-shell-w", `${geom.chrome + area}px`);
|
|
1456
1479
|
// Phase 1 — every panel's *start* state for this frame. Panels already on
|
|
1457
1480
|
// screen simply move (their standing transition animates it); freshly
|