staffa 0.9.0 → 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 +106 -48
- 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 +134 -71
- package/dist/components/main.js +245 -174
- package/dist/components/menu.d.ts +72 -14
- package/dist/components/menu.js +231 -34
- package/dist/components/pages.d.ts +638 -0
- package/dist/components/pages.js +1510 -0
- package/dist/components/panels.d.ts +448 -225
- package/dist/components/panels.js +819 -435
- 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 +4 -4
- package/dist/index.js +3 -4
- 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/BoxOptions.md +7 -12
- package/skill/IconButtonOptions.md +41 -0
- package/skill/MainOptions.md +106 -58
- 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 +172 -64
- package/skill/ScrollStripOptions.md +21 -0
- package/skill/box.md +1 -4
- package/skill/closeNav.md +3 -3
- 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 +381 -188
- package/src/components/menu.ts +265 -37
- package/src/components/panels.ts +1136 -526
- package/src/components/tabs.ts +134 -68
- package/src/core.ts +1 -1
- package/src/index.ts +4 -4
- 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 AncestorTable, 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,18 +121,18 @@ 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
|
/**
|
|
@@ -113,31 +168,31 @@ export interface MainOptions<R = Routes> {
|
|
|
113
168
|
*
|
|
114
169
|
* This is asked for every origin-less navigation, so a nav item and a fresh
|
|
115
170
|
* tab still land on the same columns; a link *inside* a panel builds on that
|
|
116
|
-
* panel instead and never asks. It
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* {@link
|
|
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.
|
|
120
175
|
*/
|
|
121
176
|
ancestors?: AncestorTable<NoInfer<R>>;
|
|
122
177
|
/**
|
|
123
|
-
* Set `false` to show only the
|
|
124
|
-
* sidebar still sits beside it). Everything else behaves the same: the
|
|
125
|
-
* the back button,
|
|
126
|
-
* only changes how many you see. Defaults to `true`.
|
|
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`.
|
|
127
182
|
*/
|
|
128
183
|
stacking?: boolean;
|
|
129
184
|
/** Footer content, pinned below the scroll area. */
|
|
130
185
|
footer?: Slot;
|
|
131
186
|
/**
|
|
132
|
-
* Max width for the
|
|
187
|
+
* Max width for the panel's *content*, e.g. `"60rem"`. The header and footer
|
|
133
188
|
* backgrounds still span the full shell width, but their contents — and the
|
|
134
189
|
* sidebar + separator + content trio (or just the content when there's no
|
|
135
190
|
* sidebar) — cap to this width and centre horizontally. When unset, everything
|
|
136
|
-
* fills the available width. Either way the content shares the
|
|
191
|
+
* fills the available width. Either way the content shares the panel surface —
|
|
137
192
|
* it is not boxed.
|
|
138
193
|
*
|
|
139
194
|
* Ignored when you pass {@link MainOptions.routes}: there the open panels
|
|
140
|
-
* decide the width (see {@link
|
|
195
|
+
* decide the width (see {@link Panel.maxWidth}), and the header and footer line
|
|
141
196
|
* themselves up with them.
|
|
142
197
|
*/
|
|
143
198
|
maxWidth?: string;
|
|
@@ -146,28 +201,27 @@ export interface MainOptions<R = Routes> {
|
|
|
146
201
|
/** Aberdeen attr/style string applied to the top bar. */
|
|
147
202
|
topbarAttrs?: Attributes;
|
|
148
203
|
/**
|
|
149
|
-
* Navigation menu
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* 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.
|
|
153
207
|
*
|
|
154
208
|
* `items` may be a reactive array: the shell reads it inside the sidebar's own
|
|
155
209
|
* scope, so an item arriving or leaving redraws the sidebar and nothing else.
|
|
156
|
-
* The content beside it — in routed mode, the whole
|
|
157
|
-
* 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.
|
|
158
213
|
*/
|
|
159
214
|
nav?: MenuOptions;
|
|
160
215
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* 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.
|
|
169
223
|
*/
|
|
170
|
-
navPosition?: "left" | "right"
|
|
224
|
+
navPosition?: "left" | "right";
|
|
171
225
|
/** Aberdeen attr/style string applied to the sidebar nav panel. */
|
|
172
226
|
navAttrs?: Attributes;
|
|
173
227
|
/** Aberdeen attr/style string applied to the narrow-screen full-page nav. */
|
|
@@ -175,23 +229,27 @@ export interface MainOptions<R = Routes> {
|
|
|
175
229
|
}
|
|
176
230
|
/**
|
|
177
231
|
* An application shell that wires up the things almost every app needs: a sticky
|
|
178
|
-
* top bar (
|
|
232
|
+
* top bar (logo, title, action menu), a scrollable content area, and a
|
|
179
233
|
* footer. With {@link MainOptions.maxWidth} the content area is centred and its
|
|
180
|
-
* width capped. Add a `nav` to get a
|
|
181
|
-
*
|
|
182
|
-
* 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
|
|
183
236
|
* left; picking an item slides it away as the chosen screen enters from the
|
|
184
237
|
* right.
|
|
185
238
|
*
|
|
186
239
|
* Instead of a single `content` slot, pass {@link MainOptions.routes} and the
|
|
187
240
|
* shell takes over navigation: each route draws one screen, called a panel,
|
|
188
|
-
* and as many
|
|
189
|
-
* 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}.
|
|
190
248
|
*
|
|
191
249
|
* @example
|
|
192
250
|
* ```ts
|
|
193
251
|
* S.main({
|
|
194
|
-
*
|
|
252
|
+
* logo: "✦",
|
|
195
253
|
* title: "Staffa Demo",
|
|
196
254
|
* maxWidth: "56rem",
|
|
197
255
|
* nav: {
|
|
@@ -211,11 +269,16 @@ export interface MainOptions<R = Routes> {
|
|
|
211
269
|
* }
|
|
212
270
|
* ```
|
|
213
271
|
*/
|
|
214
|
-
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;
|
|
215
278
|
/**
|
|
216
|
-
* Close the navigation, if it's showing as an overlay
|
|
217
|
-
* on a narrow shell
|
|
218
|
-
*
|
|
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.
|
|
219
282
|
*
|
|
220
283
|
* A navigation closes the nav by itself, links in your own custom rows included,
|
|
221
284
|
* so this is for the items that *don't* navigate — one that opens a dialog, or
|