@stacksjs/components 0.2.110 → 0.2.112
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 → Sidebar-bg47fqmq.stx} +100 -3
- package/dist/{SidebarItem-gtrcw4ky.stx → SidebarItem-9rvbwhxr.stx} +11 -7
- package/dist/index.js +15 -12
- package/dist/ui/sidebar/themes.d.ts +26 -3
- package/package.json +4 -3
- package/src/components/CodeBlock.stx +54 -0
- package/src/components/Footer.stx +66 -0
- package/src/components/Hero.stx +25 -0
- package/src/components/Installation.stx +37 -0
- package/src/components/index.ts +4 -0
- package/src/composables/index.ts +4 -0
- package/src/composables/useCopyCode.ts +73 -0
- package/src/composables/useDarkMode.ts +100 -0
- package/src/composables/useSEO.ts +230 -0
- package/src/composables/utilities/index.ts +7 -0
- package/src/composables/utilities/useClickOutside.ts +72 -0
- package/src/composables/utilities/useFocusTrap.ts +159 -0
- package/src/composables/utilities/useKeyboard.ts +124 -0
- package/src/composables/utilities/useLocalStorage.ts +132 -0
- package/src/composables/utilities/useMediaQuery.ts +118 -0
- package/src/index.ts +99 -0
- package/src/stx.d.ts +4 -0
- package/src/ui/accordion/Accordion.stx +146 -0
- package/src/ui/accordion/AccordionItem.stx +37 -0
- package/src/ui/accordion/index.ts +47 -0
- package/src/ui/audio/Audio.stx +50 -0
- package/src/ui/audio/README.md +41 -0
- package/src/ui/audio/index.ts +13 -0
- package/src/ui/auth/Login.stx +161 -0
- package/src/ui/auth/README.md +142 -0
- package/src/ui/auth/Signup.stx +159 -0
- package/src/ui/auth/TwoFactorChallenge.stx +111 -0
- package/src/ui/auth/index.ts +32 -0
- package/src/ui/avatar/Avatar.stx +77 -0
- package/src/ui/avatar/index.ts +12 -0
- package/src/ui/badge/Badge.stx +62 -0
- package/src/ui/badge/index.ts +10 -0
- package/src/ui/breadcrumb/Breadcrumb.stx +79 -0
- package/src/ui/breadcrumb/index.ts +15 -0
- package/src/ui/button/Button.stx +74 -0
- package/src/ui/button/README.md +80 -0
- package/src/ui/button/index.ts +85 -0
- package/src/ui/calendar/Calendar.stx +189 -0
- package/src/ui/calendar/README.md +254 -0
- package/src/ui/calendar/index.ts +14 -0
- package/src/ui/card/Card.stx +68 -0
- package/src/ui/card/index.ts +13 -0
- package/src/ui/checkbox/Checkbox.stx +86 -0
- package/src/ui/checkbox/index.ts +16 -0
- package/src/ui/combobox/Combobox.stx +96 -0
- package/src/ui/combobox/ComboboxButton.stx +25 -0
- package/src/ui/combobox/ComboboxInput.stx +32 -0
- package/src/ui/combobox/ComboboxOption.stx +49 -0
- package/src/ui/combobox/ComboboxOptions.stx +16 -0
- package/src/ui/combobox/README.md +150 -0
- package/src/ui/combobox/index.ts +40 -0
- package/src/ui/command-palette/CommandPalette.stx +81 -0
- package/src/ui/command-palette/CommandPaletteItem.stx +27 -0
- package/src/ui/command-palette/README.md +42 -0
- package/src/ui/command-palette/index.ts +15 -0
- package/src/ui/dialog/Dialog.stx +41 -0
- package/src/ui/dialog/DialogBackdrop.stx +9 -0
- package/src/ui/dialog/DialogDescription.stx +11 -0
- package/src/ui/dialog/DialogPanel.stx +16 -0
- package/src/ui/dialog/DialogTitle.stx +11 -0
- package/src/ui/dialog/README.md +121 -0
- package/src/ui/dialog/index.ts +32 -0
- package/src/ui/drawer/Drawer.stx +120 -0
- package/src/ui/drawer/README.md +90 -0
- package/src/ui/drawer/index.ts +9 -0
- package/src/ui/dropdown/Dropdown.stx +82 -0
- package/src/ui/dropdown/DropdownButton.stx +29 -0
- package/src/ui/dropdown/DropdownItem.stx +35 -0
- package/src/ui/dropdown/DropdownItems.stx +18 -0
- package/src/ui/dropdown/README.md +114 -0
- package/src/ui/dropdown/index.ts +29 -0
- package/src/ui/form/Form.stx +95 -0
- package/src/ui/form/index.ts +67 -0
- package/src/ui/heatmap/Heatmap.stx +294 -0
- package/src/ui/image/Image.stx +165 -0
- package/src/ui/image/README.md +164 -0
- package/src/ui/image/index.ts +22 -0
- package/src/ui/input/EmailInput.stx +30 -0
- package/src/ui/input/NumberInput.stx +126 -0
- package/src/ui/input/PasswordInput.stx +143 -0
- package/src/ui/input/SearchInput.stx +44 -0
- package/src/ui/input/TextInput.stx +132 -0
- package/src/ui/input/index.ts +180 -0
- package/src/ui/listbox/Listbox.stx +101 -0
- package/src/ui/listbox/ListboxButton.stx +28 -0
- package/src/ui/listbox/ListboxLabel.stx +11 -0
- package/src/ui/listbox/ListboxOption.stx +49 -0
- package/src/ui/listbox/ListboxOptions.stx +16 -0
- package/src/ui/listbox/README.md +135 -0
- package/src/ui/listbox/index.ts +39 -0
- package/src/ui/navigator/Navigator.stx +113 -0
- package/src/ui/navigator/README.md +352 -0
- package/src/ui/navigator/index.ts +20 -0
- package/src/ui/notification/Notification.stx +117 -0
- package/src/ui/notification/README.md +114 -0
- package/src/ui/notification/index.ts +12 -0
- package/src/ui/pagination/Pagination.stx +150 -0
- package/src/ui/pagination/index.ts +11 -0
- package/src/ui/payment/Checkout.stx +31 -0
- package/src/ui/payment/DefaultPaymentMethod.stx +62 -0
- package/src/ui/payment/PaymentMethods.stx +104 -0
- package/src/ui/payment/README.md +345 -0
- package/src/ui/payment/SubscriptionCheckout.stx +323 -0
- package/src/ui/payment/index.ts +59 -0
- package/src/ui/popover/Popover.stx +60 -0
- package/src/ui/popover/PopoverButton.stx +29 -0
- package/src/ui/popover/PopoverPanel.stx +23 -0
- package/src/ui/popover/README.md +91 -0
- package/src/ui/popover/index.ts +22 -0
- package/src/ui/portal/Portal.stx +57 -0
- package/src/ui/portal/index.ts +6 -0
- package/src/ui/progress/Progress.stx +131 -0
- package/src/ui/progress/index.ts +12 -0
- package/src/ui/radio/Radio.stx +77 -0
- package/src/ui/radio/index.ts +15 -0
- package/src/ui/radio-group/README.md +139 -0
- package/src/ui/radio-group/RadioGroup.stx +60 -0
- package/src/ui/radio-group/RadioGroupDescription.stx +11 -0
- package/src/ui/radio-group/RadioGroupLabel.stx +11 -0
- package/src/ui/radio-group/RadioGroupOption.stx +58 -0
- package/src/ui/radio-group/index.ts +30 -0
- package/src/ui/select/README.md +180 -0
- package/src/ui/select/Select.stx +82 -0
- package/src/ui/select/index.ts +21 -0
- package/src/ui/sidebar/Sidebar.stx +573 -0
- package/src/ui/sidebar/SidebarFooter.stx +71 -0
- package/src/ui/sidebar/SidebarHeader.stx +103 -0
- package/src/ui/sidebar/SidebarItem.stx +90 -0
- package/src/ui/sidebar/SidebarSection.stx +83 -0
- package/src/ui/sidebar/index.ts +146 -0
- package/src/ui/sidebar/themes.ts +288 -0
- package/src/ui/skeleton/Skeleton.stx +48 -0
- package/src/ui/skeleton/index.ts +11 -0
- package/src/ui/spinner/Spinner.stx +109 -0
- package/src/ui/spinner/index.ts +9 -0
- package/src/ui/stepper/README.md +36 -0
- package/src/ui/stepper/Stepper.stx +17 -0
- package/src/ui/stepper/StepperStep.stx +57 -0
- package/src/ui/stepper/index.ts +19 -0
- package/src/ui/storage/README.md +193 -0
- package/src/ui/storage/index.ts +347 -0
- package/src/ui/switch/README.md +114 -0
- package/src/ui/switch/Switch.stx +72 -0
- package/src/ui/switch/index.ts +10 -0
- package/src/ui/table/README.md +39 -0
- package/src/ui/table/Table.stx +14 -0
- package/src/ui/table/TableBody.stx +10 -0
- package/src/ui/table/TableCell.stx +10 -0
- package/src/ui/table/TableHead.stx +10 -0
- package/src/ui/table/TableHeader.stx +20 -0
- package/src/ui/table/TableRow.stx +11 -0
- package/src/ui/table/index.ts +35 -0
- package/src/ui/tabs/TabPanel.stx +25 -0
- package/src/ui/tabs/Tabs.stx +202 -0
- package/src/ui/tabs/index.ts +50 -0
- package/src/ui/teleport/Teleport.stx +65 -0
- package/src/ui/teleport/index.ts +7 -0
- package/src/ui/textarea/Textarea.stx +108 -0
- package/src/ui/textarea/index.ts +20 -0
- package/src/ui/tooltip/Tooltip.stx +71 -0
- package/src/ui/tooltip/index.ts +10 -0
- package/src/ui/transition/README.md +30 -0
- package/src/ui/transition/Transition.stx +102 -0
- package/src/ui/transition/index.ts +25 -0
- package/src/ui/video/README.md +47 -0
- package/src/ui/video/Video.stx +45 -0
- package/src/ui/video/index.ts +16 -0
- package/src/ui/virtual-list/VirtualList.stx +67 -0
- package/src/ui/virtual-list/index.ts +10 -0
- package/src/ui/virtual-table/VirtualTable.stx +103 -0
- package/src/ui/virtual-table/index.ts +21 -0
- package/src/utils/PROP_VALIDATION.md +458 -0
- package/src/utils/THEME_SYSTEM.md +702 -0
- package/src/utils/accessibility.ts +841 -0
- package/src/utils/animation/easing.ts +368 -0
- package/src/utils/animation/helpers.ts +223 -0
- package/src/utils/animation/index.ts +5 -0
- package/src/utils/animation/keyframes.ts +230 -0
- package/src/utils/error-handling.ts +475 -0
- package/src/utils/highlighter.ts +93 -0
- package/src/utils/i18n.ts +634 -0
- package/src/utils/keep-alive.ts +819 -0
- package/src/utils/lazy-loading.ts +530 -0
- package/src/utils/prop-validation.ts +478 -0
- package/src/utils/theme.ts +626 -0
|
@@ -0,0 +1,573 @@
|
|
|
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? }] }]
|
|
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.
|
|
17
|
+
export const collapsed = $props.collapsed ?? false
|
|
18
|
+
export const collapsible = $props.collapsible ?? true
|
|
19
|
+
export const collapseMode = $props.collapseMode || (theme === 'workspace' || theme === 'desktop' ? 'rail' : 'hidden')
|
|
20
|
+
export const minWidth = $props.minWidth || 64
|
|
21
|
+
export const persistKey = $props.persistKey || ''
|
|
22
|
+
export const shellSelector = $props.shellSelector || ''
|
|
23
|
+
export const widthVar = $props.widthVar || '--stx-sidebar-width'
|
|
24
|
+
export const collapsedClass = $props.collapsedClass || ''
|
|
25
|
+
export const className = $props.className || ''
|
|
26
|
+
|
|
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 selectionModel = themeStyle.selection
|
|
33
|
+
export const hasShimmerRim = themeStyle.layers.shimmerRim === true
|
|
34
|
+
export const isFrosted = themeStyle.layers.frosted === true
|
|
35
|
+
export const tintClasses = themeStyle.layers.tint || ''
|
|
36
|
+
export const scrollAreaClasses = themeStyle.scrollArea
|
|
37
|
+
|
|
38
|
+
const borderSide = position === 'left' ? 'border-r' : 'border-l'
|
|
39
|
+
const borderClasses = bordered ? `${borderSide} border-black/5 dark:border-white/10` : ''
|
|
40
|
+
const placementClasses = {
|
|
41
|
+
fixed: `fixed inset-y-0 z-50 ${position === 'left' ? 'left-0' : 'right-0'}`,
|
|
42
|
+
sticky: 'sticky top-0 h-screen',
|
|
43
|
+
static: 'relative h-full',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const paneClasses = [
|
|
47
|
+
placementClasses[placement] || placementClasses.fixed,
|
|
48
|
+
'flex flex-col overflow-hidden',
|
|
49
|
+
'transition-[width] duration-300 ease-out',
|
|
50
|
+
themeStyle.pane,
|
|
51
|
+
borderClasses,
|
|
52
|
+
className,
|
|
53
|
+
].filter(Boolean).join(' ')
|
|
54
|
+
|
|
55
|
+
// Normalized ahead of the loop so template bindings stay simple accessors.
|
|
56
|
+
export const sectionList = sections.map(section => ({
|
|
57
|
+
id: section.id,
|
|
58
|
+
label: section.label || '',
|
|
59
|
+
items: section.items || [],
|
|
60
|
+
collapsible: section.collapsible !== false,
|
|
61
|
+
collapsed: section.collapsed === true,
|
|
62
|
+
}))
|
|
63
|
+
|
|
64
|
+
// State classes the client controller swaps on rows. Kept as data attributes
|
|
65
|
+
// so all behavior lives in one place (the controller below) and themes stay
|
|
66
|
+
// pure data.
|
|
67
|
+
export const activeClasses = themeStyle.item.active
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<script client>
|
|
71
|
+
const emit = defineEmits()
|
|
72
|
+
const sidebarUidProp = '{{ sidebarUid }}'
|
|
73
|
+
const collapsedProp = {{ collapsed }}
|
|
74
|
+
const collapsibleProp = {{ collapsible }}
|
|
75
|
+
const collapseModeProp = '{{ collapseMode }}'
|
|
76
|
+
const widthProp = {{ width }}
|
|
77
|
+
const minWidthProp = {{ minWidth }}
|
|
78
|
+
const persistKeyProp = '{{ persistKey }}'
|
|
79
|
+
const shellSelectorProp = '{{ shellSelector }}'
|
|
80
|
+
const widthVarProp = '{{ widthVar }}'
|
|
81
|
+
const collapsedClassProp = '{{ collapsedClass }}'
|
|
82
|
+
|
|
83
|
+
const collapsed = state(collapsedProp)
|
|
84
|
+
const sidebarWidth = derived(() => {
|
|
85
|
+
if (collapsed()) return collapseModeProp === 'hidden' ? '0px' : `${minWidthProp}px`
|
|
86
|
+
return `${widthProp}px`
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
let pane = null
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Selection — one row carries data-active="true"; the highlight classes come
|
|
93
|
+
// from the theme (stored on the pane as data-active-class).
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
function activeClassList() {
|
|
97
|
+
return (pane?.dataset.activeClass || '').split(' ').filter(Boolean)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function setActiveRow(button) {
|
|
101
|
+
const classes = activeClassList()
|
|
102
|
+
pane.querySelectorAll('[data-sidebar-item][data-active="true"]').forEach((el) => {
|
|
103
|
+
el.removeAttribute('data-active')
|
|
104
|
+
el.classList.remove(...classes)
|
|
105
|
+
})
|
|
106
|
+
button.setAttribute('data-active', 'true')
|
|
107
|
+
button.classList.add(...classes)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Disclosure — rows know their ancestors (data-parents="a/b"). Collapsing a
|
|
112
|
+
// row (or section) marks every descendant with data-collapsed, which the
|
|
113
|
+
// scoped CSS animates to zero height, exactly like AppKit's outline collapse.
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
function syncRowVisibility() {
|
|
117
|
+
const collapsedIds = new Set()
|
|
118
|
+
pane.querySelectorAll('[data-sidebar-row][data-expanded="false"]').forEach((row) => {
|
|
119
|
+
collapsedIds.add(row.dataset.itemId)
|
|
120
|
+
})
|
|
121
|
+
pane.querySelectorAll('[data-sidebar-section][data-expanded="false"]').forEach((section) => {
|
|
122
|
+
section.querySelectorAll('[data-sidebar-row]').forEach((row) => {
|
|
123
|
+
row.setAttribute('data-collapsed', '')
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
pane.querySelectorAll('[data-sidebar-section][data-expanded="true"] [data-sidebar-row]').forEach((row) => {
|
|
127
|
+
const parents = (row.dataset.parents || '').split('/').filter(Boolean)
|
|
128
|
+
const hidden = parents.some(id => collapsedIds.has(id))
|
|
129
|
+
if (hidden) row.setAttribute('data-collapsed', '')
|
|
130
|
+
else row.removeAttribute('data-collapsed')
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function toggleRowDisclosure(row) {
|
|
135
|
+
const expanded = row.getAttribute('data-expanded') === 'true'
|
|
136
|
+
row.setAttribute('data-expanded', String(!expanded))
|
|
137
|
+
syncRowVisibility()
|
|
138
|
+
emit('itemToggle', { id: row.dataset.itemId, expanded: !expanded })
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function toggleSection(section) {
|
|
142
|
+
const expanded = section.getAttribute('data-expanded') !== 'false'
|
|
143
|
+
section.setAttribute('data-expanded', String(!expanded))
|
|
144
|
+
syncRowVisibility()
|
|
145
|
+
emit('sectionToggle', section.dataset.sectionId)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Pointer input (event delegation — items and sections stay presentational)
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
function onClick(event) {
|
|
153
|
+
const chevron = event.target.closest('[data-sidebar-disclosure]')
|
|
154
|
+
if (chevron) {
|
|
155
|
+
event.preventDefault()
|
|
156
|
+
toggleRowDisclosure(chevron.closest('[data-sidebar-row]'))
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const sectionHeader = event.target.closest('[data-sidebar-section-header]')
|
|
161
|
+
if (sectionHeader) {
|
|
162
|
+
toggleSection(sectionHeader.closest('[data-sidebar-section]'))
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const item = event.target.closest('[data-sidebar-item]')
|
|
167
|
+
if (item && !item.hasAttribute('data-disabled')) {
|
|
168
|
+
setActiveRow(item)
|
|
169
|
+
const row = item.closest('[data-sidebar-row]')
|
|
170
|
+
emit('itemClick', {
|
|
171
|
+
item: { id: row.dataset.itemId, label: item.dataset.label || '', href: item.getAttribute('href') || '' },
|
|
172
|
+
event,
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Keyboard — matches macOS source lists: ↑/↓ move the selection over visible
|
|
179
|
+
// rows, ←/→ collapse/expand, Return opens.
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
function visibleItems() {
|
|
183
|
+
return [...pane.querySelectorAll('[data-sidebar-row]:not([data-collapsed]) [data-sidebar-item]:not([data-disabled])')]
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function onKeyDown(event) {
|
|
187
|
+
const items = visibleItems()
|
|
188
|
+
if (items.length === 0) return
|
|
189
|
+
const current = pane.querySelector('[data-sidebar-item][data-active="true"]')
|
|
190
|
+
const index = items.indexOf(current)
|
|
191
|
+
|
|
192
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
193
|
+
event.preventDefault()
|
|
194
|
+
const next = event.key === 'ArrowDown'
|
|
195
|
+
? items[Math.min(index + 1, items.length - 1)]
|
|
196
|
+
: items[Math.max(index - 1, 0)]
|
|
197
|
+
if (next && next !== current) {
|
|
198
|
+
setActiveRow(next)
|
|
199
|
+
next.scrollIntoView({ block: 'nearest' })
|
|
200
|
+
emit('itemClick', { item: { id: next.closest('[data-sidebar-row]').dataset.itemId, label: next.dataset.label || '', href: next.getAttribute('href') || '' }, event })
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else if ((event.key === 'ArrowRight' || event.key === 'ArrowLeft') && current) {
|
|
204
|
+
const row = current.closest('[data-sidebar-row]')
|
|
205
|
+
if (row.querySelector('[data-sidebar-disclosure]')) {
|
|
206
|
+
const expanded = row.getAttribute('data-expanded') === 'true'
|
|
207
|
+
if (event.key === 'ArrowRight' ? !expanded : expanded) {
|
|
208
|
+
event.preventDefault()
|
|
209
|
+
toggleRowDisclosure(row)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else if (event.key === 'Enter' && current) {
|
|
214
|
+
current.click()
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// Overlay scrollbar — a floating thumb that appears while scrolling and fades
|
|
220
|
+
// away, like macOS overlay scrollbars. The native scrollbar is hidden so rows
|
|
221
|
+
// keep their exact insets.
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
function attachOverlayScrollbar(scrollArea, thumb) {
|
|
225
|
+
let fadeTimer
|
|
226
|
+
|
|
227
|
+
function update() {
|
|
228
|
+
const { scrollTop, scrollHeight, clientHeight } = scrollArea
|
|
229
|
+
if (scrollHeight <= clientHeight) {
|
|
230
|
+
thumb.style.opacity = '0'
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
const trackHeight = clientHeight - 8
|
|
234
|
+
const height = Math.max(24, (clientHeight / scrollHeight) * trackHeight)
|
|
235
|
+
const top = 4 + (scrollTop / (scrollHeight - clientHeight)) * (trackHeight - height)
|
|
236
|
+
thumb.style.height = `${height}px`
|
|
237
|
+
thumb.style.transform = `translateY(${top}px)`
|
|
238
|
+
thumb.style.opacity = '1'
|
|
239
|
+
clearTimeout(fadeTimer)
|
|
240
|
+
fadeTimer = setTimeout(() => { thumb.style.opacity = '0' }, 900)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
scrollArea.addEventListener('scroll', update, { passive: true })
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// Collapse (sidebar-wide) — persisted, synced to the app shell and to Craft's
|
|
248
|
+
// native web-sidebar material when present.
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
function applyCollapseEffects(nextCollapsed) {
|
|
252
|
+
if (persistKeyProp) {
|
|
253
|
+
try { window.localStorage?.setItem(persistKeyProp, String(nextCollapsed)) } catch {}
|
|
254
|
+
}
|
|
255
|
+
if (shellSelectorProp) {
|
|
256
|
+
document.querySelectorAll(shellSelectorProp).forEach((shell) => {
|
|
257
|
+
shell.style?.setProperty?.(widthVarProp, nextCollapsed && collapseModeProp === 'hidden' ? '0px' : `${widthProp}px`)
|
|
258
|
+
if (shell.dataset) shell.dataset.sidebarCollapsed = String(nextCollapsed)
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
document.documentElement.dataset.stxSidebarCollapsed = String(nextCollapsed)
|
|
262
|
+
if (collapsedClassProp) document.documentElement.classList.toggle(collapsedClassProp, nextCollapsed)
|
|
263
|
+
window.craft?.window?.setWebSidebarCollapsed?.(nextCollapsed).catch?.(() => {})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function setCollapsed(nextCollapsed) {
|
|
267
|
+
collapsed.set(nextCollapsed)
|
|
268
|
+
applyCollapseEffects(nextCollapsed)
|
|
269
|
+
emit('collapse', nextCollapsed)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function toggleCollapse() {
|
|
273
|
+
if (collapsibleProp) setCollapsed(!collapsed())
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
// Appearance — native sidebars follow the OS light/dark appearance. Crosswind
|
|
278
|
+
// dark: utilities are class-based, so mirror prefers-color-scheme onto the
|
|
279
|
+
// root element unless the app manages its own theme (data-theme attribute).
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// Key-window tracking — AppKit dims a source-list selection from the accent
|
|
284
|
+
// to a neutral fill the moment the window resigns key. `data-window-key` on
|
|
285
|
+
// the pane drives the two selection rule sets in the scoped CSS above.
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
function trackKeyWindow() {
|
|
289
|
+
// Driven by real focus/blur events only — never by an initial
|
|
290
|
+
// `document.hasFocus()` reading. A freshly loaded page, a background tab
|
|
291
|
+
// that has not been clicked yet, and an embedded webview that does not
|
|
292
|
+
// report focus at all all answer `false`, which would open the sidebar in
|
|
293
|
+
// the dimmed non-key state and leave it there. Starting from the key state
|
|
294
|
+
// and reacting to an actual blur keeps the default correct everywhere and
|
|
295
|
+
// still dims exactly when AppKit would.
|
|
296
|
+
window.addEventListener('focus', () => pane.setAttribute('data-window-key', 'true'))
|
|
297
|
+
window.addEventListener('blur', () => pane.setAttribute('data-window-key', 'false'))
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function followSystemAppearance() {
|
|
301
|
+
const media = window.matchMedia('(prefers-color-scheme: dark)')
|
|
302
|
+
function sync() {
|
|
303
|
+
if (document.documentElement.dataset.theme) return
|
|
304
|
+
document.documentElement.classList.toggle('dark', media.matches)
|
|
305
|
+
}
|
|
306
|
+
sync()
|
|
307
|
+
media.addEventListener('change', sync)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
onMount(() => {
|
|
311
|
+
pane = document.getElementById(sidebarUidProp)
|
|
312
|
+
if (!pane) return
|
|
313
|
+
|
|
314
|
+
followSystemAppearance()
|
|
315
|
+
trackKeyWindow()
|
|
316
|
+
|
|
317
|
+
if (persistKeyProp) {
|
|
318
|
+
try {
|
|
319
|
+
const saved = window.localStorage?.getItem(persistKeyProp)
|
|
320
|
+
if (saved === 'true' || saved === 'false') collapsed.set(saved === 'true')
|
|
321
|
+
} catch {}
|
|
322
|
+
}
|
|
323
|
+
applyCollapseEffects(collapsed())
|
|
324
|
+
|
|
325
|
+
pane.addEventListener('click', onClick)
|
|
326
|
+
pane.addEventListener('keydown', onKeyDown)
|
|
327
|
+
|
|
328
|
+
const scrollArea = pane.querySelector('[data-sidebar-scroll]')
|
|
329
|
+
const thumb = pane.querySelector('[data-sidebar-scroll-thumb]')
|
|
330
|
+
if (scrollArea && thumb) attachOverlayScrollbar(scrollArea, thumb)
|
|
331
|
+
|
|
332
|
+
// Ensure the initial active row carries the theme's highlight classes.
|
|
333
|
+
const active = pane.querySelector('[data-sidebar-item][data-active="true"]')
|
|
334
|
+
if (active) active.classList.add(...activeClassList())
|
|
335
|
+
syncRowVisibility()
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
defineExpose({ collapsed, toggle: toggleCollapse })
|
|
339
|
+
</script>
|
|
340
|
+
|
|
341
|
+
<aside
|
|
342
|
+
id="{{ sidebarUid }}"
|
|
343
|
+
data-stx-sidebar
|
|
344
|
+
data-sidebar-theme="{{ theme }}"
|
|
345
|
+
data-sidebar-selection="{{ selectionModel }}"
|
|
346
|
+
@if(isFrosted)data-sidebar-frosted@endif
|
|
347
|
+
data-window-key="true"
|
|
348
|
+
data-active-class="{{ activeClasses }}"
|
|
349
|
+
class="{{ paneClasses }}"
|
|
350
|
+
:style="`width: ${sidebarWidth()}`"
|
|
351
|
+
:aria-hidden="String(collapsed() && collapseModeProp === 'hidden')"
|
|
352
|
+
role="navigation"
|
|
353
|
+
aria-label="Sidebar"
|
|
354
|
+
tabindex="0"
|
|
355
|
+
>
|
|
356
|
+
@if(tintClasses)
|
|
357
|
+
<div class="absolute inset-0 pointer-events-none {{ tintClasses }}"></div>
|
|
358
|
+
@endif
|
|
359
|
+
@if(hasShimmerRim)
|
|
360
|
+
<div class="stx-sidebar-rim" aria-hidden="true"></div>
|
|
361
|
+
@endif
|
|
362
|
+
|
|
363
|
+
@if($slots.header)
|
|
364
|
+
<div class="relative z-10 flex-shrink-0">
|
|
365
|
+
<slot name="header" />
|
|
366
|
+
</div>
|
|
367
|
+
@endif
|
|
368
|
+
|
|
369
|
+
<div class="relative z-10 min-h-0 flex-1">
|
|
370
|
+
<div data-sidebar-scroll class="{{ scrollAreaClasses }} h-full stx-sidebar-scroll">
|
|
371
|
+
@if(sectionList.length > 0)
|
|
372
|
+
<nav class="flex flex-col">
|
|
373
|
+
@foreach(sectionList as section)
|
|
374
|
+
<SidebarSection
|
|
375
|
+
:id="section.id"
|
|
376
|
+
:label="section.label"
|
|
377
|
+
:items="section.items"
|
|
378
|
+
:collapsible="section.collapsible"
|
|
379
|
+
:collapsed="section.collapsed"
|
|
380
|
+
:theme="theme"
|
|
381
|
+
/>
|
|
382
|
+
@endforeach
|
|
383
|
+
</nav>
|
|
384
|
+
@else
|
|
385
|
+
<slot />
|
|
386
|
+
@endif
|
|
387
|
+
</div>
|
|
388
|
+
<div data-sidebar-scroll-thumb class="stx-sidebar-scroll-thumb" aria-hidden="true"></div>
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
@if($slots.footer)
|
|
392
|
+
<div class="relative z-10 flex-shrink-0">
|
|
393
|
+
<slot name="footer" />
|
|
394
|
+
</div>
|
|
395
|
+
@endif
|
|
396
|
+
</aside>
|
|
397
|
+
|
|
398
|
+
<style scoped>
|
|
399
|
+
[data-stx-sidebar] {
|
|
400
|
+
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', sans-serif;
|
|
401
|
+
-webkit-font-smoothing: antialiased;
|
|
402
|
+
outline: none;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/* Liquid Glass edge highlight: a hairline rim that catches light along the
|
|
406
|
+
pane's edges, with a slow sheen drifting across it. */
|
|
407
|
+
.stx-sidebar-rim {
|
|
408
|
+
position: absolute;
|
|
409
|
+
inset: 0;
|
|
410
|
+
border-radius: inherit;
|
|
411
|
+
pointer-events: none;
|
|
412
|
+
z-index: 5;
|
|
413
|
+
box-shadow:
|
|
414
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.50),
|
|
415
|
+
inset 1px 0 0 rgba(255, 255, 255, 0.26),
|
|
416
|
+
inset -1px 0 0 rgba(255, 255, 255, 0.10),
|
|
417
|
+
inset 0 -1px 0 rgba(255, 255, 255, 0.14);
|
|
418
|
+
}
|
|
419
|
+
.stx-sidebar-rim::after {
|
|
420
|
+
content: '';
|
|
421
|
+
position: absolute;
|
|
422
|
+
inset: 0;
|
|
423
|
+
border-radius: inherit;
|
|
424
|
+
padding: 1.5px;
|
|
425
|
+
background: linear-gradient(
|
|
426
|
+
115deg,
|
|
427
|
+
rgba(255, 255, 255, 0.85) 0%,
|
|
428
|
+
rgba(255, 255, 255, 0.00) 28%,
|
|
429
|
+
rgba(255, 255, 255, 0.18) 52%,
|
|
430
|
+
rgba(255, 255, 255, 0.00) 74%,
|
|
431
|
+
rgba(255, 255, 255, 0.65) 100%
|
|
432
|
+
);
|
|
433
|
+
background-size: 250% 250%;
|
|
434
|
+
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
|
435
|
+
-webkit-mask-composite: xor;
|
|
436
|
+
mask-composite: exclude;
|
|
437
|
+
animation: stx-sidebar-shimmer 10s ease-in-out infinite alternate;
|
|
438
|
+
}
|
|
439
|
+
@keyframes stx-sidebar-shimmer {
|
|
440
|
+
from { background-position: 0% 0%; }
|
|
441
|
+
to { background-position: 100% 100%; }
|
|
442
|
+
}
|
|
443
|
+
@media (prefers-color-scheme: dark) {
|
|
444
|
+
.stx-sidebar-rim {
|
|
445
|
+
box-shadow:
|
|
446
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.18),
|
|
447
|
+
inset 1px 0 0 rgba(255, 255, 255, 0.10),
|
|
448
|
+
inset -1px 0 0 rgba(255, 255, 255, 0.05),
|
|
449
|
+
inset 0 -1px 0 rgba(255, 255, 255, 0.06);
|
|
450
|
+
}
|
|
451
|
+
.stx-sidebar-rim::after { opacity: 0.35; }
|
|
452
|
+
}
|
|
453
|
+
@media (prefers-reduced-motion: reduce) {
|
|
454
|
+
.stx-sidebar-rim::after { animation: none; }
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/* Frosted sidebar material. Hand-written CSS on purpose: as the utilities
|
|
458
|
+
`backdrop-blur-[50px]` / `backdrop-saturate-[180%]` the arbitrary values
|
|
459
|
+
did not survive class extraction in the stx dev server, leaving the pane
|
|
460
|
+
with `backdrop-filter: none`; an earlier `backdrop-saturate-[1.7]` lost
|
|
461
|
+
its brackets on the way to the generator, was read as a percentage, and
|
|
462
|
+
compiled to `saturate(0.017)`, which drained the colour out of the whole
|
|
463
|
+
backdrop. In a Craft window with webSidebarMaterial the native
|
|
464
|
+
NSVisualEffectView supplies the real vibrancy; this approximates it
|
|
465
|
+
everywhere else. */
|
|
466
|
+
[data-sidebar-frosted] {
|
|
467
|
+
-webkit-backdrop-filter: saturate(180%) blur(50px);
|
|
468
|
+
backdrop-filter: saturate(180%) blur(50px);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/* ---------------------------------------------------------------------------
|
|
472
|
+
* Selection, `accent` model (macOS and its derivatives).
|
|
473
|
+
*
|
|
474
|
+
* AppKit paints a source-list selection in two states. In a key window the
|
|
475
|
+
* row takes the system accent and its label, icon, count and disclosure
|
|
476
|
+
* chevron all turn white; the label also goes semibold. When the window
|
|
477
|
+
* resigns key the fill drops to a neutral gray and the label stays dark.
|
|
478
|
+
* Measured from Mail.app: key fill is the accent, non-key fill sits about
|
|
479
|
+
* 5.5% black over the pane, and the icons desaturate to secondary gray.
|
|
480
|
+
*
|
|
481
|
+
* These are attribute rules rather than utility classes because the row's
|
|
482
|
+
* children have to recolor together, and because the controller toggles
|
|
483
|
+
* `data-active` on the client — CSS keyed on the attribute keeps the
|
|
484
|
+
* previously selected row's own icon tint intact when selection moves.
|
|
485
|
+
* ------------------------------------------------------------------------- */
|
|
486
|
+
[data-sidebar-selection="accent"] {
|
|
487
|
+
--stx-sidebar-accent: #007aff;
|
|
488
|
+
--stx-sidebar-selection-idle: rgba(0, 0, 0, 0.055);
|
|
489
|
+
--stx-sidebar-on-selection: #fff;
|
|
490
|
+
--stx-sidebar-icon-idle: #8e8e93;
|
|
491
|
+
}
|
|
492
|
+
@media (prefers-color-scheme: dark) {
|
|
493
|
+
[data-sidebar-selection="accent"] {
|
|
494
|
+
--stx-sidebar-accent: #0a84ff;
|
|
495
|
+
--stx-sidebar-selection-idle: rgba(255, 255, 255, 0.11);
|
|
496
|
+
--stx-sidebar-icon-idle: #98989d;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/* Per-item tint, published by SidebarItem as a custom property. */
|
|
501
|
+
[data-sidebar-selection="accent"] [data-sidebar-icon] {
|
|
502
|
+
color: var(--stx-sidebar-icon, currentColor);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
[data-sidebar-selection="accent"][data-window-key="true"] [data-sidebar-item][data-active="true"] {
|
|
506
|
+
background: var(--stx-sidebar-accent);
|
|
507
|
+
color: var(--stx-sidebar-on-selection);
|
|
508
|
+
font-weight: 590;
|
|
509
|
+
}
|
|
510
|
+
[data-sidebar-selection="accent"][data-window-key="true"] [data-sidebar-item][data-active="true"] [data-sidebar-icon],
|
|
511
|
+
[data-sidebar-selection="accent"][data-window-key="true"] [data-sidebar-item][data-active="true"] [data-sidebar-count],
|
|
512
|
+
[data-sidebar-selection="accent"][data-window-key="true"] [data-sidebar-item][data-active="true"] [data-sidebar-chevron] {
|
|
513
|
+
color: var(--stx-sidebar-on-selection);
|
|
514
|
+
}
|
|
515
|
+
/* Hover must not lighten the selected row out from under the accent. */
|
|
516
|
+
[data-sidebar-selection="accent"][data-window-key="true"] [data-sidebar-item][data-active="true"]:hover {
|
|
517
|
+
background: var(--stx-sidebar-accent);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/* Window resigned key: neutral fill, label stays dark, icons desaturate. */
|
|
521
|
+
[data-sidebar-selection="accent"][data-window-key="false"] [data-sidebar-item][data-active="true"] {
|
|
522
|
+
background: var(--stx-sidebar-selection-idle);
|
|
523
|
+
font-weight: 590;
|
|
524
|
+
}
|
|
525
|
+
[data-sidebar-selection="accent"][data-window-key="false"] [data-sidebar-icon] {
|
|
526
|
+
color: var(--stx-sidebar-icon-idle);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/* Rows collapse like AppKit outlines: fixed row height animates to zero.
|
|
530
|
+
The 32px pitch is the row itself — AppKit leaves no gap, so a run of
|
|
531
|
+
selected or hovered rows reads as one continuous block. */
|
|
532
|
+
[data-sidebar-row] {
|
|
533
|
+
height: 32px;
|
|
534
|
+
transition: height 0.22s ease, opacity 0.16s ease;
|
|
535
|
+
}
|
|
536
|
+
[data-sidebar-row][data-collapsed] {
|
|
537
|
+
height: 0;
|
|
538
|
+
opacity: 0;
|
|
539
|
+
overflow: hidden;
|
|
540
|
+
pointer-events: none;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/* Disclosure chevron rotates from ▸ to ▾ */
|
|
544
|
+
[data-sidebar-row][data-expanded="true"] [data-sidebar-disclosure] > * {
|
|
545
|
+
transform: rotate(90deg);
|
|
546
|
+
}
|
|
547
|
+
[data-sidebar-section][data-expanded="false"] [data-sidebar-section-chevron] {
|
|
548
|
+
transform: rotate(-90deg);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/* Native scrollbar hidden; the floating thumb below takes its place. */
|
|
552
|
+
.stx-sidebar-scroll {
|
|
553
|
+
scrollbar-width: none;
|
|
554
|
+
}
|
|
555
|
+
.stx-sidebar-scroll::-webkit-scrollbar {
|
|
556
|
+
display: none;
|
|
557
|
+
}
|
|
558
|
+
.stx-sidebar-scroll-thumb {
|
|
559
|
+
position: absolute;
|
|
560
|
+
top: 0;
|
|
561
|
+
right: 3px;
|
|
562
|
+
width: 7px;
|
|
563
|
+
border-radius: 4px;
|
|
564
|
+
background: rgba(0, 0, 0, 0.35);
|
|
565
|
+
opacity: 0;
|
|
566
|
+
transition: opacity 0.3s ease;
|
|
567
|
+
pointer-events: none;
|
|
568
|
+
z-index: 20;
|
|
569
|
+
}
|
|
570
|
+
@media (prefers-color-scheme: dark) {
|
|
571
|
+
.stx-sidebar-scroll-thumb { background: rgba(255, 255, 255, 0.35); }
|
|
572
|
+
}
|
|
573
|
+
</style>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
// The bottom strip of a macOS sidebar — e.g. the account row in Music
|
|
3
|
+
// ("Chris Breuer" with an avatar) or custom actions.
|
|
4
|
+
export const theme = $props.theme || $props.variant || 'macos'
|
|
5
|
+
export const avatar = $props.avatar || ''
|
|
6
|
+
export const name = $props.name || ''
|
|
7
|
+
export const detail = $props.detail || ''
|
|
8
|
+
export const actions = $props.actions || [] // [{ id, icon, label }]
|
|
9
|
+
|
|
10
|
+
export const isMacos = theme === 'macos' || theme === 'tahoe' || theme === 'macos-tahoe' || theme === 'macos-latest'
|
|
11
|
+
|
|
12
|
+
export const profileClasses = [
|
|
13
|
+
'flex w-full items-center gap-[8px]',
|
|
14
|
+
'h-[44px] rounded-[10px] px-[6px]',
|
|
15
|
+
'text-[13px] font-medium text-black dark:text-white',
|
|
16
|
+
'transition-colors duration-150',
|
|
17
|
+
'hover:bg-black/4 active:bg-black/8',
|
|
18
|
+
'dark:hover:bg-white/6 dark:active:bg-white/12',
|
|
19
|
+
].join(' ')
|
|
20
|
+
|
|
21
|
+
export const actionClasses = [
|
|
22
|
+
'grid h-[28px] w-[28px] place-items-center rounded-[7px]',
|
|
23
|
+
'text-[#3c3c43]/70 dark:text-[#ebebf5]/70',
|
|
24
|
+
'transition-colors duration-150',
|
|
25
|
+
'hover:bg-black/5 dark:hover:bg-white/10',
|
|
26
|
+
].join(' ')
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<script client>
|
|
30
|
+
const emit = defineEmits()
|
|
31
|
+
|
|
32
|
+
function onProfileClick() {
|
|
33
|
+
emit('profileClick')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function onAction(event) {
|
|
37
|
+
emit('action', event.target.closest('[data-sidebar-action]')?.dataset.sidebarAction)
|
|
38
|
+
}
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
@if(isMacos)
|
|
42
|
+
<div class="shrink-0 px-[10px] pb-[8px] pt-[4px]">
|
|
43
|
+
@if(name)
|
|
44
|
+
<button type="button" class="{{ profileClasses }}" @click="onProfileClick()">
|
|
45
|
+
@if(avatar)
|
|
46
|
+
<img src="{{ avatar }}" alt="" class="h-[30px] w-[30px] rounded-full object-cover shadow-sm" />
|
|
47
|
+
@else
|
|
48
|
+
<span class="grid h-[30px] w-[30px] place-items-center rounded-full bg-black/10 text-[13px] font-semibold dark:bg-white/15">{{ name.charAt(0) }}</span>
|
|
49
|
+
@endif
|
|
50
|
+
<span class="min-w-0 flex-1 truncate text-left">{{ name }}</span>
|
|
51
|
+
@if(detail)
|
|
52
|
+
<span class="shrink-0 text-[12px] text-[#3c3c43]/60 dark:text-[#ebebf5]/60">{{ detail }}</span>
|
|
53
|
+
@endif
|
|
54
|
+
</button>
|
|
55
|
+
@endif
|
|
56
|
+
@if(actions.length > 0)
|
|
57
|
+
<div class="flex items-center gap-[4px] pt-[4px]">
|
|
58
|
+
@foreach(actions as action)
|
|
59
|
+
<button type="button" class="{{ actionClasses }}" aria-label="{{ action.label }}" data-sidebar-action="{{ action.id }}" @click="onAction($event)">
|
|
60
|
+
<span class="{{ action.icon }} h-[16px] w-[16px]" aria-hidden="true"></span>
|
|
61
|
+
</button>
|
|
62
|
+
@endforeach
|
|
63
|
+
</div>
|
|
64
|
+
@endif
|
|
65
|
+
<slot />
|
|
66
|
+
</div>
|
|
67
|
+
@else
|
|
68
|
+
<div class="shrink-0 border-t border-black/5 p-3 dark:border-white/10">
|
|
69
|
+
<slot />
|
|
70
|
+
</div>
|
|
71
|
+
@endif
|