@uniweb/kit 0.9.34 → 0.9.35
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/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/kit",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.35",
|
|
4
4
|
"description": "Standard component library for Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.js",
|
|
8
8
|
"./xref": "./src/xref/index.js",
|
|
9
|
-
"./theme-tokens.css": "./src/theme-tokens.css"
|
|
9
|
+
"./theme-tokens.css": "./src/theme-tokens.css",
|
|
10
|
+
"./prose-tokens.css": "./src/prose-tokens.css"
|
|
10
11
|
},
|
|
11
12
|
"files": [
|
|
12
13
|
"src",
|
|
@@ -39,8 +40,8 @@
|
|
|
39
40
|
"fuse.js": "^7.0.0",
|
|
40
41
|
"shiki": "^3.0.0",
|
|
41
42
|
"tailwind-merge": "^3.6.0",
|
|
42
|
-
"@uniweb/
|
|
43
|
-
"@uniweb/
|
|
43
|
+
"@uniweb/scene": "0.1.2",
|
|
44
|
+
"@uniweb/core": "0.7.27"
|
|
44
45
|
},
|
|
45
46
|
"peerDependencies": {
|
|
46
47
|
"react": "^19.0.0",
|
package/src/hooks/useHeadings.js
CHANGED
|
@@ -41,8 +41,15 @@ import { useActiveRoute } from './useActiveRoute.js'
|
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* Pull headings out of the page's own content — available during prerender.
|
|
44
|
+
*
|
|
45
|
+
* Skips the headings the semantic parser claimed as the section's own
|
|
46
|
+
* title/pretitle/subtitle. In this framework a leading `###` is a pretitle and
|
|
47
|
+
* the `##` after the title is a subtitle — structure, not body headings — so a
|
|
48
|
+
* typed section would otherwise open every contents rail with two entries no
|
|
49
|
+
* reader recognises as sections of the article. An untyped document has none of
|
|
50
|
+
* those fields set, and everything in it counts.
|
|
44
51
|
*/
|
|
45
|
-
function headingsFromContent(page, levels) {
|
|
52
|
+
export function headingsFromContent(page, levels = [2, 3]) {
|
|
46
53
|
const blocks = page?.getBodyBlocks?.() ?? []
|
|
47
54
|
const found = []
|
|
48
55
|
|
|
@@ -50,13 +57,23 @@ function headingsFromContent(page, levels) {
|
|
|
50
57
|
const nodes = block?.rawContent?.content
|
|
51
58
|
if (!Array.isArray(nodes)) continue
|
|
52
59
|
|
|
60
|
+
const parsed = block.parsedContent || {}
|
|
61
|
+
const claimed = new Set(
|
|
62
|
+
[parsed.title, parsed.pretitle, parsed.subtitle]
|
|
63
|
+
.flat()
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
.map(value => String(value).trim())
|
|
66
|
+
)
|
|
67
|
+
|
|
53
68
|
for (const node of nodes) {
|
|
54
69
|
if (node?.type !== 'heading') continue
|
|
55
70
|
const level = node.attrs?.level ?? 1
|
|
56
71
|
if (!levels.includes(level)) continue
|
|
57
72
|
|
|
58
73
|
const text = nodeText(node).trim()
|
|
59
|
-
if (text
|
|
74
|
+
if (!text || claimed.has(text)) continue
|
|
75
|
+
|
|
76
|
+
found.push({ id: headingId(text), text, level })
|
|
60
77
|
}
|
|
61
78
|
}
|
|
62
79
|
|
package/src/index.js
CHANGED
|
@@ -124,7 +124,6 @@ export {
|
|
|
124
124
|
// Styled Components (Tailwind-based)
|
|
125
125
|
// ============================================================================
|
|
126
126
|
|
|
127
|
-
export { SidebarLayout } from './styled/SidebarLayout/index.js'
|
|
128
127
|
export { Section, Render } from './styled/Section/index.js'
|
|
129
128
|
export { Prose } from './styled/Prose/index.jsx'
|
|
130
129
|
export { Article } from './styled/Article/index.jsx'
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Tailwind Typography, wired to the theme.
|
|
3
|
+
*
|
|
4
|
+
* Usage — after the plugin, so these win on equal specificity:
|
|
5
|
+
*
|
|
6
|
+
* @plugin "@tailwindcss/typography";
|
|
7
|
+
* @import "@uniweb/kit/prose-tokens.css";
|
|
8
|
+
*
|
|
9
|
+
* `prose` ships its own greys, which means long-form body copy is the one part
|
|
10
|
+
* of a foundation that ignores the site's theme.yml unless someone bridges it
|
|
11
|
+
* by hand. Everyone does bridge it by hand, identically, and the bridge is easy
|
|
12
|
+
* to get subtly wrong — so it lives here.
|
|
13
|
+
*
|
|
14
|
+
* After this import, a prose container answers to the site like everything else
|
|
15
|
+
* and flips with the visitor's scheme on its own. Do not add a `prose-invert`
|
|
16
|
+
* or a `dark:` variant: the tokens have already changed by the time the dark
|
|
17
|
+
* scheme is active, and `prose-invert` would override this with fixed greys.
|
|
18
|
+
*
|
|
19
|
+
* For the same reason, do not add a palette modifier — `prose-gray`,
|
|
20
|
+
* `prose-slate`, `prose-neutral`. Those re-declare every variable below.
|
|
21
|
+
*
|
|
22
|
+
* ── One container ──
|
|
23
|
+
*
|
|
24
|
+
* These variables are inherited, so a `prose` container nested inside another
|
|
25
|
+
* silently resets the lot for its subtree. Two containers is always a bug, and
|
|
26
|
+
* a quiet one: the outer looks right and everything inside it does not. Decide
|
|
27
|
+
* who owns prose — the section that renders the document is usually the better
|
|
28
|
+
* owner, since it then renders correctly under any layout — and make sure the
|
|
29
|
+
* other one only supplies column width and padding.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
.prose {
|
|
33
|
+
--tw-prose-body: var(--body);
|
|
34
|
+
--tw-prose-headings: var(--heading);
|
|
35
|
+
--tw-prose-lead: var(--subtle);
|
|
36
|
+
--tw-prose-links: var(--link, var(--primary));
|
|
37
|
+
--tw-prose-bold: var(--heading);
|
|
38
|
+
--tw-prose-counters: var(--subtle);
|
|
39
|
+
--tw-prose-bullets: var(--border);
|
|
40
|
+
--tw-prose-hr: var(--border);
|
|
41
|
+
--tw-prose-quotes: var(--subtle);
|
|
42
|
+
--tw-prose-quote-borders: var(--primary);
|
|
43
|
+
--tw-prose-captions: var(--subtle);
|
|
44
|
+
--tw-prose-kbd: var(--heading);
|
|
45
|
+
--tw-prose-code: var(--heading);
|
|
46
|
+
--tw-prose-th-borders: var(--border);
|
|
47
|
+
--tw-prose-td-borders: var(--border);
|
|
48
|
+
|
|
49
|
+
/* Fenced code keeps whatever the highlighter paints — a site retunes that
|
|
50
|
+
through `code:` in theme.yml, not here. */
|
|
51
|
+
--tw-prose-pre-code: var(--code-text, var(--body));
|
|
52
|
+
--tw-prose-pre-bg: var(--code-surface, var(--muted));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Inline code reads as a chip on whatever surface it sits on, rather than the
|
|
56
|
+
plugin's fixed grey. Scoped to `:not(pre)` so fenced blocks are untouched. */
|
|
57
|
+
.prose :not(pre) > code {
|
|
58
|
+
background-color: color-mix(in oklch, var(--section), var(--heading) 6%);
|
|
59
|
+
border-radius: 0.25rem;
|
|
60
|
+
padding: 0.125rem 0.375rem;
|
|
61
|
+
font-weight: 500;
|
|
62
|
+
font-size: 0.875em;
|
|
63
|
+
}
|
|
64
|
+
.prose :not(pre) > code::before,
|
|
65
|
+
.prose :not(pre) > code::after {
|
|
66
|
+
content: none;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Anchors land clear of a fixed site header when something scrolls to them —
|
|
70
|
+
a contents rail, or a link with a #fragment. */
|
|
71
|
+
.prose :is(h1, h2, h3, h4) {
|
|
72
|
+
scroll-margin-top: calc(var(--header-height, 4rem) + 1rem);
|
|
73
|
+
}
|
package/src/styled/index.js
CHANGED
|
@@ -7,14 +7,13 @@
|
|
|
7
7
|
* For unstyled primitives, use the main '@uniweb/kit' export.
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
10
|
-
* import {
|
|
10
|
+
* import { Section, Prose, Media } from '@uniweb/kit (styled)'
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
// ============================================================================
|
|
14
14
|
// Layout
|
|
15
15
|
// ============================================================================
|
|
16
16
|
|
|
17
|
-
export { SidebarLayout } from './SidebarLayout/index.js'
|
|
18
17
|
|
|
19
18
|
// ============================================================================
|
|
20
19
|
// Content Rendering
|
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
import React, { useEffect } from 'react'
|
|
2
|
-
import { cn } from '../../utils/index.js'
|
|
3
|
-
import { useMobileMenu } from '../../hooks/useMobileMenu.js'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* SidebarLayout Component
|
|
7
|
-
*
|
|
8
|
-
* A flexible layout with optional left and/or right sidebars.
|
|
9
|
-
* On desktop, sidebars appear inline at their configured breakpoints.
|
|
10
|
-
* On mobile, the left panel is accessible via a slide-out drawer with FAB toggle.
|
|
11
|
-
* The right panel is hidden on mobile (common pattern: nav essential, TOC optional).
|
|
12
|
-
*
|
|
13
|
-
* The layout is "sandwiched" - header and footer span the full width,
|
|
14
|
-
* with the sidebars and content area between them.
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* // In foundation's src/foundation.js
|
|
18
|
-
* import { SidebarLayout } from '@uniweb/kit'
|
|
19
|
-
*
|
|
20
|
-
* export default {
|
|
21
|
-
* Layout: SidebarLayout,
|
|
22
|
-
* }
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* // With custom configuration
|
|
26
|
-
* import { SidebarLayout } from '@uniweb/kit'
|
|
27
|
-
*
|
|
28
|
-
* function CustomLayout(props) {
|
|
29
|
-
* return (
|
|
30
|
-
* <SidebarLayout
|
|
31
|
-
* {...props}
|
|
32
|
-
* leftBreakpoint="lg"
|
|
33
|
-
* rightBreakpoint="xl"
|
|
34
|
-
* leftWidth="w-72"
|
|
35
|
-
* />
|
|
36
|
-
* )
|
|
37
|
-
* }
|
|
38
|
-
*
|
|
39
|
-
* export default { Layout: CustomLayout }
|
|
40
|
-
*/
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Hamburger menu icon
|
|
44
|
-
*/
|
|
45
|
-
function MenuIcon({ className }) {
|
|
46
|
-
return (
|
|
47
|
-
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
48
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
|
49
|
-
</svg>
|
|
50
|
-
)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Close (X) icon
|
|
55
|
-
*/
|
|
56
|
-
function CloseIcon({ className }) {
|
|
57
|
-
return (
|
|
58
|
-
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
59
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
60
|
-
</svg>
|
|
61
|
-
)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Mobile drawer component (always slides from left)
|
|
66
|
-
*/
|
|
67
|
-
function MobileDrawer({ isOpen, onClose, width, stickyHeader, children }) {
|
|
68
|
-
// Prevent body scroll when drawer is open
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
if (isOpen) {
|
|
71
|
-
document.body.style.overflow = 'hidden'
|
|
72
|
-
return () => {
|
|
73
|
-
document.body.style.overflow = ''
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}, [isOpen])
|
|
77
|
-
|
|
78
|
-
// Position below header if sticky, otherwise from top
|
|
79
|
-
const topOffset = stickyHeader ? 'top-[var(--header-height,4rem)]' : 'top-0'
|
|
80
|
-
const height = stickyHeader ? 'h-[calc(100vh-var(--header-height,4rem))]' : 'h-screen'
|
|
81
|
-
|
|
82
|
-
return (
|
|
83
|
-
<>
|
|
84
|
-
{/* Backdrop */}
|
|
85
|
-
<div
|
|
86
|
-
className={cn(
|
|
87
|
-
// kit-palette-ok: a modal scrim is the same black in either scheme
|
|
88
|
-
'fixed inset-0 bg-black/50 z-40 transition-opacity duration-300',
|
|
89
|
-
isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
|
90
|
-
)}
|
|
91
|
-
onClick={onClose}
|
|
92
|
-
aria-hidden="true"
|
|
93
|
-
/>
|
|
94
|
-
|
|
95
|
-
{/* Drawer (always from left) */}
|
|
96
|
-
<div
|
|
97
|
-
className={cn(
|
|
98
|
-
'fixed left-0 bg-section z-50 shadow-xl',
|
|
99
|
-
topOffset,
|
|
100
|
-
height,
|
|
101
|
-
width,
|
|
102
|
-
'transform transition-transform duration-300 ease-in-out',
|
|
103
|
-
isOpen ? 'translate-x-0' : '-translate-x-full'
|
|
104
|
-
)}
|
|
105
|
-
role="dialog"
|
|
106
|
-
aria-modal="true"
|
|
107
|
-
aria-label="Sidebar navigation"
|
|
108
|
-
>
|
|
109
|
-
{/* Close button */}
|
|
110
|
-
<button
|
|
111
|
-
onClick={onClose}
|
|
112
|
-
className="absolute top-4 right-4 p-1.5 rounded-md hover:bg-muted transition-colors"
|
|
113
|
-
aria-label="Close sidebar"
|
|
114
|
-
>
|
|
115
|
-
<CloseIcon className="w-5 h-5 text-subtle" />
|
|
116
|
-
</button>
|
|
117
|
-
|
|
118
|
-
{/* Drawer content */}
|
|
119
|
-
<div className="h-full overflow-y-auto overscroll-contain">
|
|
120
|
-
{children}
|
|
121
|
-
</div>
|
|
122
|
-
</div>
|
|
123
|
-
</>
|
|
124
|
-
)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Floating action button for mobile menu (always bottom-left)
|
|
129
|
-
*/
|
|
130
|
-
function FloatingMenuButton({ onClick }) {
|
|
131
|
-
return (
|
|
132
|
-
<button
|
|
133
|
-
onClick={onClick}
|
|
134
|
-
className={cn(
|
|
135
|
-
'fixed bottom-4 left-4 z-30',
|
|
136
|
-
'p-3 bg-primary text-primary-foreground rounded-full shadow-lg',
|
|
137
|
-
'hover:bg-primary/90 active:scale-95 transition-all',
|
|
138
|
-
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2'
|
|
139
|
-
)}
|
|
140
|
-
aria-label="Open navigation menu"
|
|
141
|
-
>
|
|
142
|
-
<MenuIcon className="w-6 h-6" />
|
|
143
|
-
</button>
|
|
144
|
-
)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Get responsive classes for showing/hiding at breakpoint
|
|
149
|
-
*/
|
|
150
|
-
function getBreakpointClasses(breakpoint) {
|
|
151
|
-
const showClass = {
|
|
152
|
-
sm: 'sm:block',
|
|
153
|
-
md: 'md:block',
|
|
154
|
-
lg: 'lg:block',
|
|
155
|
-
xl: 'xl:block',
|
|
156
|
-
}[breakpoint] || 'md:block'
|
|
157
|
-
|
|
158
|
-
const hideClass = {
|
|
159
|
-
sm: 'sm:hidden',
|
|
160
|
-
md: 'md:hidden',
|
|
161
|
-
lg: 'lg:hidden',
|
|
162
|
-
xl: 'xl:hidden',
|
|
163
|
-
}[breakpoint] || 'md:hidden'
|
|
164
|
-
|
|
165
|
-
return { showClass, hideClass }
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* SidebarLayout main component
|
|
170
|
-
*
|
|
171
|
-
* @param {Object} props
|
|
172
|
-
* @param {React.ReactNode} props.header - Header content (from layout/header.md)
|
|
173
|
-
* @param {React.ReactNode} props.body - Main body content (page sections)
|
|
174
|
-
* @param {React.ReactNode} props.footer - Footer content (from layout/footer.md)
|
|
175
|
-
* @param {React.ReactNode} props.left - Left panel content (from layout/left.md)
|
|
176
|
-
* @param {React.ReactNode} props.right - Right panel content (from layout/right.md)
|
|
177
|
-
* @param {React.ReactNode} props.leftPanel - Alias for left (backwards compatibility)
|
|
178
|
-
* @param {React.ReactNode} props.rightPanel - Alias for right (backwards compatibility)
|
|
179
|
-
* @param {string} [props.leftWidth='w-64'] - Tailwind width class for left sidebar
|
|
180
|
-
* @param {string} [props.rightWidth='w-64'] - Tailwind width class for right sidebar
|
|
181
|
-
* @param {string} [props.drawerWidth='w-72'] - Tailwind width class for mobile drawer
|
|
182
|
-
* @param {string} [props.leftBreakpoint='md'] - Breakpoint for showing left sidebar inline
|
|
183
|
-
* @param {string} [props.rightBreakpoint='xl'] - Breakpoint for showing right sidebar inline
|
|
184
|
-
* @param {boolean} [props.stickyHeader=true] - Whether header sticks to top
|
|
185
|
-
* @param {boolean} [props.stickySidebar=true] - Whether sidebars stick below header
|
|
186
|
-
* @param {string} [props.maxWidth='max-w-7xl'] - Max width of content area
|
|
187
|
-
* @param {string} [props.contentPadding='px-4 py-8 sm:px-6 lg:px-8'] - Padding for main content
|
|
188
|
-
* @param {string} [props.className] - Additional classes for the root element
|
|
189
|
-
*/
|
|
190
|
-
export function SidebarLayout({
|
|
191
|
-
// Pre-rendered layout areas from runtime
|
|
192
|
-
header,
|
|
193
|
-
body,
|
|
194
|
-
footer,
|
|
195
|
-
left,
|
|
196
|
-
right,
|
|
197
|
-
leftPanel,
|
|
198
|
-
rightPanel,
|
|
199
|
-
// Configuration
|
|
200
|
-
leftWidth = 'w-64',
|
|
201
|
-
rightWidth = 'w-64',
|
|
202
|
-
drawerWidth = 'w-72',
|
|
203
|
-
leftBreakpoint = 'md',
|
|
204
|
-
rightBreakpoint = 'xl',
|
|
205
|
-
stickyHeader = true,
|
|
206
|
-
stickySidebar = true,
|
|
207
|
-
maxWidth = 'max-w-7xl',
|
|
208
|
-
contentPadding = 'px-4 py-8 sm:px-6 lg:px-8',
|
|
209
|
-
className,
|
|
210
|
-
}) {
|
|
211
|
-
const { isOpen, open, close } = useMobileMenu()
|
|
212
|
-
|
|
213
|
-
// Resolve panel content (support both naming conventions)
|
|
214
|
-
const leftContent = left || leftPanel
|
|
215
|
-
const rightContent = right || rightPanel
|
|
216
|
-
|
|
217
|
-
// Get breakpoint classes for each panel
|
|
218
|
-
const leftClasses = getBreakpointClasses(leftBreakpoint)
|
|
219
|
-
const rightClasses = getBreakpointClasses(rightBreakpoint)
|
|
220
|
-
|
|
221
|
-
// Sticky positioning
|
|
222
|
-
const headerClasses = stickyHeader
|
|
223
|
-
? 'sticky top-0 z-30'
|
|
224
|
-
: ''
|
|
225
|
-
|
|
226
|
-
const sidebarClasses = stickySidebar && stickyHeader
|
|
227
|
-
? 'sticky top-[var(--header-height,4rem)] h-[calc(100vh-var(--header-height,4rem))]'
|
|
228
|
-
: stickySidebar
|
|
229
|
-
? 'sticky top-0 h-screen'
|
|
230
|
-
: ''
|
|
231
|
-
|
|
232
|
-
return (
|
|
233
|
-
<div className={cn('min-h-screen flex flex-col bg-section', className)}>
|
|
234
|
-
{/* Header */}
|
|
235
|
-
{header && (
|
|
236
|
-
<header className={cn(
|
|
237
|
-
'w-full border-b border-border bg-section/95 backdrop-blur supports-[backdrop-filter]:bg-section/80',
|
|
238
|
-
headerClasses
|
|
239
|
-
)}>
|
|
240
|
-
{header}
|
|
241
|
-
</header>
|
|
242
|
-
)}
|
|
243
|
-
|
|
244
|
-
{/* Mobile Drawer (left panel only) */}
|
|
245
|
-
{leftContent && (
|
|
246
|
-
<div className={leftClasses.hideClass}>
|
|
247
|
-
<MobileDrawer
|
|
248
|
-
isOpen={isOpen}
|
|
249
|
-
onClose={close}
|
|
250
|
-
width={drawerWidth}
|
|
251
|
-
stickyHeader={stickyHeader}
|
|
252
|
-
>
|
|
253
|
-
{leftContent}
|
|
254
|
-
</MobileDrawer>
|
|
255
|
-
</div>
|
|
256
|
-
)}
|
|
257
|
-
|
|
258
|
-
{/* Main Content Area */}
|
|
259
|
-
<div className={cn('flex-1 w-full mx-auto', maxWidth)}>
|
|
260
|
-
<div className="flex">
|
|
261
|
-
{/* Left Sidebar (desktop) */}
|
|
262
|
-
{leftContent && (
|
|
263
|
-
<aside className={cn(
|
|
264
|
-
'hidden flex-shrink-0 overflow-y-auto border-r border-border',
|
|
265
|
-
leftClasses.showClass,
|
|
266
|
-
leftWidth,
|
|
267
|
-
sidebarClasses
|
|
268
|
-
)}>
|
|
269
|
-
{leftContent}
|
|
270
|
-
</aside>
|
|
271
|
-
)}
|
|
272
|
-
|
|
273
|
-
{/* Main Content */}
|
|
274
|
-
<main className="flex-1 min-w-0">
|
|
275
|
-
<div className={contentPadding}>
|
|
276
|
-
{body}
|
|
277
|
-
</div>
|
|
278
|
-
</main>
|
|
279
|
-
|
|
280
|
-
{/* Right Sidebar (desktop only, hidden on mobile) */}
|
|
281
|
-
{rightContent && (
|
|
282
|
-
<aside className={cn(
|
|
283
|
-
'hidden flex-shrink-0 overflow-y-auto border-l border-border',
|
|
284
|
-
rightClasses.showClass,
|
|
285
|
-
rightWidth,
|
|
286
|
-
sidebarClasses
|
|
287
|
-
)}>
|
|
288
|
-
{rightContent}
|
|
289
|
-
</aside>
|
|
290
|
-
)}
|
|
291
|
-
</div>
|
|
292
|
-
</div>
|
|
293
|
-
|
|
294
|
-
{/* Footer */}
|
|
295
|
-
{footer && (
|
|
296
|
-
<footer className="w-full border-t border-border">
|
|
297
|
-
{footer}
|
|
298
|
-
</footer>
|
|
299
|
-
)}
|
|
300
|
-
|
|
301
|
-
{/* Mobile FAB (only if left panel exists) */}
|
|
302
|
-
{leftContent && (
|
|
303
|
-
<div className={leftClasses.hideClass}>
|
|
304
|
-
<FloatingMenuButton onClick={open} />
|
|
305
|
-
</div>
|
|
306
|
-
)}
|
|
307
|
-
</div>
|
|
308
|
-
)
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
export default SidebarLayout
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { SidebarLayout, default } from './SidebarLayout.jsx'
|