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
package/skill/MainOptions.md
CHANGED
|
@@ -14,31 +14,68 @@ Aberdeen attr/style string applied to the outermost shell element.
|
|
|
14
14
|
|
|
15
15
|
### mainOptions.title · member
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
The app's name, shown in the top bar in the brand's own styling, with the
|
|
18
|
+
breadcrumb stack of open panels on the line beneath it (in routed mode).
|
|
19
|
+
In routed mode it is a link to the app's `MainOptions.home`, as the
|
|
20
|
+
`MainOptions.logo` is.
|
|
18
21
|
|
|
19
22
|
**Type:** `Slot`
|
|
20
23
|
|
|
21
24
|
### mainOptions.subtitle · member
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
A tagline for the app, on the line under its name.
|
|
27
|
+
|
|
28
|
+
In routed mode that line is the breadcrumb stack's, and the tagline only
|
|
29
|
+
gets it while the stack would be saying nothing the screen doesn't
|
|
30
|
+
already: exactly one panel open, that panel being one a nav item leads to
|
|
31
|
+
(so the sidebar has it highlighted), and the sidebar actually on screen.
|
|
32
|
+
Open a panel on top of it, or narrow the shell until the nav is behind the
|
|
33
|
+
☰, and the stack takes the line back — it is then the only thing naming
|
|
34
|
+
the screen. Pass no subtitle and the stack simply always has it.
|
|
35
|
+
|
|
36
|
+
Outside routed mode nothing competes for the line, so it always shows.
|
|
24
37
|
|
|
25
38
|
**Type:** `Slot`
|
|
26
39
|
|
|
27
|
-
### mainOptions.
|
|
40
|
+
### mainOptions.logo · member
|
|
28
41
|
|
|
29
|
-
|
|
42
|
+
The brand mark: the bar's leading slot while the nav is a sidebar.
|
|
43
|
+
|
|
44
|
+
A narrow shell *displaces* it with the ☰ that opens the collapsed nav. The
|
|
45
|
+
app never branches on which: it hands over a logo and the shell works out
|
|
46
|
+
whether there is room for it. In routed mode it is a link to the app's
|
|
47
|
+
`MainOptions.home`, as the app's name is.
|
|
30
48
|
|
|
31
49
|
**Type:** `Slot`
|
|
32
50
|
|
|
51
|
+
### mainOptions.home · member
|
|
52
|
+
|
|
53
|
+
The app's home: where the name and the `MainOptions.logo` in the
|
|
54
|
+
top bar link, as every logo on the web does. Defaults to `"/"`; set it
|
|
55
|
+
when your home screen lives elsewhere. It's an ordinary link, so the
|
|
56
|
+
usual rules apply: a home that is already open in the stack — its first
|
|
57
|
+
panel, usually — is returned to, closing nothing, and one that isn't is
|
|
58
|
+
opened the way a nav item would be. Routed mode only.
|
|
59
|
+
|
|
60
|
+
**Type:** `string`
|
|
61
|
+
|
|
33
62
|
### mainOptions.menu · member
|
|
34
63
|
|
|
35
|
-
|
|
64
|
+
The app's own chrome, at the trailing end of the top bar: an account
|
|
65
|
+
button, a global search box, a settings menu. It may grow into the bar's
|
|
66
|
+
free space (so a search box is at home here); the title truncates before it
|
|
67
|
+
gives any of it back.
|
|
68
|
+
|
|
69
|
+
In routed mode a narrow shell hands this slot to the current panel's
|
|
70
|
+
`Panel.actions` whenever it has any — on a phone the screen's own
|
|
71
|
+
verbs win the space — and keeps the app's menu for the screens that
|
|
72
|
+
declare none.
|
|
36
73
|
|
|
37
74
|
**Type:** `Slot`
|
|
38
75
|
|
|
39
76
|
### mainOptions.content · member
|
|
40
77
|
|
|
41
|
-
The scrollable
|
|
78
|
+
The scrollable panel content. A string is rendered as rich text.
|
|
42
79
|
Mutually exclusive with `MainOptions.routes`.
|
|
43
80
|
|
|
44
81
|
**Type:** `Slot`
|
|
@@ -47,7 +84,7 @@ Mutually exclusive with `MainOptions.routes`.
|
|
|
47
84
|
|
|
48
85
|
Paths mapped to the functions that draw them, which hands navigation over
|
|
49
86
|
to the shell. Each route draws one screen of your app, called a panel, and
|
|
50
|
-
as many
|
|
87
|
+
as many columns as fit are shown at a time: one at a time on a phone,
|
|
51
88
|
several side by side on a wider screen. Mutually exclusive with
|
|
52
89
|
`MainOptions.content`.
|
|
53
90
|
|
|
@@ -57,7 +94,7 @@ a string, `[name=integer]` matches one segment as a number, and a trailing
|
|
|
57
94
|
string, so it has to come last and needs at least one segment to match.
|
|
58
95
|
The first key that matches wins, a segment a param refuses falls through
|
|
59
96
|
to a later route (or to `MainOptions.notFound`), and each handler's
|
|
60
|
-
`$
|
|
97
|
+
`$panel.params` is typed from its own key.
|
|
61
98
|
|
|
62
99
|
`integer` accepts only spellings that survive a round trip back to the
|
|
63
100
|
same URL, so `/tasks/0042` is not a second path for `/tasks/42`. Ids that
|
|
@@ -65,26 +102,38 @@ aren't safe integers, such as snowflakes, want a plain `[id]`.
|
|
|
65
102
|
|
|
66
103
|
Navigating is just links: the shell handles the clicks itself, so do *not*
|
|
67
104
|
also call Aberdeen's `interceptLinks()`. A link opens its target on top of
|
|
68
|
-
the panel it sits in, closing
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
105
|
+
the panel it sits in, closing everything after that panel first. The
|
|
106
|
+
`data-panel` attribute picks another of the three `PanelStack`
|
|
107
|
+
navigations instead: `replace` puts the target in place of the link's own
|
|
108
|
+
panel, and `open` leaves that panel behind and gives the target its own
|
|
109
|
+
stack, the way a nav item does. A link
|
|
110
|
+
to something already open goes back to it rather than opening it twice —
|
|
111
|
+
a move along the stack that closes nothing: the panels right of it stay
|
|
112
|
+
open, parked past the viewport's right edge, until a *new* panel prunes
|
|
113
|
+
them (pinned panels excepted — see `Panel.pinned`).
|
|
114
|
+
From code, use `pushPanel` and friends: navigating
|
|
115
|
+
with `aberdeen/route`'s own `go()` works too — a panel with
|
|
116
|
+
`Panel.unsaved` work still survives it — but builds the whole stack
|
|
117
|
+
from the path. A navigation guard the app registered with
|
|
118
|
+
`route.setGuard` (an auth redirect, say) keeps working: the shell
|
|
119
|
+
registers none of its own.
|
|
120
|
+
|
|
121
|
+
**A panel declares its chrome; the shell places it.** A panel says what it
|
|
122
|
+
is called (`Panel.title` — unset, its first line of text stands in)
|
|
123
|
+
and what it can do (`Panel.actions`); everything else in a column is
|
|
124
|
+
the panel's own content, boxes included. The shell writes the stack of
|
|
125
|
+
open panels as breadcrumbs in the top bar — click one to go back to it,
|
|
126
|
+
closing nothing — and places each panel's actions where the room is: on
|
|
127
|
+
its own column while several fit, in the bar once the shell is narrow and
|
|
128
|
+
the current panel *is* the screen. Nothing in an app measures the
|
|
129
|
+
viewport to lay its screens out twice.
|
|
130
|
+
|
|
131
|
+
Only one routed shell can be mounted at a time (a second one throws) —
|
|
132
|
+
the URL is global, so two of them would fight over it. Nothing else is:
|
|
133
|
+
the `PanelStack` belongs to its shell, which hands it back, and
|
|
134
|
+
each handler gets its own `$panel` rather than there being one global
|
|
135
|
+
"current panel", since several panels are alive at once. It's that
|
|
136
|
+
argument that carries the per-route typing of `params`.
|
|
88
137
|
|
|
89
138
|
**Type:** `R`
|
|
90
139
|
|
|
@@ -95,18 +144,18 @@ S.main({
|
|
|
95
144
|
title: "Trackle",
|
|
96
145
|
nav: { items: [{ label: "Projects", href: "/projects" }] },
|
|
97
146
|
routes: {
|
|
98
|
-
"/projects": ($
|
|
99
|
-
"/projects/[id]": ($
|
|
147
|
+
"/projects": ($panel) => { $panel.title = "Projects"; drawProjects(); },
|
|
148
|
+
"/projects/[id]": ($panel) => drawProject($panel.params.id), // typed string
|
|
100
149
|
},
|
|
101
|
-
notFound: ($
|
|
150
|
+
notFound: ($panel) => S.box({ header: "Not found", content: $panel.path }),
|
|
102
151
|
});
|
|
103
152
|
```
|
|
104
153
|
|
|
105
154
|
### mainOptions.notFound · member
|
|
106
155
|
|
|
107
156
|
Draws the panel for a path none of the routes match. There are no params
|
|
108
|
-
to go with it, so `$
|
|
109
|
-
`$
|
|
157
|
+
to go with it, so `$panel.params` is empty; the path itself is in
|
|
158
|
+
`$panel.path`.
|
|
110
159
|
|
|
111
160
|
**Type:** `RouteHandler<{}>`
|
|
112
161
|
|
|
@@ -144,19 +193,19 @@ it belongs. Paths you have no route for are skipped, as they are there.
|
|
|
144
193
|
|
|
145
194
|
This is asked for every origin-less navigation, so a nav item and a fresh
|
|
146
195
|
tab still land on the same columns; a link *inside* a panel builds on that
|
|
147
|
-
panel instead and never asks. It
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
`
|
|
196
|
+
panel instead and never asks. It's consulted while the navigation is
|
|
197
|
+
still being worked out — before any route handler runs — so it has to
|
|
198
|
+
answer without drawing anything. From code,
|
|
199
|
+
`PanelStack.openPanelStack` takes the same list directly.
|
|
151
200
|
|
|
152
201
|
**Type:** `AncestorTable<NoInfer<R>>`
|
|
153
202
|
|
|
154
203
|
### mainOptions.stacking · member
|
|
155
204
|
|
|
156
|
-
Set `false` to show only the
|
|
157
|
-
sidebar still sits beside it). Everything else behaves the same: the
|
|
158
|
-
the back button,
|
|
159
|
-
only changes how many you see. Defaults to `true`.
|
|
205
|
+
Set `false` to show only the current panel, however wide the screen (the
|
|
206
|
+
nav sidebar still sits beside it). Everything else behaves the same: the
|
|
207
|
+
URL, the back button, unsaved panels, and the panels' own close buttons.
|
|
208
|
+
This only changes how many you see. Defaults to `true`.
|
|
160
209
|
|
|
161
210
|
**Type:** `boolean`
|
|
162
211
|
|
|
@@ -168,15 +217,15 @@ Footer content, pinned below the scroll area.
|
|
|
168
217
|
|
|
169
218
|
### mainOptions.maxWidth · member
|
|
170
219
|
|
|
171
|
-
Max width for the
|
|
220
|
+
Max width for the panel's *content*, e.g. `"60rem"`. The header and footer
|
|
172
221
|
backgrounds still span the full shell width, but their contents — and the
|
|
173
222
|
sidebar + separator + content trio (or just the content when there's no
|
|
174
223
|
sidebar) — cap to this width and centre horizontally. When unset, everything
|
|
175
|
-
fills the available width. Either way the content shares the
|
|
224
|
+
fills the available width. Either way the content shares the panel surface —
|
|
176
225
|
it is not boxed.
|
|
177
226
|
|
|
178
227
|
Ignored when you pass `MainOptions.routes`: there the open panels
|
|
179
|
-
decide the width (see `
|
|
228
|
+
decide the width (see `Panel.maxWidth`), and the header and footer line
|
|
180
229
|
themselves up with them.
|
|
181
230
|
|
|
182
231
|
**Type:** `string`
|
|
@@ -195,30 +244,29 @@ Aberdeen attr/style string applied to the top bar.
|
|
|
195
244
|
|
|
196
245
|
### mainOptions.nav · member
|
|
197
246
|
|
|
198
|
-
Navigation menu
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
nav as a full page sliding in from the left, not as a dropdown.
|
|
247
|
+
Navigation menu, rendered as a sidebar beside the content. The sidebar
|
|
248
|
+
collapses to a ☰ in the top bar when the shell is narrow, and there it
|
|
249
|
+
opens the nav as a full panel sliding in from the left, not as a dropdown.
|
|
202
250
|
|
|
203
251
|
`items` may be a reactive array: the shell reads it inside the sidebar's own
|
|
204
252
|
scope, so an item arriving or leaving redraws the sidebar and nothing else.
|
|
205
|
-
The content beside it — in routed mode, the whole
|
|
206
|
-
alone.
|
|
253
|
+
The content beside it — in routed mode, the whole stack — is left
|
|
254
|
+
alone. `button` customizes the ☰; `dropdownAttrs` does nothing here, since
|
|
255
|
+
a collapsed nav is a panel rather than a dropdown.
|
|
207
256
|
|
|
208
257
|
**Type:** `MenuOptions`
|
|
209
258
|
|
|
210
259
|
### mainOptions.navPosition · member
|
|
211
260
|
|
|
212
|
-
|
|
213
|
-
- `"left"` / `"right"`: sidebar next to the content area; collapses to a
|
|
214
|
-
button in the top bar when the shell width drops below 640 px.
|
|
215
|
-
- `"button"`: always a button, never a sidebar.
|
|
261
|
+
Which side the nav sidebar sits on. Defaults to `"left"`.
|
|
216
262
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
263
|
+
Either way it collapses to a ☰ in the top bar once the shell width drops to
|
|
264
|
+
640 px or below — the one threshold everything else keys off too, which is
|
|
265
|
+
why there is no "always a button" mode: it would make "narrow" and "the nav
|
|
266
|
+
is collapsed" two different things, and every rule about where a panel's
|
|
267
|
+
chrome goes assumes they are one.
|
|
220
268
|
|
|
221
|
-
**Type:** `"
|
|
269
|
+
**Type:** `"left" | "right"`
|
|
222
270
|
|
|
223
271
|
### mainOptions.navAttrs · member
|
|
224
272
|
|
package/skill/MenuItem.md
CHANGED
|
@@ -29,7 +29,7 @@ Click handler.
|
|
|
29
29
|
|
|
30
30
|
Render as a link (`<a>`) pointing here. Pairs naturally with
|
|
31
31
|
`interceptLinks()` — the item is highlighted automatically when the URL
|
|
32
|
-
matches.
|
|
32
|
+
matches, and scrolled into view if its list had scrolled it out.
|
|
33
33
|
|
|
34
34
|
**Type:** `string`
|
|
35
35
|
|
|
@@ -50,3 +50,18 @@ Disables the item.
|
|
|
50
50
|
Aberdeen attr/style string on the item element.
|
|
51
51
|
|
|
52
52
|
**Type:** `string`
|
|
53
|
+
|
|
54
|
+
### menuItem.items · member
|
|
55
|
+
|
|
56
|
+
Child entries, which turn the item into a collapsible **branch** of a
|
|
57
|
+
tree. Only the branch holding the current page is expanded; navigate away
|
|
58
|
+
and it folds back up. Clicking a branch *selects* rather than toggles: it
|
|
59
|
+
follows the item's own `href`, or failing that the first linked leaf
|
|
60
|
+
below it — which is what expands it. A branch with no link anywhere below
|
|
61
|
+
it falls back to plain open/close toggling.
|
|
62
|
+
|
|
63
|
+
Expanding is not selecting: a branch click never counts as picking an
|
|
64
|
+
item (see `onLeafSelect` on `menu`), so on a phone the nav stays up
|
|
65
|
+
while a section unfolds.
|
|
66
|
+
|
|
67
|
+
**Type:** `MenuEntry[]`
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
## MenuListOptions · interface
|
|
2
|
+
|
|
3
|
+
Options for `menu`.
|
|
4
|
+
|
|
5
|
+
### menuListOptions.items · member
|
|
6
|
+
|
|
7
|
+
The entries: items, separators, custom slots — and collapsible branches,
|
|
8
|
+
via `MenuItem.items`.
|
|
9
|
+
|
|
10
|
+
**Type:** `MenuEntry[]`
|
|
11
|
+
|
|
12
|
+
### menuListOptions.onLeafSelect · member
|
|
13
|
+
|
|
14
|
+
Run when a **leaf** item is activated. A branch expanding is not a
|
|
15
|
+
selection, so it doesn't run this — which is what lets a menu that
|
|
16
|
+
dismisses itself on selection stay up while a section unfolds.
|
|
17
|
+
|
|
18
|
+
**Type:** `() => void`
|
|
19
|
+
|
|
20
|
+
### menuListOptions.attrs · member
|
|
21
|
+
|
|
22
|
+
Aberdeen attr/style string on the list element.
|
|
23
|
+
|
|
24
|
+
**Type:** `string`
|
package/skill/MenuOptions.md
CHANGED
|
@@ -13,8 +13,9 @@ Items shown in the dropdown or sidebar nav.
|
|
|
13
13
|
Customize the trigger button rendered by `menuButton`. Defaults to a
|
|
14
14
|
`☰` icon button. The `click` handler is managed internally.
|
|
15
15
|
|
|
16
|
-
When used as a `nav` in `S.main()`, this also customizes the
|
|
17
|
-
|
|
16
|
+
When used as a `nav` in `S.main()`, this also customizes the ☰ the sidebar
|
|
17
|
+
collapses into — which is an `iconButton`, so only `icon`,
|
|
18
|
+
`ariaLabel` and `attrs` apply there.
|
|
18
19
|
|
|
19
20
|
**Type:** `ButtonOptions`
|
|
20
21
|
|
package/skill/Panel.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
## Panel · interface
|
|
2
|
+
|
|
3
|
+
What a route handler gets: the params from its route, plus everything the
|
|
4
|
+
shell needs to know about the panel it is drawing. It's an Aberdeen proxy, so
|
|
5
|
+
you can set things later, such as a `title` that arrives with your data or
|
|
6
|
+
`loading` going back to `false`, and the shell keeps up.
|
|
7
|
+
|
|
8
|
+
Search params and the `#hash` belong to the current panel only. Any other
|
|
9
|
+
panel keeps just its path, so anything a panel needs in order to redraw
|
|
10
|
+
itself has to live in that path. (A panel browsed away from does get its
|
|
11
|
+
search and hash back when a crumb makes it current again.)
|
|
12
|
+
|
|
13
|
+
**Type Parameters:**
|
|
14
|
+
|
|
15
|
+
- `P = Record<string, string | number | string[]>`
|
|
16
|
+
|
|
17
|
+
### panel.params · member
|
|
18
|
+
|
|
19
|
+
The params matched from this panel's path, typed per its route key:
|
|
20
|
+
`[x]` is a `string`, `[x=integer]` a `number`, `[...x]` a `string`.
|
|
21
|
+
Read-only.
|
|
22
|
+
|
|
23
|
+
**Type:** `P`
|
|
24
|
+
|
|
25
|
+
### panel.stack · member
|
|
26
|
+
|
|
27
|
+
The stack this panel is in — the very object `S.main()` hands back. It is
|
|
28
|
+
here as well because a route handler runs *while* that call is still
|
|
29
|
+
going, so its return value isn't available to it yet; this always is.
|
|
30
|
+
|
|
31
|
+
**Type:** `PanelStack`
|
|
32
|
+
|
|
33
|
+
### panel.path · member
|
|
34
|
+
|
|
35
|
+
This panel's path, e.g. `"/projects/7"`. Read-only.
|
|
36
|
+
|
|
37
|
+
**Type:** `string`
|
|
38
|
+
|
|
39
|
+
### panel.title · member
|
|
40
|
+
|
|
41
|
+
Names this screen, in the top bar's breadcrumb stack and in
|
|
42
|
+
`document.title` while the panel is current. A panel that doesn't set one
|
|
43
|
+
borrows the first line of text in its own body, so the stack never shows a
|
|
44
|
+
blank — but a borrowed paragraph makes a poor name, so say it yourself.
|
|
45
|
+
|
|
46
|
+
It does **not** conjure a heading: naming a screen and heading its content
|
|
47
|
+
are different jobs, and a screen that wants its name in its own body
|
|
48
|
+
writes it there, where it owns the typography.
|
|
49
|
+
|
|
50
|
+
**Type:** `string`
|
|
51
|
+
|
|
52
|
+
### panel.actions · member
|
|
53
|
+
|
|
54
|
+
This screen's own actions: a couple of buttons, a menu. The shell draws
|
|
55
|
+
them — never the panel — and *where* depends on facts only the shell has:
|
|
56
|
+
a quiet strip at the top of this panel's column while several columns are
|
|
57
|
+
up, and the top bar (where they take the app's own `menu` slot) once the
|
|
58
|
+
shell is narrow and this panel is the screen.
|
|
59
|
+
|
|
60
|
+
They are drawn in exactly one of those places at a time, so crossing the
|
|
61
|
+
threshold redraws them; anything stateful inside (the focus in a search
|
|
62
|
+
box) is lost. Buttons and menus are fine.
|
|
63
|
+
|
|
64
|
+
**Type:** `Slot`
|
|
65
|
+
|
|
66
|
+
### panel.width · member
|
|
67
|
+
|
|
68
|
+
How wide this panel's column actually is, in pixels — what
|
|
69
|
+
`Panel.maxWidth` asked for, resolved against the window. Reactive and
|
|
70
|
+
read-only, and correct *before* your handler draws, so content that sizes
|
|
71
|
+
itself can read it instead of measuring.
|
|
72
|
+
|
|
73
|
+
You rarely need it: the shell places the chrome for you. It's for content
|
|
74
|
+
that genuinely differs by width, such as a table that becomes a list.
|
|
75
|
+
|
|
76
|
+
**Type:** `number`
|
|
77
|
+
|
|
78
|
+
### panel.visible · member
|
|
79
|
+
|
|
80
|
+
Whether this panel is on screen right now: not crowded out from under the
|
|
81
|
+
visible run, not parked past its right end, and not on its way out.
|
|
82
|
+
Reactive and read-only.
|
|
83
|
+
|
|
84
|
+
The one to hang per-panel floating UI on (a FAB, a "3 selected" bar), for
|
|
85
|
+
which "am I the current panel?" is the wrong question — two columns can be
|
|
86
|
+
visible at once, and both of them are really there.
|
|
87
|
+
|
|
88
|
+
**Type:** `boolean`
|
|
89
|
+
|
|
90
|
+
### panel.maxWidth · member
|
|
91
|
+
|
|
92
|
+
The widest this panel can usefully be. Every panel must work at 360–540px,
|
|
93
|
+
because that is what it gets when two columns fit; this says how much
|
|
94
|
+
*more* it can take.
|
|
95
|
+
|
|
96
|
+
- `"half"` — nothing more. Half the content area (360–540px), so a second
|
|
97
|
+
column fits beside it. For lists and detail forms.
|
|
98
|
+
- `"full"` (the default) — the whole content area, up to ~1100px.
|
|
99
|
+
- `"screen"` — the whole window, unbounded: boards, wide tables, dense
|
|
100
|
+
dashboards. While one is open the shell itself stretches to the screen
|
|
101
|
+
edges instead of stopping at the standard 1280px page.
|
|
102
|
+
|
|
103
|
+
Below the width two columns need, everything takes the content area
|
|
104
|
+
whatever it asked for. Widths depend only on the window, never on what
|
|
105
|
+
else is open, so opening or closing a panel never resizes another.
|
|
106
|
+
|
|
107
|
+
Set it at the top of your handler and the panel is already that wide when
|
|
108
|
+
you draw (see `Panel.width`); set it later — when your data tells you
|
|
109
|
+
— and the panel reflows without being redrawn, keeping its state, while
|
|
110
|
+
the columns beside it move over.
|
|
111
|
+
|
|
112
|
+
**Type:** `"half" | "full" | "screen"`
|
|
113
|
+
|
|
114
|
+
### panel.loading · member
|
|
115
|
+
|
|
116
|
+
Set this while you're fetching what the panel needs, and back to `false`
|
|
117
|
+
when you're done. A new panel waits a moment before sliding in, so it can
|
|
118
|
+
arrive with real content instead of empty; if the wait drags on it slides
|
|
119
|
+
in anyway and shows a loading indicator until the flag clears. It only
|
|
120
|
+
affects the animation; the stack and the URL never wait for it.
|
|
121
|
+
|
|
122
|
+
**Type:** `boolean`
|
|
123
|
+
|
|
124
|
+
### panel.pinned · member
|
|
125
|
+
|
|
126
|
+
Keeps this panel from being closed by navigation happening *elsewhere*.
|
|
127
|
+
Opening a new panel normally closes everything after the panel it came
|
|
128
|
+
from; a pinned panel survives that, staying in the stack — parked past the
|
|
129
|
+
right edge of the viewport — slotted in beneath the new panel, one crumb
|
|
130
|
+
click away. The user toggles it from the crumb's context menu
|
|
131
|
+
(right-click or long-press), which is also where the pin shows; setting
|
|
132
|
+
it from code does the same thing.
|
|
133
|
+
|
|
134
|
+
A pin never blocks an *explicit* close: Escape at the stack's end,
|
|
135
|
+
`Panel.close`, the crumb menu's Close and `data-panel=replace` all
|
|
136
|
+
still close the panel.
|
|
137
|
+
|
|
138
|
+
**Type:** `boolean`
|
|
139
|
+
|
|
140
|
+
### panel.unsaved · member
|
|
141
|
+
|
|
142
|
+
Set this while the panel holds work that must not be lost — a dirty form,
|
|
143
|
+
an upload in flight. An unsaved panel cannot be closed, by anything:
|
|
144
|
+
navigation that would prune it parks it instead, past the viewport's
|
|
145
|
+
right edge, wearing a ● in its crumb — even the browser's back button
|
|
146
|
+
only parks it. `Panel.close` and the crumb menu's Close refuse,
|
|
147
|
+
Escape on it steps left along the stack rather than closing, and closing
|
|
148
|
+
the browser tab runs into the browser's own are-you-sure (after which the
|
|
149
|
+
shell brings the unsaved panel back on screen).
|
|
150
|
+
|
|
151
|
+
Only the app clears it; the user has no toggle. A Save or Discard button
|
|
152
|
+
clears it and then closes:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
A(() => { $panel.unsaved = $form.dirty || undefined; });
|
|
156
|
+
S.button({ content: "Discard", attrs: ".neutral", click: () => {
|
|
157
|
+
$panel.unsaved = false; // explicitly — see below
|
|
158
|
+
void $panel.close();
|
|
159
|
+
}});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The explicit `unsaved = false` before `close()` matters when the flag is
|
|
163
|
+
kept by a reactive scope, as above: resetting the form marks that scope
|
|
164
|
+
dirty, but it reruns *after* the running handler — after `close()` has
|
|
165
|
+
already been refused.
|
|
166
|
+
|
|
167
|
+
**Type:** `boolean`
|
|
168
|
+
|
|
169
|
+
### panel.close · member
|
|
170
|
+
|
|
171
|
+
Closes **this** panel, wherever it sits in the stack. Closing the current
|
|
172
|
+
panel hands the focus to the panel on its left; closing any other panel
|
|
173
|
+
takes just it away, leaving the columns around it where they are, with
|
|
174
|
+
their state. Either way it becomes a history entry, so the browser's
|
|
175
|
+
back button brings the panel back.
|
|
176
|
+
|
|
177
|
+
Resolves `false` if the panel didn't close: it holds
|
|
178
|
+
`Panel.unsaved` work, it was the only panel on the stack (so
|
|
179
|
+
there's nothing to show instead), or another navigation got there first.
|
|
180
|
+
The shell's breadcrumbs already travel back, so reach for this when a
|
|
181
|
+
screen wants a more explicit way out: a Cancel button, or a Save that
|
|
182
|
+
closes.
|
|
183
|
+
|
|
184
|
+
**Type:** `() => Promise<boolean>`
|
|
185
|
+
|
|
186
|
+
**Examples:**
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
S.button({ content: "Cancel", attrs: ".neutral", click: () => void $panel.close() });
|
|
190
|
+
```
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
## PanelStack · interface
|
|
2
|
+
|
|
3
|
+
The panel stack behind a routed `S.main()`, and what that call hands back:
|
|
4
|
+
the open `Panel`s, which of them is current, and the four ways to
|
|
5
|
+
change that. Everything on it is scoped to its own shell.
|
|
6
|
+
|
|
7
|
+
Every navigation settles asynchronously (closes travel through the
|
|
8
|
+
browser's history), so the methods resolve once it has: `true` when it
|
|
9
|
+
landed, `false` when it didn't — an unsaved panel refused to close, an
|
|
10
|
+
app-registered route guard said no, or another navigation superseded it.
|
|
11
|
+
Ignore the promise unless you care.
|
|
12
|
+
|
|
13
|
+
**Examples:**
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const shell = S.main({ title: "Trackle", routes: { ... } });
|
|
17
|
+
|
|
18
|
+
S.button({ content: "New task", click: async () => {
|
|
19
|
+
const task = await createTask();
|
|
20
|
+
shell.pushPanel(`/tasks/${task.id}`);
|
|
21
|
+
}});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### panelStack.panels · member
|
|
25
|
+
|
|
26
|
+
The open panels, oldest first — the stack itself, as live objects rather
|
|
27
|
+
than a copy of it. Writing through one is how you pin a panel, or rename
|
|
28
|
+
it, from outside its own handler. Reactive on the stack's shape; don't
|
|
29
|
+
hold a `Panel` across a navigation, since a closed one is dropped
|
|
30
|
+
here while its element plays out its exit.
|
|
31
|
+
|
|
32
|
+
**Type:** `readonly Panel<Record<string, string | number | string[]>>[]`
|
|
33
|
+
|
|
34
|
+
### panelStack.currentPanelIndex · member
|
|
35
|
+
|
|
36
|
+
Index into `PanelStack.panels` of the current panel. Reactive.
|
|
37
|
+
|
|
38
|
+
**Type:** `number`
|
|
39
|
+
|
|
40
|
+
### panelStack.currentPanel · member
|
|
41
|
+
|
|
42
|
+
The current panel — shorthand for `panels[currentPanelIndex]`, and
|
|
43
|
+
`undefined` only while the stack is still empty. Reactive on *which*
|
|
44
|
+
panel is current; the fields you then read (`title`, `actions`, …) are
|
|
45
|
+
reactive in their own right.
|
|
46
|
+
|
|
47
|
+
**Type:** `Panel<Record<string, string | number | string[]>>`
|
|
48
|
+
|
|
49
|
+
### panelStack.pushPanel · member
|
|
50
|
+
|
|
51
|
+
Opens `path` in a new panel on top of the current one, closing the
|
|
52
|
+
unpinned panels that were after it (pinned ones stay, sliding in beneath
|
|
53
|
+
the new panel).
|
|
54
|
+
|
|
55
|
+
The same rules as a link click apply: pushing a path that is already open
|
|
56
|
+
goes back to it — a focus move along the stack, closing nothing — rather
|
|
57
|
+
than opening it twice, and a panel holding `Panel.unsaved` work is
|
|
58
|
+
never closed, only parked. That's what a plain link does, and what
|
|
59
|
+
`data-panel=push` says outright.
|
|
60
|
+
|
|
61
|
+
**Type:** `(path: string) => Promise<boolean>`
|
|
62
|
+
|
|
63
|
+
### panelStack.replacePanel · member
|
|
64
|
+
|
|
65
|
+
Opens `path` in place of the current panel, which closes. The panels
|
|
66
|
+
beneath it stay as they are. That's what a `data-panel=replace` link does.
|
|
67
|
+
|
|
68
|
+
**Type:** `(path: string) => Promise<boolean>`
|
|
69
|
+
|
|
70
|
+
### panelStack.openPanelStack · member
|
|
71
|
+
|
|
72
|
+
Opens `path` as a whole stack rather than on top of what's there: the same
|
|
73
|
+
thing a nav item or a fresh tab does. Without `beneath`, the stack under it
|
|
74
|
+
is worked out the way a cold link's is (see `MainOptions.ancestors`);
|
|
75
|
+
with it, the paths you give are opened underneath, shallowest first.
|
|
76
|
+
|
|
77
|
+
That's the one for a screen whose URL doesn't say where it belongs — the
|
|
78
|
+
thread a notification opens — and for seeding a stack from code in general.
|
|
79
|
+
Panels the new stack also holds stay as they are; ones it drops close,
|
|
80
|
+
except panels with `Panel.unsaved` work, which stay, parked.
|
|
81
|
+
|
|
82
|
+
A `data-panel=open` link does the same thing (without a `beneath`): it
|
|
83
|
+
leaves the panel it sits in behind rather than stacking on it, which is
|
|
84
|
+
what a link to somewhere else in the app wants — a search hit, a mention.
|
|
85
|
+
|
|
86
|
+
**Type:** `(path: string, beneath?: readonly string[]) => Promise<boolean>`
|
|
87
|
+
|
|
88
|
+
**Examples:**
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
shell.openPanelStack(`/thread/${id}`, [`/mailbox/${mailboxId}`]);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### panelStack.closePanel · member
|
|
95
|
+
|
|
96
|
+
Closes the current panel, or, given a `path`, whichever panel is open at
|
|
97
|
+
it. Closing the current panel hands the focus to the panel on its left;
|
|
98
|
+
closing any other panel takes just it away, leaving the columns around it
|
|
99
|
+
exactly as they are, with their state. Either way it becomes a history
|
|
100
|
+
entry, so the browser's back button brings the panel back.
|
|
101
|
+
|
|
102
|
+
Resolves `false` if the panel didn't close: it holds `Panel.unsaved`
|
|
103
|
+
work, `path` isn't open, it was the stack's only panel, or another
|
|
104
|
+
navigation got there first.
|
|
105
|
+
|
|
106
|
+
**Type:** `(path?: string) => Promise<boolean>`
|