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.
- package/CHANGELOG.md +20 -0
- package/README.md +48 -2
- package/ab-app-layout/index.tsx +81 -22
- package/ab-app-layout/styles.module.css +218 -1
- package/ab-aside-layout/index.tsx +76 -30
- package/ab-aside-layout/styles.module.css +54 -1
- package/ab-linear-progress/index.tsx +1 -1
- package/ab-main-layout/index.tsx +57 -23
- package/ab-main-layout/styles.module.css +43 -1
- package/ab-page-provider/index.tsx +144 -6
- package/ab-page-provider/scroll-registrar.tsx +71 -0
- package/ab-page-provider/slot.ts +51 -0
- package/ab-screen-layout/index.tsx +3 -3
- package/ab-screen-layout/styles.module.css +1 -1
- package/index.ts +1 -1
- package/package.json +4 -3
- package/server/ab-app-layout/index.tsx +56 -25
- package/server/ab-app-layout/styles.module.css +218 -1
- package/server/ab-aside-layout/index.tsx +62 -28
- package/server/ab-aside-layout/styles.module.css +43 -1
- package/server/ab-main-layout/index.tsx +59 -25
- package/server/ab-main-layout/styles.module.css +43 -1
- package/server/ab-screen-layout/index.tsx +3 -3
- package/server/ab-screen-layout/styles.module.css +1 -1
|
@@ -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 <
|
|
30
|
+
* @authors: Abraham Ukachi <abrahamukachi@gmail.com>
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
/*
|
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ab-nextjs-core",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
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": "
|
|
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 <
|
|
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
|
|
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
|
-
|
|
80
|
-
{header
|
|
81
|
-
|
|
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
|
-
|
|
90
|
-
{footer
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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 <
|
|
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 <
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
{header
|
|
78
|
-
|
|
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
|
-
|
|
91
|
-
{
|
|
92
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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 <
|
|
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
|
+
}
|