@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 +7 -5
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/web-panel/SKILL.md +29 -1
- package/build/components/footer/component.d.ts +7 -1
- package/build/components/footer/component.d.ts.map +1 -1
- package/build/components/footer/component.js +17 -8
- package/build/components/footer/component.js.map +1 -1
- package/build/components/footer/credit.d.ts +29 -0
- package/build/components/footer/credit.d.ts.map +1 -0
- package/build/components/footer/credit.js +41 -0
- package/build/components/footer/credit.js.map +1 -0
- package/build/components/footer/index.d.ts +1 -0
- package/build/components/footer/index.d.ts.map +1 -1
- package/build/components/footer/index.js +1 -0
- package/build/components/footer/index.js.map +1 -1
- package/build/components/nav/layout.d.ts.map +1 -1
- package/build/components/nav/layout.js +4 -2
- package/build/components/nav/layout.js.map +1 -1
- package/package.json +29 -29
- package/src/components/footer/component.tsx +24 -12
- package/src/components/footer/credit.tsx +60 -0
- package/src/components/footer/index.ts +1 -0
- package/src/components/nav/layout.tsx +28 -2
- package/tests/harness/mount.tsx +11 -1
- package/tests/nav.spec.ts +80 -0
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ but never started.
|
|
|
11
11
|
## Installation
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
|
-
bun add @owlmeans/web-panel@^0.1.18-rc.
|
|
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.
|
|
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
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/web-panel",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-09-
|
|
4
|
+
"version": "0.1.18-rc.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.
|
|
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;
|
|
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
|
-
|
|
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-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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;
|
|
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 +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 +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,
|
|
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
|
|
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":";AACA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,EAAE,EAAE,MAAM,sBAAsB,CAAA;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAA;AAE/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAGjC;;;;;;;;GAQG;AACH,MAAM,SAAS,GAAG,+BAA+B,CAAA;AAEjD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,SAAS,GAAuB,CAAC,EAC5C,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EACxE,eAAe,EAAE,gBAAgB,EAAE,kBAAkB,EACtD,EAAE,EAAE;IACH,MAAM,SAAS,GAAG,WAAW,EAAE,CAAA;IAC/B,MAAM,UAAU,GAAG,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAA;IAClG,0FAA0F;IAC1F,oFAAoF;IACpF,yFAAyF;IACzF,uFAAuF;IACvF,yFAAyF;IACzF,8FAA8F;IAC9F,+FAA+F;IAC/F,MAAM,SAAS,GAAG,EAAE,CAAC,SAAS,EAAE,kBAAkB,CAAC,CAAA;IAEnD,OAAO,eACL,SAAS,EAAE,EAAE,CAAC,0DAA0D,EAAE,SAAS,CAAC,EACpF,KAAK,EAAE,KAAK,
|
|
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.
|
|
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.
|
|
48
|
-
"@owlmeans/auth": "^0.1.18-rc.
|
|
49
|
-
"@owlmeans/auth-common": "^0.1.18-rc.
|
|
50
|
-
"@owlmeans/basic-envelope": "^0.1.18-rc.
|
|
51
|
-
"@owlmeans/client": "^0.1.18-rc.
|
|
52
|
-
"@owlmeans/client-auth": "^0.1.18-rc.
|
|
53
|
-
"@owlmeans/client-config": "^0.1.18-rc.
|
|
54
|
-
"@owlmeans/client-context": "^0.1.18-rc.
|
|
55
|
-
"@owlmeans/client-entrypoint": "^0.1.18-rc.
|
|
56
|
-
"@owlmeans/client-flow": "^0.1.18-rc.
|
|
57
|
-
"@owlmeans/client-i18n": "^0.1.18-rc.
|
|
58
|
-
"@owlmeans/client-panel": "^0.1.18-rc.
|
|
59
|
-
"@owlmeans/client-route": "^0.1.18-rc.
|
|
60
|
-
"@owlmeans/client-socket": "^0.1.18-rc.
|
|
61
|
-
"@owlmeans/config": "^0.1.18-rc.
|
|
62
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
63
|
-
"@owlmeans/entrypoint": "^0.1.18-rc.
|
|
64
|
-
"@owlmeans/error": "^0.1.18-rc.
|
|
65
|
-
"@owlmeans/flow": "^0.1.18-rc.
|
|
66
|
-
"@owlmeans/i18n": "^0.1.18-rc.
|
|
67
|
-
"@owlmeans/route": "^0.1.18-rc.
|
|
68
|
-
"@owlmeans/state": "^0.1.18-rc.
|
|
69
|
-
"@owlmeans/web-client": "^0.1.18-rc.
|
|
70
|
-
"@owlmeans/web-consent": "^0.1.18-rc.
|
|
71
|
-
"@owlmeans/web-db": "^0.1.18-rc.
|
|
72
|
-
"@owlmeans/web-flow": "^0.1.18-rc.
|
|
73
|
-
"@owlmeans/web-router": "^0.1.18-rc.
|
|
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.
|
|
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
|
-
|
|
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-
|
|
25
|
-
{
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
+
}
|
|
@@ -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
|
|
107
|
+
: footer != null
|
|
108
|
+
? <Footer containerClassName={container}>{footer}</Footer>
|
|
109
|
+
: <Footer containerClassName={container} />}
|
|
84
110
|
</div>
|
|
85
111
|
}
|
package/tests/harness/mount.tsx
CHANGED
|
@@ -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
|
|
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', () => {
|