@owlmeans/web-panel 0.1.18-rc.50 → 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.
- package/README.md +20 -11
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/web-panel/SKILL.md +111 -127
- package/build/@/components/ui/sheet.d.ts +15 -0
- package/build/@/components/ui/sheet.d.ts.map +1 -0
- package/build/@/components/ui/sheet.js +40 -0
- package/build/@/components/ui/sheet.js.map +1 -0
- package/build/components/footer/component.d.ts +11 -4
- package/build/components/footer/component.d.ts.map +1 -1
- package/build/components/footer/component.js +14 -7
- package/build/components/footer/component.js.map +1 -1
- package/build/components/footer/types.d.ts +7 -1
- package/build/components/footer/types.d.ts.map +1 -1
- package/build/components/nav/index.d.ts +1 -0
- package/build/components/nav/index.d.ts.map +1 -1
- package/build/components/nav/index.js +1 -0
- package/build/components/nav/index.js.map +1 -1
- package/build/components/nav/layout.d.ts +7 -0
- package/build/components/nav/layout.d.ts.map +1 -1
- package/build/components/nav/layout.js +37 -3
- package/build/components/nav/layout.js.map +1 -1
- package/build/components/nav/mobile.d.ts +21 -0
- package/build/components/nav/mobile.d.ts.map +1 -0
- package/build/components/nav/mobile.js +70 -0
- package/build/components/nav/mobile.js.map +1 -0
- package/build/components/nav/types.d.ts +31 -1
- package/build/components/nav/types.d.ts.map +1 -1
- package/build/components/socket/i18n/fr.json +7 -0
- package/build/components/socket/i18n.js +2 -0
- package/build/components/socket/i18n.js.map +1 -1
- package/build/consent/component.d.ts +11 -4
- package/build/consent/component.d.ts.map +1 -1
- package/build/consent/component.js +23 -9
- package/build/consent/component.js.map +1 -1
- package/build/consent/presence.d.ts +7 -1
- package/build/consent/presence.d.ts.map +1 -1
- package/build/consent/presence.js +8 -2
- package/build/consent/presence.js.map +1 -1
- package/build/consent/translate.d.ts +1 -1
- package/build/consent/translate.js +1 -1
- package/package.json +31 -29
- package/src/@/components/ui/sheet.tsx +142 -0
- package/src/components/footer/component.tsx +14 -6
- package/src/components/footer/types.ts +7 -1
- package/src/components/nav/index.ts +1 -0
- package/src/components/nav/layout.tsx +72 -8
- package/src/components/nav/mobile.tsx +147 -0
- package/src/components/nav/types.ts +31 -1
- package/src/components/socket/i18n/fr.json +7 -0
- package/src/components/socket/i18n.ts +2 -0
- package/src/consent/component.tsx +38 -13
- package/src/consent/presence.ts +9 -3
- package/src/consent/translate.ts +1 -1
- package/tests/consent.spec.ts +47 -0
- package/tests/harness/mount.tsx +51 -2
- package/tests/nav.spec.ts +187 -0
- package/build/@/components/ui/dropdown-menu.d.ts +0 -26
- package/build/@/components/ui/dropdown-menu.d.ts.map +0 -1
- package/build/@/components/ui/dropdown-menu.js +0 -51
- package/build/@/components/ui/dropdown-menu.js.map +0 -1
- package/build/components/menu/component.d.ts +0 -15
- package/build/components/menu/component.d.ts.map +0 -1
- package/build/components/menu/component.js +0 -146
- package/build/components/menu/component.js.map +0 -1
- package/build/components/menu/index.d.ts +0 -3
- package/build/components/menu/index.d.ts.map +0 -1
- package/build/components/menu/index.js +0 -3
- package/build/components/menu/index.js.map +0 -1
- package/build/components/menu/types.d.ts +0 -138
- package/build/components/menu/types.d.ts.map +0 -1
- package/build/components/menu/types.js +0 -17
- package/build/components/menu/types.js.map +0 -1
- package/build/hooks/index.d.ts +0 -2
- package/build/hooks/index.d.ts.map +0 -1
- package/build/hooks/index.js +0 -2
- package/build/hooks/index.js.map +0 -1
- package/build/hooks/use-mobile.d.ts +0 -16
- package/build/hooks/use-mobile.d.ts.map +0 -1
- package/build/hooks/use-mobile.js +0 -30
- package/build/hooks/use-mobile.js.map +0 -1
- package/build/jobs/component.d.ts +0 -19
- package/build/jobs/component.d.ts.map +0 -1
- package/build/jobs/component.js +0 -55
- package/build/jobs/component.js.map +0 -1
- package/build/jobs/index.d.ts +0 -4
- package/build/jobs/index.d.ts.map +0 -1
- package/build/jobs/index.js +0 -3
- package/build/jobs/index.js.map +0 -1
- package/build/jobs/toasts.d.ts +0 -14
- package/build/jobs/toasts.d.ts.map +0 -1
- package/build/jobs/toasts.js +0 -39
- package/build/jobs/toasts.js.map +0 -1
- package/build/jobs/types.d.ts +0 -21
- package/build/jobs/types.d.ts.map +0 -1
- package/build/jobs/types.js +0 -2
- package/build/jobs/types.js.map +0 -1
- package/build/utils.d.ts +0 -13
- package/build/utils.d.ts.map +0 -1
- package/build/utils.js +0 -14
- package/build/utils.js.map +0 -1
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.
|
|
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`,
|
|
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
|
|
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
|
-
| `
|
|
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
|
|
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.
|
|
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
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/web-panel",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-09-
|
|
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.
|
|
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`
|
|
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`, `
|
|
39
|
-
| Re-exports from the surrounding layers | `config`, `service`, `addWebService`, `AppType` / `HOME` / `ROOT` / `BASE` / `GUEST`, `DISPATCHER`, `CAUTHEN_FLOW_ENTER`, `DAUTH_GUARD`, `
|
|
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
|
-
| `
|
|
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
|
|
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`
|
|
201
|
-
package-local shadcn `navigation-menu` primitive
|
|
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()`
|
|
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
|
|
428
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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.
|
|
457
|
-
and
|
|
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 (`
|
|
460
|
-
`navigation-menu`, `progress`, `separator`, `slot`) plus React, Tailwind and the
|
|
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,
|
|
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,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
|
|
5
|
-
*
|
|
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
|
|
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
|
|
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
|
|
8
|
-
*
|
|
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
|
|
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}`)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"component.js","sourceRoot":"","sources":["../../../src/components/footer/component.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7E,OAAO,EAAE,EAAE,EAAE,MAAM,sBAAsB,CAAA;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AACjC,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAIzD
|
|
1
|
+
{"version":3,"file":"component.js","sourceRoot":"","sources":["../../../src/components/footer/component.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7E,OAAO,EAAE,EAAE,EAAE,MAAM,sBAAsB,CAAA;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AACjC,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAIzD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,MAAM,GAAoB,CAAC,EACtC,KAAK,EAAE,SAAS,GAAG,mBAAmB,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,kBAAkB,EAChG,EAAE,EAAE;IACH,MAAM,MAAM,GAAG,cAAc,EAAE,CAAA;IAC/B,MAAM,QAAQ,GAAG,CAAC,KAAK,IAAI,IAAI,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,QAAQ,IAAI,IAAI,CAAA;IACxE,IAAI,OAAO,IAAI,IAAI,IAAI,CAAC,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC;QAC7E,OAAO,IAAI,CAAA;IACb,CAAC;IAED,OAAO,iBAAQ,SAAS,EAAE,EAAE,CAAC,eAAe,EAAE,SAAS,CAAC,EAAE,KAAK,EAAE,KAAK,YACpE,eAAK,SAAS,EAAE,EAAE,CAAC,8CAA8C,EAAE,kBAAkB,CAAC,aACnF,OAAO,IAAI,IAAI,IAAI,2CAAyB,SAAS,EAAC,gCAAgC,YAAE,OAAO,GAAO,EACtG,QAAQ,IAAI,eAAK,SAAS,EAAC,0DAA0D,aACnF,KAAK,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;4BACxB,MAAM,KAAK,GAAG,eAAe,CAC3B,SAAS,EAAE,IAAI,CAAC,KAAK,EAAE,WAAW,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,CAC3F,CAAA;4BAED,OAAO,IAAI,CAAC,IAAI,IAAI,IAAI;gCACtB,CAAC,CAAC,KAAC,IAAI,IAA6B,GAAG,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,YAAG,KAAK,IAA9D,GAAG,IAAI,CAAC,IAAI,IAAI,GAAG,EAAE,CAAiD;gCACnF,CAAC,CAAC,KAAC,IAAI,IAA8B,MAAM,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,YAAG,KAAK,IAAnE,GAAG,IAAI,CAAC,KAAK,IAAI,GAAG,EAAE,CAAqD,CAAA;wBAC5F,CAAC,CAAC,EACD,QAAQ,IACL,EACN,KAAC,WAAW,KAAG,IACX,GACC,CAAA;AACX,CAAC,CAAA"}
|