ab-nextjs-core 0.1.4 โ†’ 0.1.6

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.
@@ -27,7 +27,7 @@
27
27
  * @name: AB Screen Layout - Component Stylesheet (plain CSS, Tailwind-independent)
28
28
  * @file: ab-screen-layout/styles.module.css
29
29
  * @type: CSS Module
30
- * @authors: Abraham Ukachi <abraham.ukachi@laplateforme.io>
30
+ * @authors: Abraham Ukachi <abrahamukachi@gmail.com>
31
31
  */
32
32
 
33
33
  /*
package/index.ts CHANGED
@@ -27,7 +27,7 @@
27
27
  * @name: Core - Package Catalog
28
28
  * @file: index.ts
29
29
  * @type: TypeScript
30
- * @authors: Abraham Ukachi <abraham.ukachi@laplateforme.io>
30
+ * @authors: Abraham Ukachi <abrahamukachi@gmail.com>
31
31
  */
32
32
 
33
33
  /*
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ab-nextjs-core",
3
- "version": "0.1.4",
4
- "description": "\ud83c\udf31 A lightweight collection of server & client React components as core abElements (i.e. ab-app-layout, ab-screen-layout, ...) created by Abraham Ukachi, and optimized for Next.js applications \ud83d\ude0e.",
3
+ "version": "0.1.6",
4
+ "description": "๐ŸŒฑ A lightweight collection of server & client React components as core abElements (i.e. ab-app-layout, ab-screen-layout, ...) created by Abraham Ukachi, and optimized for Next.js applications ๐Ÿ˜Ž.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
7
7
  "types": "types/index.d.ts",
@@ -16,6 +16,7 @@
16
16
  "scripts": {
17
17
  "lint": "eslint . --fix",
18
18
  "test": "vitest run",
19
+ "typecheck": "tsc --noEmit -p .",
19
20
  "check:publish": "node scripts/check-publish-deps.mjs",
20
21
  "prepublishOnly": "node scripts/check-publish-deps.mjs"
21
22
  },
@@ -31,7 +32,7 @@
31
32
  "author": {
32
33
  "name": "Abraham Ukachi",
33
34
  "url": "https://github.com/abraham-ukachi",
34
- "email": "abraham.ukachi@laplateforme.io"
35
+ "email": "abrahamukachi@gmail.com"
35
36
  },
36
37
  "license": "MIT",
37
38
  "peerDependencies": {
@@ -24,7 +24,7 @@
24
24
  * SOFTWARE.
25
25
  *
26
26
  * @project: ab-nextjs-core
27
- * @authors: Abraham Ukachi <abraham.ukachi@laplateforme.io>
27
+ * @authors: Abraham Ukachi <abrahamukachi@gmail.com>
28
28
  */
29
29
 
30
30
  /*
@@ -33,10 +33,12 @@
33
33
  * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
34
34
  */
35
35
 
36
- import type { ReactElement, ReactNode } from 'react';
36
+ import { isValidElement, type ReactElement, type ReactNode } from 'react';
37
37
  import clsx from 'clsx';
38
38
  import AbLinearProgress from '../../ab-linear-progress';
39
39
  import styles from './styles.module.css';
40
+ // ๐Ÿซฅ G50: empty-slot helper
41
+ import { toAbSlot } from '../../ab-page-provider/slot';
40
42
 
41
43
  export interface AbAppLayoutProps {
42
44
  orientation: 'vertical' | 'horizontal';
@@ -50,6 +52,13 @@ export interface AbAppLayoutProps {
50
52
  footer?: ReactNode;
51
53
  children?: ReactNode;
52
54
  menus?: ReactNode;
55
+ /** ๐Ÿงญ 0.1.6 (G17): `'panes'` = the docs shell (4 breakpoints, rail, overlay aside). Default keeps the old layout. */
56
+ variant?: 'default' | 'panes';
57
+ /** ๐Ÿงท 0.1.6: collapse the sidebar to the icon rail (tablet and up, `variant="panes"`). */
58
+ railCollapsed?: boolean;
59
+ /** ๐Ÿงฐ 0.1.6: render the legacy #backdrop / #menus / #dialogs / #toasts / progress / canvas layers.
60
+ * Default: on for `variant="default"`, off for `"panes"` (the shell brings its own menus and dialogs). */
61
+ overlays?: boolean;
53
62
  }
54
63
 
55
64
  /**
@@ -68,17 +77,33 @@ const AbAppLayout = ({
68
77
  footer,
69
78
  children,
70
79
  menus,
80
+ variant = 'default', // ๐Ÿงญ
81
+ railCollapsed = false, // ๐Ÿงท
82
+ overlays, // ๐Ÿงฐ
71
83
  }: AbAppLayoutProps): ReactElement => {
84
+ // ๐Ÿงฑ panes: the header slot is a plain grid row (0px when empty). Its child (e.g. AbAppBar) brings its own
85
+ // <header>, so we never nest one header inside another. Same when the slot child IS a <header>.
86
+ const HeaderTag = variant === 'panes' || (isValidElement(header) && header.type === 'header') ? 'div' : 'header';
87
+ // ๐Ÿงฐ legacy overlay layers (off in panes unless asked for)
88
+ const showOverlays = overlays ?? variant !== 'panes';
89
+ // ๐Ÿซฅ G50 (D37): panes never render an empty header / footer row (0px); the CSS :empty / :has() catch the rest
90
+ const top = toAbSlot(header);
91
+ const showHeader = variant !== 'panes' || top != null;
92
+ const showFooter = variant !== 'panes' || toAbSlot(footer) != null;
72
93
  return (
73
94
  <div
74
95
  className={clsx(['AbAppLayout', styles.abAppLayout, className])}
75
96
  hidden={hidden}
76
97
  data-page-name={pageName}
77
98
  data-orientation={orientation ?? ''}
99
+ data-variant={variant} // ๐Ÿงญ CSS hook for the panes
100
+ data-rail={railCollapsed ? 'true' : 'false'} // ๐Ÿงท
78
101
  >
79
- <header className={clsx(['HeaderWrapper', styles.abAppLayout__headerWrapper])}>
80
- {header}
81
- </header>
102
+ {showHeader && (
103
+ <HeaderTag className={clsx(['HeaderWrapper', styles.abAppLayout__headerWrapper])} data-ab-part="header">
104
+ {variant === 'panes' ? top : header}
105
+ </HeaderTag>
106
+ )}
82
107
 
83
108
  <div className={clsx(['ContentWrapper', styles.abAppLayout__contentWrapper])}>
84
109
  {sideBar}
@@ -86,26 +111,32 @@ const AbAppLayout = ({
86
111
  {navBar}
87
112
  </div>
88
113
 
89
- <footer className={clsx(['FooterWrapper', styles.abAppLayout__footerWrapper])}>
90
- {footer}
91
- </footer>
92
-
93
- <div id="backdrop" className={clsx(['Backdrop', styles.abAppLayout__backdrop])} hidden />
94
- <div id="menus" className={clsx(['Menus', styles.abAppLayout__menus])} hidden>
95
- {menus}
96
- </div>
97
- <div id="dialogs" className={clsx(['Dialogs', styles.abAppLayout__dialogs])} hidden />
98
- <div id="toasts" className={clsx(['Toasts', styles.abAppLayout__toasts])} hidden />
99
-
100
- <AbLinearProgress
101
- className="absolute top-0 left-0 right-0 z-50 !h-0.5 lg:!h-1"
102
- color="secondary"
103
- hidden={true}
104
- isIndeterminate={true}
105
- progress={25}
106
- />
107
-
108
- <canvas className={clsx(['Canvas', styles.abAppLayout__canvas])} />
114
+ {showFooter && (
115
+ <footer className={clsx(['FooterWrapper', styles.abAppLayout__footerWrapper])} data-ab-part="footer">
116
+ {footer}
117
+ </footer>
118
+ )}
119
+ {/* ๐Ÿงฐ legacy layers (ids kept for old apps) */}
120
+ {showOverlays && (
121
+ <>
122
+ <div id="backdrop" className={clsx(['Backdrop', styles.abAppLayout__backdrop])} hidden />
123
+ <div id="menus" className={clsx(['Menus', styles.abAppLayout__menus])} hidden>
124
+ {menus}
125
+ </div>
126
+ <div id="dialogs" className={clsx(['Dialogs', styles.abAppLayout__dialogs])} hidden />
127
+ <div id="toasts" className={clsx(['Toasts', styles.abAppLayout__toasts])} hidden />
128
+ <AbLinearProgress
129
+ className="absolute top-0 left-0 right-0 z-50 !h-0.5 lg:!h-1"
130
+ color="secondary"
131
+ hidden={true}
132
+ isIndeterminate={true}
133
+ progress={25}
134
+ />
135
+ <canvas className={clsx(['Canvas', styles.abAppLayout__canvas])} />
136
+ </>
137
+ )}
138
+ {/* ๐Ÿ“‹ panes: still render menus when the app passes some */}
139
+ {!showOverlays && menus != null && <div className={clsx(['Menus', styles.abAppLayout__menus])}>{menus}</div>}
109
140
  </div>
110
141
  );
111
142
  };
@@ -27,7 +27,7 @@
27
27
  * @name: AB App Layout - Component Stylesheet (plain CSS, Tailwind-independent)
28
28
  * @file: server/ab-app-layout/styles.module.css
29
29
  * @type: CSS Module
30
- * @authors: Abraham Ukachi <abraham.ukachi@laplateforme.io>
30
+ * @authors: Abraham Ukachi <abrahamukachi@gmail.com>
31
31
  */
32
32
 
33
33
  /*
@@ -143,3 +143,220 @@
143
143
  from { opacity: 0; }
144
144
  to { opacity: 1; }
145
145
  }
146
+
147
+ /* ================================================================== */
148
+ /* ๐Ÿงญ 0.1.6 (G17 + D34/D35 layout rule): docs-shell panes */
149
+ /* opt in with variant="panes". Children of the content wrapper */
150
+ /* are matched by data-ab-part: */
151
+ /* sidebar | rail (nav) ยท main ยท aside ยท bottomBar (nav) */
152
+ /* ๐Ÿ“ฑ <768 ยท ๐Ÿ“Ÿ 768โ€“1023 ยท ๐Ÿ’ป 1024โ€“1439 joined ยท ๐Ÿ–ฅ๏ธ โ‰ฅ1440 gapped */
153
+ /* */
154
+ /* ๐Ÿ“ THE LAYOUT RULE (God, D34): */
155
+ /* ยท the app is ONE 100dvh box and the DOCUMENT NEVER SCROLLS */
156
+ /* ยท header / footer rows are 0px (not rendered / :empty) when empty*/
157
+ /* ยท sidebar, main content and aside content each scroll on their */
158
+ /* own (min-height:0 + overflow:auto + overscroll containment) */
159
+ /* ยท no hard-coded bar heights: rows size to their content */
160
+ /* Tokens: --ab-pane-gap / --ab-pane-radius / --ab-divider-hairline */
161
+ /* (ab-nextjs-theme โ‰ฅ 0.3.4), with safe fallbacks */
162
+ /* ================================================================== */
163
+
164
+ /* ๐Ÿ”’ G54: lock the document while a panes layout is on the page (CSS-only via :has, so the server twin
165
+ * works without JS; the client twin also sets html[data-ab-layout-lock] for browsers without :has) */
166
+ :global(html:has(.AbAppLayout[data-variant="panes"])),
167
+ :global(html:has(.AbAppLayout[data-variant="panes"])) :global(body),
168
+ :global(html[data-ab-layout-lock]),
169
+ :global(html[data-ab-layout-lock]) :global(body) {
170
+ height: 100%; /* ๐Ÿ“ */
171
+ overflow: hidden; /* ๐Ÿšซ the page itself never scrolls */
172
+ overscroll-behavior: none; /* ๐Ÿšซ no iOS rubber band pulling the whole app */
173
+ }
174
+
175
+ .abAppLayout[data-variant="panes"] {
176
+ --ab-sidebar-width: 280px; /* ๐Ÿ—‚๏ธ expanded sidebar */
177
+ --ab-rail-width: 72px; /* ๐Ÿงท collapsed rail (icons only) */
178
+ --ab-aside-width: min(40vw, 360px); /* ๐Ÿ“‘ "On this page" */
179
+ --ab-rail-duration: 200ms; /* ๐ŸŽž๏ธ rail collapse (brief ยง4) */
180
+ display: grid; /* ๐Ÿงฑ rows: header ยท panes ยท footer */
181
+ grid-template-rows: auto minmax(0, 1fr) auto; /* ๐Ÿ“ empty header/footer rows are 0px */
182
+ grid-template-columns: minmax(0, 1fr); /* ๐Ÿšซ no horizontal overflow */
183
+ width: 100%; /* โ†”๏ธ */
184
+ height: 100vh; /* ๐Ÿ•ฐ๏ธ fallback */
185
+ height: 100dvh; /* ๐Ÿ“ฑ the dynamic viewport (mobile toolbars) */
186
+ min-height: 0; /* ๐Ÿšซ */
187
+ overflow: hidden; /* ๐Ÿ”’ the app box never scrolls */
188
+ overscroll-behavior: contain; /* ๐Ÿšซ scroll chaining */
189
+ }
190
+
191
+ /* ๐Ÿ”๐Ÿฆถ app header / footer: plain grid rows in the flow, 0px + invisible when empty (G50) */
192
+ .abAppLayout[data-variant="panes"] > .abAppLayout__headerWrapper,
193
+ .abAppLayout[data-variant="panes"] > .abAppLayout__footerWrapper {
194
+ position: relative; /* ๐Ÿ“Œ in the flow (no overlay) */
195
+ inset: auto; /* ๐Ÿงน the legacy absolute overlay */
196
+ z-index: 25; /* โฌ†๏ธ above the panes, below the sheet (30) */
197
+ pointer-events: auto; /* ๐Ÿ–ฑ๏ธ the legacy wrapper disabled them */
198
+ min-width: 0; /* ๐Ÿšซ overflow */
199
+ padding-inline: env(safe-area-inset-left, 0px) env(safe-area-inset-right, 0px); /* ๐Ÿ“ฑ notch */
200
+ }
201
+ .abAppLayout[data-variant="panes"] > .abAppLayout__headerWrapper { grid-row: 1; padding-top: env(safe-area-inset-top, 0px); } /* ๐Ÿ” row 1 ยท ๐Ÿ“ฑ */
202
+ /* ๐Ÿ“ WebKit e2e (2026-10-20): pin the panes to the 1fr row. Auto-placement moved them into row 1 (auto) when
203
+ the header was absent or collapsed, and the panes measured 0px tall */
204
+ .abAppLayout[data-variant="panes"] > .abAppLayout__contentWrapper { grid-row: 2; } /* ๐Ÿงฑ the minmax(0,1fr) row */
205
+ .abAppLayout[data-variant="panes"] > .abAppLayout__footerWrapper { grid-row: 3; padding-bottom: env(safe-area-inset-bottom, 0px); } /* ๐Ÿ“ฑ */
206
+ /* ๐Ÿซฅ D37: not rendered when the prop is null; :empty / :has() collapse what still reaches the DOM */
207
+ .abAppLayout[data-variant="panes"] > .abAppLayout__headerWrapper:empty,
208
+ .abAppLayout[data-variant="panes"] > .abAppLayout__footerWrapper:empty,
209
+ .abAppLayout[data-variant="panes"] > .abAppLayout__headerWrapper:not(:has(> *)),
210
+ .abAppLayout[data-variant="panes"] > .abAppLayout__footerWrapper:not(:has(> *)) {
211
+ display: none; /* ๐Ÿ“ 0px */
212
+ }
213
+ /* ๐Ÿ“‹ menus the app passes: no full-screen click shield */
214
+ .abAppLayout[data-variant="panes"] > .abAppLayout__menus {
215
+ display: contents; /* ๐Ÿซฅ */
216
+ }
217
+
218
+ /* ๐Ÿงฑ the panes row: a bounded box (the positioning context for sheets and future drawers) */
219
+ .abAppLayout[data-variant="panes"] .abAppLayout__contentWrapper {
220
+ position: relative; /* ๐Ÿ“ sheets / drawers are scoped to it */
221
+ flex-direction: column; /* ๐Ÿ“ฑ stack */
222
+ height: 100%; /* ๐Ÿ“ the 1fr row */
223
+ min-height: 0; /* ๐Ÿšซ lets the children shrink and scroll */
224
+ min-width: 0; /* ๐Ÿšซ overflow */
225
+ overflow: hidden; /* ๐Ÿ”’ */
226
+ justify-content: flex-start; /* โฌ†๏ธ */
227
+ }
228
+
229
+ /* ๐Ÿ“œ every pane: bounded, scrolls on its own, never chains to the page (G51 + G54) */
230
+ .abAppLayout[data-variant="panes"] .abAppLayout__contentWrapper > :is([data-ab-part="sidebar"], [data-ab-part="rail"], [data-ab-part="main"], [data-ab-part="aside"]) {
231
+ min-height: 0; /* ๐Ÿšซ */
232
+ max-height: none; /* ๐Ÿงน legacy 100vh caps */
233
+ box-sizing: border-box; /* ๐Ÿ“ฆ */
234
+ overscroll-behavior: contain; /* ๐Ÿšซ scroll chaining */
235
+ }
236
+
237
+ /* ๐Ÿ“ฑ mobile: one column, no sidebar, bottom bar visible, aside only as a full-pane sheet */
238
+ .abAppLayout[data-variant="panes"] .abAppLayout__contentWrapper > :is([data-ab-part="sidebar"], [data-ab-part="rail"]) {
239
+ display: none; /* ๐Ÿ™ˆ the bottom bar takes over */
240
+ }
241
+ .abAppLayout[data-variant="panes"] .abAppLayout__contentWrapper > [data-ab-part="main"] {
242
+ flex: 1 1 0; /* โ†”๏ธโ†•๏ธ the rest of the box */
243
+ min-width: 0; /* ๐Ÿšซ overflow */
244
+ height: auto; /* ๐Ÿ“ the flex item size */
245
+ overflow: hidden; /* ๐Ÿ”’ its content wrapper is the scroller */
246
+ }
247
+ .abAppLayout[data-variant="panes"] .abAppLayout__contentWrapper > [data-ab-part="aside"] {
248
+ position: absolute; /* ๐Ÿ“„ sheet over main, INSIDE the app box */
249
+ inset: 0; /* ๐Ÿงฑ 100% (TOC button) */
250
+ width: 100%;
251
+ height: auto; /* ๐Ÿ“ from inset */
252
+ z-index: 30; /* โฌ†๏ธ above main + header, below menus/dialogs (50) */
253
+ overflow: hidden; /* ๐Ÿ”’ its content wrapper scrolls */
254
+ }
255
+ .abAppLayout[data-variant="panes"] .abAppLayout__contentWrapper > [data-ab-part="aside"]:not([data-open-on-mobile="true"]) {
256
+ display: none; /* ๐Ÿ™ˆ closed by default on mobile */
257
+ }
258
+ /* ๐ŸŒซ๏ธ sheet backdrop (rendered by AbAsideLayout): only while the sheet is open */
259
+ .abAppLayout[data-variant="panes"] .abAppLayout__contentWrapper > [data-ab-part="aside-backdrop"] {
260
+ display: none; /* ๐Ÿ™ˆ */
261
+ }
262
+ .abAppLayout[data-variant="panes"] .abAppLayout__contentWrapper > [data-ab-part="bottomBar"] {
263
+ flex: none; /* ๐Ÿ“ its own height: a row at the bottom of the box (no sticky needed) */
264
+ padding-bottom: env(safe-area-inset-bottom, 0px); /* ๐Ÿ“ฑ home indicator */
265
+ }
266
+
267
+ /* ๐Ÿ“Ÿ tablet: sidebar (or rail) + main; the aside overlays on the right over a backdrop; no bottom bar */
268
+ @media (min-width: 768px) {
269
+ .abAppLayout[data-variant="panes"] .abAppLayout__contentWrapper {
270
+ flex-direction: row; /* โžก๏ธ */
271
+ align-items: stretch; /* ๐Ÿ“ every pane is full height */
272
+ }
273
+ .abAppLayout[data-variant="panes"] .abAppLayout__contentWrapper > :is([data-ab-part="sidebar"], [data-ab-part="rail"]) {
274
+ display: flex; /* ๐Ÿ‘€ */
275
+ flex: none;
276
+ width: var(--ab-sidebar-width); /* ๐Ÿ—‚๏ธ */
277
+ max-width: none; /* ๐Ÿงน the component's own cap */
278
+ height: auto; /* ๐Ÿ“ stretched by the row */
279
+ overflow: hidden auto; /* ๐Ÿ“œ D42: the sidebar is the third scroller */
280
+ scrollbar-gutter: stable; /* ๐Ÿ“ no jump when it starts scrolling */
281
+ transition: width var(--ab-rail-duration) ease; /* ๐ŸŽž๏ธ rail */
282
+ }
283
+ .abAppLayout[data-variant="panes"][data-rail="true"] .abAppLayout__contentWrapper > :is([data-ab-part="sidebar"], [data-ab-part="rail"]),
284
+ .abAppLayout[data-variant="panes"] .abAppLayout__contentWrapper > [data-ab-part="rail"] {
285
+ width: var(--ab-rail-width); /* ๐Ÿงท rail (layout flag or AbSidebar's own rail markup) */
286
+ }
287
+ .abAppLayout[data-variant="panes"] .abAppLayout__contentWrapper > [data-ab-part="bottomBar"] {
288
+ display: none; /* ๐Ÿ™ˆ sidebar replaces it */
289
+ }
290
+ .abAppLayout[data-variant="panes"] .abAppLayout__contentWrapper > [data-ab-part="aside"] {
291
+ inset: 0 0 0 auto; /* โžก๏ธ pinned right, inside the panes box */
292
+ width: var(--ab-aside-width); /* ๐Ÿ“‘ ~30โ€“40% */
293
+ border-left: var(--ab-divider-hairline, 1px solid rgb(0 0 0 / 0.12)); /* โž– */
294
+ background: var(--ab-color-surface, Canvas); /* ๐ŸŽจ opaque over the page */
295
+ }
296
+ /* ๐ŸŒซ๏ธ tablet overlay backdrop (James review): dims the panes, click closes the sheet */
297
+ .abAppLayout[data-variant="panes"] .abAppLayout__contentWrapper > [data-ab-part="aside-backdrop"][data-open="true"] {
298
+ display: block; /* ๐Ÿ‘€ */
299
+ position: absolute; /* ๐Ÿ“„ inside the panes box */
300
+ inset: 0; /* ๐Ÿงฑ */
301
+ z-index: 29; /* โฌ‡๏ธ just under the sheet */
302
+ background: rgb(0 0 0 / 0.32); /* ๐ŸŒซ๏ธ scrim */
303
+ }
304
+ }
305
+
306
+ /* ๐Ÿ’ป laptop: three joined panes with hairline dividers, aside in the flow */
307
+ @media (min-width: 1024px) {
308
+ .abAppLayout[data-variant="panes"] .abAppLayout__contentWrapper > :is([data-ab-part="sidebar"], [data-ab-part="rail"]) {
309
+ border-right: var(--ab-divider-hairline, 1px solid rgb(0 0 0 / 0.12)); /* โž– joined */
310
+ }
311
+ .abAppLayout[data-variant="panes"] .abAppLayout__contentWrapper > [data-ab-part="aside"] {
312
+ position: relative; /* ๐Ÿ“Œ back in the flow, a full-height column */
313
+ inset: auto; /* ๐Ÿงน the tablet overlay */
314
+ height: auto; /* ๐Ÿ“ stretched by the row */
315
+ flex: none;
316
+ z-index: auto;
317
+ background: none; /* ๐ŸŽจ */
318
+ }
319
+ .abAppLayout[data-variant="panes"] .abAppLayout__contentWrapper > [data-ab-part="aside"][data-opened="true"] {
320
+ display: flex; /* ๐Ÿ‘€ laptop+ follows data-opened (isAsideOpen), not the small-screen flag (same specificity as the :not() rule, later wins) */
321
+ }
322
+ .abAppLayout[data-variant="panes"] .abAppLayout__contentWrapper > [data-ab-part="aside"]:not([data-opened="true"]) {
323
+ display: none; /* ๐Ÿ™ˆ closed with the ยป/ยซ toggle */
324
+ }
325
+ .abAppLayout[data-variant="panes"] .abAppLayout__contentWrapper > [data-ab-part="aside-backdrop"] {
326
+ display: none !important; /* ๐Ÿ™ˆ no sheet on laptop+ */
327
+ }
328
+ }
329
+
330
+ /* ๐Ÿ–ฅ๏ธ desktop: gapped panes with radii, no dividers */
331
+ @media (min-width: 1440px) {
332
+ .abAppLayout[data-variant="panes"] .abAppLayout__contentWrapper {
333
+ gap: var(--ab-pane-gap, 12px); /* ๐Ÿงฑ */
334
+ padding: var(--ab-pane-gap, 12px); /* ๐Ÿ–ผ๏ธ outer gap */
335
+ box-sizing: border-box;
336
+ }
337
+ .abAppLayout[data-variant="panes"] .abAppLayout__contentWrapper > :is([data-ab-part="sidebar"], [data-ab-part="rail"]),
338
+ .abAppLayout[data-variant="panes"] .abAppLayout__contentWrapper > [data-ab-part="main"],
339
+ .abAppLayout[data-variant="panes"] .abAppLayout__contentWrapper > [data-ab-part="aside"] {
340
+ border: 0; /* ๐Ÿšซ dividers */
341
+ border-radius: var(--ab-pane-radius, 16px); /* ๐Ÿ”ฒ */
342
+ overflow: hidden; /* โœ‚๏ธ radius */
343
+ margin: 0; /* ๐Ÿงน legacy 2xl margins */
344
+ }
345
+ .abAppLayout[data-variant="panes"] .abAppLayout__contentWrapper > :is([data-ab-part="sidebar"], [data-ab-part="rail"]) {
346
+ overflow: hidden auto; /* ๐Ÿ“œ the sidebar still scrolls (radius kept) */
347
+ }
348
+ }
349
+
350
+ /* โ™ฟ reduced motion: the rail snaps instead of animating */
351
+ @media (prefers-reduced-motion: reduce) {
352
+ .abAppLayout[data-variant="panes"] .abAppLayout__contentWrapper > :is([data-ab-part="sidebar"], [data-ab-part="rail"]) {
353
+ transition: none;
354
+ }
355
+ }
356
+
357
+ /* ๐Ÿšซ JS off: nothing can toggle the rail, so the sidebar stays expanded (gap-review, Joseph) */
358
+ @media (scripting: none) {
359
+ .abAppLayout[data-variant="panes"][data-rail="true"] .abAppLayout__contentWrapper > :is([data-ab-part="sidebar"], [data-ab-part="rail"]) {
360
+ width: var(--ab-sidebar-width);
361
+ }
362
+ }
@@ -24,7 +24,7 @@
24
24
  * SOFTWARE.
25
25
  *
26
26
  * @project: ab-nextjs-core
27
- * @authors: Abraham Ukachi <abraham.ukachi@laplateforme.io>
27
+ * @authors: Abraham Ukachi <abrahamukachi@gmail.com>
28
28
  */
29
29
 
30
30
  /*
@@ -37,18 +37,33 @@ import type { ReactElement, ReactNode } from 'react';
37
37
  import clsx from 'clsx';
38
38
  import AbLinearProgress from '../../ab-linear-progress';
39
39
  import styles from './styles.module.css';
40
+ // ๐Ÿซฅ G52: empty-slot helper (strings get a <span>, so the :has() collapse never hides text)
41
+ import { toAbSlot } from '../../ab-page-provider/slot';
42
+ // ๐Ÿ“Œ G55 (Joseph): client island that publishes this pane's scroller on useAbPage()
43
+ import AbScrollRegistrar from '../../ab-page-provider/scroll-registrar';
40
44
 
41
45
  export interface AbAsideLayoutProps {
42
46
  orientation: 'vertical' | 'horizontal';
43
47
  className?: string;
44
48
  hidden?: boolean;
45
49
  pageName?: string;
50
+ /** ๐Ÿ” 0.1.6 (G52): the aside pane's header slot (data-ab-part="aside-header"); 0px and not rendered when empty. */
51
+ asideHeader?: ReactNode;
52
+ /** ๐Ÿ”š 0.1.6 (G52): the aside pane's bottom bar slot (data-ab-part="aside-bar"); 0px and not rendered when empty. */
53
+ asideBar?: ReactNode;
54
+ /** @deprecated 0.1.6: use `asideHeader` (kept as an alias for one minor). */
46
55
  header?: ReactNode;
47
56
  content?: ReactNode;
57
+ /** @deprecated 0.1.6: use `asideBar` (kept as an alias for one minor). */
48
58
  footer?: ReactNode;
49
59
  children?: ReactNode;
50
60
  dividerHidden?: boolean;
51
61
  menus?: ReactNode;
62
+ /** ๐Ÿ†” 0.1.6: element id (e.g. the skip-link target), no inner wrapper needed. */
63
+ id?: string;
64
+ /** @deprecated 0.1.6: `false` drops the legacy backdrop / menus / dialogs / toasts / progress / canvas layers.
65
+ * Empty header / bar slots are never rendered any more, so this only gates those layers (default `true`). */
66
+ chrome?: boolean;
52
67
  }
53
68
 
54
69
  /**
@@ -59,23 +74,33 @@ const AbAsideLayout = ({
59
74
  className,
60
75
  hidden,
61
76
  pageName,
62
- header,
77
+ asideHeader,
78
+ asideBar,
79
+ header, // ๐Ÿ•ฐ๏ธ deprecated alias
63
80
  content,
64
- footer,
81
+ footer, // ๐Ÿ•ฐ๏ธ deprecated alias
65
82
  children,
66
83
  dividerHidden,
67
84
  menus,
85
+ id, // ๐Ÿ†”
86
+ chrome = true, // ๐Ÿงฐ
68
87
  }: AbAsideLayoutProps): ReactElement => {
88
+ // ๐Ÿ”๐Ÿ”š G52: the new slot names win, the 0.1.5 names still work
89
+ const top = toAbSlot(asideHeader ?? header);
90
+ const bar = toAbSlot(asideBar ?? footer);
69
91
  return (
70
92
  <aside
93
+ id={id ?? 'ab-aside'} // ๐Ÿ†” 0.1.6 (James review): same default as AbPageProvider's asideId, for aria-controls
71
94
  className={clsx(['AbAsideLayout', styles.abAsideLayout, className])}
72
95
  hidden={hidden}
73
96
  data-page-name={pageName}
74
97
  data-ab-part="aside"
75
98
  >
76
- <header className={clsx(['HeaderWrapper', styles.abAsideLayout__headerWrapper])}>
77
- {header}
78
- </header>
99
+ {top != null && ( // ๐Ÿซฅ not rendered when empty (D37)
100
+ <header className={clsx(['HeaderWrapper', styles.abAsideLayout__headerWrapper])} data-ab-part="aside-header">
101
+ {top}
102
+ </header>
103
+ )}
79
104
 
80
105
  <div
81
106
  className={clsx([
@@ -83,34 +108,43 @@ const AbAsideLayout = ({
83
108
  styles.abAsideLayout__contentWrapper,
84
109
  orientation === 'horizontal' && '!flex-row',
85
110
  ])}
111
+ data-ab-part="aside-content" // ๐Ÿ“œ G51/G55: THE scroller of this pane (useAbScrollSpy root)
86
112
  >
113
+ {/* ๐Ÿ“œ sets asideScrollElement (server components can't hold refs) */}
114
+ <AbScrollRegistrar pane="aside" />
87
115
  {content ?? children}
88
116
  </div>
89
117
 
90
- <footer className={clsx(['FooterWrapper', styles.abAsideLayout__footerWrapper])}>
91
- {footer}
92
- </footer>
93
-
94
- <div className={clsx(['Backdrop', styles.abAsideLayout__backdrop])} hidden />
95
- <div className={clsx(['Menus', styles.abAsideLayout__menus])} hidden>
96
- {menus}
97
- </div>
98
- <div className={clsx(['Dialogs', styles.abAsideLayout__dialogs])} hidden />
99
- <div className={clsx(['Toasts', styles.abAsideLayout__toasts])} hidden />
100
-
101
- {!dividerHidden && (
102
- <span className={clsx(['divider', 'vertical', 'left', styles.abAsideLayout__divider])} />
118
+ {bar != null && ( // ๐Ÿซฅ not rendered when empty (D37)
119
+ <footer className={clsx(['FooterWrapper', styles.abAsideLayout__footerWrapper])} data-ab-part="aside-bar">
120
+ {bar}
121
+ </footer>
103
122
  )}
104
123
 
105
- <AbLinearProgress
106
- className="absolute top-0 left-0 right-0 z-50 !h-0.5 lg:!h-1"
107
- color="secondary"
108
- hidden={true}
109
- isIndeterminate={true}
110
- progress={25}
111
- />
112
-
113
- <canvas className={clsx(['Canvas', styles.abAsideLayout__canvas])} />
124
+ {chrome && ( // ๐Ÿงฐ 0.1.6: legacy layers
125
+ <>
126
+ <div className={clsx(['Backdrop', styles.abAsideLayout__backdrop])} hidden />
127
+ <div className={clsx(['Menus', styles.abAsideLayout__menus])} hidden>
128
+ {menus}
129
+ </div>
130
+ <div className={clsx(['Dialogs', styles.abAsideLayout__dialogs])} hidden />
131
+ <div className={clsx(['Toasts', styles.abAsideLayout__toasts])} hidden />
132
+
133
+ {!dividerHidden && (
134
+ <span className={clsx(['divider', 'vertical', 'left', styles.abAsideLayout__divider])} />
135
+ )}
136
+
137
+ <AbLinearProgress
138
+ className="absolute top-0 left-0 right-0 z-50 !h-0.5 lg:!h-1"
139
+ color="secondary"
140
+ hidden={true}
141
+ isIndeterminate={true}
142
+ progress={25}
143
+ />
144
+
145
+ <canvas className={clsx(['Canvas', styles.abAsideLayout__canvas])} />
146
+ </>
147
+ )}
114
148
  </aside>
115
149
  );
116
150
  };
@@ -27,7 +27,7 @@
27
27
  * @name: AB Aside Layout (server) - Component Stylesheet (plain CSS, Tailwind-independent)
28
28
  * @file: server/ab-aside-layout/styles.module.css
29
29
  * @type: CSS Module
30
- * @authors: Abraham Ukachi <abraham.ukachi@laplateforme.io>
30
+ * @authors: Abraham Ukachi <abrahamukachi@gmail.com>
31
31
  */
32
32
 
33
33
  /*
@@ -153,3 +153,45 @@
153
153
  from { opacity: 0; }
154
154
  to { opacity: 1; }
155
155
  }
156
+
157
+ /* ================================================================== */
158
+ /* ๐Ÿ“ 0.1.6 layout rule (G51 + G52 + G54): a bounded box whose CONTENT */
159
+ /* scrolls on its own; aside-header / aside-bar collapse when empty */
160
+ /* ================================================================== */
161
+ /* ๐Ÿซฅ D37: an empty slot is 0px (also outside panes: an empty wrapper had no size anyway) */
162
+ .abAsideLayout__headerWrapper:empty,
163
+ .abAsideLayout__footerWrapper:empty,
164
+ :global(.AbAppLayout[data-variant="panes"]) .abAsideLayout__headerWrapper:not(:has(> *)),
165
+ :global(.AbAppLayout[data-variant="panes"]) .abAsideLayout__footerWrapper:not(:has(> *)) {
166
+ display: none; /* ๐Ÿ“ 0px, and no footer gradient */
167
+ }
168
+ /* ๐Ÿงญ inside AbAppLayout variant="panes": a full-height column, only the content wrapper scrolls */
169
+ :global(.AbAppLayout[data-variant="panes"]) .abAsideLayout {
170
+ height: 100%; /* ๐Ÿ“ the pane */
171
+ min-height: 0; /* ๐Ÿšซ */
172
+ max-height: none; /* ๐Ÿงน the legacy 100vh caps */
173
+ overflow: hidden; /* ๐Ÿ”’ */
174
+ display: flex; /* ๐Ÿงฑ header ยท content ยท bar */
175
+ flex-direction: column;
176
+ margin: 0; /* ๐Ÿงน legacy 2xl margins */
177
+ }
178
+ :global(.AbAppLayout[data-variant="panes"]) .abAsideLayout__headerWrapper,
179
+ :global(.AbAppLayout[data-variant="panes"]) .abAsideLayout__footerWrapper {
180
+ flex: none; /* ๐Ÿ“ rows size to their content: no hard-coded heights */
181
+ pointer-events: auto; /* ๐Ÿ–ฑ๏ธ real chrome, not an overlay */
182
+ }
183
+ :global(.AbAppLayout[data-variant="panes"]) .abAsideLayout__footerWrapper {
184
+ padding-bottom: env(safe-area-inset-bottom, 0px); /* ๐Ÿ“ฑ home indicator */
185
+ }
186
+ :global(.AbAppLayout[data-variant="panes"]) .abAsideLayout__footerWrapper::before {
187
+ content: none; /* ๐Ÿงน no gradient over the scroller */
188
+ }
189
+ :global(.AbAppLayout[data-variant="panes"]) .abAsideLayout__contentWrapper {
190
+ flex: 1 1 0; /* โ†•๏ธ the rest */
191
+ height: auto; /* ๐Ÿ“ */
192
+ min-height: 0; /* ๐Ÿšซ lets it scroll */
193
+ overflow: hidden auto; /* ๐Ÿ“œ THE scroller of this pane */
194
+ overscroll-behavior: contain; /* ๐Ÿšซ never chains to the page (G54) */
195
+ scrollbar-gutter: stable; /* ๐Ÿ“ no layout jump */
196
+ justify-content: flex-start; /* โฌ†๏ธ */
197
+ }