@stacksjs/components 0.2.91 → 0.2.93
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-4hn7vw4n.stx +476 -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 +12 -6
- package/src/ui/sidebar/Sidebar.stx +373 -227
- 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,476 @@
|
|
|
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
|
-
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Keyboard — matches macOS source lists: ↑/↓ move the selection over visible
|
|
177
|
+
// rows, ←/→ collapse/expand, Return opens.
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
function visibleItems() {
|
|
181
|
+
return [...pane.querySelectorAll('[data-sidebar-row]:not([data-collapsed]) [data-sidebar-item]:not([data-disabled])')]
|
|
156
182
|
}
|
|
157
183
|
|
|
158
|
-
function
|
|
159
|
-
|
|
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
|
+
}
|
|
160
214
|
}
|
|
161
215
|
|
|
162
|
-
|
|
163
|
-
|
|
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 })
|
|
164
242
|
}
|
|
165
243
|
|
|
166
|
-
|
|
167
|
-
|
|
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?.(() => {})
|
|
168
262
|
}
|
|
169
263
|
|
|
170
|
-
function
|
|
171
|
-
|
|
264
|
+
function setCollapsed(nextCollapsed) {
|
|
265
|
+
collapsed.set(nextCollapsed)
|
|
266
|
+
applyCollapseEffects(nextCollapsed)
|
|
267
|
+
emit('collapse', nextCollapsed)
|
|
172
268
|
}
|
|
173
269
|
|
|
174
|
-
function
|
|
175
|
-
|
|
270
|
+
function toggleCollapse() {
|
|
271
|
+
if (collapsibleProp) setCollapsed(!collapsed())
|
|
176
272
|
}
|
|
177
273
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
// Appearance — native sidebars follow the OS light/dark appearance. Crosswind
|
|
276
|
+
// dark: utilities are class-based, so mirror prefers-color-scheme onto the
|
|
277
|
+
// root element unless the app manages its own theme (data-theme attribute).
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
function followSystemAppearance() {
|
|
281
|
+
const media = window.matchMedia('(prefers-color-scheme: dark)')
|
|
282
|
+
function sync() {
|
|
283
|
+
if (document.documentElement.dataset.theme) return
|
|
284
|
+
document.documentElement.classList.toggle('dark', media.matches)
|
|
285
|
+
}
|
|
286
|
+
sync()
|
|
287
|
+
media.addEventListener('change', sync)
|
|
183
288
|
}
|
|
184
289
|
|
|
185
|
-
onMount(
|
|
290
|
+
onMount(() => {
|
|
291
|
+
pane = document.getElementById(sidebarUidProp)
|
|
292
|
+
if (!pane) return
|
|
293
|
+
|
|
294
|
+
followSystemAppearance()
|
|
295
|
+
|
|
186
296
|
if (persistKeyProp) {
|
|
187
297
|
try {
|
|
188
298
|
const saved = window.localStorage?.getItem(persistKeyProp)
|
|
189
299
|
if (saved === 'true' || saved === 'false') collapsed.set(saved === 'true')
|
|
190
|
-
}
|
|
191
|
-
catch {}
|
|
300
|
+
} catch {}
|
|
192
301
|
}
|
|
193
|
-
|
|
194
302
|
applyCollapseEffects(collapsed())
|
|
195
303
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (document.body?.dataset) document.body.dataset.sidebarMaterial = 'desktop'
|
|
199
|
-
}
|
|
304
|
+
pane.addEventListener('click', onClick)
|
|
305
|
+
pane.addEventListener('keydown', onKeyDown)
|
|
200
306
|
|
|
201
|
-
|
|
307
|
+
const scrollArea = pane.querySelector('[data-sidebar-scroll]')
|
|
308
|
+
const thumb = pane.querySelector('[data-sidebar-scroll-thumb]')
|
|
309
|
+
if (scrollArea && thumb) attachOverlayScrollbar(scrollArea, thumb)
|
|
202
310
|
|
|
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
|
-
}
|
|
311
|
+
// Ensure the initial active row carries the theme's highlight classes.
|
|
312
|
+
const active = pane.querySelector('[data-sidebar-item][data-active="true"]')
|
|
313
|
+
if (active) active.classList.add(...activeClassList())
|
|
314
|
+
syncRowVisibility()
|
|
231
315
|
})
|
|
232
316
|
|
|
233
317
|
defineExpose({ collapsed, toggle: toggleCollapse })
|
|
234
318
|
</script>
|
|
235
319
|
|
|
236
320
|
<aside
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
data-sidebar-
|
|
240
|
-
data-
|
|
321
|
+
id="{{ sidebarUid }}"
|
|
322
|
+
data-stx-sidebar
|
|
323
|
+
data-sidebar-theme="{{ theme }}"
|
|
324
|
+
data-active-class="{{ activeClasses }}"
|
|
325
|
+
class="{{ paneClasses }}"
|
|
326
|
+
:style="`width: ${sidebarWidth()}`"
|
|
241
327
|
: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
328
|
role="navigation"
|
|
246
|
-
aria-label="Sidebar
|
|
329
|
+
aria-label="Sidebar"
|
|
330
|
+
tabindex="0"
|
|
247
331
|
>
|
|
248
|
-
@if(
|
|
249
|
-
<div class="absolute inset-0 pointer-events-none
|
|
332
|
+
@if(tintClasses)
|
|
333
|
+
<div class="absolute inset-0 pointer-events-none {{ tintClasses }}"></div>
|
|
250
334
|
@endif
|
|
251
|
-
@if(
|
|
252
|
-
<div class="
|
|
335
|
+
@if(hasShimmerRim)
|
|
336
|
+
<div class="stx-sidebar-rim" aria-hidden="true"></div>
|
|
253
337
|
@endif
|
|
254
338
|
|
|
255
339
|
@if($slots.header)
|
|
256
|
-
<div class="flex-shrink-0
|
|
340
|
+
<div class="relative z-10 flex-shrink-0">
|
|
257
341
|
<slot name="header" />
|
|
258
342
|
</div>
|
|
259
343
|
@endif
|
|
260
344
|
|
|
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 }}">
|
|
345
|
+
<div class="relative z-10 min-h-0 flex-1">
|
|
346
|
+
<div data-sidebar-scroll class="{{ scrollAreaClasses }} h-full stx-sidebar-scroll">
|
|
347
|
+
@if(sectionList.length > 0)
|
|
348
|
+
<nav class="flex flex-col">
|
|
349
|
+
@foreach(sectionList as section)
|
|
350
|
+
<SidebarSection
|
|
351
|
+
:id="section.id"
|
|
352
|
+
:label="section.label"
|
|
353
|
+
:items="section.items"
|
|
354
|
+
:collapsible="section.collapsible"
|
|
355
|
+
:collapsed="section.collapsed"
|
|
356
|
+
:theme="theme"
|
|
357
|
+
/>
|
|
358
|
+
@endforeach
|
|
359
|
+
</nav>
|
|
360
|
+
@else
|
|
281
361
|
<slot />
|
|
282
|
-
|
|
283
|
-
|
|
362
|
+
@endif
|
|
363
|
+
</div>
|
|
364
|
+
<div data-sidebar-scroll-thumb class="stx-sidebar-scroll-thumb" aria-hidden="true"></div>
|
|
284
365
|
</div>
|
|
285
366
|
|
|
286
367
|
@if($slots.footer)
|
|
287
|
-
<div class="flex-shrink-0
|
|
368
|
+
<div class="relative z-10 flex-shrink-0">
|
|
288
369
|
<slot name="footer" />
|
|
289
370
|
</div>
|
|
290
371
|
@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
372
|
</aside>
|
|
306
373
|
|
|
307
374
|
<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);
|
|
375
|
+
[data-stx-sidebar] {
|
|
376
|
+
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', sans-serif;
|
|
377
|
+
-webkit-font-smoothing: antialiased;
|
|
378
|
+
outline: none;
|
|
316
379
|
}
|
|
317
|
-
|
|
318
|
-
|
|
380
|
+
|
|
381
|
+
/* Liquid Glass edge highlight: a hairline rim that catches light along the
|
|
382
|
+
pane's edges, with a slow sheen drifting across it. */
|
|
383
|
+
.stx-sidebar-rim {
|
|
384
|
+
position: absolute;
|
|
385
|
+
inset: 0;
|
|
386
|
+
border-radius: inherit;
|
|
387
|
+
pointer-events: none;
|
|
388
|
+
z-index: 5;
|
|
389
|
+
box-shadow:
|
|
390
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.50),
|
|
391
|
+
inset 1px 0 0 rgba(255, 255, 255, 0.26),
|
|
392
|
+
inset -1px 0 0 rgba(255, 255, 255, 0.10),
|
|
393
|
+
inset 0 -1px 0 rgba(255, 255, 255, 0.14);
|
|
394
|
+
}
|
|
395
|
+
.stx-sidebar-rim::after {
|
|
396
|
+
content: '';
|
|
397
|
+
position: absolute;
|
|
398
|
+
inset: 0;
|
|
399
|
+
border-radius: inherit;
|
|
400
|
+
padding: 1.5px;
|
|
401
|
+
background: linear-gradient(
|
|
402
|
+
115deg,
|
|
403
|
+
rgba(255, 255, 255, 0.85) 0%,
|
|
404
|
+
rgba(255, 255, 255, 0.00) 28%,
|
|
405
|
+
rgba(255, 255, 255, 0.18) 52%,
|
|
406
|
+
rgba(255, 255, 255, 0.00) 74%,
|
|
407
|
+
rgba(255, 255, 255, 0.65) 100%
|
|
408
|
+
);
|
|
409
|
+
background-size: 250% 250%;
|
|
410
|
+
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
|
411
|
+
-webkit-mask-composite: xor;
|
|
412
|
+
mask-composite: exclude;
|
|
413
|
+
animation: stx-sidebar-shimmer 10s ease-in-out infinite alternate;
|
|
319
414
|
}
|
|
320
|
-
|
|
321
|
-
|
|
415
|
+
@keyframes stx-sidebar-shimmer {
|
|
416
|
+
from { background-position: 0% 0%; }
|
|
417
|
+
to { background-position: 100% 100%; }
|
|
322
418
|
}
|
|
323
419
|
@media (prefers-color-scheme: dark) {
|
|
324
|
-
.
|
|
325
|
-
|
|
420
|
+
.stx-sidebar-rim {
|
|
421
|
+
box-shadow:
|
|
422
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.18),
|
|
423
|
+
inset 1px 0 0 rgba(255, 255, 255, 0.10),
|
|
424
|
+
inset -1px 0 0 rgba(255, 255, 255, 0.05),
|
|
425
|
+
inset 0 -1px 0 rgba(255, 255, 255, 0.06);
|
|
326
426
|
}
|
|
327
|
-
.
|
|
328
|
-
|
|
427
|
+
.stx-sidebar-rim::after { opacity: 0.35; }
|
|
428
|
+
}
|
|
429
|
+
@media (prefers-reduced-motion: reduce) {
|
|
430
|
+
.stx-sidebar-rim::after { animation: none; }
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/* Rows collapse like AppKit outlines: fixed row height animates to zero. */
|
|
434
|
+
[data-sidebar-row] {
|
|
435
|
+
height: 32px; /* 30px row + 2px gap */
|
|
436
|
+
padding-bottom: 2px;
|
|
437
|
+
transition: height 0.22s ease, opacity 0.16s ease;
|
|
438
|
+
}
|
|
439
|
+
[data-sidebar-row][data-collapsed] {
|
|
440
|
+
height: 0;
|
|
441
|
+
opacity: 0;
|
|
442
|
+
overflow: hidden;
|
|
443
|
+
pointer-events: none;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/* Disclosure chevron rotates from ▸ to ▾ */
|
|
447
|
+
[data-sidebar-row][data-expanded="true"] [data-sidebar-disclosure] > * {
|
|
448
|
+
transform: rotate(90deg);
|
|
449
|
+
}
|
|
450
|
+
[data-sidebar-section][data-expanded="false"] [data-sidebar-section-chevron] {
|
|
451
|
+
transform: rotate(-90deg);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/* Native scrollbar hidden; the floating thumb below takes its place. */
|
|
455
|
+
.stx-sidebar-scroll {
|
|
456
|
+
scrollbar-width: none;
|
|
457
|
+
}
|
|
458
|
+
.stx-sidebar-scroll::-webkit-scrollbar {
|
|
459
|
+
display: none;
|
|
460
|
+
}
|
|
461
|
+
.stx-sidebar-scroll-thumb {
|
|
462
|
+
position: absolute;
|
|
463
|
+
top: 0;
|
|
464
|
+
right: 3px;
|
|
465
|
+
width: 7px;
|
|
466
|
+
border-radius: 4px;
|
|
467
|
+
background: rgba(0, 0, 0, 0.35);
|
|
468
|
+
opacity: 0;
|
|
469
|
+
transition: opacity 0.3s ease;
|
|
470
|
+
pointer-events: none;
|
|
471
|
+
z-index: 20;
|
|
472
|
+
}
|
|
473
|
+
@media (prefers-color-scheme: dark) {
|
|
474
|
+
.stx-sidebar-scroll-thumb { background: rgba(255, 255, 255, 0.35); }
|
|
329
475
|
}
|
|
330
476
|
</style>
|