@owlmeans/web-panel 0.1.18-rc.46 → 0.1.18-rc.48

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.46
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 |
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/web-panel",
4
- "version": "0.1.18-rc.46",
5
- "generatedAt": "2026-09-15T13:19:14.374Z",
4
+ "version": "0.1.18-rc.48",
5
+ "generatedAt": "2026-09-15T19:14:43.314Z",
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.46"` in `dependencies`
11
+ **Install:** `"@owlmeans/web-panel": "^0.1.18-rc.48"` 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;AAKjE,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,CAsB5C,CAAA"}
@@ -0,0 +1,42 @@
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
+ import { OWLMEANS_URL } from '../login/credit.js';
7
+ /**
8
+ * The platform credit and the owner's own copyright notice, resolved from the same
9
+ * `security.auth.login.credit` configuration the sign-in screen reads (`resolveCredit`,
10
+ * `@owlmeans/client-auth/login`) — one source, so an owner who pays to drop the platform credit
11
+ * drops it everywhere it can appear, not only on a screen a signed-in visitor may never see
12
+ * again.
13
+ *
14
+ * Exported separately from {@link ShellCredit} so `Footer` can decide, WITHOUT rendering
15
+ * anything, whether it has nothing at all to show — an empty bordered strip is worse than no
16
+ * footer, and only the resolved credit knows it is empty.
17
+ */
18
+ export const useShellCredit = () => {
19
+ const context = useContext();
20
+ const cfg = context.cfg.security?.auth?.login?.credit;
21
+ const brand = context.cfg.brand;
22
+ return resolveCredit(cfg, brand, context.cfg.service);
23
+ };
24
+ /**
25
+ * The platform credit and the owner's own copyright notice, in every area's footer.
26
+ *
27
+ * There is deliberately no prop to hide this component: the only way to remove the platform
28
+ * half is the config `poweredBy: false` the entitlement gate sets, and the owner's own notice is
29
+ * never something a restyle should be able to delete.
30
+ *
31
+ * Order is the opposite of the sign-in screen's `LoginCredit`: a footer is read as "whose page is
32
+ * this, and who built it", so the owner's own copyright leads and the platform credit follows.
33
+ */
34
+ export const ShellCredit = ({ className }) => {
35
+ const { poweredBy, line } = useShellCredit();
36
+ const t = useI18nLib('auth');
37
+ if (!poweredBy && line == null) {
38
+ return null;
39
+ }
40
+ 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("a", { "data-login-powered": true, href: OWLMEANS_URL, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-4 hover:text-foreground hover:underline", children: t('login.credit.powered', { defaultValue: 'Powered by OwlMeans' }) })] });
41
+ };
42
+ //# 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;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAMjD;;;;;;;;;;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,wCAEZ,IAAI,EAAE,YAAY,EAAE,MAAM,EAAC,QAAQ,EAAC,GAAG,EAAC,qBAAqB,EAC7D,SAAS,EAAC,0DAA0D,YAEnE,CAAC,CAAC,sBAAsB,EAAE,EAAE,YAAY,EAAE,qBAAqB,EAAE,CAAC,GACjE,IACF,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,5 +1,7 @@
1
1
  import type { FC } from 'react';
2
2
  import type { LoginCreditModel } from '@owlmeans/client-panel/auth';
3
+ /** Where "Powered by OwlMeans" leads, on the sign-in screen and in every shell footer alike. */
4
+ export declare const OWLMEANS_URL = "https://owlmeans.com";
3
5
  export interface LoginCreditProps {
4
6
  model: LoginCreditModel;
5
7
  translate: (key: string, defaultValue: string) => string;
@@ -1 +1 @@
1
- {"version":3,"file":"credit.d.ts","sourceRoot":"","sources":["../../../src/components/login/credit.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAE/B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAA;AAEnE,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,gBAAgB,CAAA;IACvB,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAA;IACxD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC,gBAAgB,CAe5C,CAAA"}
1
+ {"version":3,"file":"credit.d.ts","sourceRoot":"","sources":["../../../src/components/login/credit.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAE/B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAA;AAEnE,gGAAgG;AAChG,eAAO,MAAM,YAAY,yBAAyB,CAAA;AAElD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,gBAAgB,CAAA;IACvB,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAA;IACxD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC,gBAAgB,CAmB5C,CAAA"}
@@ -1,5 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cn } from '../../@/lib/utils.js';
3
+ /** Where "Powered by OwlMeans" leads, on the sign-in screen and in every shell footer alike. */
4
+ export const OWLMEANS_URL = 'https://owlmeans.com';
3
5
  /**
4
6
  * The line at the bottom of a sign-in screen.
5
7
  *
@@ -12,6 +14,6 @@ export const LoginCredit = ({ model, translate, className }) => {
12
14
  if (!model.poweredBy && model.line == null) {
13
15
  return null;
14
16
  }
15
- return _jsxs("p", { "data-login-credit": true, className: cn('text-center text-xs text-muted-foreground', className), children: [model.poweredBy && _jsx("span", { "data-login-powered": true, children: translate('login.credit.powered', 'Powered by OwlMeans') }), model.poweredBy && model.line != null && _jsx("span", { "aria-hidden": "true", children: " \u00B7 " }), model.line != null && _jsx("span", { children: model.line })] });
17
+ return _jsxs("p", { "data-login-credit": true, className: cn('text-center text-xs text-muted-foreground', className), children: [model.poweredBy && _jsx("a", { "data-login-powered": true, href: OWLMEANS_URL, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-4 hover:text-foreground hover:underline", children: translate('login.credit.powered', 'Powered by OwlMeans') }), model.poweredBy && model.line != null && _jsx("span", { "aria-hidden": "true", children: " \u00B7 " }), model.line != null && _jsx("span", { children: model.line })] });
16
18
  };
17
19
  //# sourceMappingURL=credit.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"credit.js","sourceRoot":"","sources":["../../../src/components/login/credit.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,sBAAsB,CAAA;AASzC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAAyB,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,EAAE;IACnF,IAAI,CAAC,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC;QAC3C,OAAO,IAAI,CAAA;IACb,CAAC;IAED,OAAO,wCAEL,SAAS,EAAE,EAAE,CAAC,2CAA2C,EAAE,SAAS,CAAC,aAEpE,KAAK,CAAC,SAAS,IAAI,qDACjB,SAAS,CAAC,sBAAsB,EAAE,qBAAqB,CAAC,GACpD,EACN,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,IAAI,IAAI,IAAI,IAAI,8BAAkB,MAAM,yBAAW,EAC5E,KAAK,CAAC,IAAI,IAAI,IAAI,IAAI,yBAAO,KAAK,CAAC,IAAI,GAAQ,IAC9C,CAAA;AACN,CAAC,CAAA"}
1
+ {"version":3,"file":"credit.js","sourceRoot":"","sources":["../../../src/components/login/credit.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,sBAAsB,CAAA;AAGzC,gGAAgG;AAChG,MAAM,CAAC,MAAM,YAAY,GAAG,sBAAsB,CAAA;AAQlD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAAyB,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,EAAE;IACnF,IAAI,CAAC,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC;QAC3C,OAAO,IAAI,CAAA;IACb,CAAC;IAED,OAAO,wCAEL,SAAS,EAAE,EAAE,CAAC,2CAA2C,EAAE,SAAS,CAAC,aAEpE,KAAK,CAAC,SAAS,IAAI,wCAElB,IAAI,EAAE,YAAY,EAAE,MAAM,EAAC,QAAQ,EAAC,GAAG,EAAC,qBAAqB,EAC7D,SAAS,EAAC,0DAA0D,YAEnE,SAAS,CAAC,sBAAsB,EAAE,qBAAqB,CAAC,GACvD,EACH,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,IAAI,IAAI,IAAI,IAAI,8BAAkB,MAAM,yBAAW,EAC5E,KAAK,CAAC,IAAI,IAAI,IAAI,IAAI,yBAAO,KAAK,CAAC,IAAI,GAAQ,IAC9C,CAAA;AACN,CAAC,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.46",
3
+ "version": "0.1.18-rc.48",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -67,7 +67,7 @@
67
67
  "@owlmeans/route": "^0.1.18-rc.25",
68
68
  "@owlmeans/state": "^0.1.18-rc.25",
69
69
  "@owlmeans/web-client": "^0.1.18-rc.40",
70
- "@owlmeans/web-consent": "^0.1.18-rc.20",
70
+ "@owlmeans/web-consent": "^0.1.18-rc.21",
71
71
  "@owlmeans/web-db": "^0.1.18-rc.29",
72
72
  "@owlmeans/web-flow": "^0.1.18-rc.32",
73
73
  "@owlmeans/web-router": "^0.1.18-rc.29",
@@ -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,65 @@
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
+ import { OWLMEANS_URL } from '../login/credit.js'
9
+
10
+ export interface ShellCreditProps {
11
+ className?: string
12
+ }
13
+
14
+ /**
15
+ * The platform credit and the owner's own copyright notice, resolved from the same
16
+ * `security.auth.login.credit` configuration the sign-in screen reads (`resolveCredit`,
17
+ * `@owlmeans/client-auth/login`) — one source, so an owner who pays to drop the platform credit
18
+ * drops it everywhere it can appear, not only on a screen a signed-in visitor may never see
19
+ * again.
20
+ *
21
+ * Exported separately from {@link ShellCredit} so `Footer` can decide, WITHOUT rendering
22
+ * anything, whether it has nothing at all to show — an empty bordered strip is worse than no
23
+ * footer, and only the resolved credit knows it is empty.
24
+ */
25
+ export const useShellCredit = (): ResolvedCredit => {
26
+ const context = useContext()
27
+ const cfg = (context.cfg as CommonConfig).security?.auth?.login?.credit
28
+ const brand = (context.cfg as CommonConfig).brand
29
+
30
+ return resolveCredit(cfg, brand, context.cfg.service)
31
+ }
32
+
33
+ /**
34
+ * The platform credit and the owner's own copyright notice, in every area's footer.
35
+ *
36
+ * There is deliberately no prop to hide this component: the only way to remove the platform
37
+ * half is the config `poweredBy: false` the entitlement gate sets, and the owner's own notice is
38
+ * never something a restyle should be able to delete.
39
+ *
40
+ * Order is the opposite of the sign-in screen's `LoginCredit`: a footer is read as "whose page is
41
+ * this, and who built it", so the owner's own copyright leads and the platform credit follows.
42
+ */
43
+ export const ShellCredit: FC<ShellCreditProps> = ({ className }) => {
44
+ const { poweredBy, line } = useShellCredit()
45
+ const t = useI18nLib('auth')
46
+
47
+ if (!poweredBy && line == null) {
48
+ return null
49
+ }
50
+
51
+ return <p
52
+ data-shell-credit
53
+ className={cn('text-center text-xs text-muted-foreground', className)}
54
+ >
55
+ {line != null && <span>{line}</span>}
56
+ {poweredBy && line != null && <span aria-hidden="true"> · </span>}
57
+ {poweredBy && <a
58
+ data-login-powered
59
+ href={OWLMEANS_URL} target="_blank" rel="noopener noreferrer"
60
+ className="underline-offset-4 hover:text-foreground hover:underline"
61
+ >
62
+ {t('login.credit.powered', { defaultValue: 'Powered by OwlMeans' })}
63
+ </a>}
64
+ </p>
65
+ }
@@ -1,3 +1,4 @@
1
1
  export type * from './types.js'
2
2
 
3
3
  export * from './component.js'
4
+ export * from './credit.js'
@@ -2,6 +2,9 @@ import type { FC } from 'react'
2
2
  import { cn } from '../../@/lib/utils.js'
3
3
  import type { LoginCreditModel } from '@owlmeans/client-panel/auth'
4
4
 
5
+ /** Where "Powered by OwlMeans" leads, on the sign-in screen and in every shell footer alike. */
6
+ export const OWLMEANS_URL = 'https://owlmeans.com'
7
+
5
8
  export interface LoginCreditProps {
6
9
  model: LoginCreditModel
7
10
  translate: (key: string, defaultValue: string) => string
@@ -25,9 +28,13 @@ export const LoginCredit: FC<LoginCreditProps> = ({ model, translate, className
25
28
  data-login-credit
26
29
  className={cn('text-center text-xs text-muted-foreground', className)}
27
30
  >
28
- {model.poweredBy && <span data-login-powered>
31
+ {model.poweredBy && <a
32
+ data-login-powered
33
+ href={OWLMEANS_URL} target="_blank" rel="noopener noreferrer"
34
+ className="underline-offset-4 hover:text-foreground hover:underline"
35
+ >
29
36
  {translate('login.credit.powered', 'Powered by OwlMeans')}
30
- </span>}
37
+ </a>}
31
38
  {model.poweredBy && model.line != null && <span aria-hidden="true"> · </span>}
32
39
  {model.line != null && <span>{model.line}</span>}
33
40
  </p>
@@ -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 />
@@ -43,6 +43,8 @@ describe('@owlmeans/web-panel — the sign-in screen', () => {
43
43
  expect(await page.locator('#login-logo').count()).toBe(1)
44
44
  const credit = await page.locator('[data-login-credit]').textContent()
45
45
  expect(credit).toContain('Powered by OwlMeans')
46
+ expect(await page.locator('[data-login-credit] a[data-login-powered]').getAttribute('href'))
47
+ .toBe('https://owlmeans.com')
46
48
  expect(credit).toContain('Harness')
47
49
  // A copyright NOTICE, not a name: the mark and the year are what make it one.
48
50
  expect(credit).toContain(`© ${new Date().getFullYear()} Acme`)
package/tests/nav.spec.ts CHANGED
@@ -84,7 +84,7 @@ describe('@owlmeans/web-panel — two-layer navigation', () => {
84
84
  const { page, close } = await open('/dash')
85
85
  try {
86
86
  await page.waitForSelector('#dash')
87
- const links = page.locator('footer a')
87
+ const links = page.locator('footer a:not([data-login-powered])')
88
88
  expect(await links.count()).toBe(2)
89
89
  expect(await links.nth(0).getAttribute('href')).toBe('/dash')
90
90
  expect(await links.nth(1).getAttribute('href')).toBe('https://owlmeans.com')
@@ -178,6 +178,89 @@ 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
+ const powered = page.locator('footer [data-shell-credit] a[data-login-powered]')
232
+ expect(await powered.getAttribute('href')).toBe('https://owlmeans.com')
233
+ expect(await powered.getAttribute('target')).toBe('_blank')
234
+ } finally {
235
+ await close()
236
+ }
237
+ }, TIMEOUT)
238
+
239
+ test('the footer links and the credit are centred, not left-aligned', async () => {
240
+ const { page, close } = await open('/dash')
241
+ try {
242
+ await page.waitForSelector('#dash')
243
+ const justify = await page.locator('footer > div').first()
244
+ .evaluate(el => window.getComputedStyle(el).alignItems)
245
+ expect(justify).toBe('center')
246
+ } finally {
247
+ await close()
248
+ }
249
+ }, TIMEOUT)
250
+
251
+ test('a layout with no footer prop still shows the credit', async () => {
252
+ // `NavLayout` used to skip `Footer` entirely when no `footer` prop was passed, which made the
253
+ // credit disappear from any area layout that never wired footer links. `?footer=none` omits
254
+ // the prop from the harness's own `NavLayout` call.
255
+ const { page, close } = await open('/dash?footer=none')
256
+ try {
257
+ await page.waitForSelector('#dash')
258
+ expect(await page.locator('footer a:not([data-login-powered])').count()).toBe(0)
259
+ expect(await page.locator('footer [data-shell-credit]').count()).toBe(1)
260
+ } finally {
261
+ await close()
262
+ }
263
+ }, TIMEOUT)
181
264
  })
182
265
 
183
266
  describe('@owlmeans/web-panel — layout rhythm and link styling', () => {