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