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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +27 -3
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/web-panel/SKILL.md +64 -5
  4. package/build/components/footer/component.d.ts +6 -1
  5. package/build/components/footer/component.d.ts.map +1 -1
  6. package/build/components/footer/component.js +15 -4
  7. package/build/components/footer/component.js.map +1 -1
  8. package/build/components/footer/types.d.ts +10 -0
  9. package/build/components/footer/types.d.ts.map +1 -1
  10. package/build/components/index.d.ts +1 -0
  11. package/build/components/index.d.ts.map +1 -1
  12. package/build/components/index.js +1 -0
  13. package/build/components/index.js.map +1 -1
  14. package/build/components/nav/layout.d.ts.map +1 -1
  15. package/build/components/nav/layout.js +15 -4
  16. package/build/components/nav/layout.js.map +1 -1
  17. package/build/components/nav/types.d.ts +8 -0
  18. package/build/components/nav/types.d.ts.map +1 -1
  19. package/build/components/scheme/hook.d.ts +16 -0
  20. package/build/components/scheme/hook.d.ts.map +1 -0
  21. package/build/components/scheme/hook.js +51 -0
  22. package/build/components/scheme/hook.js.map +1 -0
  23. package/build/components/scheme/index.d.ts +4 -0
  24. package/build/components/scheme/index.d.ts.map +1 -0
  25. package/build/components/scheme/index.js +3 -0
  26. package/build/components/scheme/index.js.map +1 -0
  27. package/build/components/scheme/toggle.d.ts +14 -0
  28. package/build/components/scheme/toggle.d.ts.map +1 -0
  29. package/build/components/scheme/toggle.js +34 -0
  30. package/build/components/scheme/toggle.js.map +1 -0
  31. package/build/components/scheme/types.d.ts +21 -0
  32. package/build/components/scheme/types.d.ts.map +1 -0
  33. package/build/components/scheme/types.js +2 -0
  34. package/build/components/scheme/types.js.map +1 -0
  35. package/build/scheme/index.d.ts +3 -0
  36. package/build/scheme/index.d.ts.map +1 -0
  37. package/build/scheme/index.js +2 -0
  38. package/build/scheme/index.js.map +1 -0
  39. package/build/scheme/scheme.d.ts +44 -0
  40. package/build/scheme/scheme.d.ts.map +1 -0
  41. package/build/scheme/scheme.js +78 -0
  42. package/build/scheme/scheme.js.map +1 -0
  43. package/package.json +9 -2
  44. package/src/components/footer/component.tsx +24 -4
  45. package/src/components/footer/types.ts +9 -0
  46. package/src/components/index.ts +1 -0
  47. package/src/components/nav/layout.tsx +16 -4
  48. package/src/components/nav/types.ts +8 -0
  49. package/src/components/scheme/hook.ts +64 -0
  50. package/src/components/scheme/index.ts +4 -0
  51. package/src/components/scheme/toggle.tsx +43 -0
  52. package/src/components/scheme/types.ts +23 -0
  53. package/src/scheme/index.ts +4 -0
  54. package/src/scheme/scheme.ts +86 -0
  55. package/tests/harness/mount.tsx +3 -0
  56. package/tests/scheme.spec.ts +149 -0
  57. package/zz-tw.d.mts +2 -0
  58. package/zz-tw.d.mts.map +1 -0
package/README.md CHANGED
@@ -11,7 +11,7 @@ but never started.
11
11
  ## Installation
12
12
 
13
13
  ```sh
14
- bun add @owlmeans/web-panel@^0.1.18-rc.52
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`,
@@ -224,11 +224,12 @@ alias. Default keys are `nav.<section>` and `modules.<alias>`.
224
224
 
225
225
  | Component | Props |
226
226
  |---|---|
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
+ | `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?` |
228
228
  | `TopNav` | `config`, `translate?`, `ariaLabel?`, `className?`, `style?` |
229
229
  | `SideNav` | the same, plus `variant?: 'side' \| 'bar'` |
230
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` |
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 |
232
233
  | `ShellCredit`, `useShellCredit` | `className?` — "Powered by OwlMeans" plus the owner's copyright, resolved the same way the sign-in screen's credit is |
233
234
 
234
235
  ### 4. Forms, panels and status
@@ -345,6 +346,8 @@ The dialog's strings are the lib-tier `socket` namespace (`reload.title`, `reloa
345
346
  | `BlockProps`, `TextProps`, `LinkProps`, `StatusProps`, `StyledProps`, `TextVariant` | type | Their props |
346
347
  | `ImageUploader`, `ImageUploaderProps` | component, type | Drop target with a `previewUrl` |
347
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 |
348
351
  | `SocketReloadDialog` | component | Blocking reload prompt for `'lost'` sockets |
349
352
  | `LoginScreen`, `LocalizedLoginScreen` | component | Identity-provider choice screen; the localized one binds `translate` to the app's resources |
350
353
  | `appendLoginScreen(ctx, setup?)`, `LoginScreenSetup` | function, type | Register the screen on the login service, with `Logo` and other `LoginScreenProps` |
@@ -388,6 +391,24 @@ relying party.
388
391
  `@owlmeans/web-panel/auth/entrypoints` exports `entrypoints`: `@owlmeans/client-auth/manager`'s
389
392
  bindings plus `@owlmeans/api-config-client`'s.
390
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
+
391
412
  ### `@owlmeans/web-panel/consent`
392
413
 
393
414
  | Symbol | Kind | Purpose |
@@ -445,6 +466,9 @@ The public names match the MUI implementation; these props and types differ:
445
466
  does nothing and `PanelCookieConsent` keeps its floating button.
446
467
  - **Pass an application footer layout as a node `footer`.** It renders full-width above the credit;
447
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.
448
472
  - **The shell already renders the skip link and `<main id="main">`.** A screen never renders a
449
473
  second `<main>` or `#main`; an app with its own skip link passes `skipLinkLabel={false}`.
450
474
  - **`mobileMenu` is opt-in.** Without it a narrow viewport keeps the section menu and the screen
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/web-panel",
4
- "version": "0.1.18-rc.52",
5
- "generatedAt": "2026-09-21T21:54:22.981Z",
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.52"` in `dependencies`
11
+ **Install:** `"@owlmeans/web-panel": "^0.1.18-rc.53"` in `dependencies`
12
12
 
13
13
  ## Key Exports
14
14
 
@@ -18,6 +18,7 @@ user-invocable: false
18
18
  | `NavLayout` | The application shell — header, section menu, screen menu, content, footer |
19
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
+ | `ThemeToggle` / `useColorScheme` | The light/dark switcher (`ThemeToggleProps`, `ThemeToggleLabels`) and the hook behind it (`ColorSchemeModel`) — see *Light and dark* below |
21
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 |
22
23
  | `Link` | An `<a>` addressing an entrypoint alias (or a literal `src`), with the label taken from i18n |
23
24
  | `LoginScreen` / `LocalizedLoginScreen` / `appendLoginScreen` | The identity-provider choice screen — see `login-methods` |
@@ -42,6 +43,9 @@ user-invocable: false
42
43
  - `./consent` — the cookie consent dialog and policy, bound to this app's i18n, plus a menu-row
43
44
  widget and a presence service so a host's own collapsed menu can take over the floating button's
44
45
  job
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
45
49
 
46
50
  ## Usage
47
51
 
@@ -104,11 +108,11 @@ header. Both render the same items; only one is visible at a time.
104
108
 
105
109
  | Component | Props |
106
110
  |---|---|
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?` |
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?` |
108
112
  | `TopNav` | `config: PanelNavConfig`, `translate?`, `ariaLabel?`, `className?`, `style?` |
109
113
  | `SideNav` | the same, plus `variant?: 'side' \| 'bar'` |
110
114
  | `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
+ | `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?` |
112
116
  | `ShellCredit` / `useShellCredit` | `className?`; the platform/owner credit line `Footer` always renders — see below |
113
117
 
114
118
  **The narrow-viewport menu is opt-in: `mobileMenu`.** Off (the default) the shell is exactly the
@@ -153,8 +157,9 @@ row. A node — an application's own footer layout: a brand, a description, link
153
157
  `Footer`'s `content` and renders as a `w-full self-stretch text-start` block (`data-footer-content`)
154
158
  inside the footer's container, above the credit. The container keeps `items-center` for the link
155
159
  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`).
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`).
158
163
 
159
164
  **The style slots are REGIONS, and each region is its own SURFACE.** `className` is the root —
160
165
  the full-height page *behind* the header, side menu and footer. `headerClassName` is the sticky
@@ -244,6 +249,60 @@ Rules that make the shell behave:
244
249
  uses the package-local shadcn `navigation-menu` primitive and `MobileNav` the package-local
245
250
  `sheet` (over `@radix-ui/react-dialog`, a peer) — see the package-boundary rule below.
246
251
 
252
+ ### Light and dark — `ThemeToggle`, `useColorScheme`, `./scheme`
253
+
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.
257
+
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:
263
+
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
+ }
275
+ ```
276
+
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.
305
+
247
306
  ### Toasts — `Toaster`
248
307
 
249
308
  An action that succeeded or failed says so in a toast. The surface is `Toaster`; the messages are
@@ -19,7 +19,12 @@ import type { FooterProps } from './types.js';
19
19
  * The credit is rendered unconditionally — never behind a prop, so a restyle cannot delete it.
20
20
  * `useShellCredit` decides emptiness here (rather than letting `ShellCredit` render null inside
21
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.
22
+ * links, children, the theme switcher AND the resolved credit are all absent.
23
+ *
24
+ * `themeToggle` puts the light/dark `ThemeToggle` in the bottom row beside the credit. It is the
25
+ * footer's own row rather than something an application adds through `content`, so every area of
26
+ * an application offers it in the same place and the credit keeps its position whatever the
27
+ * application's own footer layout does.
23
28
  */
24
29
  export declare const Footer: FC<FooterProps>;
25
30
  //# 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;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,MAAM,EAAE,EAAE,CAAC,WAAW,CA2BlC,CAAA"}
1
+ {"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../../../src/components/footer/component.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAO/B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,MAAM,EAAE,EAAE,CAAC,WAAW,CAyClC,CAAA"}
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { defaultNavTranslate, resolveNavLabel } from '@owlmeans/client-panel';
3
3
  import { cn } from '../../@/lib/utils.js';
4
4
  import { Link } from '../link.js';
5
+ import { ThemeToggle } from '../scheme/toggle.js';
5
6
  import { ShellCredit, useShellCredit } from './credit.js';
6
7
  /**
7
8
  * The standard footer — an optional full-width `content` block, a centred row of links plus
@@ -22,12 +23,20 @@ import { ShellCredit, useShellCredit } from './credit.js';
22
23
  * The credit is rendered unconditionally — never behind a prop, so a restyle cannot delete it.
23
24
  * `useShellCredit` decides emptiness here (rather than letting `ShellCredit` render null inside
24
25
  * 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.
26
+ * links, children, the theme switcher AND the resolved credit are all absent.
27
+ *
28
+ * `themeToggle` puts the light/dark `ThemeToggle` in the bottom row beside the credit. It is the
29
+ * footer's own row rather than something an application adds through `content`, so every area of
30
+ * an application offers it in the same place and the credit keeps its position whatever the
31
+ * application's own footer layout does.
26
32
  */
27
- export const Footer = ({ links, translate = defaultNavTranslate, children, content, className, style, containerClassName }) => {
33
+ export const Footer = ({ links, translate = defaultNavTranslate, children, content, className, style, containerClassName, themeToggle }) => {
28
34
  const credit = useShellCredit();
29
35
  const hasLinks = (links != null && links.length > 0) || children != null;
30
- if (content == null && !hasLinks && !credit.poweredBy && credit.line == null) {
36
+ const toggle = themeToggle == null || themeToggle === false
37
+ ? null
38
+ : themeToggle === true ? {} : themeToggle;
39
+ if (content == null && !hasLinks && toggle == null && !credit.poweredBy && credit.line == null) {
31
40
  return null;
32
41
  }
33
42
  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) => {
@@ -35,6 +44,8 @@ export const Footer = ({ links, translate = defaultNavTranslate, children, conte
35
44
  return link.href != null
36
45
  ? _jsx(Link, { src: link.href, open: link.open, children: label }, `${link.href}:${idx}`)
37
46
  : _jsx(Link, { module: link.alias, open: link.open, children: label }, `${link.alias}:${idx}`);
38
- }), children] }), _jsx(ShellCredit, {})] }) });
47
+ }), children] }), toggle != null
48
+ ? _jsxs("div", { "data-footer-bottom": true, className: "flex flex-wrap items-center justify-center gap-2", children: [_jsx(ShellCredit, {}), _jsx(ThemeToggle, { ...(toggle.labels != null ? { labels: toggle.labels } : {}) })] })
49
+ : _jsx(ShellCredit, {})] }) });
39
50
  };
40
51
  //# sourceMappingURL=component.js.map
@@ -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;;;;;;;;;;;;;;;;;;;;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"}
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,MAAM,qBAAqB,CAAA;AACjD,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAIzD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,CAAC,MAAM,MAAM,GAAoB,CAAC,EACtC,KAAK,EAAE,SAAS,GAAG,mBAAmB,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,kBAAkB,EAC/F,WAAW,EACZ,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,MAAM,MAAM,GAAG,WAAW,IAAI,IAAI,IAAI,WAAW,KAAK,KAAK;QACzD,CAAC,CAAC,IAAI;QACN,CAAC,CAAC,WAAW,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,CAAA;IAC3C,IAAI,OAAO,IAAI,IAAI,IAAI,CAAC,QAAQ,IAAI,MAAM,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC;QAC/F,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,EAML,MAAM,IAAI,IAAI;oBACb,CAAC,CAAC,2CAAwB,SAAS,EAAC,kDAAkD,aACpF,KAAC,WAAW,KAAG,EACf,KAAC,WAAW,OAAK,CAAC,MAAM,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,GAAI,IACzE;oBACN,CAAC,CAAC,KAAC,WAAW,KAAG,IACf,GACC,CAAA;AACX,CAAC,CAAA"}
@@ -1,6 +1,11 @@
1
1
  import type { PropsWithChildren, ReactNode } from 'react';
2
2
  import type { NavTranslate, PanelNavLink } from '@owlmeans/client-panel';
3
+ import type { ThemeToggleLabels } from '../scheme/types.js';
3
4
  import type { StyledProps } from '../types.js';
5
+ /** `true` for the English labels; an object to pass the application's own. */
6
+ export type FooterThemeToggle = boolean | {
7
+ labels?: ThemeToggleLabels;
8
+ };
4
9
  export interface FooterProps extends PropsWithChildren<StyledProps> {
5
10
  links?: PanelNavLink[];
6
11
  /**
@@ -9,6 +14,11 @@ export interface FooterProps extends PropsWithChildren<StyledProps> {
9
14
  * the link row and the credit line. `children`, by contrast, join the centred link row.
10
15
  */
11
16
  content?: ReactNode;
17
+ /**
18
+ * Render the light/dark `ThemeToggle` in the bottom row, beside the credit line. Absent or
19
+ * `false`, the footer is exactly what it is without a switcher.
20
+ */
21
+ themeToggle?: FooterThemeToggle;
12
22
  /** The shell's horizontal rhythm, so the footer row lines up with the header and the content. */
13
23
  containerClassName?: string;
14
24
  /** See {@link NavTranslate} — omitted, literal labels and humanized aliases are used. */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/footer/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AACxE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAE9C,MAAM,WAAW,WAAY,SAAQ,iBAAiB,CAAC,WAAW,CAAC;IACjE,KAAK,CAAC,EAAE,YAAY,EAAE,CAAA;IACtB;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,iGAAiG;IACjG,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,yFAAyF;IACzF,SAAS,CAAC,EAAE,YAAY,CAAA;CACzB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/footer/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AACxE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAE9C,8EAA8E;AAC9E,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG;IAAE,MAAM,CAAC,EAAE,iBAAiB,CAAA;CAAE,CAAA;AAExE,MAAM,WAAW,WAAY,SAAQ,iBAAiB,CAAC,WAAW,CAAC;IACjE,KAAK,CAAC,EAAE,YAAY,EAAE,CAAA;IACtB;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB;;;OAGG;IACH,WAAW,CAAC,EAAE,iBAAiB,CAAA;IAC/B,iGAAiG;IACjG,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,yFAAyF;IACzF,SAAS,CAAC,EAAE,YAAY,CAAA;CACzB"}
@@ -16,4 +16,5 @@ export * from './helper.js';
16
16
  export * from './status.js';
17
17
  export * from './toaster.js';
18
18
  export * from './socket/index.js';
19
+ export * from './scheme/index.js';
19
20
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,YAAY,CAAA;AAE/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,mBAAmB,CAAA;AACjC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,mBAAmB,CAAA;AACjC,cAAc,kBAAkB,CAAA;AAEhC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,wBAAwB,CAAA;AACtC,cAAc,qBAAqB,CAAA;AAEnC,cAAc,mBAAmB,CAAA;AAEjC,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,YAAY,CAAA;AAE/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,mBAAmB,CAAA;AACjC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,mBAAmB,CAAA;AACjC,cAAc,kBAAkB,CAAA;AAEhC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,wBAAwB,CAAA;AACtC,cAAc,qBAAqB,CAAA;AAEnC,cAAc,mBAAmB,CAAA;AAEjC,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,mBAAmB,CAAA;AACjC,cAAc,mBAAmB,CAAA"}
@@ -15,4 +15,5 @@ export * from './helper.js';
15
15
  export * from './status.js';
16
16
  export * from './toaster.js';
17
17
  export * from './socket/index.js';
18
+ export * from './scheme/index.js';
18
19
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAEA,cAAc,sBAAsB,CAAA;AACpC,cAAc,mBAAmB,CAAA;AACjC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,mBAAmB,CAAA;AACjC,cAAc,kBAAkB,CAAA;AAEhC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,wBAAwB,CAAA;AACtC,cAAc,qBAAqB,CAAA;AAEnC,cAAc,mBAAmB,CAAA;AAEjC,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAEA,cAAc,sBAAsB,CAAA;AACpC,cAAc,mBAAmB,CAAA;AACjC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,mBAAmB,CAAA;AACjC,cAAc,kBAAkB,CAAA;AAEhC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,wBAAwB,CAAA;AACtC,cAAc,qBAAqB,CAAA;AAEnC,cAAc,mBAAmB,CAAA;AAEjC,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,mBAAmB,CAAA;AACjC,cAAc,mBAAmB,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../../src/components/nav/layout.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,EAAc,MAAM,OAAO,CAAA;AAS3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AA2BhD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,SAAS,EAAE,EAAE,CAAC,cAAc,CA0HxC,CAAA"}
1
+ {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../../src/components/nav/layout.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,EAAc,MAAM,OAAO,CAAA;AAS3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AA2BhD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,SAAS,EAAE,EAAE,CAAC,cAAc,CAsIxC,CAAA"}
@@ -45,8 +45,19 @@ const SKIP_LINK = [
45
45
  * The skip link comes FIRST in the root, ahead of the header — the first thing a keyboard reaches
46
46
  * — and moves focus to `<main id="main">`, past the brand, the menus and the actions.
47
47
  */
48
- export const NavLayout = ({ nav, translate = defaultNavTranslate, title, home, actions, footer, children, className, style, headerClassName, contentClassName, containerClassName, mobileMenu = false, skipLinkLabel }) => {
48
+ export const NavLayout = ({ nav, translate = defaultNavTranslate, title, home, actions, footer, children, className, style, headerClassName, contentClassName, containerClassName, mobileMenu = false, skipLinkLabel, themeToggle }) => {
49
49
  const navigator = useNavigate();
50
+ // The switcher's names go through `translate` like every other shell string, with the caller's
51
+ // own labels winning; `false`/absent stays absent, so the footer renders exactly as it did.
52
+ const toggle = themeToggle == null || themeToggle === false ? undefined : {
53
+ labels: {
54
+ toLight: (themeToggle === true ? undefined : themeToggle.labels?.toLight)
55
+ ?? translate('shell.toLight', 'Switch to light mode'),
56
+ toDark: (themeToggle === true ? undefined : themeToggle.labels?.toDark)
57
+ ?? translate('shell.toDark', 'Switch to dark mode'),
58
+ },
59
+ };
60
+ const toggleProp = toggle != null ? { themeToggle: toggle } : {};
50
61
  const main = useRef(null);
51
62
  const skipLabel = skipLinkLabel === false
52
63
  ? null
@@ -68,9 +79,9 @@ export const NavLayout = ({ nav, translate = defaultNavTranslate, title, home, a
68
79
  // value stays a width-only change; `px-8` still overrides the padding when that is the intent.
69
80
  const container = cn(CONTAINER, containerClassName);
70
81
  return _jsxs("div", { className: cn('flex min-h-screen flex-col bg-background text-foreground', className), style: style, children: [skipLabel != null && _jsx("a", { href: "#main", onClick: skip, "data-skip-link": true, className: SKIP_LINK, children: skipLabel }), _jsxs("header", { className: cn('sticky top-0 z-40 isolate border-b bg-background text-foreground', headerClassName), children: [_jsxs("div", { className: cn('flex h-14 items-center gap-6', container), children: [title != null ? _jsx("a", { onClick: brandAlias != null ? navigator.press(brandAlias) : undefined, className: cn('brand flex items-center gap-2 text-lg font-semibold', brandAlias != null && 'cursor-pointer'), children: title }) : null, _jsx(TopNav, { config: nav, translate: translate, ariaLabel: "Sections", className: mobileMenu ? 'hidden md:flex' : undefined }), _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [actions, mobileMenu && _jsx(MobileNav, { config: nav, translate: translate, ariaLabel: "Sections", className: "md:hidden" })] })] }), !mobileMenu && _jsx(SideNav, { config: nav, translate: translate, variant: "bar", ariaLabel: "Screens", className: "md:hidden" }), _jsx("div", { "aria-hidden": "true", "data-nav-backdrop": true, className: "absolute inset-0 -z-10 bg-background pointer-events-none" })] }), _jsxs("div", { className: "flex flex-1", children: [_jsx(SideNav, { config: nav, translate: translate, variant: "side", ariaLabel: "Screens", className: "hidden md:block" }), _jsx("main", { ref: main, ...(skipLabel != null ? { id: 'main', tabIndex: -1 } : {}), className: cn('flex-1 py-8 outline-none', contentClassName), children: _jsx("div", { className: cn(container), children: children }) })] }), Array.isArray(footer)
71
- ? _jsx(Footer, { links: footer, translate: translate, containerClassName: container })
82
+ ? _jsx(Footer, { links: footer, translate: translate, containerClassName: container, ...toggleProp })
72
83
  : footer != null
73
- ? _jsx(Footer, { containerClassName: container, content: footer })
74
- : _jsx(Footer, { containerClassName: container })] });
84
+ ? _jsx(Footer, { containerClassName: container, content: footer, ...toggleProp })
85
+ : _jsx(Footer, { containerClassName: container, ...toggleProp })] });
75
86
  };
76
87
  //# sourceMappingURL=layout.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"layout.js","sourceRoot":"","sources":["../../../src/components/nav/layout.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"layout.js","sourceRoot":"","sources":["../../../src/components/nav/layout.tsx"],"names":[],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import type { PropsWithChildren, ReactNode } from 'react';
2
2
  import type { NavTranslate, PanelNavConfig, PanelNavLink } from '@owlmeans/client-panel';
3
+ import type { FooterThemeToggle } from '../footer/types.js';
3
4
  import type { StyledProps } from '../types.js';
4
5
  interface NavCommonProps extends StyledProps {
5
6
  config: PanelNavConfig;
@@ -48,6 +49,13 @@ export interface NavLayoutProps extends PropsWithChildren<StyledProps> {
48
49
  * without the `main` id, for an application that renders its own skip link and target.
49
50
  */
50
51
  skipLinkLabel?: string | false;
52
+ /**
53
+ * The light/dark switcher in the footer's bottom row, beside the credit — forwarded to
54
+ * `Footer`, whether `footer` is links, a node or absent. The accessible names resolve
55
+ * `shell.toLight` / `shell.toDark` through `translate` unless `labels` gives them. Absent or
56
+ * `false`, the footer renders exactly as it does without one.
57
+ */
58
+ themeToggle?: FooterThemeToggle;
51
59
  /**
52
60
  * An array renders the standard footer: a centred row of links above the credit line.
53
61
  *
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/nav/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AACxF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAE9C,UAAU,cAAe,SAAQ,WAAW;IAC1C,MAAM,EAAE,cAAc,CAAA;IACtB,yFAAyF;IACzF,SAAS,CAAC,EAAE,YAAY,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,WAAY,SAAQ,cAAc;CAAI;AAEvD,MAAM,WAAW,YAAa,SAAQ,cAAc;IAClD;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAe,SAAQ,cAAc;CAAI;AAE1D,MAAM,WAAW,cAAe,SAAQ,iBAAiB,CAAC,WAAW,CAAC;IACpE,GAAG,EAAE,cAAc,CAAA;IACnB,SAAS,CAAC,EAAE,YAAY,CAAA;IACxB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,gFAAgF;IAChF,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,yFAAyF;IACzF,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;IAC9B;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,YAAY,EAAE,GAAG,SAAS,CAAA;IACnC;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,yEAAyE;IACzE,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB;;;;;;;;;OASG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/nav/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AACxF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAE9C,UAAU,cAAe,SAAQ,WAAW;IAC1C,MAAM,EAAE,cAAc,CAAA;IACtB,yFAAyF;IACzF,SAAS,CAAC,EAAE,YAAY,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,WAAY,SAAQ,cAAc;CAAI;AAEvD,MAAM,WAAW,YAAa,SAAQ,cAAc;IAClD;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAe,SAAQ,cAAc;CAAI;AAE1D,MAAM,WAAW,cAAe,SAAQ,iBAAiB,CAAC,WAAW,CAAC;IACpE,GAAG,EAAE,cAAc,CAAA;IACnB,SAAS,CAAC,EAAE,YAAY,CAAA;IACxB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,gFAAgF;IAChF,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,yFAAyF;IACzF,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;IAC9B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,iBAAiB,CAAA;IAC/B;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,YAAY,EAAE,GAAG,SAAS,CAAA;IACnC;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,yEAAyE;IACzE,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB;;;;;;;;;OASG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B"}
@@ -0,0 +1,16 @@
1
+ import type { ColorSchemeModel } from './types.js';
2
+ /**
3
+ * The page's colour scheme, and the way to choose one.
4
+ *
5
+ * Both halves are read synchronously on the first render — the stored choice from storage, the
6
+ * system preference from `matchMedia` — so a toggle never renders the wrong icon for a frame.
7
+ * Afterwards it follows three sources: `applyColorScheme` anywhere in this document (its event),
8
+ * another tab changing the stored key (`storage`), and the operating system switching while no
9
+ * choice is stored.
10
+ *
11
+ * Mounting also re-applies a stored choice's class. The head bootstrap has normally done that
12
+ * before the first paint; an application without it still ends up on the right scheme, one frame
13
+ * late rather than never.
14
+ */
15
+ export declare const useColorScheme: () => ColorSchemeModel;
16
+ //# sourceMappingURL=hook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook.d.ts","sourceRoot":"","sources":["../../../src/components/scheme/hook.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAQlD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,cAAc,QAAO,gBAqCjC,CAAA"}
@@ -0,0 +1,51 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import { COLOR_SCHEME_EVENT, COLOR_SCHEME_KEY, applyColorScheme, readColorScheme, setColorSchemeClass, } from '../../scheme/scheme.js';
3
+ const DARK_QUERY = '(prefers-color-scheme: dark)';
4
+ const systemDark = () => typeof window !== 'undefined' && typeof window.matchMedia === 'function'
5
+ && window.matchMedia(DARK_QUERY).matches;
6
+ /**
7
+ * The page's colour scheme, and the way to choose one.
8
+ *
9
+ * Both halves are read synchronously on the first render — the stored choice from storage, the
10
+ * system preference from `matchMedia` — so a toggle never renders the wrong icon for a frame.
11
+ * Afterwards it follows three sources: `applyColorScheme` anywhere in this document (its event),
12
+ * another tab changing the stored key (`storage`), and the operating system switching while no
13
+ * choice is stored.
14
+ *
15
+ * Mounting also re-applies a stored choice's class. The head bootstrap has normally done that
16
+ * before the first paint; an application without it still ends up on the right scheme, one frame
17
+ * late rather than never.
18
+ */
19
+ export const useColorScheme = () => {
20
+ const [choice, setState] = useState(readColorScheme);
21
+ const [dark, setDark] = useState(systemDark);
22
+ useEffect(() => {
23
+ const stored = readColorScheme();
24
+ if (stored != null) {
25
+ setColorSchemeClass(stored);
26
+ }
27
+ setState(stored);
28
+ const onChoice = () => setState(readColorScheme());
29
+ const onStorage = (event) => {
30
+ if (event.key === COLOR_SCHEME_KEY || event.key == null) {
31
+ const next = readColorScheme();
32
+ setColorSchemeClass(next);
33
+ setState(next);
34
+ }
35
+ };
36
+ window.addEventListener(COLOR_SCHEME_EVENT, onChoice);
37
+ window.addEventListener('storage', onStorage);
38
+ const media = typeof window.matchMedia === 'function' ? window.matchMedia(DARK_QUERY) : null;
39
+ const onSystem = () => setDark(media?.matches === true);
40
+ media?.addEventListener('change', onSystem);
41
+ onSystem();
42
+ return () => {
43
+ window.removeEventListener(COLOR_SCHEME_EVENT, onChoice);
44
+ window.removeEventListener('storage', onStorage);
45
+ media?.removeEventListener('change', onSystem);
46
+ };
47
+ }, []);
48
+ const setChoice = useCallback((next) => applyColorScheme(next), []);
49
+ return { scheme: choice ?? (dark ? 'dark' : 'light'), choice, setChoice };
50
+ };
51
+ //# sourceMappingURL=hook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook.js","sourceRoot":"","sources":["../../../src/components/scheme/hook.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACxD,OAAO,EACL,kBAAkB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,eAAe,EAAE,mBAAmB,GAC7F,MAAM,wBAAwB,CAAA;AAI/B,MAAM,UAAU,GAAG,8BAA8B,CAAA;AAEjD,MAAM,UAAU,GAAG,GAAY,EAAE,CAC/B,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,MAAM,CAAC,UAAU,KAAK,UAAU;OACnE,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,OAAO,CAAA;AAE5C;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,GAAqB,EAAE;IACnD,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAA2B,eAAe,CAAC,CAAA;IAC9E,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAU,UAAU,CAAC,CAAA;IAErD,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,MAAM,GAAG,eAAe,EAAE,CAAA;QAChC,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;YACnB,mBAAmB,CAAC,MAAM,CAAC,CAAA;QAC7B,CAAC;QACD,QAAQ,CAAC,MAAM,CAAC,CAAA;QAEhB,MAAM,QAAQ,GAAG,GAAG,EAAE,CAAC,QAAQ,CAAC,eAAe,EAAE,CAAC,CAAA;QAClD,MAAM,SAAS,GAAG,CAAC,KAAmB,EAAE,EAAE;YACxC,IAAI,KAAK,CAAC,GAAG,KAAK,gBAAgB,IAAI,KAAK,CAAC,GAAG,IAAI,IAAI,EAAE,CAAC;gBACxD,MAAM,IAAI,GAAG,eAAe,EAAE,CAAA;gBAC9B,mBAAmB,CAAC,IAAI,CAAC,CAAA;gBACzB,QAAQ,CAAC,IAAI,CAAC,CAAA;YAChB,CAAC;QACH,CAAC,CAAA;QACD,MAAM,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,QAAQ,CAAC,CAAA;QACrD,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;QAE7C,MAAM,KAAK,GAAG,OAAO,MAAM,CAAC,UAAU,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;QAC5F,MAAM,QAAQ,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,CAAA;QACvD,KAAK,EAAE,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;QAC3C,QAAQ,EAAE,CAAA;QAEV,OAAO,GAAG,EAAE;YACV,MAAM,CAAC,mBAAmB,CAAC,kBAAkB,EAAE,QAAQ,CAAC,CAAA;YACxD,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;YAChD,KAAK,EAAE,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;QAChD,CAAC,CAAA;IACH,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,SAAS,GAAG,WAAW,CAAC,CAAC,IAA8B,EAAE,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAA;IAE7F,OAAO,EAAE,MAAM,EAAE,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAA;AAC3E,CAAC,CAAA"}
@@ -0,0 +1,4 @@
1
+ export type * from './types.js';
2
+ export * from './hook.js';
3
+ export * from './toggle.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/scheme/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,YAAY,CAAA;AAE/B,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA"}
@@ -0,0 +1,3 @@
1
+ export * from './hook.js';
2
+ export * from './toggle.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/components/scheme/index.ts"],"names":[],"mappings":"AAEA,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA"}
@@ -0,0 +1,14 @@
1
+ import type { FC } from 'react';
2
+ import type { ThemeToggleProps } from './types.js';
3
+ /**
4
+ * The light/dark switcher.
5
+ *
6
+ * It flips the RESOLVED scheme, not the stored choice: on a system that prefers dark and with no
7
+ * choice stored, the page is dark, so the first press means "light" — a toggle keyed on the
8
+ * choice alone would answer that press with "dark", which changes nothing the visitor can see.
9
+ * The icon is the scheme the page is in (a sun while light, a moon while dark), chosen in script
10
+ * from the same state rather than by a `dark:` variant, so it is right whatever the app's CSS
11
+ * calls dark. The accessible name says what pressing does, from `labels` or the English default.
12
+ */
13
+ export declare const ThemeToggle: FC<ThemeToggleProps>;
14
+ //# sourceMappingURL=toggle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toggle.d.ts","sourceRoot":"","sources":["../../../src/components/scheme/toggle.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAI/B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAclD;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC,gBAAgB,CAc5C,CAAA"}
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Moon, Sun } from 'lucide-react';
3
+ import { cn } from '../../@/lib/utils.js';
4
+ import { useColorScheme } from './hook.js';
5
+ /**
6
+ * A 44px round target in the page's own tokens — muted at rest, the foreground on hover — with
7
+ * the 3px ring every interactive element shows on keyboard focus. No fill, border, gradient or
8
+ * shadow: it sits in a footer's bottom row beside the credit and must read as part of it.
9
+ */
10
+ const TOGGLE = [
11
+ 'inline-flex size-11 shrink-0 cursor-pointer items-center justify-center rounded-full',
12
+ 'bg-transparent text-muted-foreground transition-colors hover:text-foreground',
13
+ 'outline-none focus-visible:ring-[3px] focus-visible:ring-ring focus-visible:ring-offset-2',
14
+ 'focus-visible:ring-offset-background',
15
+ ].join(' ');
16
+ /**
17
+ * The light/dark switcher.
18
+ *
19
+ * It flips the RESOLVED scheme, not the stored choice: on a system that prefers dark and with no
20
+ * choice stored, the page is dark, so the first press means "light" — a toggle keyed on the
21
+ * choice alone would answer that press with "dark", which changes nothing the visitor can see.
22
+ * The icon is the scheme the page is in (a sun while light, a moon while dark), chosen in script
23
+ * from the same state rather than by a `dark:` variant, so it is right whatever the app's CSS
24
+ * calls dark. The accessible name says what pressing does, from `labels` or the English default.
25
+ */
26
+ export const ThemeToggle = ({ labels, className, style }) => {
27
+ const { scheme, setChoice } = useColorScheme();
28
+ const dark = scheme === 'dark';
29
+ const label = dark
30
+ ? labels?.toLight ?? 'Switch to light mode'
31
+ : labels?.toDark ?? 'Switch to dark mode';
32
+ return _jsx("button", { type: "button", "data-theme-toggle": true, "data-scheme": scheme, "aria-label": label, title: label, onClick: () => setChoice(dark ? 'light' : 'dark'), className: cn(TOGGLE, className), style: style, children: dark ? _jsx(Moon, { className: "size-5", "aria-hidden": "true" }) : _jsx(Sun, { className: "size-5", "aria-hidden": "true" }) });
33
+ };
34
+ //# sourceMappingURL=toggle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toggle.js","sourceRoot":"","sources":["../../../src/components/scheme/toggle.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,EAAE,EAAE,MAAM,sBAAsB,CAAA;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAG1C;;;;GAIG;AACH,MAAM,MAAM,GAAG;IACb,sFAAsF;IACtF,8EAA8E;IAC9E,2FAA2F;IAC3F,sCAAsC;CACvC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AAEX;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,WAAW,GAAyB,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,EAAE;IAChF,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,cAAc,EAAE,CAAA;IAC9C,MAAM,IAAI,GAAG,MAAM,KAAK,MAAM,CAAA;IAC9B,MAAM,KAAK,GAAG,IAAI;QAChB,CAAC,CAAC,MAAM,EAAE,OAAO,IAAI,sBAAsB;QAC3C,CAAC,CAAC,MAAM,EAAE,MAAM,IAAI,qBAAqB,CAAA;IAE3C,OAAO,iBACL,IAAI,EAAC,QAAQ,4CAAgC,MAAM,gBAAc,KAAK,EAAE,KAAK,EAAE,KAAK,EACpF,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,EACjD,SAAS,EAAE,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,KAAK,EAAE,KAAK,YAE7C,IAAI,CAAC,CAAC,CAAC,KAAC,IAAI,IAAC,SAAS,EAAC,QAAQ,iBAAa,MAAM,GAAG,CAAC,CAAC,CAAC,KAAC,GAAG,IAAC,SAAS,EAAC,QAAQ,iBAAa,MAAM,GAAG,GAC/F,CAAA;AACX,CAAC,CAAA"}
@@ -0,0 +1,21 @@
1
+ import type { ColorSchemeChoice } from '../../scheme/scheme.js';
2
+ import type { StyledProps } from '../types.js';
3
+ export interface ColorSchemeModel {
4
+ /** What the page is showing: the stored choice, or the operating system's preference. */
5
+ scheme: 'light' | 'dark';
6
+ /** The visitor's explicit choice, or `null` while the page follows the operating system. */
7
+ choice: ColorSchemeChoice | null;
8
+ /** Choose — `null` clears the choice and hands the page back to the operating system. */
9
+ setChoice: (choice: ColorSchemeChoice | null) => void;
10
+ }
11
+ /** The toggle's accessible names — what pressing it DOES. English defaults when absent. */
12
+ export interface ThemeToggleLabels {
13
+ /** Shown while the page is dark. Default "Switch to light mode". */
14
+ toLight?: string;
15
+ /** Shown while the page is light. Default "Switch to dark mode". */
16
+ toDark?: string;
17
+ }
18
+ export interface ThemeToggleProps extends StyledProps {
19
+ labels?: ThemeToggleLabels;
20
+ }
21
+ //# sourceMappingURL=types.d.ts.map