staffa 0.8.1 → 0.10.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.
- package/README.md +130 -64
- package/dist/components/autocomplete.js +1 -1
- package/dist/components/box.d.ts +8 -16
- package/dist/components/box.js +21 -27
- package/dist/components/button.d.ts +40 -0
- package/dist/components/button.js +85 -12
- package/dist/components/buttonChooser.js +1 -1
- package/dist/components/checkbox.js +3 -3
- package/dist/components/field.js +3 -3
- package/dist/components/main.d.ts +187 -64
- package/dist/components/main.js +284 -158
- package/dist/components/menu.d.ts +72 -14
- package/dist/components/menu.js +235 -31
- package/dist/components/pages.d.ts +638 -0
- package/dist/components/pages.js +1510 -0
- package/dist/components/panels.d.ts +512 -206
- package/dist/components/panels.js +926 -414
- package/dist/components/tabs.d.ts +37 -0
- package/dist/components/tabs.js +128 -69
- package/dist/core.d.ts +1 -1
- package/dist/core.js +1 -1
- package/dist/glyphs.d.ts +24 -0
- package/dist/glyphs.js +25 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +4 -5
- package/dist/staffa.esm.js +1 -1
- package/dist/theme.d.ts +67 -0
- package/dist/theme.js +12 -2
- package/package.json +2 -2
- package/skill/AncestorTable.md +10 -0
- package/skill/BoxOptions.md +7 -12
- package/skill/IconButtonOptions.md +41 -0
- package/skill/MainOptions.md +143 -54
- package/skill/MenuItem.md +16 -1
- package/skill/MenuListOptions.md +24 -0
- package/skill/MenuOptions.md +3 -2
- package/skill/Panel.md +190 -0
- package/skill/PanelStack.md +106 -0
- package/skill/SKILL.md +214 -77
- package/skill/ScrollStripOptions.md +21 -0
- package/skill/box.md +1 -4
- package/skill/closeNav.md +23 -0
- package/skill/iconButton.md +27 -0
- package/skill/main.md +13 -9
- package/skill/menu.md +29 -0
- package/skill/scrollStrip.md +28 -0
- package/src/components/autocomplete.ts +1 -1
- package/src/components/box.ts +29 -39
- package/src/components/button.ts +109 -8
- package/src/components/buttonChooser.ts +1 -1
- package/src/components/checkbox.ts +3 -3
- package/src/components/field.ts +3 -3
- package/src/components/main.ts +459 -167
- package/src/components/menu.ts +268 -34
- package/src/components/panels.ts +1254 -497
- package/src/components/tabs.ts +134 -68
- package/src/core.ts +1 -1
- package/src/index.ts +5 -5
- package/src/theme.ts +14 -3
- package/skill/Page.md +0 -119
- package/skill/panels.md +0 -10
|
@@ -20,6 +20,11 @@ A.insertGlobalCss({
|
|
|
20
20
|
// button (which is already near-white) darkens toward its ink instead.
|
|
21
21
|
"&.tonal:hover, &.outlined:hover": "background: color-mix(in srgb, $s-bg 24%, transparent);",
|
|
22
22
|
"&.neutral:hover": "filter:none background: color-mix(in srgb, $s-text 8%, $s-bg);",
|
|
23
|
+
// The button sizes its glyph, for the same reason `.s-icon-btn` does below:
|
|
24
|
+
// a caller can't know what the button beside it passed, and only a rule
|
|
25
|
+
// here makes every icon in a row come out alike. It rides the font size,
|
|
26
|
+
// so a `.small`/`.large` button scales its icon with its text.
|
|
27
|
+
"> svg": "width:1.25em height:1.25em",
|
|
23
28
|
// Subtle press feedback.
|
|
24
29
|
"&:active:not(:disabled)": "transform: translateY(1px)",
|
|
25
30
|
// Size: set on the button itself, or inherited from a `.small`/`.large`
|
|
@@ -27,7 +32,86 @@ A.insertGlobalCss({
|
|
|
27
32
|
"&.small, .small > &": "padding: $m1 $m2; font-size:0.85em border-radius:$s-radius-sm",
|
|
28
33
|
"&.large, .large > &": "font-size:1.4em border-radius:$s-radius-lg",
|
|
29
34
|
},
|
|
35
|
+
// A bare glyph in a square hit area: no fill and no edge, just ink that lifts
|
|
36
|
+
// on hover. Deliberately *not* a `.s-s` surface — chrome that sits beside a
|
|
37
|
+
// title (a ✕, a ☰) should read as an affordance on the bar, not as
|
|
38
|
+
// another button competing with it, and a filled or outlined box around a
|
|
39
|
+
// 16px glyph is exactly what makes a top bar look busy.
|
|
40
|
+
".s-icon-btn": {
|
|
41
|
+
"&": "display:inline-flex align-items:center justify-content:center flex-shrink:0 " +
|
|
42
|
+
"width:2rem height:2rem p:0 border:0 background:transparent cursor:pointer " +
|
|
43
|
+
"fg:$s-muted r:$s-radius-sm line-height:1 font-size:1rem text-decoration:none " +
|
|
44
|
+
"transition: color 0.12s, background 0.12s;",
|
|
45
|
+
// The container sizes the glyph, rather than trusting whatever the caller
|
|
46
|
+
// passed: a row of icon buttons only reads as a row when every glyph in it
|
|
47
|
+
// is the same size, and the caller of one of them can't know about the
|
|
48
|
+
// others. CSS beats the `width`/`height` attributes the icon set writes, so
|
|
49
|
+
// `iconButton({ icon: trash2 })` and a hand-sized glyph come out alike; an
|
|
50
|
+
// `attrs` override still wins over this, being an inline style. The same
|
|
51
|
+
// rule is on `.s-btn` above and on a floating menu's rows in menu.ts, so
|
|
52
|
+
// one `1.25em` governs the lot. (`S.main`'s nav rows are deliberately out
|
|
53
|
+
// of it — see the note there.)
|
|
54
|
+
"> svg": "width:1.25em height:1.25em",
|
|
55
|
+
// The ink resolves against whatever surface it sits on, so one treatment
|
|
56
|
+
// works on the page, in a box header, and on a coloured bar alike.
|
|
57
|
+
"&:hover:not(:disabled):not([aria-disabled=true])": "fg:$s-text background: color-mix(in srgb, $s-text 10%, transparent);",
|
|
58
|
+
"&:focus-visible": "outline: 3px solid $s-focus; outline-offset:1px",
|
|
59
|
+
// The glyph rides the font size, so it scales with the hit area.
|
|
60
|
+
"&.small, .small > &": "width:1.6rem height:1.6rem font-size:0.8rem",
|
|
61
|
+
"&.large, .large > &": "width:2.4rem height:2.4rem font-size:1.2rem",
|
|
62
|
+
},
|
|
30
63
|
});
|
|
64
|
+
/**
|
|
65
|
+
* A bare glyph in a square hit area — no fill, no border, just ink that lifts on
|
|
66
|
+
* hover. The quiet end of the button family, for chrome that has to sit beside
|
|
67
|
+
* something more important without competing with it: a ✕ on a box, the ☰ a
|
|
68
|
+
* routed `S.main()` puts in its top bar, the verbs in a
|
|
69
|
+
* {@link Panel.actions | page's actions}.
|
|
70
|
+
*
|
|
71
|
+
* Reach for {@link button} instead whenever the thing has a name worth reading;
|
|
72
|
+
* an icon alone is only unambiguous for a handful of universal actions.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* import { trash2, share2 } from "staffa/icons";
|
|
77
|
+
*
|
|
78
|
+
* $panel.actions = () => {
|
|
79
|
+
* S.iconButton({ icon: share2, ariaLabel: "Share", click: share });
|
|
80
|
+
* S.iconButton({ icon: trash2, ariaLabel: "Delete", click: del, attrs: "fg:$s-danger" });
|
|
81
|
+
* };
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export function iconButton(opts) {
|
|
85
|
+
const tag = opts.href != null ? "a" : "button";
|
|
86
|
+
A(`${tag}.s-icon-btn`, opts.attrs, () => {
|
|
87
|
+
applyActionBehavior(opts);
|
|
88
|
+
A("aria-label=", opts.ariaLabel);
|
|
89
|
+
drawSlot(opts.icon);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The link-or-button plumbing {@link button} and {@link iconButton} share:
|
|
94
|
+
* href/type, disabling, label and click. A disabled link keeps `role=button`
|
|
95
|
+
* and `aria-disabled` but loses its `href` — an anchor without one is out of
|
|
96
|
+
* the tab order and follows nothing, which is what makes it as disabled as
|
|
97
|
+
* the `<button>` form's real `disabled` attribute.
|
|
98
|
+
*/
|
|
99
|
+
function applyActionBehavior(o) {
|
|
100
|
+
if (o.href != null) {
|
|
101
|
+
A("role=button");
|
|
102
|
+
if (o.disabled)
|
|
103
|
+
A("aria-disabled=true");
|
|
104
|
+
else
|
|
105
|
+
A("href=", o.href);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
A("type=", o.type ?? "button");
|
|
109
|
+
if (o.disabled)
|
|
110
|
+
A("disabled=true");
|
|
111
|
+
}
|
|
112
|
+
if (o.click && !o.disabled)
|
|
113
|
+
A("click=", o.click);
|
|
114
|
+
}
|
|
31
115
|
/**
|
|
32
116
|
* A button. Tonal and outlined variants show a border; filled variants rely on
|
|
33
117
|
* their solid background for affordance.
|
|
@@ -60,20 +144,9 @@ export function button(opts = {}) {
|
|
|
60
144
|
// role (`.danger`, `.neutral`, a custom `.brand`) or variant (`.outlined`); no
|
|
61
145
|
// role detection needed, since the default lives in CSS, not here.
|
|
62
146
|
A(`${tag}.s-btn.s-s.shadow`, o.attrs, () => {
|
|
63
|
-
|
|
64
|
-
A(`href=${o.href} role=button`);
|
|
65
|
-
if (o.disabled)
|
|
66
|
-
A("aria-disabled=true");
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
A("type=", o.type ?? "button");
|
|
70
|
-
if (o.disabled)
|
|
71
|
-
A("disabled=true");
|
|
72
|
-
}
|
|
147
|
+
applyActionBehavior(o);
|
|
73
148
|
if (o.ariaLabel)
|
|
74
149
|
A("aria-label=", o.ariaLabel);
|
|
75
|
-
if (o.click)
|
|
76
|
-
A("click=", o.click);
|
|
77
150
|
drawSlot(o.icon);
|
|
78
151
|
drawSlot(o.content);
|
|
79
152
|
});
|
|
@@ -35,6 +35,6 @@ export function buttonChooser(opts) {
|
|
|
35
35
|
});
|
|
36
36
|
if (opts.name) {
|
|
37
37
|
// Hidden input carries the value into native form submission.
|
|
38
|
-
A(() => A(
|
|
38
|
+
A(() => A("input type=hidden name=", opts.name, "value=", opts.bind.value ?? ""));
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -24,11 +24,11 @@ A.insertGlobalCss({
|
|
|
24
24
|
export function checkbox(opts = {}) {
|
|
25
25
|
const id = opts.id ?? uniqueId("check");
|
|
26
26
|
A("div.s-check", opts.attrs, () => {
|
|
27
|
-
A(
|
|
27
|
+
A("label for=", id, () => {
|
|
28
28
|
A("input type=checkbox", opts.inputAttrs, () => {
|
|
29
|
-
A(
|
|
29
|
+
A("id=", id);
|
|
30
30
|
if (opts.name)
|
|
31
|
-
A(
|
|
31
|
+
A("name=", opts.name);
|
|
32
32
|
// `checked` is a boolean attribute: only set it when actually true.
|
|
33
33
|
if (opts.checked && !opts.bind)
|
|
34
34
|
A("checked=true");
|
package/dist/components/field.js
CHANGED
|
@@ -37,7 +37,7 @@ export function drawField(opts, drawControl) {
|
|
|
37
37
|
A("div.s-field", opts.attrs, () => {
|
|
38
38
|
A(() => {
|
|
39
39
|
if (opts.label != null) {
|
|
40
|
-
A(
|
|
40
|
+
A("label for=", id, () => {
|
|
41
41
|
drawSlot(opts.label);
|
|
42
42
|
if (opts.required)
|
|
43
43
|
A("span.s-req aria-hidden=true #*");
|
|
@@ -61,9 +61,9 @@ export function drawField(opts, drawControl) {
|
|
|
61
61
|
* each get their own scope so the control element is never recreated.
|
|
62
62
|
*/
|
|
63
63
|
export function applyControlAttrs(opts, id, isInvalid, bind) {
|
|
64
|
-
A(
|
|
64
|
+
A("id=", id);
|
|
65
65
|
if (opts.name)
|
|
66
|
-
A(
|
|
66
|
+
A("name=", opts.name);
|
|
67
67
|
A(() => {
|
|
68
68
|
if (opts.disabled)
|
|
69
69
|
A("disabled=true");
|
|
@@ -1,27 +1,70 @@
|
|
|
1
1
|
import { type Slot, type Attributes } from "../core.js";
|
|
2
2
|
import { type MenuOptions } from "./menu.js";
|
|
3
|
-
import { type RouteHandler, type RouteTable, type Routes } from "./panels.js";
|
|
3
|
+
import { type PanelStack, type AncestorTable, type RouteHandler, type RouteTable, type Routes } from "./panels.js";
|
|
4
4
|
/** Options for {@link main}. */
|
|
5
5
|
export interface MainOptions<R = Routes> {
|
|
6
6
|
/** Aberdeen attr/style string applied to the outermost shell element. */
|
|
7
7
|
attrs?: Attributes;
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* The app's name, shown in the top bar in the brand's own styling, with the
|
|
10
|
+
* breadcrumb stack of open panels on the line beneath it (in routed mode).
|
|
11
|
+
* In routed mode it is a link to the app's {@link MainOptions.home}, as the
|
|
12
|
+
* {@link MainOptions.logo} is.
|
|
13
|
+
*/
|
|
9
14
|
title?: Slot;
|
|
10
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* A tagline for the app, on the line under its name.
|
|
17
|
+
*
|
|
18
|
+
* In routed mode that line is the breadcrumb stack's, and the tagline only
|
|
19
|
+
* gets it while the stack would be saying nothing the screen doesn't
|
|
20
|
+
* already: exactly one panel open, that panel being one a nav item leads to
|
|
21
|
+
* (so the sidebar has it highlighted), and the sidebar actually on screen.
|
|
22
|
+
* Open a panel on top of it, or narrow the shell until the nav is behind the
|
|
23
|
+
* ☰, and the stack takes the line back — it is then the only thing naming
|
|
24
|
+
* the screen. Pass no subtitle and the stack simply always has it.
|
|
25
|
+
*
|
|
26
|
+
* Outside routed mode nothing competes for the line, so it always shows.
|
|
27
|
+
*/
|
|
11
28
|
subtitle?: Slot;
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
29
|
+
/**
|
|
30
|
+
* The brand mark: the bar's leading slot while the nav is a sidebar.
|
|
31
|
+
*
|
|
32
|
+
* A narrow shell *displaces* it with the ☰ that opens the collapsed nav. The
|
|
33
|
+
* app never branches on which: it hands over a logo and the shell works out
|
|
34
|
+
* whether there is room for it. In routed mode it is a link to the app's
|
|
35
|
+
* {@link MainOptions.home}, as the app's name is.
|
|
36
|
+
*/
|
|
37
|
+
logo?: Slot;
|
|
38
|
+
/**
|
|
39
|
+
* The app's home: where the name and the {@link MainOptions.logo} in the
|
|
40
|
+
* top bar link, as every logo on the web does. Defaults to `"/"`; set it
|
|
41
|
+
* when your home screen lives elsewhere. It's an ordinary link, so the
|
|
42
|
+
* usual rules apply: a home that is already open in the stack — its first
|
|
43
|
+
* panel, usually — is returned to, closing nothing, and one that isn't is
|
|
44
|
+
* opened the way a nav item would be. Routed mode only.
|
|
45
|
+
*/
|
|
46
|
+
home?: string;
|
|
47
|
+
/**
|
|
48
|
+
* The app's own chrome, at the trailing end of the top bar: an account
|
|
49
|
+
* button, a global search box, a settings menu. It may grow into the bar's
|
|
50
|
+
* free space (so a search box is at home here); the title truncates before it
|
|
51
|
+
* gives any of it back.
|
|
52
|
+
*
|
|
53
|
+
* In routed mode a narrow shell hands this slot to the current panel's
|
|
54
|
+
* {@link Panel.actions} whenever it has any — on a phone the screen's own
|
|
55
|
+
* verbs win the space — and keeps the app's menu for the screens that
|
|
56
|
+
* declare none.
|
|
57
|
+
*/
|
|
15
58
|
menu?: Slot;
|
|
16
59
|
/**
|
|
17
|
-
* The scrollable
|
|
60
|
+
* The scrollable panel content. A string is rendered as rich text.
|
|
18
61
|
* Mutually exclusive with {@link MainOptions.routes}.
|
|
19
62
|
*/
|
|
20
63
|
content?: Slot;
|
|
21
64
|
/**
|
|
22
65
|
* Paths mapped to the functions that draw them, which hands navigation over
|
|
23
66
|
* to the shell. Each route draws one screen of your app, called a panel, and
|
|
24
|
-
* as many
|
|
67
|
+
* as many columns as fit are shown at a time: one at a time on a phone,
|
|
25
68
|
* several side by side on a wider screen. Mutually exclusive with
|
|
26
69
|
* {@link MainOptions.content}.
|
|
27
70
|
*
|
|
@@ -31,7 +74,7 @@ export interface MainOptions<R = Routes> {
|
|
|
31
74
|
* string, so it has to come last and needs at least one segment to match.
|
|
32
75
|
* The first key that matches wins, a segment a param refuses falls through
|
|
33
76
|
* to a later route (or to {@link MainOptions.notFound}), and each handler's
|
|
34
|
-
* `$
|
|
77
|
+
* `$panel.params` is typed from its own key.
|
|
35
78
|
*
|
|
36
79
|
* `integer` accepts only spellings that survive a round trip back to the
|
|
37
80
|
* same URL, so `/tasks/0042` is not a second path for `/tasks/42`. Ids that
|
|
@@ -39,26 +82,38 @@ export interface MainOptions<R = Routes> {
|
|
|
39
82
|
*
|
|
40
83
|
* Navigating is just links: the shell handles the clicks itself, so do *not*
|
|
41
84
|
* also call Aberdeen's `interceptLinks()`. A link opens its target on top of
|
|
42
|
-
* the panel it sits in, closing
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
85
|
+
* the panel it sits in, closing everything after that panel first. The
|
|
86
|
+
* `data-panel` attribute picks another of the three {@link PanelStack}
|
|
87
|
+
* navigations instead: `replace` puts the target in place of the link's own
|
|
88
|
+
* panel, and `open` leaves that panel behind and gives the target its own
|
|
89
|
+
* stack, the way a nav item does. A link
|
|
90
|
+
* to something already open goes back to it rather than opening it twice —
|
|
91
|
+
* a move along the stack that closes nothing: the panels right of it stay
|
|
92
|
+
* open, parked past the viewport's right edge, until a *new* panel prunes
|
|
93
|
+
* them (pinned panels excepted — see {@link Panel.pinned}).
|
|
94
|
+
* From code, use {@link pushPanel} and friends: navigating
|
|
95
|
+
* with `aberdeen/route`'s own `go()` works too — a panel with
|
|
96
|
+
* {@link Panel.unsaved} work still survives it — but builds the whole stack
|
|
97
|
+
* from the path. A navigation guard the app registered with
|
|
98
|
+
* `route.setGuard` (an auth redirect, say) keeps working: the shell
|
|
99
|
+
* registers none of its own.
|
|
100
|
+
*
|
|
101
|
+
* **A panel declares its chrome; the shell places it.** A panel says what it
|
|
102
|
+
* is called ({@link Panel.title} — unset, its first line of text stands in)
|
|
103
|
+
* and what it can do ({@link Panel.actions}); everything else in a column is
|
|
104
|
+
* the panel's own content, boxes included. The shell writes the stack of
|
|
105
|
+
* open panels as breadcrumbs in the top bar — click one to go back to it,
|
|
106
|
+
* closing nothing — and places each panel's actions where the room is: on
|
|
107
|
+
* its own column while several fit, in the bar once the shell is narrow and
|
|
108
|
+
* the current panel *is* the screen. Nothing in an app measures the
|
|
109
|
+
* viewport to lay its screens out twice.
|
|
110
|
+
*
|
|
111
|
+
* Only one routed shell can be mounted at a time (a second one throws) —
|
|
112
|
+
* the URL is global, so two of them would fight over it. Nothing else is:
|
|
113
|
+
* the {@link PanelStack} belongs to its shell, which hands it back, and
|
|
114
|
+
* each handler gets its own `$panel` rather than there being one global
|
|
115
|
+
* "current panel", since several panels are alive at once. It's that
|
|
116
|
+
* argument that carries the per-route typing of `params`.
|
|
62
117
|
*
|
|
63
118
|
* @example
|
|
64
119
|
* ```ts
|
|
@@ -66,39 +121,78 @@ export interface MainOptions<R = Routes> {
|
|
|
66
121
|
* title: "Trackle",
|
|
67
122
|
* nav: { items: [{ label: "Projects", href: "/projects" }] },
|
|
68
123
|
* routes: {
|
|
69
|
-
* "/projects": ($
|
|
70
|
-
* "/projects/[id]": ($
|
|
124
|
+
* "/projects": ($panel) => { $panel.title = "Projects"; drawProjects(); },
|
|
125
|
+
* "/projects/[id]": ($panel) => drawProject($panel.params.id), // typed string
|
|
71
126
|
* },
|
|
72
|
-
* notFound: ($
|
|
127
|
+
* notFound: ($panel) => S.box({ header: "Not found", content: $panel.path }),
|
|
73
128
|
* });
|
|
74
129
|
* ```
|
|
75
130
|
*/
|
|
76
131
|
routes?: R;
|
|
77
132
|
/**
|
|
78
133
|
* Draws the panel for a path none of the routes match. There are no params
|
|
79
|
-
* to go with it, so `$
|
|
80
|
-
* `$
|
|
134
|
+
* to go with it, so `$panel.params` is empty; the path itself is in
|
|
135
|
+
* `$panel.path`.
|
|
81
136
|
*/
|
|
82
137
|
notFound?: RouteHandler<{}>;
|
|
83
138
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
139
|
+
* What to open **beneath** a path that arrives cold — a shared link, a
|
|
140
|
+
* bookmark, a push notification, a nav item — with no stack of its own to
|
|
141
|
+
* restore. Keyed by path template exactly like {@link MainOptions.routes}, so
|
|
142
|
+
* each entry gets that key's params, matched and typed, rather than taking
|
|
143
|
+
* the path apart a second time.
|
|
144
|
+
*
|
|
145
|
+
* Without this, the stack is derived from the path: every prefix that has a
|
|
146
|
+
* route becomes a column, so `/projects/7/tasks/42` opens three deep. That
|
|
147
|
+
* only works for URLs that spell their own context out. A flat one —
|
|
148
|
+
* `/thread/[id]`, where a notification lands — has no prefix to walk, so it
|
|
149
|
+
* opens as a single column with nothing under it and nothing to close back
|
|
150
|
+
* to. This is where you say what that context is:
|
|
151
|
+
*
|
|
152
|
+
* ```ts
|
|
153
|
+
* S.main({
|
|
154
|
+
* routes: {
|
|
155
|
+
* "/mailbox/[id]": drawMailbox,
|
|
156
|
+
* "/thread/[id=integer]": drawThread,
|
|
157
|
+
* },
|
|
158
|
+
* ancestors: {
|
|
159
|
+
* "/thread/[id=integer]": ({ id }) => [`/mailbox/${mailboxOf(id)}`], // id: number
|
|
160
|
+
* },
|
|
161
|
+
* });
|
|
162
|
+
* ```
|
|
163
|
+
*
|
|
164
|
+
* Return the paths shallowest first; the path itself goes on top. Return
|
|
165
|
+
* nothing to leave a path to the prefix derivation, which is also what an
|
|
166
|
+
* unlisted one gets — so you only list the routes whose URL doesn't say where
|
|
167
|
+
* it belongs. Paths you have no route for are skipped, as they are there.
|
|
168
|
+
*
|
|
169
|
+
* This is asked for every origin-less navigation, so a nav item and a fresh
|
|
170
|
+
* tab still land on the same columns; a link *inside* a panel builds on that
|
|
171
|
+
* panel instead and never asks. It's consulted while the navigation is
|
|
172
|
+
* still being worked out — before any route handler runs — so it has to
|
|
173
|
+
* answer without drawing anything. From code,
|
|
174
|
+
* {@link PanelStack.openPanelStack} takes the same list directly.
|
|
175
|
+
*/
|
|
176
|
+
ancestors?: AncestorTable<NoInfer<R>>;
|
|
177
|
+
/**
|
|
178
|
+
* Set `false` to show only the current panel, however wide the screen (the
|
|
179
|
+
* nav sidebar still sits beside it). Everything else behaves the same: the
|
|
180
|
+
* URL, the back button, unsaved panels, and the panels' own close buttons.
|
|
181
|
+
* This only changes how many you see. Defaults to `true`.
|
|
88
182
|
*/
|
|
89
183
|
stacking?: boolean;
|
|
90
184
|
/** Footer content, pinned below the scroll area. */
|
|
91
185
|
footer?: Slot;
|
|
92
186
|
/**
|
|
93
|
-
* Max width for the
|
|
187
|
+
* Max width for the panel's *content*, e.g. `"60rem"`. The header and footer
|
|
94
188
|
* backgrounds still span the full shell width, but their contents — and the
|
|
95
189
|
* sidebar + separator + content trio (or just the content when there's no
|
|
96
190
|
* sidebar) — cap to this width and centre horizontally. When unset, everything
|
|
97
|
-
* fills the available width. Either way the content shares the
|
|
191
|
+
* fills the available width. Either way the content shares the panel surface —
|
|
98
192
|
* it is not boxed.
|
|
99
193
|
*
|
|
100
194
|
* Ignored when you pass {@link MainOptions.routes}: there the open panels
|
|
101
|
-
* decide the width (see {@link
|
|
195
|
+
* decide the width (see {@link Panel.maxWidth}), and the header and footer line
|
|
102
196
|
* themselves up with them.
|
|
103
197
|
*/
|
|
104
198
|
maxWidth?: string;
|
|
@@ -107,28 +201,27 @@ export interface MainOptions<R = Routes> {
|
|
|
107
201
|
/** Aberdeen attr/style string applied to the top bar. */
|
|
108
202
|
topbarAttrs?: Attributes;
|
|
109
203
|
/**
|
|
110
|
-
* Navigation menu
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* nav as a full page sliding in from the left, not as a dropdown.
|
|
204
|
+
* Navigation menu, rendered as a sidebar beside the content. The sidebar
|
|
205
|
+
* collapses to a ☰ in the top bar when the shell is narrow, and there it
|
|
206
|
+
* opens the nav as a full panel sliding in from the left, not as a dropdown.
|
|
114
207
|
*
|
|
115
208
|
* `items` may be a reactive array: the shell reads it inside the sidebar's own
|
|
116
209
|
* scope, so an item arriving or leaving redraws the sidebar and nothing else.
|
|
117
|
-
* The content beside it — in routed mode, the whole
|
|
118
|
-
* alone.
|
|
210
|
+
* The content beside it — in routed mode, the whole stack — is left
|
|
211
|
+
* alone. `button` customizes the ☰; `dropdownAttrs` does nothing here, since
|
|
212
|
+
* a collapsed nav is a panel rather than a dropdown.
|
|
119
213
|
*/
|
|
120
214
|
nav?: MenuOptions;
|
|
121
215
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* screen with a matching slide in from the right.
|
|
216
|
+
* Which side the nav sidebar sits on. Defaults to `"left"`.
|
|
217
|
+
*
|
|
218
|
+
* Either way it collapses to a ☰ in the top bar once the shell width drops to
|
|
219
|
+
* 640 px or below — the one threshold everything else keys off too, which is
|
|
220
|
+
* why there is no "always a button" mode: it would make "narrow" and "the nav
|
|
221
|
+
* is collapsed" two different things, and every rule about where a panel's
|
|
222
|
+
* chrome goes assumes they are one.
|
|
130
223
|
*/
|
|
131
|
-
navPosition?: "left" | "right"
|
|
224
|
+
navPosition?: "left" | "right";
|
|
132
225
|
/** Aberdeen attr/style string applied to the sidebar nav panel. */
|
|
133
226
|
navAttrs?: Attributes;
|
|
134
227
|
/** Aberdeen attr/style string applied to the narrow-screen full-page nav. */
|
|
@@ -136,23 +229,27 @@ export interface MainOptions<R = Routes> {
|
|
|
136
229
|
}
|
|
137
230
|
/**
|
|
138
231
|
* An application shell that wires up the things almost every app needs: a sticky
|
|
139
|
-
* top bar (
|
|
232
|
+
* top bar (logo, title, action menu), a scrollable content area, and a
|
|
140
233
|
* footer. With {@link MainOptions.maxWidth} the content area is centred and its
|
|
141
|
-
* width capped. Add a `nav` to get a
|
|
142
|
-
*
|
|
143
|
-
* Below 640 px that button opens the nav as a full page sliding in from the
|
|
234
|
+
* width capped. Add a `nav` to get a sidebar that collapses to a ☰ in the top bar
|
|
235
|
+
* below 640 px, which there opens the nav as a full panel sliding in from the
|
|
144
236
|
* left; picking an item slides it away as the chosen screen enters from the
|
|
145
237
|
* right.
|
|
146
238
|
*
|
|
147
239
|
* Instead of a single `content` slot, pass {@link MainOptions.routes} and the
|
|
148
240
|
* shell takes over navigation: each route draws one screen, called a panel,
|
|
149
|
-
* and as many
|
|
150
|
-
* and one at a time on a phone.
|
|
241
|
+
* and as many columns as fit are shown at a time, side by side on a wide screen
|
|
242
|
+
* and one at a time on a phone. Each panel *declares* its chrome — its
|
|
243
|
+
* {@link Panel.title} and its {@link Panel.actions} — and this shell places it:
|
|
244
|
+
* the stack of titles as breadcrumbs in the bar, the actions on the panel's
|
|
245
|
+
* column while several fit and in the bar once the shell is narrow enough
|
|
246
|
+
* that the current panel is the whole screen. See {@link MainOptions.routes} and
|
|
247
|
+
* {@link Panel}.
|
|
151
248
|
*
|
|
152
249
|
* @example
|
|
153
250
|
* ```ts
|
|
154
251
|
* S.main({
|
|
155
|
-
*
|
|
252
|
+
* logo: "✦",
|
|
156
253
|
* title: "Staffa Demo",
|
|
157
254
|
* maxWidth: "56rem",
|
|
158
255
|
* nav: {
|
|
@@ -172,4 +269,30 @@ export interface MainOptions<R = Routes> {
|
|
|
172
269
|
* }
|
|
173
270
|
* ```
|
|
174
271
|
*/
|
|
175
|
-
export declare function main<R extends RouteTable<R>>(opts
|
|
272
|
+
export declare function main<R extends RouteTable<R>>(opts: MainOptions<R> & {
|
|
273
|
+
routes: object;
|
|
274
|
+
}): PanelStack;
|
|
275
|
+
export declare function main(opts?: MainOptions<{}> & {
|
|
276
|
+
routes?: undefined;
|
|
277
|
+
}): void;
|
|
278
|
+
/**
|
|
279
|
+
* Close the navigation, if it's showing as an overlay — the full panel it becomes
|
|
280
|
+
* on a narrow shell. A sidebar isn't an overlay and has nothing to dismiss, so
|
|
281
|
+
* on a wider shell this does nothing.
|
|
282
|
+
*
|
|
283
|
+
* A navigation closes the nav by itself, links in your own custom rows included,
|
|
284
|
+
* so this is for the items that *don't* navigate — one that opens a dialog, or
|
|
285
|
+
* flips a setting, and should still get the nav out of the way.
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```ts
|
|
289
|
+
* S.main({
|
|
290
|
+
* nav: { items: [
|
|
291
|
+
* { label: "Inbox", href: "/inbox" },
|
|
292
|
+
* () => S.button({ content: "New message", click: () => { S.closeNav(); compose(); } }),
|
|
293
|
+
* ]},
|
|
294
|
+
* routes: { ... },
|
|
295
|
+
* });
|
|
296
|
+
* ```
|
|
297
|
+
*/
|
|
298
|
+
export declare function closeNav(): void;
|