@owlmeans/web-panel 0.1.18-rc.45 → 0.1.18-rc.47

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 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.43
14
+ bun add @owlmeans/web-panel@^0.1.18-rc.47
15
15
  ```
16
16
 
17
17
  Peer requirements (the consuming app provides these): `react`, `react-dom`,
@@ -226,7 +226,8 @@ alias. Default keys are `nav.<section>` and `modules.<alias>`.
226
226
  | `NavLayout` | `nav`, `translate?`, `title?`, `home?` (brand target; defaults to the first section's first item), `actions?`, `footer?` (`PanelNavLink[]` renders the standard footer, a node replaces it), `headerClassName?`, `contentClassName?`, `containerClassName?`, `className?`, `style?` |
227
227
  | `TopNav` | `config`, `translate?`, `ariaLabel?`, `className?`, `style?` |
228
228
  | `SideNav` | the same, plus `variant?: 'side' \| 'bar'` |
229
- | `Footer` | `links?`, `translate?`, `containerClassName?`, `children?`, `className?`, `style?` |
229
+ | `Footer` | `links?`, `translate?`, `containerClassName?`, `children?`, `className?`, `style?` — always renders the platform/owner credit line too, via `ShellCredit` |
230
+ | `ShellCredit`, `useShellCredit` | `className?` — "Powered by OwlMeans" plus the owner's copyright, resolved the same way the sign-in screen's credit is |
230
231
 
231
232
  ### 4. Forms, panels and status
232
233
 
@@ -330,8 +331,9 @@ The dialog's strings are the lib-tier `socket` namespace (`reload.title`, `reloa
330
331
 
331
332
  | Symbol | Kind | Purpose |
332
333
  |---|---|---|
333
- | `NavLayout`, `TopNav`, `SideNav`, `Footer` | component | Navigation shell and its pieces |
334
- | `NavLayoutProps`, `TopNavProps`, `SideNavProps`, `FooterProps` | type | Their props |
334
+ | `NavLayout`, `TopNav`, `SideNav`, `Footer`, `ShellCredit` | component | Navigation shell and its pieces, including the footer's platform/owner credit |
335
+ | `NavLayoutProps`, `TopNavProps`, `SideNavProps`, `FooterProps`, `ShellCreditProps` | type | Their props |
336
+ | `useShellCredit` | hook | Resolves the credit `ShellCredit` renders, without rendering it |
335
337
  | `Layout`, `LayoutProps` | component, type | Plain content wrapper |
336
338
  | `Form`, `WebFormProps` | component, type | Web form; `FormProps` plus `className` / `style` |
337
339
  | `TextInput`, `TextInputProps` | component, type | `react-hook-form` controlled input |
@@ -461,7 +463,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
461
463
  your project's skill store (`.agents/skills/`):
462
464
 
463
465
  ```sh
464
- npx @owlmeans/agent-skills@^0.1.18-rc.24
466
+ npx @owlmeans/agent-skills@^0.1.18-rc.27
465
467
  ```
466
468
 
467
469
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/web-panel",
4
- "version": "0.1.18-rc.45",
5
- "generatedAt": "2026-09-15T12:59:01.076Z",
4
+ "version": "0.1.18-rc.47",
5
+ "generatedAt": "2026-09-15T18:16:10.668Z",
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.45"` in `dependencies`
11
+ **Install:** `"@owlmeans/web-panel": "^0.1.18-rc.47"` in `dependencies`
12
12
 
13
13
  ## Key Exports
14
14
 
@@ -112,6 +112,7 @@ header. Both render the same items; only one is visible at a time.
112
112
  | `TopNav` | `config: PanelNavConfig`, `translate?`, `ariaLabel?`, `className?`, `style?` |
113
113
  | `SideNav` | the same, plus `variant?: 'side' \| 'bar'` |
114
114
  | `Footer` | `links?: PanelNavLink[]`, `translate?`, `containerClassName?` (the shell's rhythm, so the footer row lines up with the header and content), `children?`, `className?`, `style?` |
115
+ | `ShellCredit` / `useShellCredit` | `className?`; the platform/owner credit line `Footer` always renders — see below |
115
116
 
116
117
  **The style slots are REGIONS, and each region is its own SURFACE.** `className` is the root —
117
118
  the full-height page *behind* the header, side menu and footer. `headerClassName` is the sticky
@@ -129,6 +130,33 @@ invisible. Pinned by `tests/nav.spec.ts` → "the header is its own surface", wh
129
130
  rendered lightness rather than class names; the harness root carries a dark shell permanently so
130
131
  every navigation test runs against that case.
131
132
 
133
+ **The header is opaque even when `headerClassName` fails to be.** Behind the header's own visible
134
+ content sits a `[data-nav-backdrop]` layer that always paints `bg-background`, at negative
135
+ z-index inside the header's own `isolate` stacking context — so it never covers a caller's actual
136
+ header content, only whatever the header's own background would have been. It exists because
137
+ `headerClassName` is free text a design or restyle pass writes, and three shapes of it leave
138
+ tailwind-merge with no background utility at all on the header: a bare `bg-transparent`, an alpha
139
+ surface (`bg-<x>/50`), or Tailwind v3's dead syntax for referencing a CSS variable
140
+ (`bg-[--my-var]` — square brackets; v4's real shorthand is `bg-(--my-var)`, parentheses). Any of
141
+ those used to leave the sticky header fully see-through, with page content visibly scrolling
142
+ through the menu. A caller that genuinely wants a coloured bar still gets it: `headerClassName`'s
143
+ own background renders on top of the backdrop. Pinned by `nav.spec.ts` → "a broken headerClassName
144
+ still leaves the backdrop layer opaque".
145
+
146
+ **`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
148
+ `<Footer>` unconditionally now), `ShellCredit` shows "Powered by OwlMeans" and the owner's own
149
+ copyright notice, resolved from `security.auth.login.credit` via `resolveCredit`
150
+ (`@owlmeans/client-auth/login`) — the SAME resolver and the SAME config the sign-in screen's
151
+ `LoginCredit` reads, so an owner who pays to drop the platform credit (`poweredBy: false`) drops
152
+ it everywhere, not only on a screen a signed-in visitor may never see again. Order is the opposite
153
+ of the sign-in screen's: the owner's own notice leads, the platform credit follows — a footer is
154
+ read as "whose page is this, and who built it". The links row and the credit are both centred
155
+ (`justify-center`), not left-aligned. `useShellCredit()` is exported separately so a caller that
156
+ needs to know whether there is anything to show (without rendering it) can ask, exactly how
157
+ `Footer` itself decides whether to render `null`. Pinned by `nav.spec.ts` → "the shell footer
158
+ credit".
159
+
132
160
  **Every style slot is MERGED over its default — none of them substitutes.** `className`,
133
161
  `headerClassName`, `contentClassName` and `containerClassName` all go through `cn`, so a caller
134
162
  names only the utility it wants to move and tailwind-merge drops just the one it conflicts with.
@@ -1,12 +1,18 @@
1
1
  import type { FC } from 'react';
2
2
  import type { FooterProps } from './types.js';
3
3
  /**
4
- * The standard footer — a row of links plus whatever the app adds as children.
4
+ * The standard footer — a centred row of links plus whatever the app adds as children, followed
5
+ * by the platform/owner credit line.
5
6
  *
6
7
  * Labels are resolved HERE and handed to `Link` as children: `Link`'s own default label
7
8
  * looks up `modules.<alias>` with no fallback, so an app without i18n would print the raw
8
9
  * key. Resolving first means a literal label, a translation, or a humanized alias — never
9
10
  * a key.
11
+ *
12
+ * The credit is rendered unconditionally — never behind a prop, so a restyle cannot delete it.
13
+ * `useShellCredit` decides emptiness here (rather than letting `ShellCredit` render null inside
14
+ * an otherwise-empty bordered strip): the footer as a whole returns `null` only when links,
15
+ * children AND the resolved credit are all empty.
10
16
  */
11
17
  export declare const Footer: FC<FooterProps>;
12
18
  //# 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;AAK/B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C;;;;;;;GAOG;AACH,eAAO,MAAM,MAAM,EAAE,EAAE,CAAC,WAAW,CAqBlC,CAAA"}
1
+ {"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../../../src/components/footer/component.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAM/B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,MAAM,EAAE,EAAE,CAAC,WAAW,CA0BlC,CAAA"}
@@ -2,23 +2,32 @@ 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 { ShellCredit, useShellCredit } from './credit.js';
5
6
  /**
6
- * The standard footer — a row of links plus whatever the app adds as children.
7
+ * The standard footer — a centred row of links plus whatever the app adds as children, followed
8
+ * by the platform/owner credit line.
7
9
  *
8
10
  * Labels are resolved HERE and handed to `Link` as children: `Link`'s own default label
9
11
  * looks up `modules.<alias>` with no fallback, so an app without i18n would print the raw
10
12
  * key. Resolving first means a literal label, a translation, or a humanized alias — never
11
13
  * a key.
14
+ *
15
+ * The credit is rendered unconditionally — never behind a prop, so a restyle cannot delete it.
16
+ * `useShellCredit` decides emptiness here (rather than letting `ShellCredit` render null inside
17
+ * an otherwise-empty bordered strip): the footer as a whole returns `null` only when links,
18
+ * children AND the resolved credit are all empty.
12
19
  */
13
20
  export const Footer = ({ links, translate = defaultNavTranslate, children, className, style, containerClassName }) => {
14
- if ((links == null || links.length < 1) && children == null) {
21
+ const credit = useShellCredit();
22
+ const hasLinks = (links != null && links.length > 0) || children != null;
23
+ if (!hasLinks && !credit.poweredBy && credit.line == null) {
15
24
  return null;
16
25
  }
17
- return _jsx("footer", { className: cn('border-t py-6', className), style: style, children: _jsxs("div", { className: cn('flex flex-wrap items-center gap-4 text-sm px-4', containerClassName), children: [links?.map((link, idx) => {
18
- const label = resolveNavLabel(translate, link.label, `modules.${link.alias ?? link.href ?? ''}`, link.alias ?? link.href);
19
- return link.href != null
20
- ? _jsx(Link, { src: link.href, open: link.open, children: label }, `${link.href}:${idx}`)
21
- : _jsx(Link, { module: link.alias, open: link.open, children: label }, `${link.alias}:${idx}`);
22
- }), children] }) });
26
+ return _jsx("footer", { className: cn('border-t py-6', className), style: style, children: _jsxs("div", { className: cn('flex flex-col items-center gap-3 text-center', containerClassName), children: [hasLinks && _jsxs("div", { className: "flex flex-wrap items-center justify-center gap-4 text-sm", children: [links?.map((link, idx) => {
27
+ const label = resolveNavLabel(translate, link.label, `modules.${link.alias ?? link.href ?? ''}`, link.alias ?? link.href);
28
+ return link.href != null
29
+ ? _jsx(Link, { src: link.href, open: link.open, children: label }, `${link.href}:${idx}`)
30
+ : _jsx(Link, { module: link.alias, open: link.open, children: label }, `${link.alias}:${idx}`);
31
+ }), children] }), _jsx(ShellCredit, {})] }) });
23
32
  };
24
33
  //# 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;AAIjC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,MAAM,GAAoB,CAAC,EACtC,KAAK,EAAE,SAAS,GAAG,mBAAmB,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,kBAAkB,EACvF,EAAE,EAAE;IACH,IAAI,CAAC,KAAK,IAAI,IAAI,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,QAAQ,IAAI,IAAI,EAAE,CAAC;QAC5D,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,gDAAgD,EAAE,kBAAkB,CAAC,aACrF,KAAK,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;oBACxB,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;oBAED,OAAO,IAAI,CAAC,IAAI,IAAI,IAAI;wBACtB,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;wBACnF,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;gBAC5F,CAAC,CAAC,EACD,QAAQ,IACL,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,cAAc,EAAE,MAAM,aAAa,CAAA;AAIzD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,MAAM,GAAoB,CAAC,EACtC,KAAK,EAAE,SAAS,GAAG,mBAAmB,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,kBAAkB,EACvF,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,CAAC,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC;QAC1D,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,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"}
@@ -0,0 +1,29 @@
1
+ import type { FC } from 'react';
2
+ import type { ResolvedCredit } from '@owlmeans/client-auth/login';
3
+ export interface ShellCreditProps {
4
+ className?: string;
5
+ }
6
+ /**
7
+ * The platform credit and the owner's own copyright notice, resolved from the same
8
+ * `security.auth.login.credit` configuration the sign-in screen reads (`resolveCredit`,
9
+ * `@owlmeans/client-auth/login`) — one source, so an owner who pays to drop the platform credit
10
+ * drops it everywhere it can appear, not only on a screen a signed-in visitor may never see
11
+ * again.
12
+ *
13
+ * Exported separately from {@link ShellCredit} so `Footer` can decide, WITHOUT rendering
14
+ * anything, whether it has nothing at all to show — an empty bordered strip is worse than no
15
+ * footer, and only the resolved credit knows it is empty.
16
+ */
17
+ export declare const useShellCredit: () => ResolvedCredit;
18
+ /**
19
+ * The platform credit and the owner's own copyright notice, in every area's footer.
20
+ *
21
+ * There is deliberately no prop to hide this component: the only way to remove the platform
22
+ * half is the config `poweredBy: false` the entitlement gate sets, and the owner's own notice is
23
+ * never something a restyle should be able to delete.
24
+ *
25
+ * Order is the opposite of the sign-in screen's `LoginCredit`: a footer is read as "whose page is
26
+ * this, and who built it", so the owner's own copyright leads and the platform credit follows.
27
+ */
28
+ export declare const ShellCredit: FC<ShellCreditProps>;
29
+ //# sourceMappingURL=credit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credit.d.ts","sourceRoot":"","sources":["../../../src/components/footer/credit.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAI/B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAIjE,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc,QAAO,cAMjC,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC,gBAAgB,CAkB5C,CAAA"}
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useContext } from '@owlmeans/client';
3
+ import { useI18nLib } from '@owlmeans/client-i18n';
4
+ import { resolveCredit } from '@owlmeans/client-auth/login';
5
+ import { cn } from '../../@/lib/utils.js';
6
+ /**
7
+ * The platform credit and the owner's own copyright notice, resolved from the same
8
+ * `security.auth.login.credit` configuration the sign-in screen reads (`resolveCredit`,
9
+ * `@owlmeans/client-auth/login`) — one source, so an owner who pays to drop the platform credit
10
+ * drops it everywhere it can appear, not only on a screen a signed-in visitor may never see
11
+ * again.
12
+ *
13
+ * Exported separately from {@link ShellCredit} so `Footer` can decide, WITHOUT rendering
14
+ * anything, whether it has nothing at all to show — an empty bordered strip is worse than no
15
+ * footer, and only the resolved credit knows it is empty.
16
+ */
17
+ export const useShellCredit = () => {
18
+ const context = useContext();
19
+ const cfg = context.cfg.security?.auth?.login?.credit;
20
+ const brand = context.cfg.brand;
21
+ return resolveCredit(cfg, brand, context.cfg.service);
22
+ };
23
+ /**
24
+ * The platform credit and the owner's own copyright notice, in every area's footer.
25
+ *
26
+ * There is deliberately no prop to hide this component: the only way to remove the platform
27
+ * half is the config `poweredBy: false` the entitlement gate sets, and the owner's own notice is
28
+ * never something a restyle should be able to delete.
29
+ *
30
+ * Order is the opposite of the sign-in screen's `LoginCredit`: a footer is read as "whose page is
31
+ * this, and who built it", so the owner's own copyright leads and the platform credit follows.
32
+ */
33
+ export const ShellCredit = ({ className }) => {
34
+ const { poweredBy, line } = useShellCredit();
35
+ const t = useI18nLib('auth');
36
+ if (!poweredBy && line == null) {
37
+ return null;
38
+ }
39
+ return _jsxs("p", { "data-shell-credit": true, className: cn('text-center text-xs text-muted-foreground', className), children: [line != null && _jsx("span", { children: line }), poweredBy && line != null && _jsx("span", { "aria-hidden": "true", children: " \u00B7 " }), poweredBy && _jsx("span", { "data-login-powered": true, children: t('login.credit.powered', { defaultValue: 'Powered by OwlMeans' }) })] });
40
+ };
41
+ //# sourceMappingURL=credit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credit.js","sourceRoot":"","sources":["../../../src/components/footer/credit.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAG3D,OAAO,EAAE,EAAE,EAAE,MAAM,sBAAsB,CAAA;AAMzC;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,GAAmB,EAAE;IACjD,MAAM,OAAO,GAAG,UAAU,EAAE,CAAA;IAC5B,MAAM,GAAG,GAAI,OAAO,CAAC,GAAoB,CAAC,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAA;IACvE,MAAM,KAAK,GAAI,OAAO,CAAC,GAAoB,CAAC,KAAK,CAAA;IAEjD,OAAO,aAAa,CAAC,GAAG,EAAE,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;AACvD,CAAC,CAAA;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,WAAW,GAAyB,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE;IACjE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,cAAc,EAAE,CAAA;IAC5C,MAAM,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC,CAAA;IAE5B,IAAI,CAAC,SAAS,IAAI,IAAI,IAAI,IAAI,EAAE,CAAC;QAC/B,OAAO,IAAI,CAAA;IACb,CAAC;IAED,OAAO,wCAEL,SAAS,EAAE,EAAE,CAAC,2CAA2C,EAAE,SAAS,CAAC,aAEpE,IAAI,IAAI,IAAI,IAAI,yBAAO,IAAI,GAAQ,EACnC,SAAS,IAAI,IAAI,IAAI,IAAI,IAAI,8BAAkB,MAAM,yBAAW,EAChE,SAAS,IAAI,qDACX,CAAC,CAAC,sBAAsB,EAAE,EAAE,YAAY,EAAE,qBAAqB,EAAE,CAAC,GAC9D,IACL,CAAA;AACN,CAAC,CAAA"}
@@ -1,3 +1,4 @@
1
1
  export type * from './types.js';
2
2
  export * from './component.js';
3
+ export * from './credit.js';
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/footer/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,YAAY,CAAA;AAE/B,cAAc,gBAAgB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/footer/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,YAAY,CAAA;AAE/B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA"}
@@ -1,2 +1,3 @@
1
1
  export * from './component.js';
2
+ export * from './credit.js';
2
3
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/components/footer/index.ts"],"names":[],"mappings":"AAEA,cAAc,gBAAgB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/components/footer/index.ts"],"names":[],"mappings":"AAEA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../../src/components/nav/layout.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAO/B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAahD;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,EAAE,EAAE,CAAC,cAAc,CAwDxC,CAAA"}
1
+ {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../../src/components/nav/layout.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAO/B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAahD;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,EAAE,EAAE,CAAC,cAAc,CAkFxC,CAAA"}
@@ -33,8 +33,10 @@ export const NavLayout = ({ nav, translate, title, home, actions, footer, childr
33
33
  // winning on the utility it names and leaves the rest of the rhythm standing, so a width-only
34
34
  // value stays a width-only change; `px-8` still overrides the padding when that is the intent.
35
35
  const container = cn(CONTAINER, containerClassName);
36
- return _jsxs("div", { className: cn('flex min-h-screen flex-col bg-background text-foreground', className), style: style, children: [_jsxs("header", { className: cn('sticky top-0 z-40 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" }), _jsx("div", { className: "ml-auto flex items-center gap-2", children: actions })] }), _jsx(SideNav, { config: nav, translate: translate, variant: "bar", ariaLabel: "Screens", className: "md:hidden" })] }), _jsxs("div", { className: "flex flex-1", children: [_jsx(SideNav, { config: nav, translate: translate, variant: "side", ariaLabel: "Screens", className: "hidden md:block" }), _jsx("main", { className: cn('flex-1 py-8', contentClassName), children: _jsx("div", { className: cn(container), children: children }) })] }), Array.isArray(footer)
36
+ return _jsxs("div", { className: cn('flex min-h-screen flex-col bg-background text-foreground', className), style: style, children: [_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" }), _jsx("div", { className: "ml-auto flex items-center gap-2", children: actions })] }), _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", { className: cn('flex-1 py-8', contentClassName), children: _jsx("div", { className: cn(container), children: children }) })] }), Array.isArray(footer)
37
37
  ? _jsx(Footer, { links: footer, translate: translate, containerClassName: container })
38
- : footer != null ? _jsx(Footer, { containerClassName: container, children: footer }) : null] });
38
+ : footer != null
39
+ ? _jsx(Footer, { containerClassName: container, children: footer })
40
+ : _jsx(Footer, { containerClassName: container })] });
39
41
  };
40
42
  //# 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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/web-panel",
3
- "version": "0.1.18-rc.45",
3
+ "version": "0.1.18-rc.47",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -44,33 +44,33 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@hookform/resolvers": "^5.2.2",
47
- "@owlmeans/api-config-client": "^0.1.18-rc.25",
48
- "@owlmeans/auth": "^0.1.18-rc.21",
49
- "@owlmeans/auth-common": "^0.1.18-rc.25",
50
- "@owlmeans/basic-envelope": "^0.1.18-rc.24",
51
- "@owlmeans/client": "^0.1.18-rc.28",
52
- "@owlmeans/client-auth": "^0.1.18-rc.33",
53
- "@owlmeans/client-config": "^0.1.18-rc.24",
54
- "@owlmeans/client-context": "^0.1.18-rc.25",
55
- "@owlmeans/client-entrypoint": "^0.1.18-rc.25",
56
- "@owlmeans/client-flow": "^0.1.18-rc.29",
57
- "@owlmeans/client-i18n": "^0.1.18-rc.27",
58
- "@owlmeans/client-panel": "^0.1.18-rc.36",
59
- "@owlmeans/client-route": "^0.1.18-rc.25",
60
- "@owlmeans/client-socket": "^0.1.18-rc.26",
61
- "@owlmeans/config": "^0.1.18-rc.24",
62
- "@owlmeans/context": "^0.1.18-rc.20",
63
- "@owlmeans/entrypoint": "^0.1.18-rc.23",
64
- "@owlmeans/error": "^0.1.18-rc.20",
65
- "@owlmeans/flow": "^0.1.18-rc.26",
66
- "@owlmeans/i18n": "^0.1.18-rc.20",
67
- "@owlmeans/route": "^0.1.18-rc.21",
68
- "@owlmeans/state": "^0.1.18-rc.22",
69
- "@owlmeans/web-client": "^0.1.18-rc.37",
70
- "@owlmeans/web-consent": "^0.1.18-rc.19",
71
- "@owlmeans/web-db": "^0.1.18-rc.26",
72
- "@owlmeans/web-flow": "^0.1.18-rc.29",
73
- "@owlmeans/web-router": "^0.1.18-rc.26",
47
+ "@owlmeans/api-config-client": "^0.1.18-rc.26",
48
+ "@owlmeans/auth": "^0.1.18-rc.26",
49
+ "@owlmeans/auth-common": "^0.1.18-rc.28",
50
+ "@owlmeans/basic-envelope": "^0.1.18-rc.27",
51
+ "@owlmeans/client": "^0.1.18-rc.31",
52
+ "@owlmeans/client-auth": "^0.1.18-rc.36",
53
+ "@owlmeans/client-config": "^0.1.18-rc.28",
54
+ "@owlmeans/client-context": "^0.1.18-rc.29",
55
+ "@owlmeans/client-entrypoint": "^0.1.18-rc.28",
56
+ "@owlmeans/client-flow": "^0.1.18-rc.32",
57
+ "@owlmeans/client-i18n": "^0.1.18-rc.30",
58
+ "@owlmeans/client-panel": "^0.1.18-rc.37",
59
+ "@owlmeans/client-route": "^0.1.18-rc.29",
60
+ "@owlmeans/client-socket": "^0.1.18-rc.29",
61
+ "@owlmeans/config": "^0.1.18-rc.28",
62
+ "@owlmeans/context": "^0.1.18-rc.24",
63
+ "@owlmeans/entrypoint": "^0.1.18-rc.27",
64
+ "@owlmeans/error": "^0.1.18-rc.26",
65
+ "@owlmeans/flow": "^0.1.18-rc.29",
66
+ "@owlmeans/i18n": "^0.1.18-rc.27",
67
+ "@owlmeans/route": "^0.1.18-rc.25",
68
+ "@owlmeans/state": "^0.1.18-rc.25",
69
+ "@owlmeans/web-client": "^0.1.18-rc.40",
70
+ "@owlmeans/web-consent": "^0.1.18-rc.21",
71
+ "@owlmeans/web-db": "^0.1.18-rc.29",
72
+ "@owlmeans/web-flow": "^0.1.18-rc.32",
73
+ "@owlmeans/web-router": "^0.1.18-rc.29",
74
74
  "i18next-browser-languagedetector": "^8.0.0",
75
75
  "qrcode.react": "^4.2.0",
76
76
  "react-google-recaptcha": "^3.1.0",
@@ -95,7 +95,7 @@
95
95
  },
96
96
  "devDependencies": {
97
97
  "@owlmeans/dep-config": "workspace:*",
98
- "@owlmeans/test-ui": "^0.1.18-rc.27",
98
+ "@owlmeans/test-ui": "^0.1.18-rc.29",
99
99
  "@radix-ui/react-alert-dialog": "^1.1.15",
100
100
  "@radix-ui/react-label": "^2.1.0",
101
101
  "@radix-ui/react-navigation-menu": "^1.2.14",
@@ -2,36 +2,48 @@ import type { FC } from 'react'
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 { ShellCredit, useShellCredit } from './credit.js'
5
6
 
6
7
  import type { FooterProps } from './types.js'
7
8
 
8
9
  /**
9
- * The standard footer — a row of links plus whatever the app adds as children.
10
+ * The standard footer — a centred row of links plus whatever the app adds as children, followed
11
+ * by the platform/owner credit line.
10
12
  *
11
13
  * Labels are resolved HERE and handed to `Link` as children: `Link`'s own default label
12
14
  * looks up `modules.<alias>` with no fallback, so an app without i18n would print the raw
13
15
  * key. Resolving first means a literal label, a translation, or a humanized alias — never
14
16
  * a key.
17
+ *
18
+ * The credit is rendered unconditionally — never behind a prop, so a restyle cannot delete it.
19
+ * `useShellCredit` decides emptiness here (rather than letting `ShellCredit` render null inside
20
+ * an otherwise-empty bordered strip): the footer as a whole returns `null` only when links,
21
+ * children AND the resolved credit are all empty.
15
22
  */
16
23
  export const Footer: FC<FooterProps> = ({
17
24
  links, translate = defaultNavTranslate, children, className, style, containerClassName
18
25
  }) => {
19
- if ((links == null || links.length < 1) && children == null) {
26
+ const credit = useShellCredit()
27
+ const hasLinks = (links != null && links.length > 0) || children != null
28
+ if (!hasLinks && !credit.poweredBy && credit.line == null) {
20
29
  return null
21
30
  }
22
31
 
23
32
  return <footer className={cn('border-t py-6', className)} style={style}>
24
- <div className={cn('flex flex-wrap items-center gap-4 text-sm px-4', containerClassName)}>
25
- {links?.map((link, idx) => {
26
- const label = resolveNavLabel(
27
- translate, link.label, `modules.${link.alias ?? link.href ?? ''}`, link.alias ?? link.href
28
- )
33
+ <div className={cn('flex flex-col items-center gap-3 text-center', containerClassName)}>
34
+ {hasLinks && <div className="flex flex-wrap items-center justify-center gap-4 text-sm">
35
+ {links?.map((link, idx) => {
36
+ const label = resolveNavLabel(
37
+ translate, link.label, `modules.${link.alias ?? link.href ?? ''}`, link.alias ?? link.href
38
+ )
29
39
 
30
- return link.href != null
31
- ? <Link key={`${link.href}:${idx}`} src={link.href} open={link.open}>{label}</Link>
32
- : <Link key={`${link.alias}:${idx}`} module={link.alias} open={link.open}>{label}</Link>
33
- })}
34
- {children}
40
+ return link.href != null
41
+ ? <Link key={`${link.href}:${idx}`} src={link.href} open={link.open}>{label}</Link>
42
+ : <Link key={`${link.alias}:${idx}`} module={link.alias} open={link.open}>{label}</Link>
43
+ })}
44
+ {children}
45
+ </div>}
46
+ <ShellCredit />
35
47
  </div>
36
48
  </footer>
37
49
  }
@@ -0,0 +1,60 @@
1
+ import type { FC } from 'react'
2
+ import { useContext } from '@owlmeans/client'
3
+ import { useI18nLib } from '@owlmeans/client-i18n'
4
+ import { resolveCredit } from '@owlmeans/client-auth/login'
5
+ import type { ResolvedCredit } from '@owlmeans/client-auth/login'
6
+ import type { CommonConfig } from '@owlmeans/config'
7
+ import { cn } from '../../@/lib/utils.js'
8
+
9
+ export interface ShellCreditProps {
10
+ className?: string
11
+ }
12
+
13
+ /**
14
+ * The platform credit and the owner's own copyright notice, resolved from the same
15
+ * `security.auth.login.credit` configuration the sign-in screen reads (`resolveCredit`,
16
+ * `@owlmeans/client-auth/login`) — one source, so an owner who pays to drop the platform credit
17
+ * drops it everywhere it can appear, not only on a screen a signed-in visitor may never see
18
+ * again.
19
+ *
20
+ * Exported separately from {@link ShellCredit} so `Footer` can decide, WITHOUT rendering
21
+ * anything, whether it has nothing at all to show — an empty bordered strip is worse than no
22
+ * footer, and only the resolved credit knows it is empty.
23
+ */
24
+ export const useShellCredit = (): ResolvedCredit => {
25
+ const context = useContext()
26
+ const cfg = (context.cfg as CommonConfig).security?.auth?.login?.credit
27
+ const brand = (context.cfg as CommonConfig).brand
28
+
29
+ return resolveCredit(cfg, brand, context.cfg.service)
30
+ }
31
+
32
+ /**
33
+ * The platform credit and the owner's own copyright notice, in every area's footer.
34
+ *
35
+ * There is deliberately no prop to hide this component: the only way to remove the platform
36
+ * half is the config `poweredBy: false` the entitlement gate sets, and the owner's own notice is
37
+ * never something a restyle should be able to delete.
38
+ *
39
+ * Order is the opposite of the sign-in screen's `LoginCredit`: a footer is read as "whose page is
40
+ * this, and who built it", so the owner's own copyright leads and the platform credit follows.
41
+ */
42
+ export const ShellCredit: FC<ShellCreditProps> = ({ className }) => {
43
+ const { poweredBy, line } = useShellCredit()
44
+ const t = useI18nLib('auth')
45
+
46
+ if (!poweredBy && line == null) {
47
+ return null
48
+ }
49
+
50
+ return <p
51
+ data-shell-credit
52
+ className={cn('text-center text-xs text-muted-foreground', className)}
53
+ >
54
+ {line != null && <span>{line}</span>}
55
+ {poweredBy && line != null && <span aria-hidden="true"> · </span>}
56
+ {poweredBy && <span data-login-powered>
57
+ {t('login.credit.powered', { defaultValue: 'Powered by OwlMeans' })}
58
+ </span>}
59
+ </p>
60
+ }
@@ -1,3 +1,4 @@
1
1
  export type * from './types.js'
2
2
 
3
3
  export * from './component.js'
4
+ export * from './credit.js'
@@ -60,8 +60,17 @@ export const NavLayout: FC<NavLayoutProps> = ({
60
60
  * and it is not redundant with the root: pairing has to be restated by every element that
61
61
  * repaints its own background. `headerClassName` lands after it, so an app that wants a
62
62
  * dark bar overrides BOTH halves through tailwind-merge.
63
+ *
64
+ * The `data-nav-backdrop` layer underneath is what keeps that promise even when
65
+ * `headerClassName` does NOT: a restyle that hands it an invalid Tailwind v4 arbitrary-value
66
+ * class (`bg-[--x]`, dropped silently), a bare `bg-transparent`, or a translucent surface
67
+ * (`bg-x/50`) used to leave the sticky bar with no background paint at all, and page content
68
+ * scrolled visibly through the menu. `-z-10` keeps it behind everything `header` renders and
69
+ * `pointer-events-none` keeps it out of the way of clicks; it paints `bg-background` no
70
+ * matter what `headerClassName` does or fails to do, and a caller who genuinely wants a dark
71
+ * bar still gets it because `headerClassName`'s own background paints OVER this one.
63
72
  */}
64
- <header className={cn('sticky top-0 z-40 border-b bg-background text-foreground', headerClassName)}>
73
+ <header className={cn('sticky top-0 z-40 isolate border-b bg-background text-foreground', headerClassName)}>
65
74
  <div className={cn('flex h-14 items-center gap-6', container)}>
66
75
  {title != null ? <a
67
76
  onClick={brandAlias != null ? navigator.press(brandAlias) : undefined}
@@ -71,6 +80,15 @@ export const NavLayout: FC<NavLayoutProps> = ({
71
80
  <div className="ml-auto flex items-center gap-2">{actions}</div>
72
81
  </div>
73
82
  <SideNav config={nav} translate={translate} variant="bar" ariaLabel="Screens" className="md:hidden" />
83
+ {/*
84
+ * Last in source order, not first — `header`'s own FIRST `div` child is what the layout
85
+ * test locates to measure the shared rhythm (`header > div`, `.first()`), and this element
86
+ * is not part of that rhythm. Negative z-index inside `header`'s own `isolate` stacking
87
+ * context paints a layer behind every normal-flow child regardless of where it sits in the
88
+ * DOM, so moving it here changes nothing about what is visible — only which `div` a
89
+ * position-based query finds first.
90
+ */}
91
+ <div aria-hidden="true" data-nav-backdrop className="absolute inset-0 -z-10 bg-background pointer-events-none" />
74
92
  </header>
75
93
  <div className="flex flex-1">
76
94
  <SideNav config={nav} translate={translate} variant="side" ariaLabel="Screens" className="hidden md:block" />
@@ -78,8 +96,16 @@ export const NavLayout: FC<NavLayoutProps> = ({
78
96
  <div className={cn(container)}>{children}</div>
79
97
  </main>
80
98
  </div>
99
+ {/*
100
+ * The footer is rendered unconditionally, `footer` prop or not: `Footer` itself renders the
101
+ * platform/owner credit line even when there are no links and no children, and only that
102
+ * component gets to decide there is nothing to show. Gating this on `footer` used to make
103
+ * the credit disappear from every layout that never passed one.
104
+ */}
81
105
  {Array.isArray(footer)
82
106
  ? <Footer links={footer} translate={translate} containerClassName={container} />
83
- : footer != null ? <Footer containerClassName={container}>{footer}</Footer> : null}
107
+ : footer != null
108
+ ? <Footer containerClassName={container}>{footer}</Footer>
109
+ : <Footer containerClassName={container} />}
84
110
  </div>
85
111
  }
@@ -85,12 +85,21 @@ const LoginHarness: FC = () => <LoginScreen
85
85
  translate={(_key, defaultValue) => defaultValue}
86
86
  />
87
87
 
88
+ // `?header=broken` simulates the layout-restyle bug this harness pins: a `headerClassName`
89
+ // carrying an invalid Tailwind v4 arbitrary-value background (v3 syntax, silently dropped) and a
90
+ // bare `bg-transparent`, either of which used to leave the sticky header with no background paint
91
+ // of its own at all. One harness process serves both branches, exactly like `reloadDialog` above.
92
+ const brokenHeader = new URLSearchParams(window.location.search).get('header') === 'broken'
93
+ // `?footer=none` omits the `footer` prop entirely — the shape every area layout had before the
94
+ // shell grew a footer-links convention. `NavLayout` must still render the credit line then.
95
+ const noFooterProp = new URLSearchParams(window.location.search).get('footer') === 'none'
96
+
88
97
  const Layout: FC<PropsWithChildren> = ({ children }) => <>
89
98
  <NavLayout
90
99
  nav={navConfig}
91
100
  title="Harness"
92
101
  actions={<button id="action-slot">action</button>}
93
- footer={footerLinks}
102
+ {...(noFooterProp ? {} : { footer: footerLinks })}
94
103
  // A DARK APPLICATION SHELL, which is what a themed app does to the root: a contrasting
95
104
  // surface pair, both halves correct. The header paints its own opaque background, so it is
96
105
  // a different surface, and everything in it must stay legible against `--background`
@@ -101,6 +110,7 @@ const Layout: FC<PropsWithChildren> = ({ children }) => <>
101
110
  // page. It names the width and nothing else, so the centring and the side padding must
102
111
  // survive it — substituting this for the default is a page running flush to the window edge.
103
112
  containerClassName="max-w-[1280px]"
113
+ {...(brokenHeader ? { headerClassName: 'bg-[--nope] bg-transparent' } : {})}
104
114
  >{children}</NavLayout>
105
115
  {/* Mounted ONCE, in the layout — exactly where an application mounts it. */}
106
116
  <Toaster />
package/tests/nav.spec.ts CHANGED
@@ -178,6 +178,86 @@ describe('@owlmeans/web-panel — the header is its own surface', () => {
178
178
  await close()
179
179
  }
180
180
  }, TIMEOUT)
181
+
182
+ test('a broken headerClassName still leaves the backdrop layer opaque', async () => {
183
+ // `?header=broken` hands the header a Tailwind v3 arbitrary-value class v4 never emits, plus
184
+ // a bare `bg-transparent` — exactly what a layout-restyle pass produced once. tailwind-merge
185
+ // drops the header's own `bg-background` for both, so `header` itself legitimately reports a
186
+ // transparent computed background — that is `headerClassName` doing exactly what it says.
187
+ // What must NOT be transparent is the backdrop layer painted behind it: that is the element
188
+ // actually responsible for keeping content from showing through the bar.
189
+ const { page, close } = await open('/dash?header=broken')
190
+ try {
191
+ await page.waitForSelector('#dash')
192
+ const background = await page.locator('[data-nav-backdrop]').first()
193
+ .evaluate(el => window.getComputedStyle(el).backgroundColor)
194
+
195
+ expect(background).not.toMatch(/rgba\(0, 0, 0, 0\)|transparent/)
196
+ } finally {
197
+ await close()
198
+ }
199
+ }, TIMEOUT)
200
+
201
+ test('the backdrop layer never changes which div the shared-rhythm query finds first', async () => {
202
+ // `data-nav-backdrop` sits last in `header`'s DOM but paints behind everything via negative
203
+ // z-index — this pins that a position-based query for the header's own content div
204
+ // (`header > div`, `.first()`) still finds the rhythm div, not the backdrop.
205
+ const { page, close } = await open('/dash')
206
+ try {
207
+ await page.waitForSelector('#dash')
208
+ const first = await page.locator('header > div').first().getAttribute('data-nav-backdrop')
209
+ expect(first).toBeNull()
210
+ } finally {
211
+ await close()
212
+ }
213
+ }, TIMEOUT)
214
+ })
215
+
216
+ describe('@owlmeans/web-panel — the shell footer credit', () => {
217
+ test('the platform credit and the owner notice render, owner notice first', async () => {
218
+ // The harness configures `credit: { poweredBy: true, product: 'Harness', organization: 'Acme' }`
219
+ // with no explicit `line`, so `resolveCredit` composes one from product/organization — the
220
+ // same resolver the sign-in screen uses. Order here is the opposite of the sign-in screen's:
221
+ // the owner's own notice leads, the platform credit follows.
222
+ const { page, close } = await open('/dash')
223
+ try {
224
+ await page.waitForSelector('#dash')
225
+ const credit = page.locator('footer [data-shell-credit]')
226
+ await credit.waitFor()
227
+ const text = await credit.textContent()
228
+ expect(text).toContain('Acme')
229
+ expect(text).toContain('Powered by OwlMeans')
230
+ expect(text?.indexOf('Acme')).toBeLessThan(text!.indexOf('Powered by OwlMeans'))
231
+ } finally {
232
+ await close()
233
+ }
234
+ }, TIMEOUT)
235
+
236
+ test('the footer links and the credit are centred, not left-aligned', async () => {
237
+ const { page, close } = await open('/dash')
238
+ try {
239
+ await page.waitForSelector('#dash')
240
+ const justify = await page.locator('footer > div').first()
241
+ .evaluate(el => window.getComputedStyle(el).alignItems)
242
+ expect(justify).toBe('center')
243
+ } finally {
244
+ await close()
245
+ }
246
+ }, TIMEOUT)
247
+
248
+ test('a layout with no footer prop still shows the credit', async () => {
249
+ // `NavLayout` used to skip `Footer` entirely when no `footer` prop was passed, which made the
250
+ // credit disappear from any area layout that never wired footer links. `?footer=none` omits
251
+ // the prop from the harness's own `NavLayout` call.
252
+ const { page, close } = await open('/dash?footer=none')
253
+ try {
254
+ await page.waitForSelector('#dash')
255
+ expect(await page.locator('footer a').count()).toBe(0)
256
+ expect(await page.locator('footer [data-shell-credit]').count()).toBe(1)
257
+ } finally {
258
+ await close()
259
+ }
260
+ }, TIMEOUT)
181
261
  })
182
262
 
183
263
  describe('@owlmeans/web-panel — layout rhythm and link styling', () => {