staffa 0.11.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 +3 -2
- package/dist/components/main.d.ts +7 -0
- package/dist/components/main.js +52 -38
- package/dist/components/menu.d.ts +18 -0
- package/dist/components/menu.js +58 -13
- package/dist/components/panels.d.ts +72 -16
- package/dist/components/panels.js +92 -52
- package/dist/staffa.esm.js +1 -1
- package/package.json +4 -4
- package/skill/MainOptions.md +7 -0
- package/skill/MenuItem.md +13 -0
- package/skill/Panel.md +36 -2
- package/skill/PanelStack.md +5 -0
- package/skill/SKILL.md +3 -2
- package/src/components/main.ts +60 -41
- package/src/components/menu.ts +69 -13
- package/src/components/panels.ts +132 -53
|
@@ -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(() => {
|
|
@@ -1001,6 +1024,23 @@ export class PanelStackController {
|
|
|
1001
1024
|
return this.closePath(path ?? arr.stack[arr.focus] ?? "");
|
|
1002
1025
|
});
|
|
1003
1026
|
}
|
|
1027
|
+
// ── Live settings ──────────────────────────────────────────────────────
|
|
1028
|
+
// `main()` keeps these fed from small reactive scopes of their own, so an
|
|
1029
|
+
// app that reads them off a proxy (or through a getter) can change them at
|
|
1030
|
+
// runtime and the shell adapts in place — nothing is redrawn, no panel
|
|
1031
|
+
// loses its state. Not {@link PanelStack} API: the app talks to `main()`'s
|
|
1032
|
+
// options; these are how `main()` talks to the stack.
|
|
1033
|
+
/** Adopt a changed `columns` setting: one layout pass, nothing redrawn. */
|
|
1034
|
+
setColumns(columns) {
|
|
1035
|
+
if (this.opts.columns === columns)
|
|
1036
|
+
return;
|
|
1037
|
+
this.opts.columns = columns;
|
|
1038
|
+
this.scheduleLayout();
|
|
1039
|
+
}
|
|
1040
|
+
/** Adopt a changed `linkNavigation` default; the next click reads it. */
|
|
1041
|
+
setLinkNavigation(mode) {
|
|
1042
|
+
this.opts.linkNavigation = mode;
|
|
1043
|
+
}
|
|
1004
1044
|
/**
|
|
1005
1045
|
* The breadcrumb stack, drawn by `main()` into the top bar: every open
|
|
1006
1046
|
* panel, oldest first, the ones on screen right now in bold, pinned ones
|
|
@@ -1430,11 +1470,11 @@ export class PanelStackController {
|
|
|
1430
1470
|
if (!entry.width)
|
|
1431
1471
|
entry.width = width(entry);
|
|
1432
1472
|
}
|
|
1433
|
-
// The
|
|
1434
|
-
//
|
|
1435
|
-
//
|
|
1436
|
-
//
|
|
1437
|
-
//
|
|
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).
|
|
1438
1478
|
shell.style.setProperty("--s-shell-w", `${geom.chrome + area}px`);
|
|
1439
1479
|
// Phase 1 — every panel's *start* state for this frame. Panels already on
|
|
1440
1480
|
// screen simply move (their standing transition animates it); freshly
|