staffa 0.14.0 → 0.16.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.
Files changed (82) hide show
  1. package/README.md +99 -271
  2. package/dist/components/autocomplete.js +4 -5
  3. package/dist/components/box.js +11 -21
  4. package/dist/components/button.d.ts +20 -5
  5. package/dist/components/button.js +55 -47
  6. package/dist/components/buttonChooser.js +1 -3
  7. package/dist/components/checkbox.js +1 -2
  8. package/dist/components/dialog.d.ts +9 -2
  9. package/dist/components/dialog.js +29 -35
  10. package/dist/components/field.d.ts +5 -8
  11. package/dist/components/field.js +4 -6
  12. package/dist/components/form.d.ts +5 -7
  13. package/dist/components/form.js +6 -9
  14. package/dist/components/keyhelp.d.ts +22 -0
  15. package/dist/components/keyhelp.js +91 -0
  16. package/dist/components/main.js +190 -318
  17. package/dist/components/menu.d.ts +36 -9
  18. package/dist/components/menu.js +193 -144
  19. package/dist/components/panels.d.ts +152 -232
  20. package/dist/components/panels.js +341 -556
  21. package/dist/components/select.js +1 -3
  22. package/dist/components/tabs.d.ts +10 -13
  23. package/dist/components/tabs.js +40 -63
  24. package/dist/components/textline.d.ts +3 -5
  25. package/dist/components/textline.js +3 -5
  26. package/dist/components/toast.d.ts +1 -3
  27. package/dist/components/toast.js +3 -6
  28. package/dist/components/tooltip.d.ts +4 -5
  29. package/dist/components/tooltip.js +13 -22
  30. package/dist/core.d.ts +17 -39
  31. package/dist/core.js +13 -35
  32. package/dist/icons-helpers.d.ts +3 -3
  33. package/dist/icons-helpers.js +6 -11
  34. package/dist/index.d.ts +3 -1
  35. package/dist/index.js +5 -4
  36. package/dist/keys.d.ts +92 -0
  37. package/dist/keys.js +279 -0
  38. package/dist/staffa.esm.js +1 -1
  39. package/dist/theme.d.ts +4 -10
  40. package/dist/theme.js +58 -123
  41. package/package.json +2 -2
  42. package/skill/ButtonOptions.md +12 -0
  43. package/skill/DialogOptions.md +11 -2
  44. package/skill/FieldOptions.md +3 -5
  45. package/skill/IconButtonOptions.md +8 -0
  46. package/skill/MenuItem.md +22 -3
  47. package/skill/Panel.md +8 -0
  48. package/skill/SKILL.md +161 -294
  49. package/skill/addTooltip.md +4 -5
  50. package/skill/bindKey.md +51 -0
  51. package/skill/box.md +1 -1
  52. package/skill/form.md +5 -7
  53. package/skill/formatKey.md +21 -0
  54. package/skill/iconButton.md +4 -5
  55. package/skill/scrollStrip.md +7 -9
  56. package/skill/showFloatingMenu.md +2 -2
  57. package/skill/showKeyHelp.md +17 -0
  58. package/skill/tabs.md +3 -4
  59. package/skill/textline.md +3 -5
  60. package/src/components/autocomplete.ts +4 -5
  61. package/src/components/box.ts +11 -21
  62. package/src/components/button.ts +70 -47
  63. package/src/components/buttonChooser.ts +1 -3
  64. package/src/components/checkbox.ts +1 -2
  65. package/src/components/dialog.ts +39 -37
  66. package/src/components/field.ts +7 -11
  67. package/src/components/form.ts +6 -9
  68. package/src/components/keyhelp.ts +96 -0
  69. package/src/components/main.ts +194 -318
  70. package/src/components/menu.ts +209 -146
  71. package/src/components/panels.ts +389 -618
  72. package/src/components/select.ts +1 -3
  73. package/src/components/tabs.ts +40 -63
  74. package/src/components/textline.ts +3 -5
  75. package/src/components/toast.ts +4 -9
  76. package/src/components/tooltip.ts +13 -22
  77. package/src/core.ts +17 -43
  78. package/src/icons-helpers.ts +6 -11
  79. package/src/index.ts +5 -4
  80. package/src/keys.ts +300 -0
  81. package/src/theme.ts +58 -123
  82. package/skill/Attributes.md +0 -10
package/dist/theme.js CHANGED
@@ -2,54 +2,23 @@ import A from "aberdeen";
2
2
  /**
3
3
  * Theming and global base styles for Staffa.
4
4
  *
5
- * # The surface model
5
+ * A Staffa app is a tree of **surfaces** (`.s-s`), in two families:
6
6
  *
7
- * A Staffa app is a tree of **surfaces**. A surface is anything with its own
8
- * background and matching ink — the page, a card, a coloured button. Mark an
9
- * element with `.s-s` and (usually) one modifier class. There are two families:
7
+ * - **Neutral** — `.neutral`, and the implicit page at `:root`. Its shade steps
8
+ * with nesting depth (capped). No `tonal`/`outlined` variants.
9
+ * - **Accent** — `.primary`, `.danger`, `.success`, `.warning`, `.link` (a bare
10
+ * `.s-s` is primary): a bright fill with white ink, `.tonal`/`.outlined`
11
+ * variants supported. A surface nested *inside* one is forced back to filled,
12
+ * so it can't bleed into the vivid parent.
10
13
  *
11
- * - **Neutral surfaces** — `.s-s.neutral` (and the implicit page at `:root`). A calm
12
- * neutral whose shade steps automatically with nesting depth (page → panel →
13
- * raised, capped). No `tonal`/`outlined` variants. Use them for cards, bars,
14
- * popovers — anything that just holds content.
15
- * - **Accent surfaces** — `.s-s.primary`, `.s-s.danger`, `.s-s.success`,
16
- * `.s-s.warning`, `.s-s.link` (a bare `.s-s` defaults to primary). A bright
17
- * fill with white ink, painted as a subtle single-colour gradient. They support
18
- * `.tonal` and `.outlined` variants. A surface nested *inside* an accent surface
19
- * is always rendered filled, so it can't bleed into the vivid parent.
20
- *
21
- * # Contextual tokens
22
- *
23
- * Inside any surface (including `:root`) these inherited custom properties are
24
- * defined, so widgets adapt to wherever they're nested:
25
- *
26
- * | token | meaning |
27
- * | ------------ | --------------------------------------------------------- |
28
- * | `--s-bg` | the surface background |
29
- * | `--s-text` | default ink (also applied as `color`) |
30
- * | `--s-muted` | secondary text (subtitles, help) |
31
- * | `--s-accent` | the surface's "pop" — the brand primary on neutral surfaces, the ink on accent surfaces |
32
- * | `--s-faint` | hairline, derived from text/bg |
33
- *
34
- * The brand/semantic colours are mode-independent and settable: `--s-primary`
35
- * (the one brand colour — it tints the neutrals and defines `.s-s.primary`),
36
- * `--s-danger`, `--s-success`, `--s-warning`, and `--s-link` (the link colour,
37
- * also the fill of the `.s-s.link` surface). Links render in `--s-link` on
38
- * neutral surfaces and in the ink on accent surfaces.
39
- *
40
- * # Borders & shadows
41
- *
42
- * Neutral surfaces carry a subtle hairline border by default (so a card reads as a
43
- * card with no component help); it's applied through `:where()`, so a bar that
44
- * wants only a divider overrides it trivially. Any surface can opt into elevation
45
- * with `.shadow` or `.extra-shadow`, or drop a component's built-in shadow with
46
- * `.no-shadow` (e.g. `S.button({ attrs: ".no-shadow" })`).
47
- *
48
- * # Customising
49
- *
50
- * Re-skin by overriding the colour tokens (e.g. `--s-primary`). To add your own
51
- * accent surface, just set `--s-bg` (and, if needed, `--s-text`) — the gradient
52
- * and the rest of the tokens follow automatically:
14
+ * Every surface (and `:root`) defines the inherited tokens widgets style against,
15
+ * so they adapt to wherever they're nested: `--s-bg`, `--s-text` (also applied as
16
+ * `color`), `--s-muted` (secondary text), `--s-accent` (the surface's "pop" — the
17
+ * brand primary on neutral surfaces, the ink on accent ones) and `--s-faint`
18
+ * (hairline). The brand/semantic colours — `--s-primary`, `--s-danger`,
19
+ * `--s-success`, `--s-warning`, `--s-link` — are mode-independent and settable;
20
+ * overriding them re-skins the app. A custom accent surface needs only `--s-bg`
21
+ * (and, if needed, `--s-text`); gradient and tokens follow:
53
22
  *
54
23
  * ```ts
55
24
  * A.insertGlobalCss({ ".s-s.brand": "--s-bg:#ef6b00 --s-text:#fff" });
@@ -58,25 +27,18 @@ import A from "aberdeen";
58
27
  */
59
28
  /**
60
29
  * The subtle single-colour wash a surface is painted with, as a `background:`
61
- * declaration, at the given angle. Shared constants rather than a CSS custom
62
- * property, deliberately: `var()`s inside a custom property resolve where the
63
- * property is *defined*, so a `--s-sheen` at `:root` would paint every surface
64
- * with the page's wash instead of its own `$s-bg`'s.
30
+ * declaration, at the given angle. A shared constant rather than a `--s-sheen`
31
+ * custom property: `var()`s inside a custom property resolve where it is
32
+ * *defined*, so every surface would get the page's wash instead of its own.
65
33
  */
66
34
  const sheen = (angle) => `background: linear-gradient(${angle}, color-mix(in oklab, $s-bg, white 9%), color-mix(in oklab, $s-bg, black 9%));`;
67
35
  /** The surface sheen: the wash every surface (and the page) is painted with. */
68
36
  export const SURFACE_SHEEN = sheen("170deg");
69
37
  /**
70
- * The same wash, straight down — for panels.ts, where the routed columns and
71
- * the ground beside them have to look like one continuous surface.
72
- *
73
- * A gradient at an angle takes its extent from the box's *width* as well as its
74
- * height, so a 430px column and the 1720px region behind it would paint
75
- * different slices of the same wash and meet at a visible step. Straight down,
76
- * the extent is the height alone — which every column shares exactly with the
77
- * region (a column is `top:0 bottom:0` in it) and so with every other column.
78
- * The 10° of tilt is worth losing there; it buys the one place in the app where
79
- * boxes of *different widths* must be seamless.
38
+ * The same wash, straight down — for panels.ts, where the routed columns and the
39
+ * ground beside them have to look like one continuous surface. An angled gradient
40
+ * takes its extent from the box's *width* as well as its height, so boxes of
41
+ * different widths would paint different slices of it and meet at a visible step.
80
42
  */
81
43
  export const PANEL_SHEEN = sheen("180deg");
82
44
  const STORAGE_KEY = "staffa:darkMode";
@@ -124,13 +86,9 @@ export function getDarkMode(allowAuto = false) {
124
86
  const v = $override.value;
125
87
  return v === undefined && !allowAuto ? A.darkMode() : v;
126
88
  }
127
- // ---------------------------------------------------------------------------
128
- // The only mode-dependent thing: the neutral surface shades and their ink,
129
- // written straight onto the surfaces (no intermediate palette vars). `:root` is
130
- // the page (depth 0); each nested `.neutral` steps one shade up, capped at the
131
- // `.neutral .neutral` rule. The accent (coloured) surfaces and everything else are
132
- // mode-independent and live in the static block below.
133
- // ---------------------------------------------------------------------------
89
+ // The only mode-dependent thing: the neutral shades and their ink, written
90
+ // straight onto the surfaces (no intermediate palette vars). `:root` is the page
91
+ // (depth 0); each nested `.neutral` steps a shade up, capped at the second level.
134
92
  A(() => {
135
93
  if (getDarkMode()) {
136
94
  A.insertGlobalCss({
@@ -147,34 +105,26 @@ A(() => {
147
105
  });
148
106
  }
149
107
  });
150
- // ---------------------------------------------------------------------------
151
- // Static structure — inserted once. The colour tokens are mode-independent
152
- // (saturated fills that carry white ink on either background); shape/effect
153
- // tokens are single values kept only because they're reused across components.
154
- // Rule order matters: role fills come after the `:not(.neutral)` default, so a
155
- // caller's `attrs` override wins at equal specificity.
156
- // ---------------------------------------------------------------------------
108
+ // Static structure — inserted once, mode-independent. Rule order matters: role
109
+ // fills come after the `:not(.neutral)` default, so a caller's `attrs` override
110
+ // wins at equal specificity.
157
111
  A.setSpacingCssVars(1.1);
158
112
  A.insertGlobalCss({
159
- // What follows is a lightweight CSS reset. Semantic HTML should keep working, but less ugly/with some reasonable defaults.
113
+ // A lightweight reset: bare semantic HTML, with less ugly defaults.
160
114
  "*, *::before, *::after": "box-sizing:border-box",
161
115
  html: "text-size-adjust:100%",
162
116
  body: "m:0 p:$3 line-height:1.5 font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; -webkit-font-smoothing:antialiased background-color:$s-bg text:$s-text",
163
- // Links resolve to the contextual link foreground: the link colour on nesting
164
- // surfaces, the ink on accent surfaces.
117
+ // The contextual link colour: `--s-link` on neutral surfaces, the ink on accent ones.
165
118
  a: "color: $s-link-fg; text-decoration:underline text-underline-offset:2px; transition: color 0.12s, filter 0.12s;",
166
119
  "a:hover": "filter: brightness(1.15)",
167
120
  "input, button, textarea, select, optgroup": "font:inherit color:inherit",
168
- // Bare text-like fields get a calm bordered box derived from the surface. The
169
- // styled controls (`.s-input`, autocomplete's `.s-control`) override this via
170
- // higher specificity, so this only governs otherwise-unstyled HTML. `:where()`
171
- // keeps it at element specificity, so a component class always wins.
121
+ // Bare text-like fields get a calm bordered box derived from the surface.
122
+ // `:where()` keeps it at element specificity, so a component class always wins.
172
123
  "input:where(:not([type=checkbox],[type=radio],[type=range],[type=file],[type=color],[type=image],[type=submit],[type=button],[type=reset],[type=hidden])), textarea, select": "background:$s-bg border: 1px solid $s-faint; r:$s-radius-sm padding: 0.45em 0.65em; max-width:100%",
173
- // Checkboxes/radios: a touch larger with a pointer cursor. (The brand accent —
174
- // `accent-color` — is inherited from the surface, see the `:root, .s-s` rule.)
124
+ // Checkboxes/radios: a touch larger, with a pointer cursor. (`accent-color` is
125
+ // inherited from the surface, see the `:root, .s-s` rule.)
175
126
  "input:where([type=checkbox],[type=radio])": "width:1.15em height:1.15em cursor:pointer",
176
- // Range: a thin pill track (faint, brand-filled on the lower side in Firefox)
177
- // with a round brand thumb — no native groove/outline.
127
+ // Range: a thin faint pill track with a round brand thumb — no native groove.
178
128
  "input[type=range]": "appearance:none background:transparent cursor:pointer vertical-align:middle",
179
129
  "input[type=range]::-webkit-slider-runnable-track": "height:4px r:99px background:$s-faint",
180
130
  "input[type=range]::-moz-range-track": "height:4px r:99px background:$s-faint",
@@ -213,11 +163,10 @@ A.insertGlobalCss({
213
163
  "--s-focus: color-mix(in srgb, $s-primary 38%, transparent); " +
214
164
  // Brand sweep for the headline mark, the active nav pill, the selected tab.
215
165
  "--s-gradient: linear-gradient(135deg, color-mix(in oklab, $s-primary, white 16%), color-mix(in oklab, $s-primary, black 14%));",
216
- // Neutral surfaces (and the page): the pop colour is the brand primary, and
217
- // links use the link colour. (The bg/ink come from the mode block above.)
166
+ // Neutral surfaces (and the page); their bg/ink come from the mode block above.
218
167
  ":root, .s-s.neutral": "--s-accent:$s-primary --s-link-fg:$s-link",
219
- // Accent surfaces: bright fill, white ink. The bare `:not(.neutral)` carries the
220
- // shared defaults (with a primary fallback fill); each role names its own fill.
168
+ // Accent surfaces: bright fill, white ink. The bare `:not(.neutral)` holds the
169
+ // shared defaults; each role below names its own fill.
221
170
  ".s-s:not(.neutral)": "--s-bg:$s-primary " + // Default .s-s to .primary
222
171
  "border:0 " +
223
172
  "--s-text:#eee --s-accent:#fff --s-link-fg:#eef " +
@@ -228,55 +177,44 @@ A.insertGlobalCss({
228
177
  ".s-s.warning": "--s-bg:$s-warning",
229
178
  ".s-s.link": "--s-bg:$s-link",
230
179
  ".s-s.primary": "--s-bg:$s-primary",
231
- // Shared derive: every surface (and the page) gets a muted ink + hairline from
232
- // its resolved text/bg pair, plus `color` and themed scrollbars.
180
+ // Every surface (and the page) derives its muted ink + hairline from the
181
+ // text/bg pair it resolved to above.
233
182
  ":root, .s-s": "--s-muted: color-mix(in oklab, $s-text, $s-bg 42%); " +
234
183
  "--s-faint: color-mix(in oklab, $s-text, $s-bg 80%); " +
235
184
  "color:$s-text accent-color:$s-accent scrollbar-width:thin scrollbar-color: $s-faint transparent;",
236
185
  // Subtle single-colour gradient sheen, painted on every surface (and the page).
237
186
  ".s-s, body": SURFACE_SHEEN,
238
187
  ".s-s": "r:$s-radius",
239
- // Neutral surfaces own a subtle hairline border (a card reads as a card without
240
- // any component help). `:where()` keeps it zero-specificity, so a bar/panel that
241
- // wants only a divider (a box header, the app top bar, the nav panel) overrides
242
- // it with a single plain rule. Accent (filled) surfaces don't get it — their fill
243
- // is the edge. Buttons keep their own `border:0`.
188
+ // A neutral surface owns a hairline border, so a card reads as a card without
189
+ // any component help. `:where()` keeps it zero-specificity, so a bar that wants
190
+ // only a divider overrides it with a single plain rule.
244
191
  ":where(.s-s.neutral)": "border: 1px solid $s-faint;",
245
192
  ".s-s::-webkit-scrollbar, .s-s ::-webkit-scrollbar": "width:10px height:10px",
246
193
  ".s-s::-webkit-scrollbar-track, .s-s ::-webkit-scrollbar-track": "background:transparent",
247
194
  ".s-s::-webkit-scrollbar-thumb, .s-s ::-webkit-scrollbar-thumb": "background:$s-faint border-radius:99px border: 2px solid transparent; background-clip:padding-box",
248
- // Elevation utilities — add `.shadow` or `.extra-shadow` to any surface:
249
- // • neutral surface → a neutral drop shadow
250
- // • accent (filled) surface → a self-coloured glow, keyed on its own --s-bg (a
251
- // lit button is just this on a `.primary` surface)
252
- // • tonal/outlined surface → ignored (a translucent body has nothing to lift)
253
- // A `.neutral` button stays flat (so segmented groups gain no stray
254
- // shadows). `.no-shadow` is a hard override of any of the above — place it last
255
- // and make it !important so it beats the higher-specificity glow rule.
195
+ // Elevation: `.shadow`/`.extra-shadow` give a neutral surface a drop shadow and
196
+ // an accent one a self-coloured glow; tonal/outlined have nothing to lift. A
197
+ // `.neutral` button stays flat, so segmented groups gain no stray shadows.
198
+ // `.no-shadow` comes last and needs `!important` to beat the glow rules.
256
199
  ".s-s.shadow.neutral:not(.s-btn)": "box-shadow: 0 4px 14px rgba(0,0,0,0.13);",
257
200
  ".s-s.extra-shadow.neutral:not(.s-btn)": "box-shadow: 0 18px 50px rgba(0,0,0,0.28);",
258
201
  ".s-s.shadow:not(.neutral):not(.tonal):not(.outlined)": "box-shadow: 0 4px 14px color-mix(in srgb, $s-bg 30%, transparent);",
259
202
  ".s-s.extra-shadow:not(.neutral):not(.tonal):not(.outlined)": "box-shadow: 0 14px 40px color-mix(in srgb, $s-bg 40%, transparent);",
260
203
  ".s-s.no-shadow": "box-shadow: none !important;",
261
- // Accent variants. `tonal`: the fill colour becomes the ink over a soft
262
- // self-tint. `outlined`: the fill colour is the ink over a transparent body
263
- // with a colour edge. (Neutral surfaces ignore these.)
204
+ // Accent variants: the fill colour becomes the ink, over a soft self-tint
205
+ // (`tonal`) or a transparent body with a colour edge (`outlined`).
264
206
  ".s-s:not(.neutral).tonal, .s-s:not(.neutral).outlined": "--s-text:$s-bg --s-accent:$s-bg --s-link-fg:$s-bg --s-faint: color-mix(in srgb, $s-bg 30%, transparent); --s-muted: color-mix(in srgb, $s-bg 70%, transparent);",
265
207
  ".s-s:not(.neutral).tonal": "background: color-mix(in srgb, $s-bg 15%, transparent); border: 1px solid $s-faint;",
266
208
  ".s-s:not(.neutral).outlined": "background: transparent; border: 1px solid color-mix(in srgb, $s-bg 45%, transparent);",
267
- // A surface nested inside an accent surface is forced back to a filled look —
268
- // a translucent tonal/outlined body would bleed into the vivid parent fill.
269
- // Specificity (4 classes) beats the 2-class variant rules — no !important.
209
+ // A surface inside an accent surface is forced back to filled: a translucent
210
+ // body would bleed into the vivid parent. 4 classes beats the variant rules.
270
211
  ".s-s:not(.neutral) .s-s.tonal, .s-s:not(.neutral) .s-s.outlined": "--s-text:#fff --s-accent:#fff --s-link-fg:#fff " +
271
212
  SURFACE_SHEEN + " border-color: transparent;",
272
213
  });
273
214
  // ── Suppress transitions during the initial load ─────────────────────────────
274
- // Buttons (and links) transition their colours, so a light↔dark switch animates
275
- // smoothly. On a *cold* load that's a liability: elements mount and paint in the
276
- // active theme in one pass, but a transitioning element animates from its
277
- // default (unstyled) colours into the theme. Tag <html> until the first frame
278
- // has painted and hard-disable transitions under that tag, so the initial render
279
- // snaps straight to the right colours.
215
+ // Colour transitions make a light↔dark switch smooth, but on a cold load they'd
216
+ // animate from the unstyled colours into the theme. Tag <html> until the first
217
+ // frame has painted, so the initial render snaps to the right colours.
280
218
  A.insertGlobalCss({
281
219
  ".s-preload, .s-preload *, .s-preload *::before, .s-preload *::after": "transition: none !important; animation: none !important;",
282
220
  });
@@ -286,18 +224,15 @@ if (typeof document !== "undefined" && typeof requestAnimationFrame === "functio
286
224
  requestAnimationFrame(() => requestAnimationFrame(() => root.classList.remove("s-preload")));
287
225
  }
288
226
  // ── Disabled region ───────────────────────────────────────────────────────────
289
- // aria-disabled="true" on any container dims it and blocks pointer events on
290
- // it and all descendants, matching the per-element disabled look. Keyboard
291
- // access to focusable descendants is unaffected — add the `inert` attribute too
292
- // if you need that.
227
+ // aria-disabled="true" on any container dims it and blocks pointer events on it
228
+ // and all descendants. Keyboard access is unaffected — add `inert` for that.
293
229
  A.insertGlobalCss({
294
230
  ":disabled, [aria-disabled=true]": "opacity:0.45 filter:saturate(0.6) user-select:none",
295
231
  ":disabled, [aria-disabled=true], :disabled *, [aria-disabled=true] *": "pointer-events:none cursor:not-allowed",
296
232
  });
297
233
  // ── Flow content: vertical rhythm & light typography ─────────────────────────
298
- // Sensible block defaults for *any* content — your own UI just as much as
299
- // markdown-rendered HTML. The rhythm: strip the browser's block margins, then
300
- // give every block a *top* margin only when it isn't its parent's first child.
234
+ // Block defaults for *any* content, markdown-rendered or your own: no browser
235
+ // block margins, but a *top* margin unless the block is its parent's first child.
301
236
  const BLOCK = "p, ul, ol, dl, blockquote, pre, table, figure, hr, h1, h2, h3, h4, h5, h6";
302
237
  A.insertGlobalCss({
303
238
  [`${BLOCK}`]: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "staffa",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "An opinionated component library for the Aberdeen reactive UI library.",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -41,7 +41,7 @@
41
41
  "jsdom": "^29.1.1",
42
42
  "lucide-static": "^1.17.0",
43
43
  "readme-tsdoc": "^1.2.1",
44
- "shotest": "^2.3.2",
44
+ "shotest": "^2.4.0",
45
45
  "typescript": "^5.9.3"
46
46
  },
47
47
  "allowScripts": {
@@ -44,6 +44,18 @@ Accessible label, when the button has only an icon.
44
44
 
45
45
  **Type:** `string`
46
46
 
47
+ ### buttonOptions.key · member
48
+
49
+ A keyboard shortcut that presses this button: `"mod+s"`, `"f2"` — see
50
+ `bindKey` for the spelling and which keystrokes are yours to take.
51
+ It works from anywhere while the button is drawn, shows in a tooltip,
52
+ reaches screen readers as `aria-keyshortcuts`, and does exactly what a
53
+ click does: a `type: "submit"` submits its form, an `href` navigates. The
54
+ `?` overview (`showKeyHelp`) lists it under the button's text (or
55
+ its `ariaLabel`).
56
+
57
+ **Type:** `string`
58
+
47
59
  ### buttonOptions.attrs · member
48
60
 
49
61
  Aberdeen attr/style string applied to the button. A button is a surface, so
@@ -48,8 +48,17 @@ May be changed on a proxied options object while the dialog is open
48
48
 
49
49
  ### dialogOptions.cancelWithScope · member
50
50
 
51
- When set to `true` (default) the model will be destroyed when the `dialog()`-calling
52
- scope is destroyed.
51
+ When `true` (default), the dialog is destroyed when the scope that called
52
+ `dialog()` is destroyed.
53
+
54
+ **Type:** `boolean`
55
+
56
+ ### dialogOptions.keyboardTransparent · member
57
+
58
+ Leave the keyboard shortcuts behind this dialog working, instead of owning
59
+ the keyboard the way a modal does. For overlays that inform rather than
60
+ interrupt — the `?` shortcut overview is one. Esc still closes it first,
61
+ whatever it meant before.
53
62
 
54
63
  **Type:** `boolean`
55
64
 
@@ -1,11 +1,9 @@
1
1
  ## FieldOptions · interface
2
2
 
3
3
  Options shared by all *form field* components (textline, textarea, checkbox,
4
- autocomplete, ...).
5
-
6
- Fields share a consistent vertical layout: an optional label, the control
7
- itself, and optional help/error text below it. `form` relies on this
8
- shared structure to align groups of fields.
4
+ autocomplete, ...). Every field lays out the same way — optional label,
5
+ control, optional help/error below — which is what lets `form` align
6
+ groups of them.
9
7
 
10
8
  ### fieldOptions.attrs · member
11
9
 
@@ -20,6 +20,14 @@ Click handler.
20
20
 
21
21
  **Type:** `(event: Event) => void`
22
22
 
23
+ ### iconButtonOptions.key · member
24
+
25
+ A keyboard shortcut that presses this button — see `ButtonOptions.key`.
26
+ The tooltip shows it after the `ariaLabel` (a glyph is worth naming there
27
+ anyway), and the `?` overview lists it under that label too.
28
+
29
+ **Type:** `string`
30
+
23
31
  ### iconButtonOptions.href · member
24
32
 
25
33
  Render as a link (`<a role=button>`) pointing here instead of a `<button>`.
package/skill/MenuItem.md CHANGED
@@ -38,6 +38,26 @@ columns rather than on top of whatever panel the menu was drawn in. Pass
38
38
 
39
39
  **Type:** `string`
40
40
 
41
+ ### menuItem.key · member
42
+
43
+ A keyboard shortcut that activates this item: `"mod+k"`, `"f2"`, a bare
44
+ `"?"`. The spelling, and which keystrokes are yours to take, are
45
+ documented on `bindKey`. The combination shows at the right end of
46
+ the row (not on a touch device) and reaches screen readers as
47
+ `aria-keyshortcuts`; the `?` overview (`showKeyHelp`) lists it
48
+ under the item's label. Activating runs `click` with the `KeyboardEvent`
49
+ and follows `href` as a fresh navigation to it — the target getting its
50
+ own panel stack, as a nav item's does.
51
+
52
+ The shortcut works with the menu shut — rather the point of one on a
53
+ dropdown or context menu — for as long as whatever owns the items is
54
+ drawn: the `menu`, `menuButton` or `addContextMenu`
55
+ call, or `S.main`'s `nav`. (The bare `showFloatingMenu` binds
56
+ nothing: its menu exists only while it is up.) A disabled item's key is
57
+ not bound.
58
+
59
+ **Type:** `string`
60
+
41
61
  ### menuItem.match · member
42
62
 
43
63
  Pages this item claims *beyond* its own `href`: a string claims that path
@@ -45,9 +65,8 @@ and everything under it (`"/mail"` claims `/mail/…`, not `/mailbox`), a
45
65
  function is asked with the current path. While a claimed page is current,
46
66
  the item is highlighted and the branches above it stay unfolded — for the
47
67
  detail screens a menu has no row of their own: the `/thread/[id]` a
48
- notification lands on, an icon's page under the gallery's row. Claims
49
- work from the very first paint, so they also cover cold deep links,
50
- which no amount of fold-state keeping can.
68
+ notification lands on, an icon's page under the gallery's row. Claims work
69
+ from the first paint, so cold deep links are covered too.
51
70
 
52
71
  **Type:** `string | ((path: string) => boolean)`
53
72
 
package/skill/Panel.md CHANGED
@@ -20,6 +20,10 @@ The params matched from this panel's path, typed per its route key:
20
20
  `[x]` is a `string`, `[x=integer]` a `number`, `[...x]` a `string`.
21
21
  Read-only.
22
22
 
23
+ Single-segment params are percent-decoded; `[...x]` is not, since decoding
24
+ it would make an encoded slash indistinguishable from a separator. Split it
25
+ yourself: `x.split("/").map(decodeURIComponent)`.
26
+
23
27
  **Type:** `P`
24
28
 
25
29
  ### panel.stack · member
@@ -108,6 +112,10 @@ the same thing: one screen at a time. And a width depends only on the
108
112
  window, never on what else is open, so opening or closing a panel never
109
113
  resizes another — the run of columns just recentres in the area.
110
114
 
115
+ The ask is a ceiling only; there is no matching floor, since the window can
116
+ be any width. Aim your layout at 360px — about the narrowest phone still in
117
+ common use — and let it degrade gracefully below that.
118
+
111
119
  Ask only for what your content can actually use: a panel that would cap
112
120
  its own content narrower than its ask is holding room that would have
113
121
  let another column fit beside it.