@stacksjs/components 0.2.93 → 0.2.94

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.
Files changed (187) hide show
  1. package/dist/index.js +4 -4
  2. package/package.json +5 -6
  3. package/src/components/CodeBlock.stx +0 -54
  4. package/src/components/Footer.stx +0 -66
  5. package/src/components/Hero.stx +0 -25
  6. package/src/components/Installation.stx +0 -37
  7. package/src/components/index.ts +0 -4
  8. package/src/composables/index.ts +0 -4
  9. package/src/composables/useCopyCode.ts +0 -73
  10. package/src/composables/useDarkMode.ts +0 -100
  11. package/src/composables/useSEO.ts +0 -230
  12. package/src/composables/utilities/index.ts +0 -7
  13. package/src/composables/utilities/useClickOutside.ts +0 -72
  14. package/src/composables/utilities/useFocusTrap.ts +0 -159
  15. package/src/composables/utilities/useKeyboard.ts +0 -124
  16. package/src/composables/utilities/useLocalStorage.ts +0 -132
  17. package/src/composables/utilities/useMediaQuery.ts +0 -118
  18. package/src/index.ts +0 -99
  19. package/src/stx.d.ts +0 -4
  20. package/src/ui/accordion/Accordion.stx +0 -146
  21. package/src/ui/accordion/AccordionItem.stx +0 -37
  22. package/src/ui/accordion/index.ts +0 -47
  23. package/src/ui/audio/Audio.stx +0 -50
  24. package/src/ui/audio/README.md +0 -41
  25. package/src/ui/audio/index.ts +0 -13
  26. package/src/ui/auth/Login.stx +0 -161
  27. package/src/ui/auth/README.md +0 -142
  28. package/src/ui/auth/Signup.stx +0 -159
  29. package/src/ui/auth/TwoFactorChallenge.stx +0 -111
  30. package/src/ui/auth/index.ts +0 -32
  31. package/src/ui/avatar/Avatar.stx +0 -77
  32. package/src/ui/avatar/index.ts +0 -12
  33. package/src/ui/badge/Badge.stx +0 -62
  34. package/src/ui/badge/index.ts +0 -10
  35. package/src/ui/breadcrumb/Breadcrumb.stx +0 -79
  36. package/src/ui/breadcrumb/index.ts +0 -15
  37. package/src/ui/button/Button.stx +0 -74
  38. package/src/ui/button/README.md +0 -80
  39. package/src/ui/button/index.ts +0 -85
  40. package/src/ui/calendar/Calendar.stx +0 -189
  41. package/src/ui/calendar/README.md +0 -254
  42. package/src/ui/calendar/index.ts +0 -14
  43. package/src/ui/card/Card.stx +0 -68
  44. package/src/ui/card/index.ts +0 -13
  45. package/src/ui/checkbox/Checkbox.stx +0 -86
  46. package/src/ui/checkbox/index.ts +0 -16
  47. package/src/ui/combobox/Combobox.stx +0 -96
  48. package/src/ui/combobox/ComboboxButton.stx +0 -25
  49. package/src/ui/combobox/ComboboxInput.stx +0 -32
  50. package/src/ui/combobox/ComboboxOption.stx +0 -49
  51. package/src/ui/combobox/ComboboxOptions.stx +0 -16
  52. package/src/ui/combobox/README.md +0 -150
  53. package/src/ui/combobox/index.ts +0 -40
  54. package/src/ui/command-palette/CommandPalette.stx +0 -81
  55. package/src/ui/command-palette/CommandPaletteItem.stx +0 -27
  56. package/src/ui/command-palette/README.md +0 -42
  57. package/src/ui/command-palette/index.ts +0 -15
  58. package/src/ui/dialog/Dialog.stx +0 -41
  59. package/src/ui/dialog/DialogBackdrop.stx +0 -9
  60. package/src/ui/dialog/DialogDescription.stx +0 -11
  61. package/src/ui/dialog/DialogPanel.stx +0 -16
  62. package/src/ui/dialog/DialogTitle.stx +0 -11
  63. package/src/ui/dialog/README.md +0 -121
  64. package/src/ui/dialog/index.ts +0 -32
  65. package/src/ui/drawer/Drawer.stx +0 -120
  66. package/src/ui/drawer/README.md +0 -90
  67. package/src/ui/drawer/index.ts +0 -9
  68. package/src/ui/dropdown/Dropdown.stx +0 -82
  69. package/src/ui/dropdown/DropdownButton.stx +0 -29
  70. package/src/ui/dropdown/DropdownItem.stx +0 -35
  71. package/src/ui/dropdown/DropdownItems.stx +0 -18
  72. package/src/ui/dropdown/README.md +0 -114
  73. package/src/ui/dropdown/index.ts +0 -29
  74. package/src/ui/form/Form.stx +0 -95
  75. package/src/ui/form/index.ts +0 -67
  76. package/src/ui/heatmap/Heatmap.stx +0 -294
  77. package/src/ui/image/Image.stx +0 -165
  78. package/src/ui/image/README.md +0 -164
  79. package/src/ui/image/index.ts +0 -22
  80. package/src/ui/input/EmailInput.stx +0 -30
  81. package/src/ui/input/NumberInput.stx +0 -126
  82. package/src/ui/input/PasswordInput.stx +0 -143
  83. package/src/ui/input/SearchInput.stx +0 -44
  84. package/src/ui/input/TextInput.stx +0 -132
  85. package/src/ui/input/index.ts +0 -180
  86. package/src/ui/listbox/Listbox.stx +0 -101
  87. package/src/ui/listbox/ListboxButton.stx +0 -28
  88. package/src/ui/listbox/ListboxLabel.stx +0 -11
  89. package/src/ui/listbox/ListboxOption.stx +0 -49
  90. package/src/ui/listbox/ListboxOptions.stx +0 -16
  91. package/src/ui/listbox/README.md +0 -135
  92. package/src/ui/listbox/index.ts +0 -39
  93. package/src/ui/navigator/Navigator.stx +0 -113
  94. package/src/ui/navigator/README.md +0 -352
  95. package/src/ui/navigator/index.ts +0 -20
  96. package/src/ui/notification/Notification.stx +0 -117
  97. package/src/ui/notification/README.md +0 -114
  98. package/src/ui/notification/index.ts +0 -12
  99. package/src/ui/pagination/Pagination.stx +0 -150
  100. package/src/ui/pagination/index.ts +0 -11
  101. package/src/ui/payment/Checkout.stx +0 -31
  102. package/src/ui/payment/DefaultPaymentMethod.stx +0 -62
  103. package/src/ui/payment/PaymentMethods.stx +0 -104
  104. package/src/ui/payment/README.md +0 -345
  105. package/src/ui/payment/SubscriptionCheckout.stx +0 -323
  106. package/src/ui/payment/index.ts +0 -59
  107. package/src/ui/popover/Popover.stx +0 -60
  108. package/src/ui/popover/PopoverButton.stx +0 -29
  109. package/src/ui/popover/PopoverPanel.stx +0 -23
  110. package/src/ui/popover/README.md +0 -91
  111. package/src/ui/popover/index.ts +0 -22
  112. package/src/ui/portal/Portal.stx +0 -57
  113. package/src/ui/portal/index.ts +0 -6
  114. package/src/ui/progress/Progress.stx +0 -131
  115. package/src/ui/progress/index.ts +0 -12
  116. package/src/ui/radio/Radio.stx +0 -77
  117. package/src/ui/radio/index.ts +0 -15
  118. package/src/ui/radio-group/README.md +0 -139
  119. package/src/ui/radio-group/RadioGroup.stx +0 -60
  120. package/src/ui/radio-group/RadioGroupDescription.stx +0 -11
  121. package/src/ui/radio-group/RadioGroupLabel.stx +0 -11
  122. package/src/ui/radio-group/RadioGroupOption.stx +0 -58
  123. package/src/ui/radio-group/index.ts +0 -30
  124. package/src/ui/select/README.md +0 -180
  125. package/src/ui/select/Select.stx +0 -82
  126. package/src/ui/select/index.ts +0 -21
  127. package/src/ui/sidebar/Sidebar.stx +0 -476
  128. package/src/ui/sidebar/SidebarFooter.stx +0 -71
  129. package/src/ui/sidebar/SidebarHeader.stx +0 -103
  130. package/src/ui/sidebar/SidebarItem.stx +0 -86
  131. package/src/ui/sidebar/SidebarSection.stx +0 -83
  132. package/src/ui/sidebar/index.ts +0 -153
  133. package/src/ui/sidebar/themes.ts +0 -240
  134. package/src/ui/skeleton/Skeleton.stx +0 -48
  135. package/src/ui/skeleton/index.ts +0 -11
  136. package/src/ui/spinner/Spinner.stx +0 -109
  137. package/src/ui/spinner/index.ts +0 -9
  138. package/src/ui/stepper/README.md +0 -36
  139. package/src/ui/stepper/Stepper.stx +0 -17
  140. package/src/ui/stepper/StepperStep.stx +0 -57
  141. package/src/ui/stepper/index.ts +0 -19
  142. package/src/ui/storage/README.md +0 -193
  143. package/src/ui/storage/index.ts +0 -347
  144. package/src/ui/switch/README.md +0 -114
  145. package/src/ui/switch/Switch.stx +0 -72
  146. package/src/ui/switch/index.ts +0 -10
  147. package/src/ui/table/README.md +0 -39
  148. package/src/ui/table/Table.stx +0 -14
  149. package/src/ui/table/TableBody.stx +0 -10
  150. package/src/ui/table/TableCell.stx +0 -10
  151. package/src/ui/table/TableHead.stx +0 -10
  152. package/src/ui/table/TableHeader.stx +0 -20
  153. package/src/ui/table/TableRow.stx +0 -11
  154. package/src/ui/table/index.ts +0 -35
  155. package/src/ui/tabs/TabPanel.stx +0 -25
  156. package/src/ui/tabs/Tabs.stx +0 -202
  157. package/src/ui/tabs/index.ts +0 -50
  158. package/src/ui/teleport/Teleport.stx +0 -65
  159. package/src/ui/teleport/index.ts +0 -7
  160. package/src/ui/textarea/Textarea.stx +0 -108
  161. package/src/ui/textarea/index.ts +0 -20
  162. package/src/ui/tooltip/Tooltip.stx +0 -71
  163. package/src/ui/tooltip/index.ts +0 -10
  164. package/src/ui/transition/README.md +0 -30
  165. package/src/ui/transition/Transition.stx +0 -102
  166. package/src/ui/transition/index.ts +0 -25
  167. package/src/ui/video/README.md +0 -47
  168. package/src/ui/video/Video.stx +0 -45
  169. package/src/ui/video/index.ts +0 -16
  170. package/src/ui/virtual-list/VirtualList.stx +0 -67
  171. package/src/ui/virtual-list/index.ts +0 -10
  172. package/src/ui/virtual-table/VirtualTable.stx +0 -103
  173. package/src/ui/virtual-table/index.ts +0 -21
  174. package/src/utils/PROP_VALIDATION.md +0 -458
  175. package/src/utils/THEME_SYSTEM.md +0 -702
  176. package/src/utils/accessibility.ts +0 -841
  177. package/src/utils/animation/easing.ts +0 -368
  178. package/src/utils/animation/helpers.ts +0 -223
  179. package/src/utils/animation/index.ts +0 -5
  180. package/src/utils/animation/keyframes.ts +0 -230
  181. package/src/utils/error-handling.ts +0 -475
  182. package/src/utils/highlighter.ts +0 -93
  183. package/src/utils/i18n.ts +0 -634
  184. package/src/utils/keep-alive.ts +0 -819
  185. package/src/utils/lazy-loading.ts +0 -530
  186. package/src/utils/prop-validation.ts +0 -478
  187. package/src/utils/theme.ts +0 -626
@@ -1,476 +0,0 @@
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 hasShimmerRim = themeStyle.layers.shimmerRim === true
33
- export const tintClasses = themeStyle.layers.tint || ''
34
- export const scrollAreaClasses = themeStyle.scrollArea
35
-
36
- const borderSide = position === 'left' ? 'border-r' : 'border-l'
37
- const borderClasses = bordered ? `${borderSide} border-black/5 dark:border-white/10` : ''
38
- const placementClasses = {
39
- fixed: `fixed inset-y-0 z-50 ${position === 'left' ? 'left-0' : 'right-0'}`,
40
- sticky: 'sticky top-0 h-screen',
41
- static: 'relative h-full',
42
- }
43
-
44
- export const paneClasses = [
45
- placementClasses[placement] || placementClasses.fixed,
46
- 'flex flex-col overflow-hidden',
47
- 'transition-[width] duration-300 ease-out',
48
- themeStyle.pane,
49
- borderClasses,
50
- className,
51
- ].filter(Boolean).join(' ')
52
-
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
66
- </script>
67
-
68
- <script client>
69
- const emit = defineEmits()
70
- const sidebarUidProp = '{{ sidebarUid }}'
71
- const collapsedProp = {{ collapsed }}
72
- const collapsibleProp = {{ collapsible }}
73
- const collapseModeProp = '{{ collapseMode }}'
74
- const widthProp = {{ width }}
75
- const minWidthProp = {{ minWidth }}
76
- const persistKeyProp = '{{ persistKey }}'
77
- const shellSelectorProp = '{{ shellSelector }}'
78
- const widthVarProp = '{{ widthVar }}'
79
- const collapsedClassProp = '{{ collapsedClass }}'
80
-
81
- const collapsed = state(collapsedProp)
82
- const sidebarWidth = derived(() => {
83
- if (collapsed()) return collapseModeProp === 'hidden' ? '0px' : `${minWidthProp}px`
84
- return `${widthProp}px`
85
- })
86
-
87
- let pane = null
88
-
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', '')
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
156
- }
157
-
158
- const sectionHeader = event.target.closest('[data-sidebar-section-header]')
159
- if (sectionHeader) {
160
- toggleSection(sectionHeader.closest('[data-sidebar-section]'))
161
- return
162
- }
163
-
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
- })
172
- }
173
- }
174
-
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])')]
182
- }
183
-
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
- }
214
- }
215
-
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 })
242
- }
243
-
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?.(() => {})
262
- }
263
-
264
- function setCollapsed(nextCollapsed) {
265
- collapsed.set(nextCollapsed)
266
- applyCollapseEffects(nextCollapsed)
267
- emit('collapse', nextCollapsed)
268
- }
269
-
270
- function toggleCollapse() {
271
- if (collapsibleProp) setCollapsed(!collapsed())
272
- }
273
-
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)
288
- }
289
-
290
- onMount(() => {
291
- pane = document.getElementById(sidebarUidProp)
292
- if (!pane) return
293
-
294
- followSystemAppearance()
295
-
296
- if (persistKeyProp) {
297
- try {
298
- const saved = window.localStorage?.getItem(persistKeyProp)
299
- if (saved === 'true' || saved === 'false') collapsed.set(saved === 'true')
300
- } catch {}
301
- }
302
- applyCollapseEffects(collapsed())
303
-
304
- pane.addEventListener('click', onClick)
305
- pane.addEventListener('keydown', onKeyDown)
306
-
307
- const scrollArea = pane.querySelector('[data-sidebar-scroll]')
308
- const thumb = pane.querySelector('[data-sidebar-scroll-thumb]')
309
- if (scrollArea && thumb) attachOverlayScrollbar(scrollArea, thumb)
310
-
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()
315
- })
316
-
317
- defineExpose({ collapsed, toggle: toggleCollapse })
318
- </script>
319
-
320
- <aside
321
- id="{{ sidebarUid }}"
322
- data-stx-sidebar
323
- data-sidebar-theme="{{ theme }}"
324
- data-active-class="{{ activeClasses }}"
325
- class="{{ paneClasses }}"
326
- :style="`width: ${sidebarWidth()}`"
327
- :aria-hidden="String(collapsed() && collapseModeProp === 'hidden')"
328
- role="navigation"
329
- aria-label="Sidebar"
330
- tabindex="0"
331
- >
332
- @if(tintClasses)
333
- <div class="absolute inset-0 pointer-events-none {{ tintClasses }}"></div>
334
- @endif
335
- @if(hasShimmerRim)
336
- <div class="stx-sidebar-rim" aria-hidden="true"></div>
337
- @endif
338
-
339
- @if($slots.header)
340
- <div class="relative z-10 flex-shrink-0">
341
- <slot name="header" />
342
- </div>
343
- @endif
344
-
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
361
- <slot />
362
- @endif
363
- </div>
364
- <div data-sidebar-scroll-thumb class="stx-sidebar-scroll-thumb" aria-hidden="true"></div>
365
- </div>
366
-
367
- @if($slots.footer)
368
- <div class="relative z-10 flex-shrink-0">
369
- <slot name="footer" />
370
- </div>
371
- @endif
372
- </aside>
373
-
374
- <style scoped>
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;
379
- }
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;
414
- }
415
- @keyframes stx-sidebar-shimmer {
416
- from { background-position: 0% 0%; }
417
- to { background-position: 100% 100%; }
418
- }
419
- @media (prefers-color-scheme: dark) {
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);
426
- }
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); }
475
- }
476
- </style>
@@ -1,71 +0,0 @@
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
@@ -1,103 +0,0 @@
1
- <script server>
2
- // The window-chrome strip at the top of a macOS sidebar: traffic lights on
3
- // the left, floating "Liquid Glass" toolbar buttons on the right, and the
4
- // whole strip acts as a window drag region inside a Craft window.
5
- export const theme = $props.theme || $props.variant || 'macos'
6
- export const showWindowControls = $props.showWindowControls ?? true
7
- export const actions = $props.actions || [] // [{ id, icon, label }]
8
- export const title = $props.title || ''
9
- export const subtitle = $props.subtitle || ''
10
- export const logo = $props.logo || ''
11
- export const showSearch = $props.showSearch ?? false
12
- export const searchPlaceholder = $props.searchPlaceholder || 'Search'
13
-
14
- export const isMacos = theme === 'macos' || theme === 'tahoe' || theme === 'macos-tahoe' || theme === 'macos-latest'
15
-
16
- // Traffic lights: 13px circles, 7px apart, inset ~21px — measured on
17
- // macOS Tahoe. Colors are Apple's exact stoplight values.
18
- export const trafficLightClasses = 'h-[13px] w-[13px] rounded-full shadow-[inset_0_0_1px_rgba(0,0,0,0.2)]'
19
-
20
- // Floating toolbar button: a 34px glass circle that brightens on hover.
21
- export const toolbarButtonClasses = [
22
- 'grid h-[34px] w-[34px] place-items-center rounded-full',
23
- 'bg-white/55 dark:bg-white/10',
24
- 'backdrop-blur-xl',
25
- 'shadow-[0_1px_4px_rgba(0,0,0,0.12),inset_0_0.5px_0_rgba(255,255,255,0.6)]',
26
- 'text-[#3c3c43]/75 dark:text-[#ebebf5]/75',
27
- 'transition-colors duration-150',
28
- 'hover:bg-white/80 dark:hover:bg-white/20',
29
- 'active:bg-white/95 dark:active:bg-white/25',
30
- ].join(' ')
31
- </script>
32
-
33
- <script client>
34
- const emit = defineEmits()
35
-
36
- function onAction(event) {
37
- emit('action', event.currentTarget?.dataset?.sidebarAction || event.target.closest('[data-sidebar-action]')?.dataset.sidebarAction)
38
- }
39
-
40
- function onSearchInput(event) {
41
- emit('search', event.target.value)
42
- }
43
-
44
- function onWindowControl(action) {
45
- const api = window.craft?.window
46
- if (action === 'close') api?.close?.()
47
- if (action === 'minimize') api?.minimize?.()
48
- if (action === 'zoom') (api?.maximize || api?.zoom)?.call(api)
49
- emit('windowControl', action)
50
- }
51
- </script>
52
-
53
- @if(isMacos)
54
- <div class="flex h-[52px] shrink-0 items-center justify-between pl-[21px] pr-[10px] [-webkit-app-region:drag]">
55
- @if(showWindowControls)
56
- <div class="group/lights flex items-center gap-[7px] [-webkit-app-region:no-drag]">
57
- <button type="button" class="{{ trafficLightClasses }} bg-[#ff5f57]" aria-label="Close window" @click="onWindowControl('close')"></button>
58
- <button type="button" class="{{ trafficLightClasses }} bg-[#febc2e]" aria-label="Minimize window" @click="onWindowControl('minimize')"></button>
59
- <button type="button" class="{{ trafficLightClasses }} bg-[#28c840]" aria-label="Zoom window" @click="onWindowControl('zoom')"></button>
60
- </div>
61
- @else
62
- <span></span>
63
- @endif
64
-
65
- <div class="flex items-center gap-[8px] [-webkit-app-region:no-drag]">
66
- @foreach(actions as action)
67
- <button type="button" class="{{ toolbarButtonClasses }}" aria-label="{{ action.label }}" data-sidebar-action="{{ action.id }}" @click="onAction($event)">
68
- <span class="{{ action.icon }} h-[16px] w-[16px]" aria-hidden="true"></span>
69
- </button>
70
- @endforeach
71
- <slot name="actions" />
72
- </div>
73
- </div>
74
-
75
- @if(showSearch)
76
- <div class="shrink-0 px-[10px] pb-[6px]">
77
- <div class="relative">
78
- <span class="i-f7-search absolute left-[8px] top-1/2 h-[13px] w-[13px] -translate-y-1/2 text-[#3c3c43]/50 dark:text-[#ebebf5]/50" aria-hidden="true"></span>
79
- <input
80
- type="text"
81
- placeholder="{{ searchPlaceholder }}"
82
- class="h-[28px] w-full rounded-[8px] border-0 bg-black/5 pl-[26px] pr-[8px] text-[13px] text-black placeholder:text-[#3c3c43]/50 focus:outline-none focus:ring-2 focus:ring-[#0088ff]/60 dark:bg-white/10 dark:text-white dark:placeholder:text-[#ebebf5]/50"
83
- @input="onSearchInput($event)"
84
- />
85
- </div>
86
- </div>
87
- @endif
88
- @else
89
- <div class="flex h-13 items-center gap-3 border-b border-black/5 px-4 dark:border-white/10">
90
- @if(logo)
91
- <img src="{{ logo }}" alt="{{ title }}" class="h-8 w-8 rounded-lg object-cover" />
92
- @endif
93
- <div class="min-w-0">
94
- @if(title)
95
- <div class="truncate text-sm font-semibold text-black/90 dark:text-white">{{ title }}</div>
96
- @endif
97
- @if(subtitle)
98
- <div class="truncate text-xs text-black/50 dark:text-white/50">{{ subtitle }}</div>
99
- @endif
100
- </div>
101
- <slot name="actions" />
102
- </div>
103
- @endif