staffa 0.13.0 → 0.15.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.
@@ -105,16 +105,15 @@ aren't safe integers, such as snowflakes, want a plain `[id]`.
105
105
  Navigating is just links: the shell handles the clicks itself, so do *not*
106
106
  also call Aberdeen's `interceptLinks()`. A link opens its target on top of
107
107
  the panel it sits in, closing everything after that panel first. The
108
- `data-panel` attribute picks another of the three `PanelStack`
109
- navigations instead: `replace` puts the target in place of the link's own
110
- panel, and `open` leaves that panel behind and gives the target its own
111
- stack, the way a nav item does. A link
112
- to something already open goes back to it rather than opening it twice —
113
- a move along the stack that closes nothing: the panels right of it stay
114
- open, parked past the viewport's right edge, until a *new* panel prunes
115
- them (pinned panels excepted — see `Panel.pinned`).
116
- From code, use `pushPanel` and friends: navigating
117
- with `aberdeen/route`'s own `go()` works too — a panel with
108
+ `data-panel` attribute keeps less of that context instead: `replace`
109
+ drops the link's own panel too, putting the target in its place, and
110
+ `open` drops it all, giving the target its own stack, the way a nav item
111
+ does. A plain link to something already open goes back to it rather than
112
+ opening it twice, closing whatever was stacked on top — pinned panels
113
+ excepted (see `Panel.pinned`), and panels holding unsaved work,
114
+ which park instead; a `replace` or `open` applies its usual shape, the
115
+ open panel moving into it alive. From code, use `pushPanel` and
116
+ friends: navigating with `aberdeen/route`'s own `go()` works too — a panel with
118
117
  `Panel.unsaved` work still survives it — but builds the whole stack
119
118
  from the path. A navigation guard the app registered with
120
119
  `route.setGuard` (an auth redirect, say) keeps working: the shell
@@ -124,11 +123,11 @@ registers none of its own.
124
123
  is called (`Panel.title` — unset, its first line of text stands in)
125
124
  and what it can do (`Panel.actions`); everything else in a column is
126
125
  the panel's own content, boxes included. The shell writes the stack of
127
- open panels as breadcrumbs in the top bar — click one to go back to it,
128
- closing nothing — and places each panel's actions where the room is: on
129
- its own column while several fit, in the bar once the shell is narrow and
130
- the current panel *is* the screen. Nothing in an app measures the
131
- viewport to lay its screens out twice.
126
+ open panels as breadcrumbs in the top bar — click one to go back to it —
127
+ and places each panel's actions where the room is: on its own column while
128
+ several fit, in the bar once the shell is narrow and the current panel *is*
129
+ the screen. Nothing in an app measures the viewport to lay its screens out
130
+ twice.
132
131
 
133
132
  Only one routed shell can be mounted at a time (a second one throws) —
134
133
  the URL is global, so two of them would fight over it. Nothing else is:
@@ -207,9 +206,10 @@ answer without drawing anything. From code,
207
206
  How many panels are *shown* at a time. `"auto"` (the default) shows as
208
207
  many columns, side by side, as comfortably fit, ending at the current
209
208
  panel; `"single"` shows only the current panel, however wide the screen
210
- — the phone experience at every size (the nav sidebar still sits beside
211
- it). Only the display differs: the stack, the breadcrumbs, the URL,
212
- Escape and the back button behave identically in both. Routed mode only.
209
+ — one screen at a time at every size, each still at its asked width,
210
+ centred (the nav sidebar still sits beside it). Only the display
211
+ differs: the stack, the breadcrumbs, the URL, Escape and the back
212
+ button behave identically in both. Routed mode only.
213
213
 
214
214
  Live: pass a proxied options object (or make this field a getter) and a
215
215
  change is adopted in place — one layout pass, every panel keeping its
@@ -220,12 +220,13 @@ state.
220
220
  ### mainOptions.linkNavigation · member
221
221
 
222
222
  What a link *without* a `data-panel` attribute does — the per-link
223
- attribute always wins. `"push"` (the default) opens the target on top of
224
- the panel the link sits in; `"replace"` opens it in that panel's place;
225
- `"open"` gives it its own stack, the way a nav item does. With `"open"`
226
- every click replaces the content as a whole — which, with flat routes,
227
- is the conventional sidebar-and-content app: one pane, swapped on every
228
- click, the crumb line simply naming it. Routed mode only.
223
+ attribute always wins. The three keep less and less of the link's own
224
+ context: `"push"` (the default) builds on the panel the link sits in,
225
+ `"replace"` swaps that panel out, and `"open"` ignores it and gives the
226
+ target its own stack, the way a nav item does. So with `"open"` every
227
+ click replaces the content as a whole — which, with flat routes, is the
228
+ conventional sidebar-and-content app: one pane, swapped on every click,
229
+ the crumb line simply naming it. Routed mode only.
229
230
 
230
231
  Live, like `MainOptions.columns`: change it and the next click
231
232
  uses the new default.
@@ -240,34 +241,23 @@ Footer content, pinned below the scroll area.
240
241
 
241
242
  ### mainOptions.maxWidth · member
242
243
 
243
- Max width for the panel's *content*, e.g. `"60rem"`. The header and footer
244
+ Max width for the shell's *content*, e.g. `"80rem"`. The header and footer
244
245
  backgrounds still span the full shell width, but their contents — and the
245
246
  sidebar + separator + content trio (or just the content when there's no
246
247
  sidebar) — cap to this width and centre horizontally. When unset, everything
247
248
  fills the available width. Either way the content shares the panel surface —
248
249
  it is not boxed.
249
250
 
250
- Ignored when you pass `MainOptions.routes`: there the open panels
251
- decide the width (see `Panel.maxWidth`), and the header and footer line
252
- themselves up with them.
253
-
254
- **Type:** `string`
255
-
256
- ### mainOptions.fullWidth · member
257
-
258
- How wide a `"full"` panel gets, in pixels — and with it the whole content
259
- area, since a `"full"` fills it exactly. A `"half"` gets half of this, and
260
- a `"screen"` ignores it and takes the window. Defaults to 1080; the window
261
- caps it when there is less room than that. Routed mode only.
262
-
263
- This plus `MainOptions.navWidth` is the app's standard page — see
264
- there.
251
+ In routed mode this is what the columns divide up (see
252
+ `Panel.maxWidth`), which is the reason to set it on a very wide
253
+ screen: left uncapped, a stack of small panels will happily march right
254
+ across a 4K display.
265
255
 
266
256
  Live, like `MainOptions.columns`: pass a proxied options object (or
267
- make this field a getter) and a change is adopted in one layout pass,
268
- every panel keeping its state.
257
+ make this field a getter) and a change is adopted in one layout pass, every
258
+ open panel keeping its state.
269
259
 
270
- **Type:** `number`
260
+ **Type:** `string`
271
261
 
272
262
  ### mainOptions.contentAttrs · member
273
263
 
@@ -310,13 +300,9 @@ chrome goes assumes they are one.
310
300
  ### mainOptions.navWidth · member
311
301
 
312
302
  How wide the nav sidebar column is, in pixels — its hairline included.
313
- Defaults to 200.
314
-
315
- Together with `MainOptions.fullWidth` this is the app's *standard
316
- page*: the width the top bar and footer keep to, and the width the
317
- columns settle back to. The defaults come to the familiar 1280px.
303
+ Defaults to 200. Whatever it takes comes off the content area beside it.
318
304
 
319
- Live, like `MainOptions.fullWidth`.
305
+ Live, like `MainOptions.maxWidth`.
320
306
 
321
307
  **Type:** `number`
322
308
 
package/skill/Panel.md CHANGED
@@ -89,35 +89,35 @@ visible at once, and both of them are really there.
89
89
 
90
90
  ### panel.maxWidth · member
91
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 (360px up to half of
97
- `MainOptions.fullWidth`), so a second column fits beside it. For
98
- lists and detail forms.
99
- - `"full"` (the default) — the whole content area, which is exactly
100
- `MainOptions.fullWidth`: 1080px unless the app says otherwise.
101
- - `"screen"` — the whole window, unbounded: boards, wide tables, dense
102
- dashboards. While one is open the columns stretch to the screen edges
103
- instead of stopping at the standard page; the top bar and footer hold
104
- the standard width throughout.
105
-
106
- Below the width two columns need, everything takes the content area
107
- whatever it asked for. Widths depend only on the window, never on what
108
- else is open, so opening or closing a panel never resizes another.
109
-
110
- This is a *layout regime*, not a width guarantee: handle whatever width
111
- the bucket yields, and ask only for what your content can actually use —
112
- a screen that would cap its own content narrower than its ask is holding
113
- room that would have let another column fit beside it.
92
+ The widest this panel can usefully be — a ceiling the shell never
93
+ exceeds, so the draw function never has to look right past it. It is
94
+ counted in the shell's *columns*: the content area divides into the
95
+ narrowest whole number of columns of at least 360px each (three columns
96
+ of 360 in a 1080px area, four of 380 in 1520px), and a column is never
97
+ wider than 540 — where the area holds just one, a small centres in it
98
+ rather than stretching. So an ask never exceeds its column count × 540:
99
+
100
+ - `"small"` — one column, never above 540px: lists, detail forms.
101
+ - `"medium"` (the default) — two columns, never above 1080px.
102
+ - `"large"` — three columns, never above 1620px: wide tables.
103
+ - `"none"` — the whole content area, unbounded: boards, dashboards.
104
+ Bound it with the shell's own `maxWidth` where that matters.
105
+
106
+ Every size is capped at the content area, so on a phone they all come to
107
+ the same thing: one screen at a time. And a width depends only on the
108
+ window, never on what else is open, so opening or closing a panel never
109
+ resizes another — the run of columns just recentres in the area.
110
+
111
+ Ask only for what your content can actually use: a panel that would cap
112
+ its own content narrower than its ask is holding room that would have
113
+ let another column fit beside it.
114
114
 
115
115
  Set it at the top of your handler and the panel is already that wide when
116
116
  you draw (see `Panel.width`); set it later — when your data tells you
117
117
  — and the panel reflows without being redrawn, keeping its state, while
118
118
  the columns beside it move over.
119
119
 
120
- **Type:** `"half" | "full" | "screen"`
120
+ **Type:** `PanelSize`
121
121
 
122
122
  ### panel.loading · member
123
123
 
@@ -132,10 +132,10 @@ affects the animation; the stack and the URL never wait for it.
132
132
  ### panel.pinned · member
133
133
 
134
134
  Keeps this panel from being closed by navigation happening *elsewhere*.
135
- Opening a new panel normally closes everything after the panel it came
136
- from; a pinned panel survives that, staying in the stack — parked past the
137
- right edge of the viewport — slotted in beneath the new panel, one crumb
138
- click away. The user toggles it from the crumb's context menu
135
+ A navigation normally closes everything after the panel it came from (or
136
+ returned to); a pinned panel survives that, staying in the stack — slotted
137
+ in beneath the new panel, or parked out of sight when the new panel was
138
+ already beneath it. Either way it is one crumb click away. The user toggles it from the crumb's context menu
139
139
  (right-click or long-press), which is also where the pin shows; setting
140
140
  it from code does the same thing.
141
141
 
@@ -149,12 +149,11 @@ still close the panel.
149
149
 
150
150
  Set this while the panel holds work that must not be lost — a dirty form,
151
151
  an upload in flight. An unsaved panel cannot be closed, by anything:
152
- navigation that would prune it parks it instead, past the viewport's
153
- right edge, wearing a ● in its crumb — even the browser's back button
154
- only parks it. `Panel.close` and the crumb menu's Close refuse,
152
+ navigation that would prune it parks it out of sight instead, wearing a
153
+ ● in its crumb — even the browser's back button only parks it. `Panel.close` and the crumb menu's Close refuse,
155
154
  Escape on it steps left along the stack rather than closing, and closing
156
- the browser tab runs into the browser's own are-you-sure (after which the
157
- shell brings the unsaved panel back on screen).
155
+ the browser tab runs into the browser's own are-you-sure, the unsaved
156
+ panel brought on screen as the question is raised.
158
157
 
159
158
  Only the app clears it; the user has no toggle. A Save or Discard button
160
159
  clears it and then closes:
@@ -53,10 +53,10 @@ unpinned panels that were after it (pinned ones stay, sliding in beneath
53
53
  the new panel).
54
54
 
55
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.
56
+ returns to it — closing whatever was stacked on top — rather than opening
57
+ it twice, and a panel holding `Panel.unsaved` work is never closed,
58
+ only parked. That's what a plain link does, and what `data-panel=push`
59
+ says outright.
60
60
 
61
61
  Note that a link builds on the panel it is *drawn in*, which is the
62
62
  current panel only while no column beside it has the focus. Code
package/skill/SKILL.md CHANGED
@@ -159,11 +159,11 @@ The first key that matches wins, and a segment a param refuses simply doesn't ma
159
159
 
160
160
  **Navigating is just links.** Write ordinary `<a href="/...">` links; Staffa handles the clicks (so don't also call Aberdeen's `interceptLinks()`).
161
161
 
162
- The open panels form a **stack**, and one of them is the **current** panel: the one the URL names, and the rightmost column on screen. Usually that's the newest panel — but going back along the stack moves the cursor without closing anything (see the breadcrumbs below), so panels can sit *after* the current one too, parked just past the viewport's right edge.
162
+ The open panels form a **stack**, and its last panel is the **current** one: the panel the URL names, and the rightmost column on screen. Whatever you navigate to lands there.
163
163
 
164
- - A link inside a panel opens its target on top of that panel, closing everything after it first. That's why clicking a second project replaces the open project instead of adding a third column — and why the panels you'd browsed past don't pile up.
165
- - A `data-panel` attribute on the link picks a different one of the three navigations. `push` is the default just described; `replace` puts the target in place of the link's own panel rather than on top of it, which is what prev/next buttons want; and `open` leaves that panel behind altogether and gives the target its own stack, exactly as a nav item would — for a link that points somewhere else in the app, a search hit or a mention, where the panel you clicked from isn't the context you want to keep.
166
- - A link to something that's already open goes back to it instead of opening it twice — a move along the stack, closing nothing. The same path is never in the stack twice.
164
+ - A link inside a panel opens its target on top of that panel, closing everything after it first. That's why clicking a second project replaces the open project instead of adding a third column.
165
+ - A `data-panel` attribute on the link picks a different one of the three navigations, which differ only in how much of the link's own context the target keeps: `push` (the default just described) keeps the link's panel and builds on it; `replace` keeps everything under that panel but not the panel itself — what prev/next buttons want; and `open` keeps none of it, giving the target its own stack exactly as a nav item would — for a search hit or a mention, where the panel you clicked from is coincidence, not context.
166
+ - A plain link to something that's already open goes back to it instead of opening it twice, closing whatever was stacked on top of it; a `replace` or `open` applies its usual shape instead, the open panel moving into place with its state intact. Either way the same path is never in the stack twice.
167
167
  - A link that isn't inside a panel (a nav item, or one in a dialog) has no panel to build on, so it replaces the stack as a whole: the panel you asked for, with its ancestor panels opened beneath it (see [below](#ancestors)). Panels that the new stack also contains stay as they are, so clicking the nav item for the section you're already in won't reset it. Clicking a nav item and opening that same URL in a fresh tab therefore give you the same columns.
168
168
 
169
169
  **The stack is an object, not a global.** In routed mode `S.main()` hands back the panel stack, and every panel gets the same object as `$panel.stack` — which is what a route handler uses, since it runs while the `S.main()` call is still going and can't see its return value yet.
@@ -185,21 +185,24 @@ Navigations settle asynchronously (closes travel through the browser's history),
185
185
 
186
186
  Navigating faster than the shell can settle is fine: closing travels through the browser's history, so it takes a moment to land, and anything asked for in the meantime waits for it rather than being dropped. Two quick Escapes (or back gestures) peel two panels, each aimed at the stack the one before it was heading for.
187
187
 
188
- **Every panel must work at 360–540px**, because that is what it gets whenever two columns fit. `$panel.maxWidth` says how much *more* it can usefully take. The content area is what `S.main()`'s `fullWidth` says it is — 1080px by default:
188
+ **The content area is the window**, minus the nav sidebar — or, when `S.main({ maxWidth })` says so, that much of it, centred. Either way it is the same width whatever is open, so the sidebar, the top bar and the footer never move.
189
+
190
+ The shell divides that area into columns: the narrowest whole number of them that keeps each one at least **360px** wide, and no column ever wider than **540**. A 1080px content area is three columns of 360; a 1520px one is four of 380; below 720px there is a single column — of at most 540, centred. `$panel.maxWidth` counts in those columns:
189
191
 
190
192
  | `maxWidth` | How wide the panel gets | Good for |
191
193
  | --- | --- | --- |
192
- | `"half"` | Half the content area: 360 to 540px. | lists, detail forms — anything that reads well at phone width |
193
- | `"full"` (default) | The whole content area: up to 1080px. | ordinary screens; the safe default |
194
- | `"screen"` | The whole window, no upper limit: ~1720px on a 1920px screen. | boards, wide tables, dense dashboards |
194
+ | `"small"` | One column — never above 540px. | lists, detail forms — anything that reads well at phone width |
195
+ | `"medium"` (default) | Two columns — never above 1080px. | ordinary screens; the safe default |
196
+ | `"large"` | Three columns — never above 1620px. | wide tables, dense forms |
197
+ | `"none"` | The whole content area, unbounded. | boards, dashboards |
195
198
 
196
- Below the width two columns need, everything takes the whole content area whatever it asked for. Nothing fits beside a `"full"` on a standard page, but on a wide enough window a `"half"` still can, and the page grows to hold both.
199
+ The ask is a promise in both directions: the shell never draws a panel wider than its column count × 540px, and every size is also capped at the content area — so a draw function has to look right from 360px up to its own ceiling, and nowhere past it. There is nothing below 360 to handle either: a narrower window is shown the 360px layout scaled down to fit (dialogs, menus and toasts scaling along), so 360 really is the floor.
197
200
 
198
- The standard page is those 1080px plus the nav sidebar's 200 — the 1280px an app is usually seen at, though neither figure is fixed: `S.main({ navWidth, fullWidth })` sets both, and everything above follows from them.
201
+ A column's width depends only on the size of the window, never on what else is open. So opening or closing a panel never resizes the ones already on screen, and never reflows what someone was reading. As many columns as fit are shown, ending at the current panel; when they don't fill the content area they sit centred in it, so an arriving column nudges the others over to share the room.
199
202
 
200
- A column's width depends only on the size of the window, never on what else is open. So opening or closing a panel never resizes the ones already on screen, and never reflows what someone was reading. A lone `"half"` leaves its other half empty, and that is exactly where the next one lands. When more columns fit than the standard page holds (three halves, say), the page itself grows, staying centred, to hold them — though the top bar and footer keep to the standard width, so the chrome holds still while the columns come and go.
203
+ `S.main({ navWidth, maxWidth })` sets the sidebar's width and the cap. Leave `maxWidth` off and the app simply fills the window — worth capping (`"1600px"`, say) if you'd rather it didn't march across a 4K display.
201
204
 
202
- Columns tile that area, separated by a hairline and no gutter — a column brings its own padding, so their contents stay comfortably apart regardless.
205
+ Columns tile the area they're given, separated by a hairline and no gutter — a column brings its own padding, so their contents stay comfortably apart regardless.
203
206
 
204
207
  The panel is sized before your handler runs, and `$panel.width` is the resolved figure in pixels — so a chart, a virtualised list or a column count has the real width from the first frame, with nothing to measure. Set `maxWidth` at the top of your handler and you draw at the new width; set it later and the panel reflows without being redrawn, so nothing in it is rebuilt or loses its state.
205
208
 
@@ -217,11 +220,11 @@ function drawTask($panel: S.Panel<{ taskId: number }>) {
217
220
 
218
221
  On a wide screen the title becomes the stack's last crumb and the Save button sits in a quiet strip at the top of the column. On a phone the crumb is still there and Save moves into the top bar, where the app menu was. Nothing in your code measures the viewport, and no screen is written twice.
219
222
 
220
- **The breadcrumbs are the navigation.** The top bar's second line writes the open panels out as breadcrumbs — `Projects / Trackle / Task 42` — with the panels currently on screen in bold. Clicking an earlier crumb goes back to it *without closing anything*: the panels right of it stay open, parked just past the viewport's right edge, and clicking their crumbs brings them back. Browsing the stack is free — it's opening a *new* panel that closes the panels after the one it came from. The app's name and logo link to the app's home (the `home` option, `/` by default; `null` links neither), going back to it when it's already open and opening it when it isn't. A stack too long for the bar scrolls sideways, in an `S.scrollStrip` like the tab strip's.
223
+ **The breadcrumbs are the navigation.** The top bar's second line writes the open panels out as breadcrumbs — `Projects / Trackle / Task 42` — with the panels currently on screen in bold. A crumb is an ordinary link to its panel, so clicking one goes back to that panel and closes what was stacked on top of it — and the browser's back button brings those columns back. The app's name and logo link to the app's home (the `home` option, `/` by default; `null` links neither), going back to it when it's already open and opening it when it isn't. A stack too long for the bar scrolls sideways, in an `S.scrollStrip` like the tab strip's.
221
224
 
222
225
  That line is the `subtitle`'s while the stack has nothing to add: one panel open, reachable from a nav item that is already highlighted in a visible sidebar. Otherwise the stack takes it, since it is then the only thing naming the screen.
223
226
 
224
- Right-click (or long-press) a crumb for **Close** — which takes just that panel out, wherever it sits in the stack — and **Pin**. A pinned panel — its crumb wears a pin — never closes as a side effect of navigation elsewhere: where opening a new panel would prune it, it rides along beneath the new panel instead, one crumb click away. Pin the reference you keep coming back to, then navigate freely. An *explicit* close (Escape, `close()`, the crumb menu, `data-panel=replace`) still closes it, and it's yours from code as `$panel.pinned`. Because a crumb is a real link whose right-click the menu takes over, the menu also offers **Open in new tab** and **Copy link**.
227
+ Right-click (or long-press) a crumb for **Close** — which takes just that panel out, wherever it sits in the stack — and **Pin**. A pinned panel — its crumb wears a pin — never closes as a side effect of navigation elsewhere: where a navigation would prune it, it rides along beneath the new panel instead (or parks out of sight, when the panel you went back to was already beneath it), one crumb click away. Pin the reference you keep coming back to, then navigate freely. An *explicit* close (Escape, `close()`, the crumb menu, `data-panel=replace`) still closes it, and it's yours from code as `$panel.pinned`. Because a crumb is a real link whose right-click the menu takes over, the menu also offers **Open in new tab** and **Copy link**.
225
228
 
226
229
  A crumb can also wear a **●**: the panel holds unsaved work, and nothing will close it (see `$panel.unsaved` below).
227
230
 
@@ -266,7 +269,7 @@ Closing the current panel hands the focus to the panel on its left. Closing one
266
269
 
267
270
  A closed panel is torn down at once: its `A.clean()` hooks run the moment it closes, so subscriptions, timers and requests stop there and then. Only its element hangs around, inert and frozen, for the length of the exit animation.
268
271
 
269
- Escape steps one panel back along the stack: at the stack's end that closes the current panel, mid-stack it just moves left and parks the panel you leave, and at the stack's start it jumps to the navigation. The browser's back button replays whole arrangements — it re-opens what a navigation closed and re-parks what a crumb click brought back.
272
+ Escape closes the current panel — or just steps left, when it holds unsaved work or panels sit parked beyond it — and at the stack's start it jumps to the navigation. The browser's back button replays whole arrangements, re-opening what a navigation closed.
270
273
 
271
274
  <a id="ancestors"></a>
272
275
 
@@ -298,7 +301,7 @@ Search params and the `#hash` belong to the current panel only. Anything another
298
301
 
299
302
  **A few more things.**
300
303
 
301
- - `columns: "single"` shows only the current panel, however wide the screen — the phone experience at every size. Only the display changes: the URL, the back button, unsaved panels and the panels' own close buttons all behave the same.
304
+ - `columns: "single"` shows only the current panel, however wide the screen — one screen at a time at every size, each still at its asked width, centred. Only the display changes: the URL, the back button, unsaved panels and the panels' own close buttons all behave the same.
302
305
  - `linkNavigation` sets what a link *without* a `data-panel` attribute does: `"push"` (the default), `"replace"`, or `"open"`. With `"open"` every click replaces the content as a whole — which, with flat routes, is the conventional sidebar-and-content app: one pane, swapped on every click, the crumb line simply naming it.
303
306
  - Both are live: pass a proxied options object (or make the field a getter) and a change is adopted in place, every open panel keeping its state.
304
307
  - Only one routed `S.main()` can be mounted at a time; a second one throws — the URL is global, so two of them would fight over it. Nothing else is global: the stack belongs to its shell, and each handler gets its own `$panel`, since several panels are alive at once.
@@ -636,6 +639,16 @@ shell needs to know about the panel it is drawing. It's an Aberdeen proxy, so
636
639
  you can set things later, such as a `title` that arrives with your data or
637
640
  `loading` going back to `false`, and the shell keeps up.
638
641
 
642
+ ## PanelSize · type
643
+
644
+ How wide a panel asks to be — a ceiling the shell never exceeds; see
645
+ `Panel.maxWidth`. `"small"` is the column the content area is divided
646
+ into; `"medium"` and `"large"` are two and three of those, and `"none"` is
647
+ the whole area. Each is capped at the content area, so on a narrow window
648
+ they all come to the same thing.
649
+
650
+ **Type:** `"small" | "medium" | "large" | "none"`
651
+
639
652
  ## Routes · type
640
653
 
641
654
  A route table: path templates mapped to panel draw functions. Used as the
package/skill/main.md CHANGED
@@ -11,7 +11,8 @@ right.
11
11
  Instead of a single `content` slot, pass `MainOptions.routes` and the
12
12
  shell takes over navigation: each route draws one screen, called a panel,
13
13
  and as many columns as fit are shown at a time, side by side on a wide screen
14
- and one at a time on a phone. Each panel *declares* its chrome — its
14
+ and one at a time on a phone; `MainOptions.maxWidth` caps how much room
15
+ they have between them. Each panel *declares* its chrome — its
15
16
  `Panel.title` and its `Panel.actions` — and this shell places it:
16
17
  the stack of titles as breadcrumbs in the bar, the actions on the panel's
17
18
  column while several fit and in the bar once the shell is narrow enough
@@ -55,7 +55,7 @@ A.insertGlobalCss({
55
55
  "position:fixed z-index:200 top:50% left:50% " +
56
56
  "display:flex flex-direction:column " +
57
57
  "transform:translate(-50%,-50%) " +
58
- "min-width:20rem max-width:min(90vw,44rem) max-height:min(88vh,800px) " +
58
+ "min-width:min(20rem,90vw) max-width:min(90vw,44rem) max-height:min(88vh,800px) " +
59
59
  "r: $s-radius-lg; overflow:hidden " +
60
60
  "transition: opacity 0.2s ease-out, transform 0.2s ease-out;",
61
61
  "> header":