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

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 (60) hide show
  1. package/README.md +20 -11
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/web-panel/SKILL.md +111 -127
  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 +11 -4
  11. package/build/components/footer/component.d.ts.map +1 -1
  12. package/build/components/footer/component.js +14 -7
  13. package/build/components/footer/component.js.map +1 -1
  14. package/build/components/footer/types.d.ts +7 -1
  15. package/build/components/footer/types.d.ts.map +1 -1
  16. package/build/components/nav/index.d.ts +1 -0
  17. package/build/components/nav/index.d.ts.map +1 -1
  18. package/build/components/nav/index.js +1 -0
  19. package/build/components/nav/index.js.map +1 -1
  20. package/build/components/nav/layout.d.ts +7 -0
  21. package/build/components/nav/layout.d.ts.map +1 -1
  22. package/build/components/nav/layout.js +37 -3
  23. package/build/components/nav/layout.js.map +1 -1
  24. package/build/components/nav/mobile.d.ts +21 -0
  25. package/build/components/nav/mobile.d.ts.map +1 -0
  26. package/build/components/nav/mobile.js +70 -0
  27. package/build/components/nav/mobile.js.map +1 -0
  28. package/build/components/nav/types.d.ts +31 -1
  29. package/build/components/nav/types.d.ts.map +1 -1
  30. package/build/components/socket/i18n/fr.json +7 -0
  31. package/build/components/socket/i18n.js +2 -0
  32. package/build/components/socket/i18n.js.map +1 -1
  33. package/build/consent/component.d.ts +11 -4
  34. package/build/consent/component.d.ts.map +1 -1
  35. package/build/consent/component.js +23 -9
  36. package/build/consent/component.js.map +1 -1
  37. package/build/consent/presence.d.ts +7 -1
  38. package/build/consent/presence.d.ts.map +1 -1
  39. package/build/consent/presence.js +8 -2
  40. package/build/consent/presence.js.map +1 -1
  41. package/build/consent/translate.d.ts +1 -1
  42. package/build/consent/translate.js +1 -1
  43. package/build/entrypoints.d.ts +7 -1
  44. package/build/entrypoints.d.ts.map +1 -1
  45. package/package.json +31 -29
  46. package/src/@/components/ui/sheet.tsx +142 -0
  47. package/src/components/footer/component.tsx +14 -6
  48. package/src/components/footer/types.ts +7 -1
  49. package/src/components/nav/index.ts +1 -0
  50. package/src/components/nav/layout.tsx +72 -8
  51. package/src/components/nav/mobile.tsx +147 -0
  52. package/src/components/nav/types.ts +31 -1
  53. package/src/components/socket/i18n/fr.json +7 -0
  54. package/src/components/socket/i18n.ts +2 -0
  55. package/src/consent/component.tsx +38 -13
  56. package/src/consent/presence.ts +9 -3
  57. package/src/consent/translate.ts +1 -1
  58. package/tests/consent.spec.ts +47 -0
  59. package/tests/harness/mount.tsx +51 -2
  60. package/tests/nav.spec.ts +187 -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.52
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,11 @@ 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`), `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), `className?`, `style?` — always renders the platform/owner credit line last, via `ShellCredit` |
230
232
  | `ShellCredit`, `useShellCredit` | `className?` — "Powered by OwlMeans" plus the owner's copyright, resolved the same way the sign-in screen's credit is |
231
233
 
232
234
  ### 4. Forms, panels and status
@@ -331,8 +333,8 @@ The dialog's strings are the lib-tier `socket` namespace (`reload.title`, `reloa
331
333
 
332
334
  | Symbol | Kind | Purpose |
333
335
  |---|---|---|
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 |
336
+ | `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 |
337
+ | `NavLayoutProps`, `TopNavProps`, `SideNavProps`, `MobileNavProps`, `FooterProps`, `ShellCreditProps` | type | Their props |
336
338
  | `useShellCredit` | hook | Resolves the credit `ShellCredit` renders, without rendering it |
337
339
  | `Layout`, `LayoutProps` | component, type | Plain content wrapper |
338
340
  | `Form`, `WebFormProps` | component, type | Web form; `FormProps` plus `className` / `style` |
@@ -391,7 +393,7 @@ bindings plus `@owlmeans/api-config-client`'s.
391
393
  | Symbol | Kind | Purpose |
392
394
  |---|---|---|
393
395
  | `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 |
396
+ | `PanelConsentMenuWidget` | component | The cookie-preferences control for a host menu or footer ("Cookie settings" — pass `label` and `className`) |
395
397
  | `appendConsentWidgetService(ctx, alias?)`, `createConsentWidgetService` | function | Ref-counted presence service over a state resource |
396
398
  | `useConsentMenuPresence()`, `useConsentWidgetPresent()` | hook | Declare that a menu shows the row; read whether one does (hides the floating button) |
397
399
  | `CONSENT_WIDGET_SERVICE`, `CONSENT_WIDGET_STATE` | const | Service alias and state alias |
@@ -439,7 +441,14 @@ The public names match the MUI implementation; these props and types differ:
439
441
  - **`reloadDialog` is off by default.** The dialog renders nothing unless `cfg.socket.reloadDialog`
440
442
  is `true`.
441
443
  - **Run `useConsentMenuPresence()` from the menu's always-mounted component**, never from inside a
442
- dropdown row, which mounts only while the menu is open.
444
+ dropdown row, which mounts only while the menu is open. Without `appendConsentWidgetService` it
445
+ does nothing and `PanelCookieConsent` keeps its floating button.
446
+ - **Pass an application footer layout as a node `footer`.** It renders full-width above the credit;
447
+ an array renders the centred link row. Neither removes the credit.
448
+ - **The shell already renders the skip link and `<main id="main">`.** A screen never renders a
449
+ second `<main>` or `#main`; an app with its own skip link passes `skipLinkLabel={false}`.
450
+ - **`mobileMenu` is opt-in.** Without it a narrow viewport keeps the section menu and the screen
451
+ strip; with it both move into the sheet.
443
452
  - **Never import `@/…` from package code or vendor its primitives in the app.** The `@` alias
444
453
  belongs to the consumer.
445
454
  - **Every override of a packaged string covers all seven languages** (`SUPPORTED_LNGS`).
@@ -463,7 +472,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
463
472
  your project's skill store (`.agents/skills/`):
464
473
 
465
474
  ```sh
466
- npx @owlmeans/agent-skills@^0.1.18-rc.30
475
+ npx @owlmeans/agent-skills@^0.1.18-rc.31
467
476
  ```
468
477
 
469
478
  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.52",
5
+ "generatedAt": "2026-09-21T21:54:22.981Z",
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.52"` in `dependencies`
12
12
 
13
13
  ## Key Exports
14
14
 
@@ -16,10 +16,9 @@ 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
+ | `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
22
  | `Link` | An `<a>` addressing an entrypoint alias (or a literal `src`), with the label taken from i18n |
24
23
  | `LoginScreen` / `LocalizedLoginScreen` / `appendLoginScreen` | The identity-provider choice screen — see `login-methods` |
25
24
  | `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 +29,11 @@ user-invocable: false
30
29
  | `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
30
  | `ImageUploader` / `Layout` | The uploader in a drop target with a preview, and a plain content wrapper (the shell is `NavLayout`) |
32
31
  | `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
32
  | `useBreakPoint()` | The current Tailwind breakpoint name, tracked on `resize` (`lg` when there is no `window`) |
36
33
  | `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
34
  | 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` |
35
+ | Re-exports from `@owlmeans/client` / `@owlmeans/client-entrypoint` / `@owlmeans/route` | `bind`, `bindAll`, `bindScreen`, `handler`, `provideRequest`, `stab`, `route`, `croute`, `frontend`, `useNavigate`, `useEntrypoint`, `useValue` |
36
+ | 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
37
 
41
38
  ## Subpath Exports
42
39
 
@@ -45,7 +42,6 @@ user-invocable: false
45
42
  - `./consent` — the cookie consent dialog and policy, bound to this app's i18n, plus a menu-row
46
43
  widget and a presence service so a host's own collapsed menu can take over the floating button's
47
44
  job
48
- - `./jobs` — `JobProgress`, `JobStatus`, `useJobToasts` over `@owlmeans/queue` records
49
45
 
50
46
  ## Usage
51
47
 
@@ -108,12 +104,58 @@ header. Both render the same items; only one is visible at a time.
108
104
 
109
105
  | Component | Props |
110
106
  |---|---|
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?` |
107
+ | `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`, `footer?: PanelNavLink[] \| ReactNode`, `headerClassName?`, `contentClassName?`, `containerClassName?`, `className?`, `style?` |
112
108
  | `TopNav` | `config: PanelNavConfig`, `translate?`, `ariaLabel?`, `className?`, `style?` |
113
109
  | `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?` |
110
+ | `MobileNav` | the same as `TopNav`; `ariaLabel` names the landmark inside the sheet, `className`/`style` land on the trigger button |
111
+ | `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), `className?`, `style?` |
115
112
  | `ShellCredit` / `useShellCredit` | `className?`; the platform/owner credit line `Footer` always renders — see below |
116
113
 
114
+ **The narrow-viewport menu is opt-in: `mobileMenu`.** Off (the default) the shell is exactly the
115
+ two-layer layout above at every width. On, below `md` (768px) the section menu is hidden
116
+ (`hidden md:flex`) and `MobileNav` puts a menu button — accessible name "Menu", a 44px target, a
117
+ solid focus ring — at the end of the actions row, so `actions` ("Get started", sign-in) stay in
118
+ the header at every width. The button opens a shadcn `Sheet` from the right listing every section:
119
+ a multi-screen section as its name over links to its screens, a single-screen section as one link
120
+ carrying the section's own label, so no destination appears twice. Labels, keys and `translate`
121
+ are the menus' own (`nav.<section>`, `modules.<alias>`); the button's name, the sheet title and
122
+ its close button resolve `shell.menu` / `shell.close` — outside the `nav.` family, because an
123
+ app may have a section called `menu`. Rules the component owns:
124
+
125
+ - **The screen strip is not mounted when `mobileMenu` is on.** The sheet already lists the active
126
+ section's screens; a strip under the header would be a second menu for the same level. The side
127
+ column (`hidden md:block`) is untouched — wide viewports are the same shell either way.
128
+ - **An entry navigates AND closes from its own click handler.** It is an `<a href>` (focusable,
129
+ openable in a new tab) whose `onClick` calls `preventDefault()` for in-app navigation — which a
130
+ dialog never notices, so a sheet left to close itself stays open over the screen it just
131
+ navigated to.
132
+ - **The sheet states both halves of its surface** (`bg-background text-foreground`): it is
133
+ portalled to the document body and inherits nothing from the shell.
134
+ - Pinned by `nav.spec.ts` → "the narrow-viewport menu", at 375px and 1280px, with and without
135
+ the prop (`?mobileMenu=1` in the harness).
136
+
137
+ **The skip link comes first, and `main` is its target.** `NavLayout` renders
138
+ `<a href="#main" data-skip-link>` as the FIRST child of its root — before `<header>`, never inside
139
+ it, because `header > div` (first) is the shared-rhythm row the layout pins locate. It is `sr-only`
140
+ until focused, then a pill fixed in the top-left corner (`bg-foreground text-background`,
141
+ `rounded-full`, `z-50`, the 3px `--ring` focus ring); its padding is `focus:`-only, since a plain
142
+ `px-*` sorts after `sr-only` and leaves a padded box behind the clip. Activating it focuses
143
+ `<main id="main" tabIndex={-1}>` by hand (`preventDefault` + `focus()`), because a fragment
144
+ navigation changes the location and the router reads that as navigation. The label is
145
+ `skipLinkLabel`, defaulting to `translate('shell.skip', 'Skip to content')`. `skipLinkLabel={false}`
146
+ renders no link AND leaves `main` without the id: an application rendering its own skip link owns
147
+ its own `#main`, and no two elements may share it. A screen never renders a second `<main>`.
148
+ Pinned by `nav.spec.ts` → "the skip link" (Tab from the page start, Enter lands on `main#main`,
149
+ `?skip=off`).
150
+
151
+ **A node `footer` is a full-width block, never a replacement.** An array renders the centred link
152
+ row. A node — an application's own footer layout: a brand, a description, link columns — goes to
153
+ `Footer`'s `content` and renders as a `w-full self-stretch text-start` block (`data-footer-content`)
154
+ inside the footer's container, above the credit. The container keeps `items-center` for the link
155
+ row and the credit; only the block stretches. It used to land INSIDE the centred link row, where
156
+ it shrink-wrapped to its content in the middle of the page. `ShellCredit` stays the container's
157
+ last child in every case. Pinned by `nav.spec.ts` → "a node footer" (`?footer=node`).
158
+
117
159
  **The style slots are REGIONS, and each region is its own SURFACE.** `className` is the root —
118
160
  the full-height page *behind* the header, side menu and footer. `headerClassName` is the sticky
119
161
  top bar. `contentClassName` is the content area. `containerClassName` is width and padding for
@@ -144,7 +186,7 @@ own background renders on top of the backdrop. Pinned by `nav.spec.ts` → "a br
144
186
  still leaves the backdrop layer opaque".
145
187
 
146
188
  **`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
189
+ Below the content block and the links (or on its own, when a layout passes no `footer` at all — `NavLayout` renders
148
190
  `<Footer>` unconditionally now), `ShellCredit` shows "Powered by OwlMeans" and the owner's own
149
191
  copyright notice, resolved from `security.auth.login.credit` via `resolveCredit`
150
192
  (`@owlmeans/client-auth/login`) — the SAME resolver and the SAME config the sign-in screen's
@@ -175,7 +217,8 @@ to follow.
175
217
  `--foreground` (active section link), `--muted-foreground` (resting section links),
176
218
  `--accent`/`--accent-foreground` (active side-menu item), `--border` (the header, side-menu and
177
219
  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
220
+ `text-primary`; `mobileMenu` adds `--ring` (the menu button's and the sheet entries' focus rings)
221
+ and a `bg-black/50` overlay. It reads **no `--sidebar*` variable at all**. So `--muted-foreground` is not merely
179
222
  the text colour of the `--muted` surface — it is secondary text sitting directly on `--background`,
180
223
  and a theme that lightens it to suit a dark muted panel loses its top menu.
181
224
 
@@ -197,72 +240,9 @@ Rules that make the shell behave:
197
240
  - **A parent route needs a `default: true` child.** A frontend entrypoint that has children but no
198
241
  child declared `default: true` renders blank at its own path — give a grouping screen an index
199
242
  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.
202
-
203
- ### Menus — `PanelMenu`
204
-
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.
209
-
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
- ]
222
-
223
- <PanelMenu entries={entries} translate={t} triggerLabel="Menu" indicator={dot} align="end" />
224
- ```
225
-
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.
243
+ - **Vendor `navigation-menu` and `sheet`.** `SideNav` builds on the package-local `Button`; `TopNav`
244
+ uses the package-local shadcn `navigation-menu` primitive and `MobileNav` the package-local
245
+ `sheet` (over `@radix-ui/react-dialog`, a peer) — see the package-boundary rule below.
266
246
 
267
247
  ### Toasts — `Toaster`
268
248
 
@@ -310,8 +290,8 @@ path (see the `client-socket` skill), so the only way out is the reload. Strings
310
290
  (`useI18nLib('socket', 'reload')`), 7 languages, under `src/components/socket/i18n/`.
311
291
 
312
292
  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.
293
+ inline "reconnecting…" state — `useSocketStatus()` from `@owlmeans/client-socket` serves that,
294
+ independent of the dialog.
315
295
 
316
296
  ### Links — `Link`
317
297
 
@@ -372,37 +352,6 @@ import { Form, TextInput, SubmitButton, Button } from '@owlmeans/web-panel'
372
352
  `contained` and the rest `outlined`, calling `onSelect(option)`. `name` prefixes each option's
373
353
  label key as `<name>.<option>`.
374
354
 
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
355
  ## Subpath: `./consent`
407
356
 
408
357
  `PanelCookieConsent` and `PanelCookiePolicy` — `@owlmeans/web-consent`'s components bound to this
@@ -424,19 +373,24 @@ import { appendConsentWidgetService } from '@owlmeans/web-panel/consent'
424
373
  appendConsentWidgetService<C, T>(context)
425
374
  ```
426
375
 
427
- Then, from the menu's own **always-mounted** shell component (never from inside a lazily-rendered
428
- row — see the `PanelMenu` rule above):
376
+ Then call `useConsentMenuPresence()` from the host menu's own **always-mounted** shell component,
377
+ never from inside the row. A dropdown's content (Radix `DropdownMenuContent` and most headless
378
+ menu content primitives) mounts its children only while the menu is OPEN, so presence announced
379
+ from inside the row hid the floating button only while the dropdown happened to be open, and
380
+ showed it again the instant it closed. The row is also not a `DropdownMenuItem`: an item takes the
381
+ inner button's focus and closes the menu on the click before the button's own handler runs.
429
382
 
430
383
  ```tsx
431
384
  import { PanelConsentMenuWidget, useConsentMenuPresence } from '@owlmeans/web-panel/consent'
432
385
 
433
386
  const MyMenu: FC = () => {
434
387
  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} ... />
388
+ return <DropdownMenu>
389
+ <DropdownMenuTrigger>…</DropdownMenuTrigger>
390
+ <DropdownMenuContent>
391
+ <div className="px-2 py-1.5"><PanelConsentMenuWidget /></div> {/* a plain row, not an item */}
392
+ </DropdownMenuContent>
393
+ </DropdownMenu>
440
394
  }
441
395
  ```
442
396
 
@@ -448,17 +402,48 @@ rather than a boolean latch, because more than one menu shell can be mounted for
448
402
  a layout transition (a stale header still showing its own collapsed menu while a new screen's own
449
403
  menu has already mounted) and because React 18 StrictMode double-invokes mount/cleanup in dev.
450
404
 
405
+ **The pair a consumer imports** — the dialog and the control that takes over its floating button:
406
+
407
+ | Job | Import from `@owlmeans/web-panel/consent` | Where it goes |
408
+ |---|---|---|
409
+ | The consent dialog (and its floating re-open button) | `PanelCookieConsent` | Beside the router — a `PanelApp` child — once |
410
+ | The "Cookie settings" control in a footer or menu | `PanelConsentMenuWidget` (`label`, `className`, `onSelect?` — defaults to `openConsent('reopen')`) | Inside the host's own footer/menu |
411
+ | Hiding the floating button while that control is reachable | `useConsentMenuPresence()` | Called by the always-mounted component that renders the control |
412
+ | Registering the presence service | `appendConsentWidgetService(context)` | The app's `context.ts` |
413
+
414
+ A footer is the simplest host, because it is always mounted — the control and the presence call
415
+ live in one component:
416
+
417
+ ```tsx
418
+ const CookieSettings: FC = () => {
419
+ useConsentMenuPresence()
420
+ return <PanelConsentMenuWidget label={t('footer.cookies')} className="w-auto text-muted-foreground" />
421
+ }
422
+ ```
423
+
424
+ The widget renders a `<button data-consent-menu-widget>` styled as a menu row (`w-full`,
425
+ `text-popover-foreground`, `hover:bg-accent`); a footer passes `className` to restyle it (merged
426
+ through `cn`) and its own `label` — the packaged default reads "Cookie preferences".
427
+
428
+ **The presence service is optional, and nothing throws without it.** `PanelCookieConsent` checks
429
+ the context for `consentWidget` and, when `appendConsentWidgetService` never ran, renders the plain
430
+ bound dialog with its floating button; `useConsentMenuPresence()` then has nothing to claim and
431
+ does nothing. The presence read itself (`useConsentWidgetPresent`) still needs the service — its
432
+ state resource is registered by it — so any other caller checks first. A dialog that read an
433
+ unregistered state resource threw inside render ("Resource consent-widget-presence not found") and
434
+ blanked the whole application. Pinned by `tests/consent.spec.ts` (`?consent=bare` / `?consent=menu`).
435
+
451
436
  ## Consumer setup — package boundary and Tailwind
452
437
 
453
438
  `web-panel` ships its shadcn primitives and `cn` helper as private implementation files under its
454
439
  own `build/@/` tree. Package source imports them only through relative specifiers; it must never
455
440
  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.
441
+ fresh installation depend on unrelated files. `cn` is not exported: a consumer keeps its own
442
+ class-name helper and never vendors or imports this package's UI primitives.
458
443
 
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.
444
+ Consumers still supply the package's peer dependencies: the Radix primitives (`alert-dialog`,
445
+ `dialog`, `label`, `navigation-menu`, `progress`, `separator`, `slot`) plus React, Tailwind and the
446
+ usual utility libraries. A consumer may have its own shadcn `@` alias, but it is unrelated to this package.
462
447
 
463
448
  Then point Tailwind at the installed package's **`src`** directory. Its oxide scanner reads the CSS root
464
449
  plus `@source` directives only, and excludes `node_modules` — so classes that exist **only** inside
@@ -478,11 +463,10 @@ in a linked workspace, so it is the reliable scan target in both modes.
478
463
 
479
464
  - `@owlmeans/web-client`, `@owlmeans/client-panel`, `@owlmeans/client-i18n`, `@owlmeans/web-router`
480
465
  - `@owlmeans/client-socket` — `appendSocketStatus`, `useSocketStatus`, behind `SocketReloadDialog`
481
- - `@owlmeans/queue` — `JobRecord` / `JobState`, read by the `./jobs` subpath
482
466
  - Peers (app-provided): `react`, `react-dom`, `react-hook-form`, `tailwindcss`, `tailwind-merge`,
483
467
  `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.
468
+ (`alert-dialog`, `dialog`, `label`, `navigation-menu`, `progress`, `separator`, `slot`). No MUI,
469
+ no react-router.
486
470
  - `ajv-formats` is imported at module scope by the form model but is declared in no dependency
487
471
  section of the manifest, which lists `ajv` alone. An install that does not otherwise pull it in
488
472
  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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sheet.js","sourceRoot":"","sources":["../../../../src/@/components/ui/sheet.tsx"],"names":[],"mappings":";AAEA,OAAO,KAAK,cAAc,MAAM,wBAAwB,CAAA;AACxD,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;AAEpC,OAAO,EAAE,EAAE,EAAE,MAAM,oBAAoB,CAAA;AAEvC,SAAS,KAAK,CAAC,EAAE,GAAG,KAAK,EAAoD;IAC3E,OAAO,KAAC,cAAc,CAAC,IAAI,iBAAW,OAAO,KAAK,KAAK,GAAI,CAAA;AAC7D,CAAC;AAED,SAAS,YAAY,CAAC,EACpB,GAAG,KAAK,EAC4C;IACpD,OAAO,KAAC,cAAc,CAAC,OAAO,iBAAW,eAAe,KAAK,KAAK,GAAI,CAAA;AACxE,CAAC;AAED,SAAS,UAAU,CAAC,EAClB,GAAG,KAAK,EAC0C;IAClD,OAAO,KAAC,cAAc,CAAC,KAAK,iBAAW,aAAa,KAAK,KAAK,GAAI,CAAA;AACpE,CAAC;AAED,SAAS,WAAW,CAAC,EACnB,GAAG,KAAK,EAC2C;IACnD,OAAO,KAAC,cAAc,CAAC,MAAM,iBAAW,cAAc,KAAK,KAAK,GAAI,CAAA;AACtE,CAAC;AAED,SAAS,YAAY,CAAC,EACpB,SAAS,EACT,GAAG,KAAK,EAC4C;IACpD,OAAO,CACL,KAAC,cAAc,CAAC,OAAO,iBACX,eAAe,EACzB,SAAS,EAAE,EAAE,CACX,wJAAwJ,EACxJ,SAAS,CACV,KACG,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,SAAS,YAAY,CAAC,EACpB,SAAS,EACT,QAAQ,EACR,IAAI,GAAG,OAAO,EACd,eAAe,GAAG,IAAI,EACtB,GAAG,KAAK,EAIT;IACC,OAAO,CACL,MAAC,WAAW,eACV,KAAC,YAAY,KAAG,EAChB,MAAC,cAAc,CAAC,OAAO,iBACX,eAAe,EACzB,SAAS,EAAE,EAAE,CACX,4MAA4M,EAC5M,IAAI,KAAK,OAAO;oBACd,kIAAkI,EACpI,IAAI,KAAK,MAAM;oBACb,+HAA+H,EACjI,IAAI,KAAK,KAAK;oBACZ,0GAA0G,EAC5G,IAAI,KAAK,QAAQ;oBACf,mHAAmH,EACrH,SAAS,CACV,KACG,KAAK,aAER,QAAQ,EACR,eAAe,IAAI,CAClB,MAAC,cAAc,CAAC,KAAK,IAAC,SAAS,EAAC,4OAA4O,aAC1Q,KAAC,KAAK,IAAC,SAAS,EAAC,QAAQ,GAAG,EAC5B,eAAM,SAAS,EAAC,SAAS,sBAAa,IACjB,CACxB,IACsB,IACb,CACf,CAAA;AACH,CAAC;AAED,SAAS,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA+B;IACvE,OAAO,CACL,2BACY,cAAc,EACxB,SAAS,EAAE,EAAE,CAAC,2BAA2B,EAAE,SAAS,CAAC,KACjD,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,SAAS,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA+B;IACvE,OAAO,CACL,2BACY,cAAc,EACxB,SAAS,EAAE,EAAE,CAAC,iCAAiC,EAAE,SAAS,CAAC,KACvD,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,SAAS,UAAU,CAAC,EAClB,SAAS,EACT,GAAG,KAAK,EAC0C;IAClD,OAAO,CACL,KAAC,cAAc,CAAC,KAAK,iBACT,aAAa,EACvB,SAAS,EAAE,EAAE,CAAC,+BAA+B,EAAE,SAAS,CAAC,KACrD,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,SAAS,gBAAgB,CAAC,EACxB,SAAS,EACT,GAAG,KAAK,EACgD;IACxD,OAAO,CACL,KAAC,cAAc,CAAC,WAAW,iBACf,mBAAmB,EAC7B,SAAS,EAAE,EAAE,CAAC,+BAA+B,EAAE,SAAS,CAAC,KACrD,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,OAAO,EACL,KAAK,EACL,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,WAAW,EACX,WAAW,EACX,UAAU,EACV,gBAAgB,GACjB,CAAA"}
@@ -1,2 +1,10 @@
1
- export declare const entrypoints: import("@owlmeans/client-entrypoint").ClientProtocolEntrypoint<any>[];
1
+ export declare const entrypoints: (import("@owlmeans/client-entrypoint").ClientProtocolEntrypoint<import("@owlmeans/entrypoint").EntrypointProtocol<import("@owlmeans/entrypoint").OpenRequest, import("@owlmeans/entrypoint").OpenValue>> | import("@owlmeans/client-entrypoint").ClientProtocolEntrypoint<import("@owlmeans/entrypoint").EntrypointProtocol<{
2
+ body: import("@owlmeans/auth").AllowanceRequest;
3
+ }, import("@owlmeans/auth").AllowanceResponse>> | import("@owlmeans/client-entrypoint").ClientProtocolEntrypoint<import("@owlmeans/entrypoint").EntrypointProtocol<{
4
+ body: import("@owlmeans/auth").AuthCredentials;
5
+ }, import("@owlmeans/auth").AuthToken>> | import("@owlmeans/client-entrypoint").ClientProtocolEntrypoint<import("@owlmeans/entrypoint").EntrypointProtocol<{
6
+ query: Partial<import("@owlmeans/auth").AuthToken>;
7
+ }, undefined>> | import("@owlmeans/client-entrypoint").ClientProtocolEntrypoint<import("@owlmeans/entrypoint").EntrypointProtocol<{
8
+ query: import("@owlmeans/auth").AuthToken;
9
+ }, undefined>>)[];
2
10
  //# sourceMappingURL=entrypoints.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"entrypoints.d.ts","sourceRoot":"","sources":["../../src/auth/entrypoints.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,WAAW,uEAAuC,CAAA"}
1
+ {"version":3,"file":"entrypoints.d.ts","sourceRoot":"","sources":["../../src/auth/entrypoints.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,WAAW;;;;;;;;iBAAuC,CAAA"}
@@ -1,8 +1,15 @@
1
1
  import type { FC } from 'react';
2
2
  import type { FooterProps } from './types.js';
3
3
  /**
4
- * The standard footer — a centred row of links plus whatever the app adds as children, followed
5
- * by the platform/owner credit line.
4
+ * The standard footer — an optional full-width `content` block, a centred row of links plus
5
+ * whatever the app adds as children, and the platform/owner credit line, last.
6
+ *
7
+ * `content` is a block of the container, not an entry of the link row. The container centres its
8
+ * children (`items-center`), which is right for a row of links and a credit line and wrong for an
9
+ * application's own footer layout: centred, a block shrink-wraps to its content and its columns
10
+ * collapse towards the middle. `self-stretch` takes that one child back to the container's full
11
+ * width, and `text-start` undoes the container's centred text, so an application lays its own
12
+ * footer out from the natural edge — while the container keeps `items-center` for everything else.
6
13
  *
7
14
  * Labels are resolved HERE and handed to `Link` as children: `Link`'s own default label
8
15
  * looks up `modules.<alias>` with no fallback, so an app without i18n would print the raw
@@ -11,8 +18,8 @@ import type { FooterProps } from './types.js';
11
18
  *
12
19
  * The credit is rendered unconditionally — never behind a prop, so a restyle cannot delete it.
13
20
  * `useShellCredit` decides emptiness here (rather than letting `ShellCredit` render null inside
14
- * an otherwise-empty bordered strip): the footer as a whole returns `null` only when links,
15
- * children AND the resolved credit are all empty.
21
+ * an otherwise-empty bordered strip): the footer as a whole returns `null` only when content,
22
+ * links, children AND the resolved credit are all empty.
16
23
  */
17
24
  export declare const Footer: FC<FooterProps>;
18
25
  //# sourceMappingURL=component.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../../../src/components/footer/component.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAM/B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,MAAM,EAAE,EAAE,CAAC,WAAW,CA0BlC,CAAA"}
1
+ {"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../../../src/components/footer/component.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAM/B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,MAAM,EAAE,EAAE,CAAC,WAAW,CA2BlC,CAAA"}
@@ -4,8 +4,15 @@ import { cn } from '../../@/lib/utils.js';
4
4
  import { Link } from '../link.js';
5
5
  import { ShellCredit, useShellCredit } from './credit.js';
6
6
  /**
7
- * The standard footer — a centred row of links plus whatever the app adds as children, followed
8
- * by the platform/owner credit line.
7
+ * The standard footer — an optional full-width `content` block, a centred row of links plus
8
+ * whatever the app adds as children, and the platform/owner credit line, last.
9
+ *
10
+ * `content` is a block of the container, not an entry of the link row. The container centres its
11
+ * children (`items-center`), which is right for a row of links and a credit line and wrong for an
12
+ * application's own footer layout: centred, a block shrink-wraps to its content and its columns
13
+ * collapse towards the middle. `self-stretch` takes that one child back to the container's full
14
+ * width, and `text-start` undoes the container's centred text, so an application lays its own
15
+ * footer out from the natural edge — while the container keeps `items-center` for everything else.
9
16
  *
10
17
  * Labels are resolved HERE and handed to `Link` as children: `Link`'s own default label
11
18
  * looks up `modules.<alias>` with no fallback, so an app without i18n would print the raw
@@ -14,16 +21,16 @@ import { ShellCredit, useShellCredit } from './credit.js';
14
21
  *
15
22
  * The credit is rendered unconditionally — never behind a prop, so a restyle cannot delete it.
16
23
  * `useShellCredit` decides emptiness here (rather than letting `ShellCredit` render null inside
17
- * an otherwise-empty bordered strip): the footer as a whole returns `null` only when links,
18
- * children AND the resolved credit are all empty.
24
+ * an otherwise-empty bordered strip): the footer as a whole returns `null` only when content,
25
+ * links, children AND the resolved credit are all empty.
19
26
  */
20
- export const Footer = ({ links, translate = defaultNavTranslate, children, className, style, containerClassName }) => {
27
+ export const Footer = ({ links, translate = defaultNavTranslate, children, content, className, style, containerClassName }) => {
21
28
  const credit = useShellCredit();
22
29
  const hasLinks = (links != null && links.length > 0) || children != null;
23
- if (!hasLinks && !credit.poweredBy && credit.line == null) {
30
+ if (content == null && !hasLinks && !credit.poweredBy && credit.line == null) {
24
31
  return null;
25
32
  }
26
- return _jsx("footer", { className: cn('border-t py-6', className), style: style, children: _jsxs("div", { className: cn('flex flex-col items-center gap-3 text-center', containerClassName), children: [hasLinks && _jsxs("div", { className: "flex flex-wrap items-center justify-center gap-4 text-sm", children: [links?.map((link, idx) => {
33
+ return _jsx("footer", { className: cn('border-t py-6', className), style: style, children: _jsxs("div", { className: cn('flex flex-col items-center gap-3 text-center', containerClassName), children: [content != null && _jsx("div", { "data-footer-content": true, className: "w-full self-stretch text-start", children: content }), hasLinks && _jsxs("div", { className: "flex flex-wrap items-center justify-center gap-4 text-sm", children: [links?.map((link, idx) => {
27
34
  const label = resolveNavLabel(translate, link.label, `modules.${link.alias ?? link.href ?? ''}`, link.alias ?? link.href);
28
35
  return link.href != null
29
36
  ? _jsx(Link, { src: link.href, open: link.open, children: label }, `${link.href}:${idx}`)