@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.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/web-panel",
4
- "version": "0.1.18-rc.10",
5
- "generatedAt": "2026-08-25T09:54:15.288Z",
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.10"` in `dependencies`
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', containerClassName ?? 'px-4'), children: [links?.map((link, idx) => {
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,2CAA2C,EAAE,kBAAkB,IAAI,MAAM,CAAC,aAC1F,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,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,CAiDxC,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,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. Override it through `containerClassName` — never by giving the content a width of its
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
- const container = containerClassName ?? CONTAINER;
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,kBAAkB,IAAI,SAAS,CAAA;IAEjD,OAAO,eACL,SAAS,EAAE,EAAE,CAAC,0DAA0D,EAAE,SAAS,CAAC,EACpF,KAAK,EAAE,KAAK,aAkBZ,kBAAQ,SAAS,EAAE,EAAE,CAAC,0DAA0D,EAAE,eAAe,CAAC,aAChG,eAAK,SAAS,EAAE,EAAE,CAAC,8BAA8B,EAAE,SAAS,CAAC,aAC1D,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,YACf,OAAO,EAAE,UAAU,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,EACrE,SAAS,EAAE,EAAE,CAAC,qDAAqD,EAAE,UAAU,IAAI,IAAI,IAAI,gBAAgB,CAAC,YAC5G,KAAK,GAAK,CAAC,CAAC,CAAC,IAAI,EACnB,KAAC,MAAM,IAAC,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAC,UAAU,GAAG,EAClE,cAAK,SAAS,EAAC,iCAAiC,YAAE,OAAO,GAAO,IAC5D,EACN,KAAC,OAAO,IAAC,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAC,KAAK,EAAC,SAAS,EAAC,SAAS,EAAC,SAAS,EAAC,WAAW,GAAG,IAC/F,EACT,eAAK,SAAS,EAAC,aAAa,aAC1B,KAAC,OAAO,IAAC,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAC,MAAM,EAAC,SAAS,EAAC,SAAS,EAAC,SAAS,EAAC,iBAAiB,GAAG,EAC7G,eAAM,SAAS,EAAE,EAAE,CAAC,aAAa,EAAE,gBAAgB,CAAC,YAClD,cAAK,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,YAAG,QAAQ,GAAO,GAC1C,IACH,EACL,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;gBACpB,CAAC,CAAC,KAAC,MAAM,IAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,kBAAkB,EAAE,SAAS,GAAI;gBAChF,CAAC,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,KAAC,MAAM,IAAC,kBAAkB,EAAE,SAAS,YAAG,MAAM,GAAU,CAAC,CAAC,CAAC,IAAI,IAChF,CAAA;AACR,CAAC,CAAA"}
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;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/nav/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AACxF,OAAO,KAAK,EAAE,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.12",
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.11",
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', containerClassName ?? 'px-4')}>
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. Override it through `containerClassName` — never by giving the content a width of its
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
- const container = containerClassName ?? CONTAINER
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
  }
@@ -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 {