@stacksjs/components 0.2.90 → 0.2.92
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/dist/Sidebar-yb115a2w.stx +458 -0
- package/dist/SidebarFooter-0aegr99p.stx +71 -0
- package/dist/SidebarHeader-ed5wb13t.stx +103 -0
- package/dist/SidebarItem-gtrcw4ky.stx +86 -0
- package/dist/SidebarSection-qf8fhy55.stx +83 -0
- package/dist/index.js +168 -12
- package/dist/ui/sidebar/index.d.ts +67 -107
- package/dist/ui/sidebar/themes.d.ts +74 -0
- package/package.json +10 -4
- package/src/ui/sidebar/Sidebar.stx +356 -228
- package/src/ui/sidebar/SidebarFooter.stx +56 -94
- package/src/ui/sidebar/SidebarHeader.stx +69 -165
- package/src/ui/sidebar/SidebarItem.stx +73 -79
- package/src/ui/sidebar/SidebarSection.stx +70 -101
- package/src/ui/sidebar/index.ts +100 -172
- package/src/ui/sidebar/themes.ts +240 -0
- package/stx-plugin.ts +19 -0
- package/dist/Sidebar-0jzasrmt.stx +0 -330
- package/dist/SidebarFooter-zsfsgcxb.stx +0 -109
- package/dist/SidebarHeader-ywpmx14d.stx +0 -199
- package/dist/SidebarItem-h53sgqzz.stx +0 -92
- package/dist/SidebarSection-g195cb4f.stx +0 -114
|
@@ -1,330 +1,458 @@
|
|
|
1
1
|
<script server>
|
|
2
|
+
import { resolveSidebarTheme } from './themes'
|
|
3
|
+
|
|
4
|
+
// Data: [{ id, label, collapsible?, collapsed?, items: [{ id, label, icon,
|
|
5
|
+
// iconColor, image, href, count, active?, disabled?, expanded?, children? }] }]
|
|
2
6
|
export const sections = $props.sections || []
|
|
7
|
+
|
|
8
|
+
// `theme` is the new name; `variant` is accepted for backwards compatibility.
|
|
9
|
+
export const theme = $props.theme || $props.variant || 'macos'
|
|
10
|
+
export const width = $props.width || 250
|
|
11
|
+
export const position = $props.position || 'left'
|
|
12
|
+
export const placement = $props.placement || 'fixed'
|
|
13
|
+
export const bordered = $props.bordered ?? false
|
|
14
|
+
|
|
15
|
+
// Collapsing. macOS sidebars hide entirely (toolbar button / drag); legacy
|
|
16
|
+
// themes may collapse to a compact rail instead.
|
|
3
17
|
export const collapsed = $props.collapsed ?? false
|
|
4
18
|
export const collapsible = $props.collapsible ?? true
|
|
5
|
-
export const
|
|
19
|
+
export const collapseMode = $props.collapseMode || (theme === 'workspace' || theme === 'desktop' ? 'rail' : 'hidden')
|
|
6
20
|
export const minWidth = $props.minWidth || 64
|
|
7
|
-
export const collapseMode = $props.collapseMode || 'rail'
|
|
8
|
-
export const variant = $props.variant || 'tahoe'
|
|
9
|
-
export const position = $props.position || 'left'
|
|
10
|
-
export const placement = $props.placement || 'fixed'
|
|
11
|
-
export const bordered = $props.bordered ?? true
|
|
12
|
-
export const materialOpacity = $props.materialOpacity ?? (variant === 'desktop' ? 0.78 : 0.7)
|
|
13
|
-
export const materialDarkOpacity = $props.materialDarkOpacity ?? (variant === 'desktop' ? 0.8 : materialOpacity)
|
|
14
|
-
export const materialScheme = $props.materialScheme || 'system'
|
|
15
21
|
export const persistKey = $props.persistKey || ''
|
|
16
22
|
export const shellSelector = $props.shellSelector || ''
|
|
17
23
|
export const widthVar = $props.widthVar || '--stx-sidebar-width'
|
|
18
24
|
export const collapsedClass = $props.collapsedClass || ''
|
|
19
|
-
export const nativeMaterialCollapse = $props.nativeMaterialCollapse ?? (variant === 'desktop' && collapseMode === 'hidden')
|
|
20
25
|
export const className = $props.className || ''
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
bg: 'bg-white/50 dark:bg-black/40',
|
|
31
|
-
border: 'border-white/20 dark:border-white/10',
|
|
32
|
-
backdrop: 'backdrop-blur-3xl backdrop-saturate-200',
|
|
33
|
-
tint: false,
|
|
34
|
-
},
|
|
35
|
-
solid: {
|
|
36
|
-
bg: 'bg-stone-100 dark:bg-neutral-900',
|
|
37
|
-
border: 'border-stone-200 dark:border-neutral-800',
|
|
38
|
-
backdrop: '',
|
|
39
|
-
tint: false,
|
|
40
|
-
},
|
|
41
|
-
transparent: {
|
|
42
|
-
bg: 'bg-transparent',
|
|
43
|
-
border: 'border-transparent',
|
|
44
|
-
backdrop: '',
|
|
45
|
-
tint: false,
|
|
46
|
-
},
|
|
47
|
-
workspace: {
|
|
48
|
-
bg: 'bg-[#f4f4f3] dark:bg-neutral-950',
|
|
49
|
-
border: 'border-transparent',
|
|
50
|
-
backdrop: '',
|
|
51
|
-
tint: false,
|
|
52
|
-
nav: 'px-5 py-5 space-y-1',
|
|
53
|
-
slot: 'px-5 py-5',
|
|
54
|
-
},
|
|
55
|
-
desktop: {
|
|
56
|
-
bg: 'bg-[#f3f3f1]/48 dark:bg-neutral-950/32',
|
|
57
|
-
border: 'border-white/15 dark:border-white/10',
|
|
58
|
-
backdrop: 'backdrop-blur-3xl backdrop-saturate-150',
|
|
59
|
-
tint: false,
|
|
60
|
-
shimmer: true,
|
|
61
|
-
nav: 'px-5 py-2 space-y-1',
|
|
62
|
-
slot: 'px-5 py-2',
|
|
63
|
-
},
|
|
64
|
-
}
|
|
27
|
+
// Unique id so the client controller can find this pane even with several
|
|
28
|
+
// sidebars on the page.
|
|
29
|
+
export const sidebarUid = $props.id || `stx-sidebar-${Math.random().toString(36).slice(2, 8)}`
|
|
30
|
+
|
|
31
|
+
const themeStyle = resolveSidebarTheme(theme)
|
|
32
|
+
export const hasShimmerRim = themeStyle.layers.shimmerRim === true
|
|
33
|
+
export const tintClasses = themeStyle.layers.tint || ''
|
|
34
|
+
export const scrollAreaClasses = themeStyle.scrollArea
|
|
65
35
|
|
|
66
|
-
const style = variantStyles[variant] || variantStyles.tahoe
|
|
67
|
-
export const variantTint = style.tint
|
|
68
|
-
export const variantShimmer = style.shimmer
|
|
69
36
|
const borderSide = position === 'left' ? 'border-r' : 'border-l'
|
|
70
|
-
const borderClasses = bordered ? `${borderSide}
|
|
71
|
-
const positionClass = position === 'left' ? 'left-0' : 'right-0'
|
|
37
|
+
const borderClasses = bordered ? `${borderSide} border-black/5 dark:border-white/10` : ''
|
|
72
38
|
const placementClasses = {
|
|
73
|
-
fixed: `fixed inset-y-0 z-50 ${
|
|
39
|
+
fixed: `fixed inset-y-0 z-50 ${position === 'left' ? 'left-0' : 'right-0'}`,
|
|
74
40
|
sticky: 'sticky top-0 h-screen',
|
|
75
41
|
static: 'relative h-full',
|
|
76
42
|
}
|
|
77
43
|
|
|
78
|
-
export const
|
|
44
|
+
export const paneClasses = [
|
|
79
45
|
placementClasses[placement] || placementClasses.fixed,
|
|
80
|
-
'flex flex-col',
|
|
81
|
-
'transition-
|
|
82
|
-
|
|
83
|
-
style.bg,
|
|
84
|
-
style.backdrop,
|
|
46
|
+
'flex flex-col overflow-hidden',
|
|
47
|
+
'transition-[width] duration-300 ease-out',
|
|
48
|
+
themeStyle.pane,
|
|
85
49
|
borderClasses,
|
|
86
50
|
className,
|
|
87
51
|
].filter(Boolean).join(' ')
|
|
88
52
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
:
|
|
92
|
-
|
|
93
|
-
|
|
53
|
+
// Normalized ahead of the loop so template bindings stay simple accessors.
|
|
54
|
+
export const sectionList = sections.map(section => ({
|
|
55
|
+
id: section.id,
|
|
56
|
+
label: section.label || '',
|
|
57
|
+
items: section.items || [],
|
|
58
|
+
collapsible: section.collapsible !== false,
|
|
59
|
+
collapsed: section.collapsed === true,
|
|
60
|
+
}))
|
|
61
|
+
|
|
62
|
+
// State classes the client controller swaps on rows. Kept as data attributes
|
|
63
|
+
// so all behavior lives in one place (the controller below) and themes stay
|
|
64
|
+
// pure data.
|
|
65
|
+
export const activeClasses = themeStyle.item.active
|
|
94
66
|
</script>
|
|
95
67
|
|
|
96
68
|
<script client>
|
|
97
69
|
const emit = defineEmits()
|
|
70
|
+
const sidebarUidProp = '{{ sidebarUid }}'
|
|
98
71
|
const collapsedProp = {{ collapsed }}
|
|
99
72
|
const collapsibleProp = {{ collapsible }}
|
|
73
|
+
const collapseModeProp = '{{ collapseMode }}'
|
|
100
74
|
const widthProp = {{ width }}
|
|
101
75
|
const minWidthProp = {{ minWidth }}
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
const materialOpacityProp = {{ materialOpacity }}
|
|
107
|
-
const materialDarkOpacityProp = {{ materialDarkOpacity }}
|
|
108
|
-
const materialSchemeProp = {{ materialScheme }}
|
|
109
|
-
const persistKeyProp = {{ persistKey }}
|
|
110
|
-
const shellSelectorProp = {{ shellSelector }}
|
|
111
|
-
const widthVarProp = {{ widthVar }}
|
|
112
|
-
const collapsedClassProp = {{ collapsedClass }}
|
|
113
|
-
const nativeMaterialCollapseProp = {{ nativeMaterialCollapse }}
|
|
76
|
+
const persistKeyProp = '{{ persistKey }}'
|
|
77
|
+
const shellSelectorProp = '{{ shellSelector }}'
|
|
78
|
+
const widthVarProp = '{{ widthVar }}'
|
|
79
|
+
const collapsedClassProp = '{{ collapsedClass }}'
|
|
114
80
|
|
|
115
81
|
const collapsed = state(collapsedProp)
|
|
116
|
-
const isHovered = state(false)
|
|
117
|
-
const hasNativeSidebar = state(false)
|
|
118
|
-
|
|
119
|
-
const showExpanded = derived(() => !collapsed() || isHovered())
|
|
120
82
|
const sidebarWidth = derived(() => {
|
|
121
|
-
if (collapsed()
|
|
122
|
-
if (collapsed() && !isHovered()) return `${minWidthProp}px`
|
|
83
|
+
if (collapsed()) return collapseModeProp === 'hidden' ? '0px' : `${minWidthProp}px`
|
|
123
84
|
return `${widthProp}px`
|
|
124
85
|
})
|
|
125
|
-
const collapseAriaLabel = derived(() => collapsed() ? 'Expand sidebar' : 'Collapse sidebar')
|
|
126
86
|
|
|
127
|
-
|
|
128
|
-
if (persistKeyProp) {
|
|
129
|
-
try {
|
|
130
|
-
window.localStorage?.setItem(persistKeyProp, nextCollapsed ? 'true' : 'false')
|
|
131
|
-
}
|
|
132
|
-
catch {}
|
|
133
|
-
}
|
|
87
|
+
let pane = null
|
|
134
88
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Selection — one row carries data-active="true"; the highlight classes come
|
|
91
|
+
// from the theme (stored on the pane as data-active-class).
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
function activeClassList() {
|
|
95
|
+
return (pane?.dataset.activeClass || '').split(' ').filter(Boolean)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function setActiveRow(button) {
|
|
99
|
+
const classes = activeClassList()
|
|
100
|
+
pane.querySelectorAll('[data-sidebar-item][data-active="true"]').forEach((el) => {
|
|
101
|
+
el.removeAttribute('data-active')
|
|
102
|
+
el.classList.remove(...classes)
|
|
103
|
+
})
|
|
104
|
+
button.setAttribute('data-active', 'true')
|
|
105
|
+
button.classList.add(...classes)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Disclosure — rows know their ancestors (data-parents="a/b"). Collapsing a
|
|
110
|
+
// row (or section) marks every descendant with data-collapsed, which the
|
|
111
|
+
// scoped CSS animates to zero height, exactly like AppKit's outline collapse.
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
function syncRowVisibility() {
|
|
115
|
+
const collapsedIds = new Set()
|
|
116
|
+
pane.querySelectorAll('[data-sidebar-row][data-expanded="false"]').forEach((row) => {
|
|
117
|
+
collapsedIds.add(row.dataset.itemId)
|
|
118
|
+
})
|
|
119
|
+
pane.querySelectorAll('[data-sidebar-section][data-expanded="false"]').forEach((section) => {
|
|
120
|
+
section.querySelectorAll('[data-sidebar-row]').forEach((row) => {
|
|
121
|
+
row.setAttribute('data-collapsed', '')
|
|
139
122
|
})
|
|
123
|
+
})
|
|
124
|
+
pane.querySelectorAll('[data-sidebar-section][data-expanded="true"] [data-sidebar-row]').forEach((row) => {
|
|
125
|
+
const parents = (row.dataset.parents || '').split('/').filter(Boolean)
|
|
126
|
+
const hidden = parents.some(id => collapsedIds.has(id))
|
|
127
|
+
if (hidden) row.setAttribute('data-collapsed', '')
|
|
128
|
+
else row.removeAttribute('data-collapsed')
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function toggleRowDisclosure(row) {
|
|
133
|
+
const expanded = row.getAttribute('data-expanded') === 'true'
|
|
134
|
+
row.setAttribute('data-expanded', String(!expanded))
|
|
135
|
+
syncRowVisibility()
|
|
136
|
+
emit('itemToggle', { id: row.dataset.itemId, expanded: !expanded })
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function toggleSection(section) {
|
|
140
|
+
const expanded = section.getAttribute('data-expanded') !== 'false'
|
|
141
|
+
section.setAttribute('data-expanded', String(!expanded))
|
|
142
|
+
syncRowVisibility()
|
|
143
|
+
emit('sectionToggle', section.dataset.sectionId)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Pointer input (event delegation — items and sections stay presentational)
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
function onClick(event) {
|
|
151
|
+
const chevron = event.target.closest('[data-sidebar-disclosure]')
|
|
152
|
+
if (chevron) {
|
|
153
|
+
event.preventDefault()
|
|
154
|
+
toggleRowDisclosure(chevron.closest('[data-sidebar-row]'))
|
|
155
|
+
return
|
|
140
156
|
}
|
|
141
157
|
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
|
|
158
|
+
const sectionHeader = event.target.closest('[data-sidebar-section-header]')
|
|
159
|
+
if (sectionHeader) {
|
|
160
|
+
toggleSection(sectionHeader.closest('[data-sidebar-section]'))
|
|
161
|
+
return
|
|
145
162
|
}
|
|
146
163
|
|
|
147
|
-
|
|
148
|
-
|
|
164
|
+
const item = event.target.closest('[data-sidebar-item]')
|
|
165
|
+
if (item && !item.hasAttribute('data-disabled')) {
|
|
166
|
+
setActiveRow(item)
|
|
167
|
+
const row = item.closest('[data-sidebar-row]')
|
|
168
|
+
emit('itemClick', {
|
|
169
|
+
item: { id: row.dataset.itemId, label: item.dataset.label || '', href: item.getAttribute('href') || '' },
|
|
170
|
+
event,
|
|
171
|
+
})
|
|
149
172
|
}
|
|
150
173
|
}
|
|
151
174
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Keyboard — matches macOS source lists: ↑/↓ move the selection over visible
|
|
177
|
+
// rows, ←/→ collapse/expand, Return opens.
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
157
179
|
|
|
158
|
-
function
|
|
159
|
-
|
|
180
|
+
function visibleItems() {
|
|
181
|
+
return [...pane.querySelectorAll('[data-sidebar-row]:not([data-collapsed]) [data-sidebar-item]:not([data-disabled])')]
|
|
160
182
|
}
|
|
161
183
|
|
|
162
|
-
function
|
|
163
|
-
|
|
184
|
+
function onKeyDown(event) {
|
|
185
|
+
const items = visibleItems()
|
|
186
|
+
if (items.length === 0) return
|
|
187
|
+
const current = pane.querySelector('[data-sidebar-item][data-active="true"]')
|
|
188
|
+
const index = items.indexOf(current)
|
|
189
|
+
|
|
190
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
191
|
+
event.preventDefault()
|
|
192
|
+
const next = event.key === 'ArrowDown'
|
|
193
|
+
? items[Math.min(index + 1, items.length - 1)]
|
|
194
|
+
: items[Math.max(index - 1, 0)]
|
|
195
|
+
if (next && next !== current) {
|
|
196
|
+
setActiveRow(next)
|
|
197
|
+
next.scrollIntoView({ block: 'nearest' })
|
|
198
|
+
emit('itemClick', { item: { id: next.closest('[data-sidebar-row]').dataset.itemId, label: next.dataset.label || '', href: next.getAttribute('href') || '' }, event })
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else if ((event.key === 'ArrowRight' || event.key === 'ArrowLeft') && current) {
|
|
202
|
+
const row = current.closest('[data-sidebar-row]')
|
|
203
|
+
if (row.querySelector('[data-sidebar-disclosure]')) {
|
|
204
|
+
const expanded = row.getAttribute('data-expanded') === 'true'
|
|
205
|
+
if (event.key === 'ArrowRight' ? !expanded : expanded) {
|
|
206
|
+
event.preventDefault()
|
|
207
|
+
toggleRowDisclosure(row)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else if (event.key === 'Enter' && current) {
|
|
212
|
+
current.click()
|
|
213
|
+
}
|
|
164
214
|
}
|
|
165
215
|
|
|
166
|
-
|
|
167
|
-
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// Overlay scrollbar — a floating thumb that appears while scrolling and fades
|
|
218
|
+
// away, like macOS overlay scrollbars. The native scrollbar is hidden so rows
|
|
219
|
+
// keep their exact insets.
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
function attachOverlayScrollbar(scrollArea, thumb) {
|
|
223
|
+
let fadeTimer
|
|
224
|
+
|
|
225
|
+
function update() {
|
|
226
|
+
const { scrollTop, scrollHeight, clientHeight } = scrollArea
|
|
227
|
+
if (scrollHeight <= clientHeight) {
|
|
228
|
+
thumb.style.opacity = '0'
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
const trackHeight = clientHeight - 8
|
|
232
|
+
const height = Math.max(24, (clientHeight / scrollHeight) * trackHeight)
|
|
233
|
+
const top = 4 + (scrollTop / (scrollHeight - clientHeight)) * (trackHeight - height)
|
|
234
|
+
thumb.style.height = `${height}px`
|
|
235
|
+
thumb.style.transform = `translateY(${top}px)`
|
|
236
|
+
thumb.style.opacity = '1'
|
|
237
|
+
clearTimeout(fadeTimer)
|
|
238
|
+
fadeTimer = setTimeout(() => { thumb.style.opacity = '0' }, 900)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
scrollArea.addEventListener('scroll', update, { passive: true })
|
|
168
242
|
}
|
|
169
243
|
|
|
170
|
-
|
|
171
|
-
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// Collapse (sidebar-wide) — persisted, synced to the app shell and to Craft's
|
|
246
|
+
// native web-sidebar material when present.
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
function applyCollapseEffects(nextCollapsed) {
|
|
250
|
+
if (persistKeyProp) {
|
|
251
|
+
try { window.localStorage?.setItem(persistKeyProp, String(nextCollapsed)) } catch {}
|
|
252
|
+
}
|
|
253
|
+
if (shellSelectorProp) {
|
|
254
|
+
document.querySelectorAll(shellSelectorProp).forEach((shell) => {
|
|
255
|
+
shell.style?.setProperty?.(widthVarProp, nextCollapsed && collapseModeProp === 'hidden' ? '0px' : `${widthProp}px`)
|
|
256
|
+
if (shell.dataset) shell.dataset.sidebarCollapsed = String(nextCollapsed)
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
document.documentElement.dataset.stxSidebarCollapsed = String(nextCollapsed)
|
|
260
|
+
if (collapsedClassProp) document.documentElement.classList.toggle(collapsedClassProp, nextCollapsed)
|
|
261
|
+
window.craft?.window?.setWebSidebarCollapsed?.(nextCollapsed).catch?.(() => {})
|
|
172
262
|
}
|
|
173
263
|
|
|
174
|
-
function
|
|
175
|
-
|
|
264
|
+
function setCollapsed(nextCollapsed) {
|
|
265
|
+
collapsed.set(nextCollapsed)
|
|
266
|
+
applyCollapseEffects(nextCollapsed)
|
|
267
|
+
emit('collapse', nextCollapsed)
|
|
176
268
|
}
|
|
177
269
|
|
|
178
|
-
function
|
|
179
|
-
|
|
180
|
-
|| window.craft?.__craft_bridge_loaded === true
|
|
181
|
-
|| !!window.craft?.window
|
|
182
|
-
|| !!window.webkit?.messageHandlers?.craft
|
|
270
|
+
function toggleCollapse() {
|
|
271
|
+
if (collapsibleProp) setCollapsed(!collapsed())
|
|
183
272
|
}
|
|
184
273
|
|
|
185
|
-
onMount(
|
|
274
|
+
onMount(() => {
|
|
275
|
+
pane = document.getElementById(sidebarUidProp)
|
|
276
|
+
if (!pane) return
|
|
277
|
+
|
|
186
278
|
if (persistKeyProp) {
|
|
187
279
|
try {
|
|
188
280
|
const saved = window.localStorage?.getItem(persistKeyProp)
|
|
189
281
|
if (saved === 'true' || saved === 'false') collapsed.set(saved === 'true')
|
|
190
|
-
}
|
|
191
|
-
catch {}
|
|
282
|
+
} catch {}
|
|
192
283
|
}
|
|
193
|
-
|
|
194
284
|
applyCollapseEffects(collapsed())
|
|
195
285
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (document.body?.dataset) document.body.dataset.sidebarMaterial = 'desktop'
|
|
199
|
-
}
|
|
286
|
+
pane.addEventListener('click', onClick)
|
|
287
|
+
pane.addEventListener('keydown', onKeyDown)
|
|
200
288
|
|
|
201
|
-
|
|
289
|
+
const scrollArea = pane.querySelector('[data-sidebar-scroll]')
|
|
290
|
+
const thumb = pane.querySelector('[data-sidebar-scroll-thumb]')
|
|
291
|
+
if (scrollArea && thumb) attachOverlayScrollbar(scrollArea, thumb)
|
|
202
292
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
position: positionProp,
|
|
208
|
-
width: widthProp,
|
|
209
|
-
minWidth: minWidthProp,
|
|
210
|
-
collapseMode: collapseModeProp,
|
|
211
|
-
collapsible: collapsibleProp,
|
|
212
|
-
variant: variantProp,
|
|
213
|
-
material: variantProp === 'desktop' ? 'sidebar' : undefined,
|
|
214
|
-
backgroundEffect: variantProp === 'desktop' ? 'vibrancy' : undefined,
|
|
215
|
-
materialOpacity: materialOpacityProp,
|
|
216
|
-
materialDarkOpacity: materialDarkOpacityProp,
|
|
217
|
-
materialScheme: materialSchemeProp,
|
|
218
|
-
allowsVibrancy: variantProp === 'desktop' || variantProp === 'vibrancy' || variantProp === 'tahoe',
|
|
219
|
-
sections: sectionsProp.map(s => ({
|
|
220
|
-
id: s.id,
|
|
221
|
-
label: s.label,
|
|
222
|
-
items: s.items?.map(i => ({ id: i.id, label: i.label, icon: i.icon, href: i.href, badge: i.badge })),
|
|
223
|
-
})),
|
|
224
|
-
})
|
|
225
|
-
if (sidebar) hasNativeSidebar.set(true)
|
|
226
|
-
}
|
|
227
|
-
catch (err) {
|
|
228
|
-
console.warn('[Sidebar] Failed to create native sidebar, using web fallback:', err)
|
|
229
|
-
}
|
|
230
|
-
}
|
|
293
|
+
// Ensure the initial active row carries the theme's highlight classes.
|
|
294
|
+
const active = pane.querySelector('[data-sidebar-item][data-active="true"]')
|
|
295
|
+
if (active) active.classList.add(...activeClassList())
|
|
296
|
+
syncRowVisibility()
|
|
231
297
|
})
|
|
232
298
|
|
|
233
299
|
defineExpose({ collapsed, toggle: toggleCollapse })
|
|
234
300
|
</script>
|
|
235
301
|
|
|
236
302
|
<aside
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
data-sidebar-
|
|
240
|
-
data-
|
|
303
|
+
id="{{ sidebarUid }}"
|
|
304
|
+
data-stx-sidebar
|
|
305
|
+
data-sidebar-theme="{{ theme }}"
|
|
306
|
+
data-active-class="{{ activeClasses }}"
|
|
307
|
+
class="{{ paneClasses }}"
|
|
308
|
+
:style="`width: ${sidebarWidth()}`"
|
|
241
309
|
:aria-hidden="String(collapsed() && collapseModeProp === 'hidden')"
|
|
242
|
-
:style="`width: ${sidebarWidth()}; --stx-sidebar-material-opacity: ${materialOpacityProp}; --stx-sidebar-material-dark-opacity: ${materialDarkOpacityProp}`"
|
|
243
|
-
@mouseenter="onMouseEnter()"
|
|
244
|
-
@mouseleave="onMouseLeave()"
|
|
245
310
|
role="navigation"
|
|
246
|
-
aria-label="Sidebar
|
|
311
|
+
aria-label="Sidebar"
|
|
312
|
+
tabindex="0"
|
|
247
313
|
>
|
|
248
|
-
@if(
|
|
249
|
-
<div class="absolute inset-0 pointer-events-none
|
|
314
|
+
@if(tintClasses)
|
|
315
|
+
<div class="absolute inset-0 pointer-events-none {{ tintClasses }}"></div>
|
|
250
316
|
@endif
|
|
251
|
-
@if(
|
|
252
|
-
<div class="
|
|
317
|
+
@if(hasShimmerRim)
|
|
318
|
+
<div class="stx-sidebar-rim" aria-hidden="true"></div>
|
|
253
319
|
@endif
|
|
254
320
|
|
|
255
321
|
@if($slots.header)
|
|
256
|
-
<div class="flex-shrink-0
|
|
322
|
+
<div class="relative z-10 flex-shrink-0">
|
|
257
323
|
<slot name="header" />
|
|
258
324
|
</div>
|
|
259
325
|
@endif
|
|
260
326
|
|
|
261
|
-
<div class="
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
@endforeach
|
|
278
|
-
</nav>
|
|
279
|
-
@else
|
|
280
|
-
<div class="{{ slotClasses }}">
|
|
327
|
+
<div class="relative z-10 min-h-0 flex-1">
|
|
328
|
+
<div data-sidebar-scroll class="{{ scrollAreaClasses }} h-full stx-sidebar-scroll">
|
|
329
|
+
@if(sectionList.length > 0)
|
|
330
|
+
<nav class="flex flex-col">
|
|
331
|
+
@foreach(sectionList as section)
|
|
332
|
+
<SidebarSection
|
|
333
|
+
:id="section.id"
|
|
334
|
+
:label="section.label"
|
|
335
|
+
:items="section.items"
|
|
336
|
+
:collapsible="section.collapsible"
|
|
337
|
+
:collapsed="section.collapsed"
|
|
338
|
+
:theme="theme"
|
|
339
|
+
/>
|
|
340
|
+
@endforeach
|
|
341
|
+
</nav>
|
|
342
|
+
@else
|
|
281
343
|
<slot />
|
|
282
|
-
|
|
283
|
-
|
|
344
|
+
@endif
|
|
345
|
+
</div>
|
|
346
|
+
<div data-sidebar-scroll-thumb class="stx-sidebar-scroll-thumb" aria-hidden="true"></div>
|
|
284
347
|
</div>
|
|
285
348
|
|
|
286
349
|
@if($slots.footer)
|
|
287
|
-
<div class="flex-shrink-0
|
|
350
|
+
<div class="relative z-10 flex-shrink-0">
|
|
288
351
|
<slot name="footer" />
|
|
289
352
|
</div>
|
|
290
353
|
@endif
|
|
291
|
-
|
|
292
|
-
@if(collapsible)
|
|
293
|
-
<button
|
|
294
|
-
type="button"
|
|
295
|
-
class="absolute top-4 {{ collapseBtnPosition }} p-1.5 rounded-lg text-stone-400 hover:text-stone-600 hover:bg-black/5 dark:hover:text-neutral-300 dark:hover:bg-white/10 transition-colors z-20"
|
|
296
|
-
@click="toggleCollapse()"
|
|
297
|
-
:aria-label="collapseAriaLabel()"
|
|
298
|
-
data-sidebar-collapse
|
|
299
|
-
data-stx-sidebar-collapse-button
|
|
300
|
-
>
|
|
301
|
-
<div :show="!collapsed()" class="i-hugeicons-sidebar-left w-5 h-5"></div>
|
|
302
|
-
<div :show="collapsed()" class="i-hugeicons-sidebar-right w-5 h-5"></div>
|
|
303
|
-
</button>
|
|
304
|
-
@endif
|
|
305
354
|
</aside>
|
|
306
355
|
|
|
307
356
|
<style scoped>
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
.sidebar-container[data-sidebar-variant="desktop"] {
|
|
313
|
-
background-color: rgba(246, 246, 244, var(--stx-sidebar-material-opacity, 0.78));
|
|
314
|
-
backdrop-filter: blur(24px) saturate(1.08);
|
|
315
|
-
-webkit-backdrop-filter: blur(24px) saturate(1.08);
|
|
357
|
+
[data-stx-sidebar] {
|
|
358
|
+
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', sans-serif;
|
|
359
|
+
-webkit-font-smoothing: antialiased;
|
|
360
|
+
outline: none;
|
|
316
361
|
}
|
|
317
|
-
|
|
318
|
-
|
|
362
|
+
|
|
363
|
+
/* Liquid Glass edge highlight: a hairline rim that catches light along the
|
|
364
|
+
pane's edges, with a slow sheen drifting across it. */
|
|
365
|
+
.stx-sidebar-rim {
|
|
366
|
+
position: absolute;
|
|
367
|
+
inset: 0;
|
|
368
|
+
border-radius: inherit;
|
|
369
|
+
pointer-events: none;
|
|
370
|
+
z-index: 5;
|
|
371
|
+
box-shadow:
|
|
372
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.50),
|
|
373
|
+
inset 1px 0 0 rgba(255, 255, 255, 0.26),
|
|
374
|
+
inset -1px 0 0 rgba(255, 255, 255, 0.10),
|
|
375
|
+
inset 0 -1px 0 rgba(255, 255, 255, 0.14);
|
|
376
|
+
}
|
|
377
|
+
.stx-sidebar-rim::after {
|
|
378
|
+
content: '';
|
|
379
|
+
position: absolute;
|
|
380
|
+
inset: 0;
|
|
381
|
+
border-radius: inherit;
|
|
382
|
+
padding: 1.5px;
|
|
383
|
+
background: linear-gradient(
|
|
384
|
+
115deg,
|
|
385
|
+
rgba(255, 255, 255, 0.85) 0%,
|
|
386
|
+
rgba(255, 255, 255, 0.00) 28%,
|
|
387
|
+
rgba(255, 255, 255, 0.18) 52%,
|
|
388
|
+
rgba(255, 255, 255, 0.00) 74%,
|
|
389
|
+
rgba(255, 255, 255, 0.65) 100%
|
|
390
|
+
);
|
|
391
|
+
background-size: 250% 250%;
|
|
392
|
+
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
|
393
|
+
-webkit-mask-composite: xor;
|
|
394
|
+
mask-composite: exclude;
|
|
395
|
+
animation: stx-sidebar-shimmer 10s ease-in-out infinite alternate;
|
|
319
396
|
}
|
|
320
|
-
|
|
321
|
-
|
|
397
|
+
@keyframes stx-sidebar-shimmer {
|
|
398
|
+
from { background-position: 0% 0%; }
|
|
399
|
+
to { background-position: 100% 100%; }
|
|
322
400
|
}
|
|
323
401
|
@media (prefers-color-scheme: dark) {
|
|
324
|
-
.
|
|
325
|
-
|
|
402
|
+
.stx-sidebar-rim {
|
|
403
|
+
box-shadow:
|
|
404
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.18),
|
|
405
|
+
inset 1px 0 0 rgba(255, 255, 255, 0.10),
|
|
406
|
+
inset -1px 0 0 rgba(255, 255, 255, 0.05),
|
|
407
|
+
inset 0 -1px 0 rgba(255, 255, 255, 0.06);
|
|
326
408
|
}
|
|
327
|
-
.
|
|
328
|
-
|
|
409
|
+
.stx-sidebar-rim::after { opacity: 0.35; }
|
|
410
|
+
}
|
|
411
|
+
@media (prefers-reduced-motion: reduce) {
|
|
412
|
+
.stx-sidebar-rim::after { animation: none; }
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/* Rows collapse like AppKit outlines: fixed row height animates to zero. */
|
|
416
|
+
[data-sidebar-row] {
|
|
417
|
+
height: 32px; /* 30px row + 2px gap */
|
|
418
|
+
padding-bottom: 2px;
|
|
419
|
+
transition: height 0.22s ease, opacity 0.16s ease;
|
|
420
|
+
}
|
|
421
|
+
[data-sidebar-row][data-collapsed] {
|
|
422
|
+
height: 0;
|
|
423
|
+
opacity: 0;
|
|
424
|
+
overflow: hidden;
|
|
425
|
+
pointer-events: none;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/* Disclosure chevron rotates from ▸ to ▾ */
|
|
429
|
+
[data-sidebar-row][data-expanded="true"] [data-sidebar-disclosure] > * {
|
|
430
|
+
transform: rotate(90deg);
|
|
431
|
+
}
|
|
432
|
+
[data-sidebar-section][data-expanded="false"] [data-sidebar-section-chevron] {
|
|
433
|
+
transform: rotate(-90deg);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/* Native scrollbar hidden; the floating thumb below takes its place. */
|
|
437
|
+
.stx-sidebar-scroll {
|
|
438
|
+
scrollbar-width: none;
|
|
439
|
+
}
|
|
440
|
+
.stx-sidebar-scroll::-webkit-scrollbar {
|
|
441
|
+
display: none;
|
|
442
|
+
}
|
|
443
|
+
.stx-sidebar-scroll-thumb {
|
|
444
|
+
position: absolute;
|
|
445
|
+
top: 0;
|
|
446
|
+
right: 3px;
|
|
447
|
+
width: 7px;
|
|
448
|
+
border-radius: 4px;
|
|
449
|
+
background: rgba(0, 0, 0, 0.35);
|
|
450
|
+
opacity: 0;
|
|
451
|
+
transition: opacity 0.3s ease;
|
|
452
|
+
pointer-events: none;
|
|
453
|
+
z-index: 20;
|
|
454
|
+
}
|
|
455
|
+
@media (prefers-color-scheme: dark) {
|
|
456
|
+
.stx-sidebar-scroll-thumb { background: rgba(255, 255, 255, 0.35); }
|
|
329
457
|
}
|
|
330
458
|
</style>
|