@owlmeans/web-panel 0.1.18-rc.51 → 0.1.18-rc.53

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 (98) hide show
  1. package/README.md +44 -11
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/web-panel/SKILL.md +164 -121
  4. package/build/@/components/ui/sheet.d.ts +15 -0
  5. package/build/@/components/ui/sheet.d.ts.map +1 -0
  6. package/build/@/components/ui/sheet.js +40 -0
  7. package/build/@/components/ui/sheet.js.map +1 -0
  8. package/build/auth/entrypoints.d.ts +9 -1
  9. package/build/auth/entrypoints.d.ts.map +1 -1
  10. package/build/components/footer/component.d.ts +16 -4
  11. package/build/components/footer/component.d.ts.map +1 -1
  12. package/build/components/footer/component.js +26 -8
  13. package/build/components/footer/component.js.map +1 -1
  14. package/build/components/footer/types.d.ts +17 -1
  15. package/build/components/footer/types.d.ts.map +1 -1
  16. package/build/components/index.d.ts +1 -0
  17. package/build/components/index.d.ts.map +1 -1
  18. package/build/components/index.js +1 -0
  19. package/build/components/index.js.map +1 -1
  20. package/build/components/nav/index.d.ts +1 -0
  21. package/build/components/nav/index.d.ts.map +1 -1
  22. package/build/components/nav/index.js +1 -0
  23. package/build/components/nav/index.js.map +1 -1
  24. package/build/components/nav/layout.d.ts +7 -0
  25. package/build/components/nav/layout.d.ts.map +1 -1
  26. package/build/components/nav/layout.js +50 -5
  27. package/build/components/nav/layout.js.map +1 -1
  28. package/build/components/nav/mobile.d.ts +21 -0
  29. package/build/components/nav/mobile.d.ts.map +1 -0
  30. package/build/components/nav/mobile.js +70 -0
  31. package/build/components/nav/mobile.js.map +1 -0
  32. package/build/components/nav/types.d.ts +39 -1
  33. package/build/components/nav/types.d.ts.map +1 -1
  34. package/build/components/scheme/hook.d.ts +16 -0
  35. package/build/components/scheme/hook.d.ts.map +1 -0
  36. package/build/components/scheme/hook.js +51 -0
  37. package/build/components/scheme/hook.js.map +1 -0
  38. package/build/components/scheme/index.d.ts +4 -0
  39. package/build/components/scheme/index.d.ts.map +1 -0
  40. package/build/components/scheme/index.js +3 -0
  41. package/build/components/scheme/index.js.map +1 -0
  42. package/build/components/scheme/toggle.d.ts +14 -0
  43. package/build/components/scheme/toggle.d.ts.map +1 -0
  44. package/build/components/scheme/toggle.js +34 -0
  45. package/build/components/scheme/toggle.js.map +1 -0
  46. package/build/components/scheme/types.d.ts +21 -0
  47. package/build/components/scheme/types.d.ts.map +1 -0
  48. package/build/components/scheme/types.js +2 -0
  49. package/build/components/scheme/types.js.map +1 -0
  50. package/build/components/socket/i18n/fr.json +7 -0
  51. package/build/components/socket/i18n.js +2 -0
  52. package/build/components/socket/i18n.js.map +1 -1
  53. package/build/consent/component.d.ts +11 -4
  54. package/build/consent/component.d.ts.map +1 -1
  55. package/build/consent/component.js +23 -9
  56. package/build/consent/component.js.map +1 -1
  57. package/build/consent/presence.d.ts +7 -1
  58. package/build/consent/presence.d.ts.map +1 -1
  59. package/build/consent/presence.js +8 -2
  60. package/build/consent/presence.js.map +1 -1
  61. package/build/consent/translate.d.ts +1 -1
  62. package/build/consent/translate.js +1 -1
  63. package/build/entrypoints.d.ts +7 -1
  64. package/build/entrypoints.d.ts.map +1 -1
  65. package/build/scheme/index.d.ts +3 -0
  66. package/build/scheme/index.d.ts.map +1 -0
  67. package/build/scheme/index.js +2 -0
  68. package/build/scheme/index.js.map +1 -0
  69. package/build/scheme/scheme.d.ts +44 -0
  70. package/build/scheme/scheme.d.ts.map +1 -0
  71. package/build/scheme/scheme.js +78 -0
  72. package/build/scheme/scheme.js.map +1 -0
  73. package/package.json +38 -29
  74. package/src/@/components/ui/sheet.tsx +142 -0
  75. package/src/components/footer/component.tsx +35 -7
  76. package/src/components/footer/types.ts +16 -1
  77. package/src/components/index.ts +1 -0
  78. package/src/components/nav/index.ts +1 -0
  79. package/src/components/nav/layout.tsx +86 -10
  80. package/src/components/nav/mobile.tsx +147 -0
  81. package/src/components/nav/types.ts +39 -1
  82. package/src/components/scheme/hook.ts +64 -0
  83. package/src/components/scheme/index.ts +4 -0
  84. package/src/components/scheme/toggle.tsx +43 -0
  85. package/src/components/scheme/types.ts +23 -0
  86. package/src/components/socket/i18n/fr.json +7 -0
  87. package/src/components/socket/i18n.ts +2 -0
  88. package/src/consent/component.tsx +38 -13
  89. package/src/consent/presence.ts +9 -3
  90. package/src/consent/translate.ts +1 -1
  91. package/src/scheme/index.ts +4 -0
  92. package/src/scheme/scheme.ts +86 -0
  93. package/tests/consent.spec.ts +47 -0
  94. package/tests/harness/mount.tsx +54 -2
  95. package/tests/nav.spec.ts +187 -0
  96. package/tests/scheme.spec.ts +149 -0
  97. package/zz-tw.d.mts +2 -0
  98. package/zz-tw.d.mts.map +1 -0
package/README.md CHANGED
@@ -11,14 +11,14 @@ but never started.
11
11
  ## Installation
12
12
 
13
13
  ```sh
14
- bun add @owlmeans/web-panel@^0.1.18-rc.51
14
+ bun add @owlmeans/web-panel@^0.1.18-rc.53
15
15
  ```
16
16
 
17
17
  Peer requirements (the consuming app provides these): `react`, `react-dom`,
18
18
  `react-hook-form`, `ajv`, `tailwindcss@^4`, `lucide-react`, `clsx`,
19
19
  `tailwind-merge`, `class-variance-authority`, plus the radix primitives
20
- listed in `peerDependencies` (`alert-dialog`, `label`, `navigation-menu`, `progress`, `separator`,
21
- `slot`). Also declare `ajv-formats` next to `ajv`: the form model imports it, but no dependency
20
+ listed in `peerDependencies` (`alert-dialog`, `dialog`, `label`, `navigation-menu`, `progress`,
21
+ `separator`, `slot`). Also declare `ajv-formats` next to `ajv`: the form model imports it, but no dependency
22
22
  section of this manifest lists it.
23
23
 
24
24
  ## Concepts
@@ -31,7 +31,8 @@ section of this manifest lists it.
31
31
  supplies the peers, its Tailwind theme tokens and an `@source` line for this package.
32
32
  - **Two-layer navigation** — the top menu lists sections and the side menu lists the active
33
33
  section's screens. A section holding a single screen renders no side menu. The model
34
- (`usePanelNav`, `PanelNav*`) is headless in `@owlmeans/client-panel`.
34
+ (`usePanelNav`, `PanelNav*`) is headless in `@owlmeans/client-panel`. With `mobileMenu`, a
35
+ narrow viewport gets both levels behind one menu button and a sheet instead.
35
36
  - **Style slots** — `className` (page root), `headerClassName` (sticky bar), `contentClassName`
36
37
  and `containerClassName` (width and padding of all three rows) are merged over their defaults
37
38
  with tailwind-merge, never substituted.
@@ -223,10 +224,12 @@ alias. Default keys are `nav.<section>` and `modules.<alias>`.
223
224
 
224
225
  | Component | Props |
225
226
  |---|---|
226
- | `NavLayout` | `nav`, `translate?`, `title?`, `home?` (brand target; defaults to the first section's first item), `actions?`, `footer?` (`PanelNavLink[]` renders the standard footer, a node replaces it), `headerClassName?`, `contentClassName?`, `containerClassName?`, `className?`, `style?` |
227
+ | `NavLayout` | `nav`, `translate?`, `title?`, `home?` (brand target; defaults to the first section's first item), `actions?`, `mobileMenu?` (below `md`, a menu button and a sheet replace the section menu and the screen strip; `actions` stay), `skipLinkLabel?` (the first-on-page skip link to `<main id="main">`; default `shell.skip` → "Skip to content"; `false` renders no link and no `#main`), `themeToggle?` (`true` or `{ labels? }`: the light/dark switcher in the footer's bottom row beside the credit; names via `shell.toLight` / `shell.toDark`), `footer?` (`PanelNavLink[]` renders the centred link row; a node renders as a full-width block above the credit), `headerClassName?`, `contentClassName?`, `containerClassName?`, `className?`, `style?` |
227
228
  | `TopNav` | `config`, `translate?`, `ariaLabel?`, `className?`, `style?` |
228
229
  | `SideNav` | the same, plus `variant?: 'side' \| 'bar'` |
229
- | `Footer` | `links?`, `translate?`, `containerClassName?`, `children?`, `className?`, `style?` — always renders the platform/owner credit line too, via `ShellCredit` |
230
+ | `MobileNav` | the same as `TopNav`; the trigger's name and the sheet title resolve `shell.menu` ("Menu"), its close button `shell.close` ("Close"); `className`/`style` land on the trigger |
231
+ | `Footer` | `links?`, `content?` (full-width block, `data-footer-content`), `translate?`, `containerClassName?`, `children?` (join the link row), `themeToggle?`, `className?`, `style?` — always renders the platform/owner credit line last, via `ShellCredit` |
232
+ | `ThemeToggle` | `labels?: { toLight?, toDark? }` (English defaults), `className?`, `style?` — a 44px `<button data-theme-toggle>` flipping the resolved scheme |
230
233
  | `ShellCredit`, `useShellCredit` | `className?` — "Powered by OwlMeans" plus the owner's copyright, resolved the same way the sign-in screen's credit is |
231
234
 
232
235
  ### 4. Forms, panels and status
@@ -331,8 +334,8 @@ The dialog's strings are the lib-tier `socket` namespace (`reload.title`, `reloa
331
334
 
332
335
  | Symbol | Kind | Purpose |
333
336
  |---|---|---|
334
- | `NavLayout`, `TopNav`, `SideNav`, `Footer`, `ShellCredit` | component | Navigation shell and its pieces, including the footer's platform/owner credit |
335
- | `NavLayoutProps`, `TopNavProps`, `SideNavProps`, `FooterProps`, `ShellCreditProps` | type | Their props |
337
+ | `NavLayout`, `TopNav`, `SideNav`, `MobileNav`, `Footer`, `ShellCredit` | component | Navigation shell and its pieces, including the narrow-viewport menu sheet and the footer's platform/owner credit |
338
+ | `NavLayoutProps`, `TopNavProps`, `SideNavProps`, `MobileNavProps`, `FooterProps`, `ShellCreditProps` | type | Their props |
336
339
  | `useShellCredit` | hook | Resolves the credit `ShellCredit` renders, without rendering it |
337
340
  | `Layout`, `LayoutProps` | component, type | Plain content wrapper |
338
341
  | `Form`, `WebFormProps` | component, type | Web form; `FormProps` plus `className` / `style` |
@@ -343,6 +346,8 @@ The dialog's strings are the lib-tier `socket` namespace (`reload.title`, `reloa
343
346
  | `BlockProps`, `TextProps`, `LinkProps`, `StatusProps`, `StyledProps`, `TextVariant` | type | Their props |
344
347
  | `ImageUploader`, `ImageUploaderProps` | component, type | Drop target with a `previewUrl` |
345
348
  | `Toaster` | component | `sonner` surface themed from app tokens; follows `.dark` on the document element |
349
+ | `ThemeToggle`, `ThemeToggleProps`, `ThemeToggleLabels` | component, type | The light/dark switcher |
350
+ | `useColorScheme()`, `ColorSchemeModel` | hook, type | `{ scheme, choice, setChoice }` — the resolved scheme, the stored choice (`null` = follow the OS), and the setter |
346
351
  | `SocketReloadDialog` | component | Blocking reload prompt for `'lost'` sockets |
347
352
  | `LoginScreen`, `LocalizedLoginScreen` | component | Identity-provider choice screen; the localized one binds `translate` to the app's resources |
348
353
  | `appendLoginScreen(ctx, setup?)`, `LoginScreenSetup` | function, type | Register the screen on the login service, with `Logo` and other `LoginScreenProps` |
@@ -386,12 +391,30 @@ relying party.
386
391
  `@owlmeans/web-panel/auth/entrypoints` exports `entrypoints`: `@owlmeans/client-auth/manager`'s
387
392
  bindings plus `@owlmeans/api-config-client`'s.
388
393
 
394
+ ### `@owlmeans/web-panel/scheme`
395
+
396
+ React-free — it imports nothing, so a Node build script can load it.
397
+
398
+ | Symbol | Kind | Purpose |
399
+ |---|---|---|
400
+ | `COLOR_SCHEME_KEY` | const | `'owlmeans:color-scheme'`, the `localStorage` key |
401
+ | `COLOR_SCHEME_EVENT` | const | The `window` event every change dispatches |
402
+ | `ColorSchemeChoice` | type | `'light' \| 'dark'` |
403
+ | `readColorScheme()` | function | The stored choice or `null` (storage errors swallowed) |
404
+ | `applyColorScheme(choice \| null)` | function | Sets the `light`/`dark` class on `<html>`, stores or clears the key, dispatches the event |
405
+ | `colorSchemeBootstrapScript()` | function | The inline head script that applies a stored choice before first paint |
406
+
407
+ The class contract: `.dark` = chosen dark, `.light` = chosen light, neither = follow the OS. A
408
+ consumer's CSS puts dark tokens under `.dark` AND under
409
+ `@media (prefers-color-scheme: dark) { :root:not(.light) { … } }`, and inlines
410
+ `colorSchemeBootstrapScript()` in the document head.
411
+
389
412
  ### `@owlmeans/web-panel/consent`
390
413
 
391
414
  | Symbol | Kind | Purpose |
392
415
  |---|---|---|
393
416
  | `PanelCookieConsent`, `PanelCookiePolicy` | component | `@owlmeans/web-consent` components bound to the app's language and translations |
394
- | `PanelConsentMenuWidget` | component | The cookie-preferences row for a host menu |
417
+ | `PanelConsentMenuWidget` | component | The cookie-preferences control for a host menu or footer ("Cookie settings" — pass `label` and `className`) |
395
418
  | `appendConsentWidgetService(ctx, alias?)`, `createConsentWidgetService` | function | Ref-counted presence service over a state resource |
396
419
  | `useConsentMenuPresence()`, `useConsentWidgetPresent()` | hook | Declare that a menu shows the row; read whether one does (hides the floating button) |
397
420
  | `CONSENT_WIDGET_SERVICE`, `CONSENT_WIDGET_STATE` | const | Service alias and state alias |
@@ -439,7 +462,17 @@ The public names match the MUI implementation; these props and types differ:
439
462
  - **`reloadDialog` is off by default.** The dialog renders nothing unless `cfg.socket.reloadDialog`
440
463
  is `true`.
441
464
  - **Run `useConsentMenuPresence()` from the menu's always-mounted component**, never from inside a
442
- dropdown row, which mounts only while the menu is open.
465
+ dropdown row, which mounts only while the menu is open. Without `appendConsentWidgetService` it
466
+ does nothing and `PanelCookieConsent` keeps its floating button.
467
+ - **Pass an application footer layout as a node `footer`.** It renders full-width above the credit;
468
+ an array renders the centred link row. Neither removes the credit.
469
+ - **A theme for `ThemeToggle` reads both classes.** Dark tokens under `.dark` only never follow
470
+ the OS; under the media query only, they ignore an explicit light choice. Inline the head
471
+ bootstrap or a stored dark choice flashes light for a frame.
472
+ - **The shell already renders the skip link and `<main id="main">`.** A screen never renders a
473
+ second `<main>` or `#main`; an app with its own skip link passes `skipLinkLabel={false}`.
474
+ - **`mobileMenu` is opt-in.** Without it a narrow viewport keeps the section menu and the screen
475
+ strip; with it both move into the sheet.
443
476
  - **Never import `@/…` from package code or vendor its primitives in the app.** The `@` alias
444
477
  belongs to the consumer.
445
478
  - **Every override of a packaged string covers all seven languages** (`SUPPORTED_LNGS`).
@@ -463,7 +496,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
463
496
  your project's skill store (`.agents/skills/`):
464
497
 
465
498
  ```sh
466
- npx @owlmeans/agent-skills@^0.1.18-rc.30
499
+ npx @owlmeans/agent-skills@^0.1.18-rc.31
467
500
  ```
468
501
 
469
502
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/web-panel",
4
- "version": "0.1.18-rc.51",
5
- "generatedAt": "2026-09-19T13:42:56.279Z",
4
+ "version": "0.1.18-rc.53",
5
+ "generatedAt": "2026-09-22T09:20:40.388Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -8,7 +8,7 @@ user-invocable: false
8
8
  # @owlmeans/web-panel
9
9
 
10
10
  **Layer:** Web (React)
11
- **Install:** `"@owlmeans/web-panel": "^0.1.18-rc.51"` in `dependencies`
11
+ **Install:** `"@owlmeans/web-panel": "^0.1.18-rc.53"` in `dependencies`
12
12
 
13
13
  ## Key Exports
14
14
 
@@ -16,10 +16,10 @@ user-invocable: false
16
16
  |--------|-------------|
17
17
  | `makeContext<C, T>(cfg)` | Base web context factory (shadcn/Radix + default OwlMeans router) |
18
18
  | `NavLayout` | The application shell — header, section menu, screen menu, content, footer |
19
- | `TopNav` / `SideNav` / `Footer` | The shell's pieces, mountable on their own |
19
+ | `TopNav` / `SideNav` / `MobileNav` / `Footer` | The shell's pieces, mountable on their own |
20
20
  | `Toaster` | The application's toast surface — mounted once, in the layout |
21
- | `SocketReloadDialog` / `useSocketStatus` | The global "reload the page" prompt (opt in via `cfg.socket.reloadDialog`) `PanelApp` mounts automatically, and the hook it reads — see below |
22
- | `PanelMenu` | The dropdown menu, described as data — items, arbitrary widget rows, labels, separators and one level of submenu. `PanelMenuEntry` / `PanelMenuEntryKind` / `PanelMenuProps` come with it |
21
+ | `ThemeToggle` / `useColorScheme` | The light/dark switcher (`ThemeToggleProps`, `ThemeToggleLabels`) and the hook behind it (`ColorSchemeModel`) — see *Light and dark* below |
22
+ | `SocketReloadDialog` | The global "reload the page" prompt (opt in via `cfg.socket.reloadDialog`) `PanelApp` mounts automatically — see below. The hook it reads, `useSocketStatus`, is imported from `@owlmeans/client-socket`, not from this package |
23
23
  | `Link` | An `<a>` addressing an entrypoint alias (or a literal `src`), with the label taken from i18n |
24
24
  | `LoginScreen` / `LocalizedLoginScreen` / `appendLoginScreen` | The identity-provider choice screen — see `login-methods` |
25
25
  | `render(context, opts?)` | Mounts the tree inside `PanelApp`, with the browser language detector installed on the i18n instance. `opts` is `RenderOptions` plus `rootClassName` |
@@ -30,13 +30,11 @@ user-invocable: false
30
30
  | `Form` / `TextInput` / `Button` / `SubmitButton` / `ButtonSelector` | The web form family. `Form` builds its own `useForm` + `ajvResolver` and borrows only `FormContext` and `schemaToFormDefault` from `@owlmeans/client-panel`; `TextInput` drives `react-hook-form`'s `Controller` directly, and the buttons call `handleSubmit` directly — none of them wraps `ClientForm`, `InputCtrl` or `ActionCtrl` |
31
31
  | `ImageUploader` / `Layout` | The uploader in a drop target with a preview, and a plain content wrapper (the shell is `NavLayout`) |
32
32
  | `scalingToStyles(h, v)` | `BlockScaling` → the width/height utility classes the panels share |
33
- | `cn(...inputs)` | The class-name merger the components are written against — an app never re-declares it |
34
- | `useIsMobile()` / `MOBILE_BREAKPOINT` | Viewport narrower than Tailwind's `md` (768), matched with `matchMedia` |
35
33
  | `useBreakPoint()` | The current Tailwind breakpoint name, tracked on `resize` (`lg` when there is no `window`) |
36
34
  | `useMapBreakpoint(map, def?, breakpoint?)` | The `map` entry for the current breakpoint (or for the `breakpoint` passed), falling back to `def`. It **throws a `SyntaxError`** when neither yields a value, so give it a `def` or cover every breakpoint |
37
35
  | Re-exports from `@owlmeans/client-panel` | Cross-platform panel primitives, incl. `usePanelNav` and the `PanelNav*` types |
38
- | Re-exports from `@owlmeans/client` / `@owlmeans/client-entrypoint` / `@owlmeans/route` | `bind`, `bindAll`, `bindScreen`, `handler`, `provideRequest`, `stab`, `route`, `croute`, `frontend`, `guard`, `useNavigate`, `useEntrypoint`, `useValue` |
39
- | Re-exports from the surrounding layers | `config`, `service`, `addWebService`, `AppType` / `HOME` / `ROOT` / `BASE` / `GUEST`, `DISPATCHER`, `CAUTHEN_FLOW_ENTER`, `DAUTH_GUARD`, `setupExternalAuthentication`, `Dispatcher`, `appendWebAuthService`, `flow` / `configureFlows` / `useFlow` / `FLOW_PARAM` / `SERVICE_PARAM`, `useI18n*` / `useLanguage` / `composePrefix`, `addI18nApp` / `addI18nLib` / `SUPPORTED_LNGS` |
36
+ | Re-exports from `@owlmeans/client` / `@owlmeans/client-entrypoint` / `@owlmeans/route` | `bind`, `bindAll`, `bindScreen`, `handler`, `provideRequest`, `stab`, `route`, `croute`, `frontend`, `useNavigate`, `useEntrypoint`, `useValue` |
37
+ | Re-exports from the surrounding layers | `config`, `service`, `addWebService`, `AppType` / `HOME` / `ROOT` / `BASE` / `GUEST`, `DISPATCHER`, `CAUTHEN_FLOW_ENTER`, `DAUTH_GUARD`, `bindExternalAuthentication`, `Dispatcher`, `appendWebAuthService`, `flow` / `configureFlows` / `useFlow` / `FLOW_PARAM` / `SERVICE_PARAM`, `useI18n*` / `useLanguage` / `composePrefix`, `addI18nApp` / `addI18nLib` / `SUPPORTED_LNGS` |
40
38
 
41
39
  ## Subpath Exports
42
40
 
@@ -45,7 +43,9 @@ user-invocable: false
45
43
  - `./consent` — the cookie consent dialog and policy, bound to this app's i18n, plus a menu-row
46
44
  widget and a presence service so a host's own collapsed menu can take over the floating button's
47
45
  job
48
- - `./jobs` — `JobProgress`, `JobStatus`, `useJobToasts` over `@owlmeans/queue` records
46
+ - `./scheme` — the React-free colour-scheme half: `COLOR_SCHEME_KEY`, `COLOR_SCHEME_EVENT`,
47
+ `ColorSchemeChoice`, `readColorScheme`, `applyColorScheme`, `colorSchemeBootstrapScript` — safe
48
+ to import from a Node build script
49
49
 
50
50
  ## Usage
51
51
 
@@ -108,12 +108,59 @@ header. Both render the same items; only one is visible at a time.
108
108
 
109
109
  | Component | Props |
110
110
  |---|---|
111
- | `NavLayout` | `nav: PanelNavConfig`, `translate?`, `title?: ReactNode`, `home?: string` (brand target — defaults to the first section's first item), `actions?: ReactNode`, `footer?: PanelNavLink[] \| ReactNode`, `headerClassName?`, `contentClassName?`, `containerClassName?`, `className?`, `style?` |
111
+ | `NavLayout` | `nav: PanelNavConfig`, `translate?`, `title?: ReactNode`, `home?: string` (brand target — defaults to the first section's first item), `actions?: ReactNode`, `mobileMenu?: boolean`, `skipLinkLabel?: string \| false`, `themeToggle?: boolean \| { labels? }`, `footer?: PanelNavLink[] \| ReactNode`, `headerClassName?`, `contentClassName?`, `containerClassName?`, `className?`, `style?` |
112
112
  | `TopNav` | `config: PanelNavConfig`, `translate?`, `ariaLabel?`, `className?`, `style?` |
113
113
  | `SideNav` | the same, plus `variant?: 'side' \| 'bar'` |
114
- | `Footer` | `links?: PanelNavLink[]`, `translate?`, `containerClassName?` (the shell's rhythm, so the footer row lines up with the header and content), `children?`, `className?`, `style?` |
114
+ | `MobileNav` | the same as `TopNav`; `ariaLabel` names the landmark inside the sheet, `className`/`style` land on the trigger button |
115
+ | `Footer` | `links?: PanelNavLink[]`, `content?: ReactNode` (a full-width block — see below), `translate?`, `containerClassName?` (the shell's rhythm, so the footer row lines up with the header and content), `children?` (joins the centred link row), `themeToggle?: boolean \| { labels? }` (the switcher beside the credit), `className?`, `style?` |
115
116
  | `ShellCredit` / `useShellCredit` | `className?`; the platform/owner credit line `Footer` always renders — see below |
116
117
 
118
+ **The narrow-viewport menu is opt-in: `mobileMenu`.** Off (the default) the shell is exactly the
119
+ two-layer layout above at every width. On, below `md` (768px) the section menu is hidden
120
+ (`hidden md:flex`) and `MobileNav` puts a menu button — accessible name "Menu", a 44px target, a
121
+ solid focus ring — at the end of the actions row, so `actions` ("Get started", sign-in) stay in
122
+ the header at every width. The button opens a shadcn `Sheet` from the right listing every section:
123
+ a multi-screen section as its name over links to its screens, a single-screen section as one link
124
+ carrying the section's own label, so no destination appears twice. Labels, keys and `translate`
125
+ are the menus' own (`nav.<section>`, `modules.<alias>`); the button's name, the sheet title and
126
+ its close button resolve `shell.menu` / `shell.close` — outside the `nav.` family, because an
127
+ app may have a section called `menu`. Rules the component owns:
128
+
129
+ - **The screen strip is not mounted when `mobileMenu` is on.** The sheet already lists the active
130
+ section's screens; a strip under the header would be a second menu for the same level. The side
131
+ column (`hidden md:block`) is untouched — wide viewports are the same shell either way.
132
+ - **An entry navigates AND closes from its own click handler.** It is an `<a href>` (focusable,
133
+ openable in a new tab) whose `onClick` calls `preventDefault()` for in-app navigation — which a
134
+ dialog never notices, so a sheet left to close itself stays open over the screen it just
135
+ navigated to.
136
+ - **The sheet states both halves of its surface** (`bg-background text-foreground`): it is
137
+ portalled to the document body and inherits nothing from the shell.
138
+ - Pinned by `nav.spec.ts` → "the narrow-viewport menu", at 375px and 1280px, with and without
139
+ the prop (`?mobileMenu=1` in the harness).
140
+
141
+ **The skip link comes first, and `main` is its target.** `NavLayout` renders
142
+ `<a href="#main" data-skip-link>` as the FIRST child of its root — before `<header>`, never inside
143
+ it, because `header > div` (first) is the shared-rhythm row the layout pins locate. It is `sr-only`
144
+ until focused, then a pill fixed in the top-left corner (`bg-foreground text-background`,
145
+ `rounded-full`, `z-50`, the 3px `--ring` focus ring); its padding is `focus:`-only, since a plain
146
+ `px-*` sorts after `sr-only` and leaves a padded box behind the clip. Activating it focuses
147
+ `<main id="main" tabIndex={-1}>` by hand (`preventDefault` + `focus()`), because a fragment
148
+ navigation changes the location and the router reads that as navigation. The label is
149
+ `skipLinkLabel`, defaulting to `translate('shell.skip', 'Skip to content')`. `skipLinkLabel={false}`
150
+ renders no link AND leaves `main` without the id: an application rendering its own skip link owns
151
+ its own `#main`, and no two elements may share it. A screen never renders a second `<main>`.
152
+ Pinned by `nav.spec.ts` → "the skip link" (Tab from the page start, Enter lands on `main#main`,
153
+ `?skip=off`).
154
+
155
+ **A node `footer` is a full-width block, never a replacement.** An array renders the centred link
156
+ row. A node — an application's own footer layout: a brand, a description, link columns — goes to
157
+ `Footer`'s `content` and renders as a `w-full self-stretch text-start` block (`data-footer-content`)
158
+ inside the footer's container, above the credit. The container keeps `items-center` for the link
159
+ row and the credit; only the block stretches. It used to land INSIDE the centred link row, where
160
+ it shrink-wrapped to its content in the middle of the page. `ShellCredit` stays the last thing the
161
+ footer renders in every case — the container's own last child, or, with `themeToggle`, first in the
162
+ bottom row `[data-footer-bottom]` with the switcher after it. Pinned by `nav.spec.ts` → "a node footer" (`?footer=node`).
163
+
117
164
  **The style slots are REGIONS, and each region is its own SURFACE.** `className` is the root —
118
165
  the full-height page *behind* the header, side menu and footer. `headerClassName` is the sticky
119
166
  top bar. `contentClassName` is the content area. `containerClassName` is width and padding for
@@ -144,7 +191,7 @@ own background renders on top of the backdrop. Pinned by `nav.spec.ts` → "a br
144
191
  still leaves the backdrop layer opaque".
145
192
 
146
193
  **`Footer` always renders the platform/owner credit line, and there is no prop that hides it.**
147
- Below the links (or on its own, when a layout passes no `footer` at all — `NavLayout` renders
194
+ Below the content block and the links (or on its own, when a layout passes no `footer` at all — `NavLayout` renders
148
195
  `<Footer>` unconditionally now), `ShellCredit` shows "Powered by OwlMeans" and the owner's own
149
196
  copyright notice, resolved from `security.auth.login.credit` via `resolveCredit`
150
197
  (`@owlmeans/client-auth/login`) — the SAME resolver and the SAME config the sign-in screen's
@@ -175,7 +222,8 @@ to follow.
175
222
  `--foreground` (active section link), `--muted-foreground` (resting section links),
176
223
  `--accent`/`--accent-foreground` (active side-menu item), `--border` (the header, side-menu and
177
224
  footer rules) and `--primary` — the footer renders its entries through `Link`, which paints
178
- `text-primary`. It reads **no `--sidebar*` variable at all**. So `--muted-foreground` is not merely
225
+ `text-primary`; `mobileMenu` adds `--ring` (the menu button's and the sheet entries' focus rings)
226
+ and a `bg-black/50` overlay. It reads **no `--sidebar*` variable at all**. So `--muted-foreground` is not merely
179
227
  the text colour of the `--muted` surface — it is secondary text sitting directly on `--background`,
180
228
  and a theme that lightens it to suit a dark muted panel loses its top menu.
181
229
 
@@ -197,72 +245,63 @@ Rules that make the shell behave:
197
245
  - **A parent route needs a `default: true` child.** A frontend entrypoint that has children but no
198
246
  child declared `default: true` renders blank at its own path — give a grouping screen an index
199
247
  child at `'/'`.
200
- - **Vendor `navigation-menu`.** `SideNav` builds on the package-local `Button`; `TopNav` uses the
201
- package-local shadcn `navigation-menu` primitive — see the package-boundary rule below.
248
+ - **Vendor `navigation-menu` and `sheet`.** `SideNav` builds on the package-local `Button`; `TopNav`
249
+ uses the package-local shadcn `navigation-menu` primitive and `MobileNav` the package-local
250
+ `sheet` (over `@radix-ui/react-dialog`, a peer) — see the package-boundary rule below.
202
251
 
203
- ### Menus — `PanelMenu`
252
+ ### Light and dark — `ThemeToggle`, `useColorScheme`, `./scheme`
204
253
 
205
- One dropdown, described as an ENTRY LIST rather than as children. Every kind has its own focus,
206
- keyboard and close-on-select behaviour, so children would silently lose all three; and the same
207
- description then serves a collapsed toolbar, a header overflow and a mobile shell without any of
208
- them re-deriving it.
254
+ The standard switcher, with no theme library behind it. `NavLayout themeToggle` (or `Footer
255
+ themeToggle`) puts it in the footer's bottom row beside the credit, whatever `footer` is; absent or
256
+ `false`, the footer's DOM is what it is without one. It is also mountable on its own.
209
257
 
210
- ```tsx
211
- import { PanelMenu, PanelMenuEntryKind } from '@owlmeans/web-panel'
212
- import type { PanelMenuEntry } from '@owlmeans/web-panel'
213
-
214
- const entries: PanelMenuEntry[] = [
215
- { kind: PanelMenuEntryKind.Widget, key: 'credits', render: <AccountCredits /> },
216
- { kind: PanelMenuEntryKind.Separator, key: 'sep' },
217
- { kind: PanelMenuEntryKind.Label, key: 'app', label: 'MyApp' },
218
- { kind: PanelMenuEntryKind.Item, key: 'home', alias: HOME, Icon: House },
219
- { kind: PanelMenuEntryKind.Item, key: 'docs', href: DOCS, open: true, hint: <ExternalLink className="size-3.5" /> },
220
- { kind: PanelMenuEntryKind.Sub, key: 'lang', label: 'Language', hint: 'EN', entries: languages },
221
- ]
258
+ **The contract is two classes on `<html>`.** `dark` = the visitor chose dark, `light` = chose light,
259
+ neither = follow the operating system. The choice is stored under `COLOR_SCHEME_KEY`
260
+ (`owlmeans:color-scheme`, `localStorage`, every access in `try`). `light` exists as a class of its
261
+ own because a visitor choosing light on a dark system has no other way to say so to a stylesheet
262
+ that paints dark under `prefers-color-scheme: dark`. So a consumer's CSS must read BOTH:
222
263
 
223
- <PanelMenu entries={entries} translate={t} triggerLabel="Menu" indicator={dot} align="end" />
264
+ ```css
265
+ :root { color-scheme: light; /* light tokens */ }
266
+ .dark { color-scheme: dark; /* dark tokens */ }
267
+ @media (prefers-color-scheme: dark) {
268
+ :root:not(.light) { color-scheme: dark; /* the same dark tokens */ }
269
+ }
270
+ /* Tailwind v4: a `dark:` variant that follows the same rule */
271
+ @custom-variant dark {
272
+ &:where(.dark, .dark *) { @slot; }
273
+ @media (prefers-color-scheme: dark) { &:where(:root:not(.light), :root:not(.light) *) { @slot; } }
274
+ }
224
275
  ```
225
276
 
226
- | Kind | What it is |
227
- |---|---|
228
- | `Item` | A focusable row that closes the menu. Exactly one of `alias` (an entrypoint) or `href`, the same union `PanelNavLink` uses; neither makes it a pure `onSelect` action. Also `Icon`, `hint` (right-aligned), `active`, `disabled`, `variant` |
229
- | `Widget` | Arbitrary content as a plain ROW — see below |
230
- | `Label` | A section heading |
231
- | `Separator` | A rule between blocks |
232
- | `Sub` | One nested level. A `Sub` inside a `Sub` renders nothing — a dropdown that nests further is a navigation tree, not this |
233
-
234
- Rules the component owns, each of which was a real failure:
235
-
236
- - **A `Widget` is a row, never a `DropdownMenuItem`.** An item takes both the focus and the
237
- activation from the controls inside it: Radix's roving tabindex swallows the inner button's
238
- keyboard access, and `onSelect` fires on any click that lands on the row — so a "Top up" button
239
- inside an item dismisses the menu before its own handler is observed. The row takes no roving
240
- focus and does not close the menu; the widget's own buttons are the click targets.
241
- - **An in-app link cannot use `onSelect`.** The anchor must call `preventDefault()` or the browser
242
- performs a full page load, and Radix composes its click handler with `checkForDefaultPrevented`
243
- — so preventing the default also cancels `onSelect`, and with it the automatic close. `PanelMenu`
244
- therefore navigates and closes explicitly from the anchor's own handler, and keeps its own open
245
- state for that (a caller's `open`/`onOpenChange` still wins).
246
- - **The href resolves synchronously.** `Link` asks `entrypoint.url()` and settles a frame later,
247
- which is fine for a link already on screen; a menu's content mounts at the moment it opens, so an
248
- href that arrives afterwards is missing exactly while the row is being read. `PanelMenu` uses
249
- `entrypoint.path()` — a lookup — and answers `undefined` for a path carrying route parameters.
250
- Never drop the `href`: an `<a>` without one is not focusable, does not answer the keyboard,
251
- cannot be opened in a new tab, and does not carry the `link` role.
252
- - **`hidden` takes the separators it orphans with it.** A caller composes the menu from optional
253
- blocks; filtering the entries alone leaves a leading rule, a doubled rule, or one under the last
254
- item. Normalisation is the whole reason the entries are data.
255
- - **A `Widget` entry's own mount is not the menu's mount.** Radix's `DropdownMenuContent` (which
256
- this and every entries-as-data reimplementation of it builds on) only mounts its children while
257
- the menu is actually OPEN — so a widget whose side effect must persist for as long as the
258
- TRIGGER is on screen (not merely while a visitor happens to have it open) cannot run that effect
259
- from inside the entry. Run it from the caller's own always-mounted component instead — see
260
- `useConsentMenuPresence` under `./consent` below for the worked example: a naive version that
261
- announced presence from inside its own menu row hid the cookie-consent floating button only
262
- while the dropdown happened to be open, and showed it again the instant it closed.
263
- - **`translate` is a prop**, defaulting to `defaultNavTranslate` — same reason as the nav shell.
264
- - `indicator` is a slot on the trigger's corner (a notification dot, a count), not a `tone` enum:
265
- what deserves attention is the application's judgement.
277
+ A theme with dark tokens under `.dark` alone never follows the OS; one with them under the media
278
+ query alone ignores an explicit light choice.
279
+
280
+ **The head bootstrap goes before any stylesheet paints.** `colorSchemeBootstrapScript()` returns a
281
+ self-contained inline script that puts the stored choice's class on `<html>` before first paint —
282
+ the only moment it can; a component doing it after mount shows a visitor who chose dark one frame
283
+ of the light page. A build step inlines it into `index.html` (import it from
284
+ `@owlmeans/web-panel/scheme` — that module imports nothing, React included) and a CSP that hashes
285
+ inline scripts must hash it. Without the bootstrap, `useColorScheme` re-applies the stored class on
286
+ mount: correct, one frame late.
287
+
288
+ Rules the pieces own:
289
+
290
+ - **The toggle flips the RESOLVED scheme** (`choice ?? system`), never the stored choice alone: on
291
+ a dark system with nothing stored the first press means light. The icon is the scheme the page
292
+ is in (sun while light, moon while dark), chosen in script, not by a `dark:` variant.
293
+ - **The accessible name says what pressing does** — `labels.toLight` / `labels.toDark`, English
294
+ defaults; `NavLayout` resolves them through `translate` (`shell.toLight`, `shell.toDark`).
295
+ - **One choice per document.** `applyColorScheme(choice | null)` sets the class, stores or clears
296
+ the key and dispatches `COLOR_SCHEME_EVENT`, so every mounted `useColorScheme` agrees at once;
297
+ another tab's change arrives through `storage`, and an OS switch through `matchMedia` while
298
+ nothing is stored. `setChoice(null)` hands the page back to the OS.
299
+ - **Look:** a 44px round target, `text-muted-foreground` → `hover:text-foreground`, the 3px
300
+ `--ring` focus ring, no fill, border, gradient or shadow.
301
+
302
+ Pinned by `tests/scheme.spec.ts` (`?themeToggle=1`): the module's imports, the bootstrap string,
303
+ the class flip and persistence, the resolved flip under an emulated dark system, a node footer,
304
+ and the absent case.
266
305
 
267
306
  ### Toasts — `Toaster`
268
307
 
@@ -310,8 +349,8 @@ path (see the `client-socket` skill), so the only way out is the reload. Strings
310
349
  (`useI18nLib('socket', 'reload')`), 7 languages, under `src/components/socket/i18n/`.
311
350
 
312
351
  Leave `cfg.socket.reloadDialog` unset (or `false`) for an app that would rather show its own
313
- inline "reconnecting…" state — `useSocketStatus()` is exported for that, independent of the
314
- dialog.
352
+ inline "reconnecting…" state — `useSocketStatus()` from `@owlmeans/client-socket` serves that,
353
+ independent of the dialog.
315
354
 
316
355
  ### Links — `Link`
317
356
 
@@ -372,37 +411,6 @@ import { Form, TextInput, SubmitButton, Button } from '@owlmeans/web-panel'
372
411
  `contained` and the rest `outlined`, calling `onSelect(option)`. `name` prefixes each option's
373
412
  label key as `<name>.<option>`.
374
413
 
375
- ## Subpath: `./jobs`
376
-
377
- Three presentational pieces for a queue job, over `JobRecord` from `@owlmeans/queue`. They take
378
- records — `@owlmeans/client-job`'s `useJobs()` maps straight onto them — and hold no store, no
379
- socket and no strings of their own.
380
-
381
- ```tsx
382
- import { JobProgress, JobStatus, useJobToasts } from '@owlmeans/web-panel/jobs'
383
-
384
- const jobs = useJobs().map(model => model.record)
385
- useJobToasts(jobs)
386
-
387
- <JobStatus job={job} labels={{ [JobState.Active]: t('jobs.running') }} />
388
- <JobProgress job={job} />
389
- ```
390
-
391
- | Export | Description |
392
- |---|---|
393
- | `JobProgress` | The shadcn `Progress` bar. `job.progress` is read as a number, `{ percent }` or `{ done, total }`; anything else animates INDETERMINATE, because zero and "the processor never called `progress()`" look identical otherwise |
394
- | `JobStatus` | The state pill. `data-state` carries the raw state, so a test never keys on the wording |
395
- | `jobProgressValue(job)` | The percentage the bar shows, or `undefined` |
396
- | `useJobToasts(jobs, opts?)` | One toast per job the first time it settles, on the `Toaster` the layout already mounts |
397
-
398
- - **No packaged wording.** The states are broker vocabulary; the sentence an app wants for them
399
- ("Queued", "Rendering", "Ready") is its own copy in its own namespace, so `JobStatus` takes a
400
- `labels` map and otherwise renders the raw state — data, not an untranslated string.
401
- - **`useJobToasts` never toasts on its first pass.** A screen opening onto a store seeded with
402
- yesterday's finished jobs would fire a stack of them at once, so everything already settled at
403
- mount is recorded as announced and only what settles afterwards is reported.
404
- - It needs the same single `Toaster` as everything else — see above.
405
-
406
414
  ## Subpath: `./consent`
407
415
 
408
416
  `PanelCookieConsent` and `PanelCookiePolicy` — `@owlmeans/web-consent`'s components bound to this
@@ -424,19 +432,24 @@ import { appendConsentWidgetService } from '@owlmeans/web-panel/consent'
424
432
  appendConsentWidgetService<C, T>(context)
425
433
  ```
426
434
 
427
- Then, from the menu's own **always-mounted** shell component (never from inside a lazily-rendered
428
- row — see the `PanelMenu` rule above):
435
+ Then call `useConsentMenuPresence()` from the host menu's own **always-mounted** shell component,
436
+ never from inside the row. A dropdown's content (Radix `DropdownMenuContent` and most headless
437
+ menu content primitives) mounts its children only while the menu is OPEN, so presence announced
438
+ from inside the row hid the floating button only while the dropdown happened to be open, and
439
+ showed it again the instant it closed. The row is also not a `DropdownMenuItem`: an item takes the
440
+ inner button's focus and closes the menu on the click before the button's own handler runs.
429
441
 
430
442
  ```tsx
431
443
  import { PanelConsentMenuWidget, useConsentMenuPresence } from '@owlmeans/web-panel/consent'
432
444
 
433
445
  const MyMenu: FC = () => {
434
446
  useConsentMenuPresence() // declares the row reachable for as long as THIS component is mounted
435
- const entries = [
436
- { kind: PanelMenuEntryKind.Widget, key: 'cookie', render: <PanelConsentMenuWidget /> },
437
- // ...
438
- ]
439
- return <PanelMenu entries={entries} ... />
447
+ return <DropdownMenu>
448
+ <DropdownMenuTrigger>…</DropdownMenuTrigger>
449
+ <DropdownMenuContent>
450
+ <div className="px-2 py-1.5"><PanelConsentMenuWidget /></div> {/* a plain row, not an item */}
451
+ </DropdownMenuContent>
452
+ </DropdownMenu>
440
453
  }
441
454
  ```
442
455
 
@@ -448,17 +461,48 @@ rather than a boolean latch, because more than one menu shell can be mounted for
448
461
  a layout transition (a stale header still showing its own collapsed menu while a new screen's own
449
462
  menu has already mounted) and because React 18 StrictMode double-invokes mount/cleanup in dev.
450
463
 
464
+ **The pair a consumer imports** — the dialog and the control that takes over its floating button:
465
+
466
+ | Job | Import from `@owlmeans/web-panel/consent` | Where it goes |
467
+ |---|---|---|
468
+ | The consent dialog (and its floating re-open button) | `PanelCookieConsent` | Beside the router — a `PanelApp` child — once |
469
+ | The "Cookie settings" control in a footer or menu | `PanelConsentMenuWidget` (`label`, `className`, `onSelect?` — defaults to `openConsent('reopen')`) | Inside the host's own footer/menu |
470
+ | Hiding the floating button while that control is reachable | `useConsentMenuPresence()` | Called by the always-mounted component that renders the control |
471
+ | Registering the presence service | `appendConsentWidgetService(context)` | The app's `context.ts` |
472
+
473
+ A footer is the simplest host, because it is always mounted — the control and the presence call
474
+ live in one component:
475
+
476
+ ```tsx
477
+ const CookieSettings: FC = () => {
478
+ useConsentMenuPresence()
479
+ return <PanelConsentMenuWidget label={t('footer.cookies')} className="w-auto text-muted-foreground" />
480
+ }
481
+ ```
482
+
483
+ The widget renders a `<button data-consent-menu-widget>` styled as a menu row (`w-full`,
484
+ `text-popover-foreground`, `hover:bg-accent`); a footer passes `className` to restyle it (merged
485
+ through `cn`) and its own `label` — the packaged default reads "Cookie preferences".
486
+
487
+ **The presence service is optional, and nothing throws without it.** `PanelCookieConsent` checks
488
+ the context for `consentWidget` and, when `appendConsentWidgetService` never ran, renders the plain
489
+ bound dialog with its floating button; `useConsentMenuPresence()` then has nothing to claim and
490
+ does nothing. The presence read itself (`useConsentWidgetPresent`) still needs the service — its
491
+ state resource is registered by it — so any other caller checks first. A dialog that read an
492
+ unregistered state resource threw inside render ("Resource consent-widget-presence not found") and
493
+ blanked the whole application. Pinned by `tests/consent.spec.ts` (`?consent=bare` / `?consent=menu`).
494
+
451
495
  ## Consumer setup — package boundary and Tailwind
452
496
 
453
497
  `web-panel` ships its shadcn primitives and `cn` helper as private implementation files under its
454
498
  own `build/@/` tree. Package source imports them only through relative specifiers; it must never
455
499
  emit an absolute `@/…` import, because that alias belongs to the consuming application and makes a
456
- fresh installation depend on unrelated files. Consumers import the public `cn` export when needed
457
- and do not vendor this package's UI primitives.
500
+ fresh installation depend on unrelated files. `cn` is not exported: a consumer keeps its own
501
+ class-name helper and never vendors or imports this package's UI primitives.
458
502
 
459
- Consumers still supply the package's peer dependencies: the Radix primitives (`label`,
460
- `navigation-menu`, `progress`, `separator`, `slot`) plus React, Tailwind and the usual utility
461
- libraries. A consumer may have its own shadcn `@` alias, but it is unrelated to this package.
503
+ Consumers still supply the package's peer dependencies: the Radix primitives (`alert-dialog`,
504
+ `dialog`, `label`, `navigation-menu`, `progress`, `separator`, `slot`) plus React, Tailwind and the
505
+ usual utility libraries. A consumer may have its own shadcn `@` alias, but it is unrelated to this package.
462
506
 
463
507
  Then point Tailwind at the installed package's **`src`** directory. Its oxide scanner reads the CSS root
464
508
  plus `@source` directives only, and excludes `node_modules` — so classes that exist **only** inside
@@ -478,11 +522,10 @@ in a linked workspace, so it is the reliable scan target in both modes.
478
522
 
479
523
  - `@owlmeans/web-client`, `@owlmeans/client-panel`, `@owlmeans/client-i18n`, `@owlmeans/web-router`
480
524
  - `@owlmeans/client-socket` — `appendSocketStatus`, `useSocketStatus`, behind `SocketReloadDialog`
481
- - `@owlmeans/queue` — `JobRecord` / `JobState`, read by the `./jobs` subpath
482
525
  - Peers (app-provided): `react`, `react-dom`, `react-hook-form`, `tailwindcss`, `tailwind-merge`,
483
526
  `clsx`, `class-variance-authority`, `lucide-react`, `ajv`, and the `@radix-ui/react-*` primitives
484
- (`alert-dialog`, `label`, `navigation-menu`, `progress`, `separator`, `slot`). No MUI, no
485
- react-router.
527
+ (`alert-dialog`, `dialog`, `label`, `navigation-menu`, `progress`, `separator`, `slot`). No MUI,
528
+ no react-router.
486
529
  - `ajv-formats` is imported at module scope by the form model but is declared in no dependency
487
530
  section of the manifest, which lists `ajv` alone. An install that does not otherwise pull it in
488
531
  fails at import time, so declare `ajv-formats` next to `ajv` in the consuming application.
@@ -0,0 +1,15 @@
1
+ import * as React from 'react';
2
+ import * as SheetPrimitive from '@radix-ui/react-dialog';
3
+ declare function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>): React.JSX.Element;
4
+ declare function SheetTrigger({ ...props }: React.ComponentProps<typeof SheetPrimitive.Trigger>): React.JSX.Element;
5
+ declare function SheetClose({ ...props }: React.ComponentProps<typeof SheetPrimitive.Close>): React.JSX.Element;
6
+ declare function SheetContent({ className, children, side, showCloseButton, ...props }: React.ComponentProps<typeof SheetPrimitive.Content> & {
7
+ side?: 'top' | 'right' | 'bottom' | 'left';
8
+ showCloseButton?: boolean;
9
+ }): React.JSX.Element;
10
+ declare function SheetHeader({ className, ...props }: React.ComponentProps<'div'>): React.JSX.Element;
11
+ declare function SheetFooter({ className, ...props }: React.ComponentProps<'div'>): React.JSX.Element;
12
+ declare function SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>): React.JSX.Element;
13
+ declare function SheetDescription({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Description>): React.JSX.Element;
14
+ export { Sheet, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription, };
15
+ //# sourceMappingURL=sheet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sheet.d.ts","sourceRoot":"","sources":["../../../../src/@/components/ui/sheet.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,cAAc,MAAM,wBAAwB,CAAA;AAKxD,iBAAS,KAAK,CAAC,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,cAAc,CAAC,IAAI,CAAC,qBAE5E;AAED,iBAAS,YAAY,CAAC,EACpB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,cAAc,CAAC,OAAO,CAAC,qBAErD;AAED,iBAAS,UAAU,CAAC,EAClB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,cAAc,CAAC,KAAK,CAAC,qBAEnD;AAwBD,iBAAS,YAAY,CAAC,EACpB,SAAS,EACT,QAAQ,EACR,IAAc,EACd,eAAsB,EACtB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,cAAc,CAAC,OAAO,CAAC,GAAG;IACvD,IAAI,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAA;IAC1C,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B,qBA8BA;AAED,iBAAS,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAQxE;AAED,iBAAS,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAQxE;AAED,iBAAS,UAAU,CAAC,EAClB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,cAAc,CAAC,KAAK,CAAC,qBAQnD;AAED,iBAAS,gBAAgB,CAAC,EACxB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,cAAc,CAAC,WAAW,CAAC,qBAQzD;AAED,OAAO,EACL,KAAK,EACL,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,WAAW,EACX,WAAW,EACX,UAAU,EACV,gBAAgB,GACjB,CAAA"}
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as SheetPrimitive from '@radix-ui/react-dialog';
3
+ import { XIcon } from 'lucide-react';
4
+ import { cn } from '../../lib/utils.js';
5
+ function Sheet({ ...props }) {
6
+ return _jsx(SheetPrimitive.Root, { "data-slot": "sheet", ...props });
7
+ }
8
+ function SheetTrigger({ ...props }) {
9
+ return _jsx(SheetPrimitive.Trigger, { "data-slot": "sheet-trigger", ...props });
10
+ }
11
+ function SheetClose({ ...props }) {
12
+ return _jsx(SheetPrimitive.Close, { "data-slot": "sheet-close", ...props });
13
+ }
14
+ function SheetPortal({ ...props }) {
15
+ return _jsx(SheetPrimitive.Portal, { "data-slot": "sheet-portal", ...props });
16
+ }
17
+ function SheetOverlay({ className, ...props }) {
18
+ return (_jsx(SheetPrimitive.Overlay, { "data-slot": "sheet-overlay", className: cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50', className), ...props }));
19
+ }
20
+ function SheetContent({ className, children, side = 'right', showCloseButton = true, ...props }) {
21
+ return (_jsxs(SheetPortal, { children: [_jsx(SheetOverlay, {}), _jsxs(SheetPrimitive.Content, { "data-slot": "sheet-content", className: cn('bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500', side === 'right' &&
22
+ 'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm', side === 'left' &&
23
+ 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm', side === 'top' &&
24
+ 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b', side === 'bottom' &&
25
+ 'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t', className), ...props, children: [children, showCloseButton && (_jsxs(SheetPrimitive.Close, { className: "ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none", children: [_jsx(XIcon, { className: "size-4" }), _jsx("span", { className: "sr-only", children: "Close" })] }))] })] }));
26
+ }
27
+ function SheetHeader({ className, ...props }) {
28
+ return (_jsx("div", { "data-slot": "sheet-header", className: cn('flex flex-col gap-1.5 p-4', className), ...props }));
29
+ }
30
+ function SheetFooter({ className, ...props }) {
31
+ return (_jsx("div", { "data-slot": "sheet-footer", className: cn('mt-auto flex flex-col gap-2 p-4', className), ...props }));
32
+ }
33
+ function SheetTitle({ className, ...props }) {
34
+ return (_jsx(SheetPrimitive.Title, { "data-slot": "sheet-title", className: cn('text-foreground font-semibold', className), ...props }));
35
+ }
36
+ function SheetDescription({ className, ...props }) {
37
+ return (_jsx(SheetPrimitive.Description, { "data-slot": "sheet-description", className: cn('text-muted-foreground text-sm', className), ...props }));
38
+ }
39
+ export { Sheet, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription, };
40
+ //# sourceMappingURL=sheet.js.map