@owlmeans/web-panel 0.1.18-rc.12 → 0.1.18-rc.13
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/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/web-panel/SKILL.md +39 -2
- package/build/components/footer/component.js +1 -1
- package/build/components/footer/component.js.map +1 -1
- package/build/components/nav/layout.d.ts.map +1 -1
- package/build/components/nav/layout.js +10 -3
- package/build/components/nav/layout.js.map +1 -1
- package/build/components/nav/types.d.ts +5 -0
- package/build/components/nav/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/footer/component.tsx +1 -1
- package/src/components/nav/layout.tsx +10 -3
- package/src/components/nav/types.ts +5 -0
- package/tests/harness/mount.tsx +4 -0
- package/tests/nav.spec.ts +28 -0
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-08-
|
|
4
|
+
"version": "0.1.18-rc.13",
|
|
5
|
+
"generatedAt": "2026-08-25T21:16:30.725Z",
|
|
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.13"` in `dependencies`
|
|
12
12
|
|
|
13
13
|
## Key Exports
|
|
14
14
|
|
|
@@ -83,11 +83,48 @@ header. Both render the same items; only one is visible at a time.
|
|
|
83
83
|
|
|
84
84
|
| Component | Props |
|
|
85
85
|
|---|---|
|
|
86
|
-
| `NavLayout` | `nav: PanelNavConfig`, `translate?`, `title?: ReactNode`, `home?: string` (brand target — defaults to the first section's first item), `actions?: ReactNode`, `footer?: PanelNavLink[] \| ReactNode`, `contentClassName?`, `className?`, `style?` |
|
|
86
|
+
| `NavLayout` | `nav: PanelNavConfig`, `translate?`, `title?: ReactNode`, `home?: string` (brand target — defaults to the first section's first item), `actions?: ReactNode`, `footer?: PanelNavLink[] \| ReactNode`, `headerClassName?`, `contentClassName?`, `containerClassName?`, `className?`, `style?` |
|
|
87
87
|
| `TopNav` | `config: PanelNavConfig`, `translate?`, `ariaLabel?`, `className?`, `style?` |
|
|
88
88
|
| `SideNav` | the same, plus `variant?: 'side' \| 'bar'` |
|
|
89
89
|
| `Footer` | `links?: PanelNavLink[]`, `translate?`, `children?`, `className?`, `style?` |
|
|
90
90
|
|
|
91
|
+
**The style slots are REGIONS, and each region is its own SURFACE.** `className` is the root —
|
|
92
|
+
the full-height page *behind* the header, side menu and footer. `headerClassName` is the sticky
|
|
93
|
+
top bar. `contentClassName` is the content area. `containerClassName` is width and padding for
|
|
94
|
+
all three at once, never colour.
|
|
95
|
+
|
|
96
|
+
The header paints an opaque background of its own, because it is sticky and content scrolls
|
|
97
|
+
beneath it. That makes it a **different surface from the root**, so it states `text-foreground`
|
|
98
|
+
alongside its `bg-background` — not as decoration, and not redundantly. A root carrying a
|
|
99
|
+
contrasting pair (`className="bg-primary text-primary-foreground"`, an ordinary dark shell)
|
|
100
|
+
otherwise inherits its near-white foreground into a near-white bar, and every header child that
|
|
101
|
+
states no colour of its own — the brand, a ghost-variant action button — is painted in the
|
|
102
|
+
foreground of a surface it is not on. It type-checks, it builds, it renders, and the menu is
|
|
103
|
+
invisible. Pinned by `tests/nav.spec.ts` → "the header is its own surface", which measures
|
|
104
|
+
rendered lightness rather than class names; the harness root carries a dark shell permanently so
|
|
105
|
+
every navigation test runs against that case.
|
|
106
|
+
|
|
107
|
+
**Every style slot is MERGED over its default — none of them substitutes.** `className`,
|
|
108
|
+
`headerClassName`, `contentClassName` and `containerClassName` all go through `cn`, so a caller
|
|
109
|
+
names only the utility it wants to move and tailwind-merge drops just the one it conflicts with.
|
|
110
|
+
This matters most for `containerClassName`, whose default is a four-part rhythm
|
|
111
|
+
(`mx-auto w-full max-w-6xl px-4`): a design asking for a wider page writes `max-w-[1280px]` and
|
|
112
|
+
means *wider*, not *unpadded and uncentred*. Substituting there took `px-4` and `mx-auto` down
|
|
113
|
+
with the width and left the header, content and footer all flush to the window edge. Pinned by
|
|
114
|
+
`nav.spec.ts` → "a width-only rhythm override keeps the side padding"; the harness passes a
|
|
115
|
+
width-only override permanently.
|
|
116
|
+
|
|
117
|
+
**A dark top bar is asked for with `headerClassName`**, giving it both halves
|
|
118
|
+
(`bg-secondary text-secondary-foreground`) — never by colouring the root and expecting the bar
|
|
119
|
+
to follow.
|
|
120
|
+
|
|
121
|
+
**The shell reads exactly five theme variables**: `--background` (page and top bar),
|
|
122
|
+
`--foreground` (active section link), `--muted-foreground` (resting section links) and
|
|
123
|
+
`--accent`/`--accent-foreground` (active side-menu item). It reads **no `--sidebar*` variable at
|
|
124
|
+
all**. So `--muted-foreground` is not merely the text colour of the `--muted` surface — it is
|
|
125
|
+
secondary text sitting directly on `--background`, and a theme that lightens it to suit a dark
|
|
126
|
+
muted panel loses its top menu.
|
|
127
|
+
|
|
91
128
|
Rules that make the shell behave:
|
|
92
129
|
|
|
93
130
|
- **Labels never reach for i18n implicitly.** `translate` is a **prop** (`NavTranslate`), defaulting
|
|
@@ -14,7 +14,7 @@ export const Footer = ({ links, translate = defaultNavTranslate, children, class
|
|
|
14
14
|
if ((links == null || links.length < 1) && children == null) {
|
|
15
15
|
return null;
|
|
16
16
|
}
|
|
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
|
|
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
18
|
const label = resolveNavLabel(translate, link.label, `modules.${link.alias ?? link.href ?? ''}`, link.alias ?? link.href);
|
|
19
19
|
return link.href != null
|
|
20
20
|
? _jsx(Link, { src: link.href, open: link.open, children: label }, `${link.href}:${idx}`)
|
|
@@ -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,aAAa,CAAA;AAChC,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,
|
|
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,aAAa,CAAA;AAChC,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 +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,CAwDxC,CAAA"}
|
|
@@ -10,8 +10,8 @@ import { TopNav } from './top.js';
|
|
|
10
10
|
*
|
|
11
11
|
* It lives in ONE constant because the three regions have to agree: a content area with its own
|
|
12
12
|
* width sits visibly inset from a full-width header, which reads as a mistake rather than as a
|
|
13
|
-
* design.
|
|
14
|
-
* own.
|
|
13
|
+
* design. Adjust it through `containerClassName`, which is MERGED over this — never by giving
|
|
14
|
+
* the content a width of its own.
|
|
15
15
|
*/
|
|
16
16
|
const CONTAINER = 'mx-auto w-full max-w-6xl px-4';
|
|
17
17
|
/**
|
|
@@ -25,7 +25,14 @@ const CONTAINER = 'mx-auto w-full max-w-6xl px-4';
|
|
|
25
25
|
export const NavLayout = ({ nav, translate, title, home, actions, footer, children, className, style, headerClassName, contentClassName, containerClassName }) => {
|
|
26
26
|
const navigator = useNavigate();
|
|
27
27
|
const brandAlias = home ?? nav.sections.find(section => section.items.length > 0)?.items[0]?.alias;
|
|
28
|
-
|
|
28
|
+
// MERGED over the default, never substituted for it. `containerClassName` is how a design
|
|
29
|
+
// adjusts ONE aspect of the rhythm — almost always the width — and a caller passing
|
|
30
|
+
// `max-w-[1280px]` means "wider", not "no padding and no centring". Substituting dropped
|
|
31
|
+
// `px-4` and `mx-auto` along with the width it replaced, which is a page whose header,
|
|
32
|
+
// content and footer all run flush to the window edge. tailwind-merge keeps the override
|
|
33
|
+
// winning on the utility it names and leaves the rest of the rhythm standing, so a width-only
|
|
34
|
+
// value stays a width-only change; `px-8` still overrides the padding when that is the intent.
|
|
35
|
+
const container = cn(CONTAINER, containerClassName);
|
|
29
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)
|
|
30
37
|
? _jsx(Footer, { links: footer, translate: translate, containerClassName: container })
|
|
31
38
|
: footer != null ? _jsx(Footer, { containerClassName: container, children: footer }) : null] });
|
|
@@ -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,aAAa,CAAA;AAChC,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,MAAM,SAAS,GAAG,
|
|
1
|
+
{"version":3,"file":"layout.js","sourceRoot":"","sources":["../../../src/components/nav/layout.tsx"],"names":[],"mappings":"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"}
|
|
@@ -43,6 +43,11 @@ export interface NavLayoutProps extends PropsWithChildren<StyledProps> {
|
|
|
43
43
|
* The page's horizontal rhythm — width and side padding — applied identically to the header
|
|
44
44
|
* row, the content and the footer row. Give the content a width of its own and it sits inset
|
|
45
45
|
* from a full-width header, which reads as a bug rather than as a layout.
|
|
46
|
+
*
|
|
47
|
+
* MERGED over the shell's default (`mx-auto w-full max-w-6xl px-4`), not substituted for it:
|
|
48
|
+
* pass `max-w-[1280px]` and only the width changes, while the centring and the side padding
|
|
49
|
+
* stay. Name the utility you actually want to move — `px-8` widens the gutters — because
|
|
50
|
+
* anything you do not name keeps its default.
|
|
46
51
|
*/
|
|
47
52
|
containerClassName?: string;
|
|
48
53
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/nav/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AACxF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAE9C,UAAU,cAAe,SAAQ,WAAW;IAC1C,MAAM,EAAE,cAAc,CAAA;IACtB,yFAAyF;IACzF,SAAS,CAAC,EAAE,YAAY,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,WAAY,SAAQ,cAAc;CAAI;AAEvD,MAAM,WAAW,YAAa,SAAQ,cAAc;IAClD;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;CACzB;AAED,MAAM,WAAW,cAAe,SAAQ,iBAAiB,CAAC,WAAW,CAAC;IACpE,GAAG,EAAE,cAAc,CAAA;IACnB,SAAS,CAAC,EAAE,YAAY,CAAA;IACxB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,gFAAgF;IAChF,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,yFAAyF;IACzF,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,YAAY,EAAE,GAAG,SAAS,CAAA;IACnC;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,yEAAyE;IACzE,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/nav/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AACxF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAE9C,UAAU,cAAe,SAAQ,WAAW;IAC1C,MAAM,EAAE,cAAc,CAAA;IACtB,yFAAyF;IACzF,SAAS,CAAC,EAAE,YAAY,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,WAAY,SAAQ,cAAc;CAAI;AAEvD,MAAM,WAAW,YAAa,SAAQ,cAAc;IAClD;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;CACzB;AAED,MAAM,WAAW,cAAe,SAAQ,iBAAiB,CAAC,WAAW,CAAC;IACpE,GAAG,EAAE,cAAc,CAAA;IACnB,SAAS,CAAC,EAAE,YAAY,CAAA;IACxB,2DAA2D;IAC3D,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,gFAAgF;IAChF,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,yFAAyF;IACzF,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,YAAY,EAAE,GAAG,SAAS,CAAA;IACnC;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,yEAAyE;IACzE,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB;;;;;;;;;OASG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B"}
|
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.13",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@owlmeans/client-flow": "^0.1.18-rc.7",
|
|
49
49
|
"@owlmeans/client-i18n": "^0.1.18-rc.7",
|
|
50
50
|
"@owlmeans/client-entrypoint": "^0.1.18-rc.7",
|
|
51
|
-
"@owlmeans/client-panel": "^0.1.18-rc.
|
|
51
|
+
"@owlmeans/client-panel": "^0.1.18-rc.12",
|
|
52
52
|
"@owlmeans/client-route": "^0.1.18-rc.7",
|
|
53
53
|
"@owlmeans/config": "^0.1.18-rc.7",
|
|
54
54
|
"@owlmeans/context": "^0.1.18-rc.6",
|
|
@@ -21,7 +21,7 @@ export const Footer: FC<FooterProps> = ({
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
return <footer className={cn('border-t py-6', className)} style={style}>
|
|
24
|
-
<div className={cn('flex flex-wrap items-center gap-4 text-sm
|
|
24
|
+
<div className={cn('flex flex-wrap items-center gap-4 text-sm px-4', containerClassName)}>
|
|
25
25
|
{links?.map((link, idx) => {
|
|
26
26
|
const label = resolveNavLabel(
|
|
27
27
|
translate, link.label, `modules.${link.alias ?? link.href ?? ''}`, link.alias ?? link.href
|
|
@@ -13,8 +13,8 @@ import type { NavLayoutProps } from './types.js'
|
|
|
13
13
|
*
|
|
14
14
|
* It lives in ONE constant because the three regions have to agree: a content area with its own
|
|
15
15
|
* width sits visibly inset from a full-width header, which reads as a mistake rather than as a
|
|
16
|
-
* design.
|
|
17
|
-
* own.
|
|
16
|
+
* design. Adjust it through `containerClassName`, which is MERGED over this — never by giving
|
|
17
|
+
* the content a width of its own.
|
|
18
18
|
*/
|
|
19
19
|
const CONTAINER = 'mx-auto w-full max-w-6xl px-4'
|
|
20
20
|
|
|
@@ -32,7 +32,14 @@ export const NavLayout: FC<NavLayoutProps> = ({
|
|
|
32
32
|
}) => {
|
|
33
33
|
const navigator = useNavigate()
|
|
34
34
|
const brandAlias = home ?? nav.sections.find(section => section.items.length > 0)?.items[0]?.alias
|
|
35
|
-
|
|
35
|
+
// MERGED over the default, never substituted for it. `containerClassName` is how a design
|
|
36
|
+
// adjusts ONE aspect of the rhythm — almost always the width — and a caller passing
|
|
37
|
+
// `max-w-[1280px]` means "wider", not "no padding and no centring". Substituting dropped
|
|
38
|
+
// `px-4` and `mx-auto` along with the width it replaced, which is a page whose header,
|
|
39
|
+
// content and footer all run flush to the window edge. tailwind-merge keeps the override
|
|
40
|
+
// winning on the utility it names and leaves the rest of the rhythm standing, so a width-only
|
|
41
|
+
// value stays a width-only change; `px-8` still overrides the padding when that is the intent.
|
|
42
|
+
const container = cn(CONTAINER, containerClassName)
|
|
36
43
|
|
|
37
44
|
return <div
|
|
38
45
|
className={cn('flex min-h-screen flex-col bg-background text-foreground', className)}
|
|
@@ -46,6 +46,11 @@ export interface NavLayoutProps extends PropsWithChildren<StyledProps> {
|
|
|
46
46
|
* The page's horizontal rhythm — width and side padding — applied identically to the header
|
|
47
47
|
* row, the content and the footer row. Give the content a width of its own and it sits inset
|
|
48
48
|
* from a full-width header, which reads as a bug rather than as a layout.
|
|
49
|
+
*
|
|
50
|
+
* MERGED over the shell's default (`mx-auto w-full max-w-6xl px-4`), not substituted for it:
|
|
51
|
+
* pass `max-w-[1280px]` and only the width changes, while the centring and the side padding
|
|
52
|
+
* stay. Name the utility you actually want to move — `px-8` widens the gutters — because
|
|
53
|
+
* anything you do not name keeps its default.
|
|
49
54
|
*/
|
|
50
55
|
containerClassName?: string
|
|
51
56
|
}
|
package/tests/harness/mount.tsx
CHANGED
|
@@ -59,6 +59,10 @@ const Layout: FC<PropsWithChildren> = ({ children }) => <NavLayout
|
|
|
59
59
|
// rather than against this. The harness carries it permanently so every navigation test
|
|
60
60
|
// runs against the hostile case instead of a default-coloured page.
|
|
61
61
|
className="bg-primary text-primary-foreground"
|
|
62
|
+
// A WIDTH-ONLY rhythm override, which is what a design pass writes when it wants a wider
|
|
63
|
+
// page. It names the width and nothing else, so the centring and the side padding must
|
|
64
|
+
// survive it — substituting this for the default is a page running flush to the window edge.
|
|
65
|
+
containerClassName="max-w-[1280px]"
|
|
62
66
|
>{children}</NavLayout>
|
|
63
67
|
|
|
64
68
|
/** A grouping screen — it renders whichever child the router matched. */
|
package/tests/nav.spec.ts
CHANGED
|
@@ -203,6 +203,34 @@ describe('@owlmeans/web-panel — layout rhythm and link styling', () => {
|
|
|
203
203
|
}
|
|
204
204
|
}, TIMEOUT)
|
|
205
205
|
|
|
206
|
+
test('a width-only rhythm override keeps the side padding', async () => {
|
|
207
|
+
// The harness passes `containerClassName="max-w-[1280px]"` — a width and nothing else.
|
|
208
|
+
// `containerClassName` is merged over the shell's default rhythm rather than substituted
|
|
209
|
+
// for it, so `px-4` and `mx-auto` survive an override that never mentioned them. Replacing
|
|
210
|
+
// instead produced a header, content and footer all flush to the window edge.
|
|
211
|
+
const { page, close } = await open('/prefs')
|
|
212
|
+
try {
|
|
213
|
+
await page.waitForSelector('#prefs')
|
|
214
|
+
const padding = async (selector: string) => page.locator(selector).first().evaluate(el => {
|
|
215
|
+
const style = window.getComputedStyle(el)
|
|
216
|
+
|
|
217
|
+
return [parseFloat(style.paddingLeft), parseFloat(style.paddingRight)]
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
const [header, content, footer] = await Promise.all([
|
|
221
|
+
padding('header > div'), padding('main > div'), padding('footer > div'),
|
|
222
|
+
])
|
|
223
|
+
|
|
224
|
+
expect({
|
|
225
|
+
header: header.every(value => value > 0),
|
|
226
|
+
content: content.every(value => value > 0),
|
|
227
|
+
footer: footer.every(value => value > 0),
|
|
228
|
+
}).toEqual({ header: true, content: true, footer: true })
|
|
229
|
+
} finally {
|
|
230
|
+
await close()
|
|
231
|
+
}
|
|
232
|
+
}, TIMEOUT)
|
|
233
|
+
|
|
206
234
|
test('section entries render as links, not buttons', async () => {
|
|
207
235
|
const { page, close } = await open('/dash')
|
|
208
236
|
try {
|