@stacksjs/components 0.2.90 → 0.2.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,109 +1,71 @@
1
1
  <script server>
2
- export const showSettings = $props.showSettings ?? true
3
- export const showThemeToggle = $props.showThemeToggle ?? true
4
- export const settingsHref = $props.settingsHref || '/settings'
5
- export const settingsLabel = $props.settingsLabel || 'Settings'
6
- export const collapsed = $props.collapsed ?? false
7
- export const actions = $props.actions || []
8
- export const variant = $props.variant || 'tahoe'
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
9
 
10
- const footerVariants = {
11
- workspace: {
12
- container: 'p-5 space-y-2',
13
- action: 'flex-1 flex items-center gap-3 rounded-[18px] px-4 py-3 text-[17px] text-zinc-700 transition-colors hover:bg-[#ececea] hover:text-zinc-950 dark:text-zinc-300 dark:hover:bg-white/10',
14
- icon: 'w-5 h-5 text-zinc-500 dark:text-zinc-400',
15
- },
16
- desktop: {
17
- container: 'p-4 space-y-2',
18
- action: 'flex-1 flex items-center gap-3 rounded-[16px] px-3.5 py-2.5 text-[15px] text-zinc-700 transition-colors hover:bg-white/34 hover:text-zinc-950 dark:text-zinc-300 dark:hover:bg-white/10',
19
- icon: 'w-5 h-5 text-zinc-500 dark:text-zinc-400',
20
- },
21
- default: {
22
- container: 'border-t border-black/5 dark:border-white/10 p-3 space-y-2',
23
- action: 'flex-1 flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-black/70 dark:text-white/70 hover:bg-black/5 dark:hover:bg-white/10 transition-colors',
24
- icon: 'w-4 h-4',
25
- },
26
- }
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(' ')
27
20
 
28
- const footerStyle = footerVariants[variant] || footerVariants.default
29
- export const footerClasses = footerStyle.container
30
- export const actionClasses = footerStyle.action
31
- export const footerIconClasses = footerStyle.icon
32
- export const settingsIconClasses = `i-hugeicons-settings-02 ${footerStyle.icon}`
33
- export const themeButtonClasses = variant === 'workspace' || variant === 'desktop'
34
- ? 'p-3 rounded-[16px] text-zinc-500 hover:text-zinc-900 hover:bg-white/50 dark:text-zinc-400 dark:hover:text-white dark:hover:bg-white/10 transition-colors'
35
- : 'p-2 rounded-lg text-black/50 hover:text-black/80 hover:bg-black/5 dark:text-white/50 dark:hover:text-white/80 dark:hover:bg-white/10 transition-colors'
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(' ')
36
27
  </script>
37
28
 
38
29
  <script client>
39
30
  const emit = defineEmits()
40
31
 
41
- function onThemeToggle() {
42
- document.documentElement.classList.toggle('dark')
43
- emit('themeToggle')
32
+ function onProfileClick() {
33
+ emit('profileClick')
44
34
  }
45
35
 
46
- function onActionClick(index) {
47
- emit('actionClick', index)
36
+ function onAction(event) {
37
+ emit('action', event.target.closest('[data-sidebar-action]')?.dataset.sidebarAction)
48
38
  }
49
39
  </script>
50
40
 
51
- <div class="{{ footerClasses }}">
52
- <!-- Custom actions slot -->
53
- <slot />
54
-
55
- <!-- Quick actions row -->
56
- @if(!collapsed && actions.length > 0)
57
- <div class="flex items-center gap-2">
58
- @foreach(actions as action)
59
- @if(action.href)
60
- <a
61
- href="{{ action.href }}"
62
- class="{{ actionClasses }}"
63
- >
64
- @if(action.icon)
65
- <div class="{{ action.icon }} {{ footerIconClasses }}"></div>
66
- @endif
67
- <span>{{ action.label }}</span>
68
- </a>
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" />
69
47
  @else
70
- <button
71
- type="button"
72
- class="{{ actionClasses }}"
73
- @click="onActionClick({{ $loop.index }})"
74
- >
75
- @if(action.icon)
76
- <div class="{{ action.icon }} {{ footerIconClasses }}"></div>
77
- @endif
78
- <span>{{ action.label }}</span>
79
- </button>
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>
80
49
  @endif
81
- @endforeach
82
-
83
- @if(showThemeToggle)
84
- <button
85
- type="button"
86
- class="{{ themeButtonClasses }}"
87
- @click="onThemeToggle()"
88
- aria-label="Toggle dark mode"
89
- >
90
- <div class="i-hugeicons-moon-02 w-4 h-4 dark:hidden"></div>
91
- <div class="i-hugeicons-sun-02 w-4 h-4 hidden dark:block"></div>
92
- </button>
93
- @endif
94
- </div>
95
- @endif
96
-
97
- <!-- Settings link -->
98
- @if(showSettings)
99
- <a
100
- href="{{ settingsHref }}"
101
- class="{{ actionClasses }}"
102
- >
103
- <div class="{{ settingsIconClasses }}"></div>
104
- @if(!collapsed)
105
- <span>{{ settingsLabel }}</span>
106
- @endif
107
- </a>
108
- @endif
109
- </div>
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,199 +1,103 @@
1
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 }]
2
8
  export const title = $props.title || ''
3
9
  export const subtitle = $props.subtitle || ''
4
10
  export const logo = $props.logo || ''
5
- export const logoIcon = $props.logoIcon || ''
6
11
  export const showSearch = $props.showSearch ?? false
7
- export const searchPlaceholder = $props.searchPlaceholder || 'Search...'
8
- export const searchValue = $props.searchValue || ''
9
- export const collapsed = $props.collapsed ?? false
10
- export const variant = $props.variant || 'tahoe'
11
- export const showWindowControls = $props.showWindowControls ?? (variant === 'desktop' ? 'auto' : false)
12
- export const showNavigationControls = $props.showNavigationControls ?? variant === 'desktop'
13
-
14
- const headerVariants = {
15
- workspace: {
16
- titlebar: 'h-18 flex items-center justify-between px-5',
17
- logo: 'w-9 h-9 rounded-[12px] bg-black text-white flex items-center justify-center font-semibold text-sm',
18
- title: 'text-[18px] font-semibold text-zinc-950 dark:text-white truncate',
19
- subtitle: 'text-sm text-zinc-500 dark:text-zinc-400 truncate',
20
- searchWrap: 'px-5 pb-4',
21
- searchInput: 'w-full rounded-[16px] border border-zinc-200/70 bg-white/70 px-10 py-2.5 text-[15px] text-zinc-900 placeholder:text-zinc-400 focus:outline-none focus:ring-2 focus:ring-zinc-300/80 dark:border-white/10 dark:bg-white/10 dark:text-white',
22
- },
23
- desktop: {
24
- titlebar: 'h-14 flex items-center gap-5 px-5 border-b border-black/5 dark:border-white/10',
25
- chrome: 'flex shrink-0 items-center justify-start gap-5 min-h-7',
26
- trafficLight: 'h-3.5 w-3.5 rounded-full shadow-[inset_0_0_0_1px_rgba(0,0,0,0.08)]',
27
- navButton: 'inline-flex h-7 w-7 items-center justify-center rounded-full text-zinc-500/80 transition-colors hover:bg-white/35 hover:text-zinc-900 disabled:opacity-35 disabled:hover:bg-transparent disabled:hover:text-zinc-500/80 dark:text-zinc-400 dark:hover:bg-white/10 dark:hover:text-white',
28
- logo: 'w-9 h-9 rounded-[12px] bg-black/80 text-white flex items-center justify-center font-semibold text-sm shadow-sm dark:bg-white/14',
29
- title: 'text-[15px] leading-[22px] font-medium text-zinc-950 dark:text-white truncate',
30
- subtitle: 'text-xs text-zinc-500 dark:text-zinc-400 truncate',
31
- searchWrap: 'px-4 pb-4',
32
- searchInput: 'w-full rounded-[16px] border border-white/40 bg-white/45 px-10 py-2.5 text-[15px] text-zinc-900 placeholder:text-zinc-500 shadow-sm focus:outline-none focus:ring-2 focus:ring-white/70 dark:border-white/10 dark:bg-white/10 dark:text-white',
33
- },
34
- default: {
35
- titlebar: 'h-13 flex items-center justify-between px-4 border-b border-black/5 dark:border-white/10',
36
- logo: 'w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center text-white font-bold text-sm',
37
- title: 'text-sm font-semibold text-black/90 dark:text-white truncate',
38
- subtitle: 'text-xs text-black/50 dark:text-white/50 truncate',
39
- searchWrap: 'px-3 py-3',
40
- searchInput: 'w-full pl-9 pr-3 py-2 text-sm bg-black/5 dark:bg-white/10 border border-black/10 dark:border-white/10 rounded-lg text-black/90 dark:text-white placeholder:text-black/40 dark:placeholder:text-white/40 focus:outline-none focus:ring-2 focus:ring-blue-500/30 focus:border-blue-400/50 transition-colors',
41
- },
42
- }
43
-
44
- const headerStyle = headerVariants[variant] || headerVariants.default
45
- export const titlebarClasses = headerStyle.titlebar
46
- export const chromeClasses = headerStyle.chrome || ''
47
- export const trafficLightClasses = headerStyle.trafficLight || ''
48
- export const navButtonClasses = headerStyle.navButton || ''
49
- export const logoClasses = headerStyle.logo
50
- export const titleClasses = headerStyle.title
51
- export const subtitleClasses = headerStyle.subtitle
52
- export const searchWrapClasses = headerStyle.searchWrap
53
- export const searchInputClasses = headerStyle.searchInput
54
- export const hasDesktopChrome = variant === 'desktop' && (showWindowControls || showNavigationControls)
55
- export const windowControlsClasses = showWindowControls === 'auto' ? 'hidden items-center gap-2' : 'flex items-center gap-2'
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(' ')
56
31
  </script>
57
32
 
58
33
  <script client>
59
34
  const emit = defineEmits()
60
35
 
61
- function isCraftRuntime() {
62
- return window.__craftNativeSidebar === true
63
- || window.craft?.__craft_bridge_loaded === true
64
- || !!window.craft?.window
65
- || !!window.webkit?.messageHandlers?.craft
66
- }
67
-
68
- function usesCustomWindowControls() {
69
- return window.__craftCustomWindowControls === true
70
- || document.documentElement.classList.contains('has-custom-window-controls')
71
- || document.body?.dataset?.customWindowControls === 'true'
72
- }
73
-
74
- function syncWindowControls() {
75
- document.querySelectorAll('[data-stx-native-window-controls]').forEach((element) => {
76
- const show = element.dataset.stxNativeWindowControls === 'auto'
77
- ? isCraftRuntime() && usesCustomWindowControls()
78
- : true
79
- element.classList.toggle('hidden', !show)
80
- element.classList.toggle('flex', show)
81
- })
36
+ function onAction(event) {
37
+ emit('action', event.currentTarget?.dataset?.sidebarAction || event.target.closest('[data-sidebar-action]')?.dataset.sidebarAction)
82
38
  }
83
39
 
84
40
  function onSearchInput(event) {
85
41
  emit('search', event.target.value)
86
42
  }
87
43
 
88
- function goBack() {
89
- window.history.back()
90
- emit('back')
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)
91
50
  }
92
-
93
- function goForward() {
94
- window.history.forward()
95
- emit('forward')
96
- }
97
-
98
- onMount(() => {
99
- syncWindowControls()
100
- window.addEventListener('craft:ready', syncWindowControls)
101
- window.addEventListener('stx:load', syncWindowControls)
102
-
103
- return () => {
104
- window.removeEventListener('craft:ready', syncWindowControls)
105
- window.removeEventListener('stx:load', syncWindowControls)
106
- }
107
- })
108
51
  </script>
109
52
 
110
- <div class="flex-shrink-0">
111
- <!-- Title bar area -->
112
- <div class="{{ titlebarClasses }}">
113
- @if(hasDesktopChrome)
114
- <div class="{{ chromeClasses }}" data-stx-sidebar-web-chrome>
115
- @if(showWindowControls)
116
- <div class="{{ windowControlsClasses }}" aria-hidden="true" data-stx-native-window-controls="{{ showWindowControls === 'auto' ? 'auto' : 'always' }}">
117
- <span class="{{ trafficLightClasses }} bg-[#ff5f57]"></span>
118
- <span class="{{ trafficLightClasses }} bg-[#ffbd2e]"></span>
119
- <span class="{{ trafficLightClasses }} bg-[#28c840]"></span>
120
- </div>
121
- @endif
122
-
123
- @if(showNavigationControls)
124
- <div class="flex items-center gap-2">
125
- <button type="button" class="{{ navButtonClasses }}" @click="goBack()" aria-label="Go back">
126
- <span class="h-5 w-5 i-hugeicons-arrow-left-02" aria-hidden="true"></span>
127
- </button>
128
- <button type="button" class="{{ navButtonClasses }}" @click="goForward()" aria-label="Go forward">
129
- <span class="h-5 w-5 i-hugeicons-arrow-right-02" aria-hidden="true"></span>
130
- </button>
131
- </div>
132
- @endif
133
- </div>
134
- @endif
135
-
136
- @if(!collapsed)
137
- <div class="flex items-center gap-3 min-w-0">
138
- <!-- Logo -->
139
- @if(logo)
140
- <img src="{{ logo }}" alt="{{ title }}" class="w-8 h-8 rounded-lg object-cover" />
141
- @elseif(logoIcon)
142
- <div class="{{ logoClasses }}">
143
- <div class="{{ logoIcon }} w-5 h-5 text-white"></div>
144
- </div>
145
- @else
146
- <div class="{{ logoClasses }}">
147
- {{ title.charAt(0) }}
148
- </div>
149
- @endif
150
-
151
- <!-- Title & Subtitle -->
152
- <div class="min-w-0">
153
- @if(title)
154
- <div class="{{ titleClasses }}">
155
- {{ title }}
156
- </div>
157
- @endif
158
- @if(subtitle)
159
- <div class="{{ subtitleClasses }}">
160
- {{ subtitle }}
161
- </div>
162
- @endif
163
- </div>
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>
164
60
  </div>
165
61
  @else
166
- <!-- Collapsed state - just logo -->
167
- @if(logo)
168
- <img src="{{ logo }}" alt="{{ title }}" class="w-8 h-8 rounded-lg object-cover mx-auto" />
169
- @elseif(logoIcon)
170
- <div class="{{ logoClasses }} mx-auto">
171
- <div class="{{ logoIcon }} w-5 h-5 text-white"></div>
172
- </div>
173
- @else
174
- <div class="{{ logoClasses }} mx-auto">
175
- {{ title.charAt(0) }}
176
- </div>
177
- @endif
62
+ <span></span>
178
63
  @endif
179
64
 
180
- <!-- Slot for custom header actions -->
181
- <slot name="actions" />
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>
182
73
  </div>
183
74
 
184
- <!-- Search (only when expanded) -->
185
- @if(showSearch && !collapsed)
186
- <div class="{{ searchWrapClasses }}">
75
+ @if(showSearch)
76
+ <div class="shrink-0 px-[10px] pb-[6px]">
187
77
  <div class="relative">
188
- <div class="i-hugeicons-search-01 absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-black/40 dark:text-white/40"></div>
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>
189
79
  <input
190
80
  type="text"
191
81
  placeholder="{{ searchPlaceholder }}"
192
- value="{{ searchValue }}"
193
- class="{{ searchInputClasses }}"
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"
194
83
  @input="onSearchInput($event)"
195
84
  />
196
85
  </div>
197
86
  </div>
198
87
  @endif
199
- </div>
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
@@ -1,92 +1,86 @@
1
1
  <script server>
2
+ import { resolveIconColor, resolveSidebarTheme } from './themes'
3
+
2
4
  export const id = $props.id || ''
3
5
  export const label = $props.label || ''
4
6
  export const icon = $props.icon || ''
7
+ export const iconColor = $props.iconColor || ''
8
+ export const image = $props.image || ''
5
9
  export const href = $props.href || ''
6
- export const badge = $props.badge || ''
10
+ export const count = $props.count ?? $props.badge ?? ''
7
11
  export const active = $props.active ?? false
8
12
  export const disabled = $props.disabled ?? false
9
- export const indent = $props.indent ?? false
10
- export const variant = $props.variant || 'tahoe'
11
-
12
- const itemVariants = {
13
- workspace: {
14
- base: 'flex w-full items-center gap-3 rounded-[16px] px-3.5 py-2 text-[14px] leading-tight transition-colors duration-150',
15
- indent: '',
16
- active: 'bg-[#e7e7e5] text-zinc-950',
17
- inactive: 'text-zinc-700 hover:bg-[#ececea] hover:text-zinc-950 dark:text-zinc-300 dark:hover:bg-white/10',
18
- icon: active ? 'text-zinc-900 dark:text-zinc-100' : 'text-zinc-500 dark:text-zinc-500',
19
- badge: 'ml-auto grid h-5 min-w-5 place-items-center rounded-full bg-white/70 px-1.5 text-[11px] font-medium text-zinc-500 shadow-sm dark:bg-white/10 dark:text-zinc-400',
20
- },
21
- desktop: {
22
- base: 'flex w-full items-center gap-3 rounded-[14px] px-3 py-1.5 text-[13.5px] leading-tight transition-colors duration-150',
23
- indent: '',
24
- active: 'bg-[#e8e8e6]/90 text-zinc-950 dark:bg-white/12 dark:text-white',
25
- inactive: 'text-zinc-700 hover:bg-white/38 hover:text-zinc-950 dark:text-zinc-300 dark:hover:bg-white/10',
26
- icon: active ? 'text-zinc-900 dark:text-white' : 'text-zinc-600 dark:text-zinc-400',
27
- badge: 'ml-auto grid h-5 min-w-5 place-items-center rounded-full bg-white/72 px-1.5 text-[11px] font-medium text-zinc-500 shadow-sm dark:bg-white/10 dark:text-zinc-400',
28
- },
29
- default: {
30
- base: 'flex items-center gap-2.5 px-2.5 py-1.5 rounded-md text-[13px] transition-all duration-150',
31
- indent: indent ? 'ml-2' : '',
32
- active: 'bg-blue-500/20 text-blue-600 dark:bg-blue-400/25 dark:text-blue-300 font-medium',
33
- inactive: 'text-black/80 dark:text-white/80 hover:bg-black/5 dark:hover:bg-white/10',
34
- icon: active ? 'text-blue-600 dark:text-blue-300' : 'text-black/50 dark:text-white/50',
35
- badge: 'ml-auto px-2 py-0.5 text-xs font-medium rounded-full bg-black/10 text-black/70 dark:bg-white/15 dark:text-white/70',
36
- },
37
- }
13
+ export const expandable = $props.expandable ?? false
14
+ export const expanded = $props.expanded ?? true
15
+ export const depth = $props.depth || 0
16
+ export const parents = $props.parents || ''
17
+ export const theme = $props.theme || $props.variant || 'macos'
38
18
 
39
- const itemStyle = itemVariants[variant] || itemVariants.default
40
- const baseItemList = [itemStyle.base, itemStyle.indent]
41
- const stateClasses = disabled ? 'opacity-50 cursor-not-allowed' : active ? itemStyle.active : itemStyle.inactive
19
+ const themeStyle = resolveSidebarTheme(theme).item
42
20
 
43
- export const itemClasses = [...baseItemList, stateClasses].filter(Boolean).join(' ')
44
- export const iconClasses = `${icon} h-[18px] w-[18px] flex-shrink-0 ${itemStyle.icon}`
45
- export const badgeClasses = itemStyle.badge
46
- </script>
21
+ export const rowClasses = [
22
+ themeStyle.base,
23
+ disabled ? themeStyle.disabled : themeStyle.hover,
24
+ disabled ? '' : themeStyle.pressed,
25
+ active ? themeStyle.active : '',
26
+ ].filter(Boolean).join(' ')
47
27
 
48
- <script client>
49
- const emit = defineEmits()
50
- const isDisabled = {{ disabled }}
51
- const itemPayload = { id: {{ id }}, label: {{ label }}, href: {{ href }} }
28
+ export const disclosureClasses = themeStyle.disclosure
29
+ export const chevronClasses = themeStyle.chevron
30
+ export const iconSlotClasses = themeStyle.iconSlot
31
+ export const iconClasses = `${icon} ${themeStyle.icon}`
32
+ export const imageClasses = themeStyle.image
33
+ export const labelClasses = themeStyle.label
34
+ export const countClasses = themeStyle.count
52
35
 
53
- function onClick(event) {
54
- if (isDisabled) {
55
- event.preventDefault()
56
- return
57
- }
58
- emit('click', { item: itemPayload, event })
59
- }
36
+ // Nested rows are indented; the icon tint accepts macOS system color names
37
+ // ("blue", "yellow", …) or any CSS color.
38
+ export const indentStyle = depth > 0 ? `padding-left: ${depth * themeStyle.indentPerLevel}px` : ''
39
+ const tint = resolveIconColor(iconColor)
40
+ export const iconStyle = tint ? `color: ${tint}` : ''
41
+ export const isLink = Boolean(href) && !disabled
60
42
  </script>
61
43
 
62
- @if(href && !disabled)
63
- <a
64
- href="{{ href }}"
65
- class="{{ itemClasses }}"
66
- @click="onClick($event)"
67
- @if(active)aria-current="page"@endif
68
- >
69
- @if(icon)
70
- <div class="{{ iconClasses }}"></div>
71
- @endif
72
- <span class="flex-1 truncate">{{ label }}</span>
73
- @if(badge)
74
- <span class="{{ badgeClasses }}">{{ badge }}</span>
75
- @endif
76
- </a>
77
- @else
78
- <button
79
- type="button"
80
- class="{{ itemClasses }}"
81
- @click="onClick($event)"
82
- @if(disabled)disabled aria-disabled="true"@endif
83
- >
84
- @if(icon)
85
- <div class="{{ iconClasses }}"></div>
86
- @endif
87
- <span class="flex-1 truncate text-left">{{ label }}</span>
88
- @if(badge)
89
- <span class="{{ badgeClasses }}">{{ badge }}</span>
90
- @endif
91
- </button>
92
- @endif
44
+ <div
45
+ data-sidebar-row
46
+ data-item-id="{{ id }}"
47
+ @if(parents)data-parents="{{ parents }}"@endif
48
+ @if(expandable)data-expanded="{{ expanded ? 'true' : 'false' }}"@endif
49
+ @if(indentStyle)style="{{ indentStyle }}"@endif
50
+ >
51
+ @if(isLink)
52
+ <a href="{{ href }}" data-sidebar-item data-label="{{ label }}" class="{{ rowClasses }}" @if(active)data-active="true" aria-current="page"@endif>
53
+ <span class="{{ disclosureClasses }}" @if(expandable)data-sidebar-disclosure role="button" aria-label="Toggle {{ label }}"@endif>
54
+ @if(expandable)
55
+ <span class="{{ chevronClasses }}" aria-hidden="true"></span>
56
+ @endif
57
+ </span>
58
+ @if(image)
59
+ <span class="{{ iconSlotClasses }}"><img src="{{ image }}" alt="" class="{{ imageClasses }}" /></span>
60
+ @elseif(icon)
61
+ <span class="{{ iconSlotClasses }}"><span class="{{ iconClasses }}" @if(iconStyle)style="{{ iconStyle }}"@endif aria-hidden="true"></span></span>
62
+ @endif
63
+ <span class="{{ labelClasses }}">{{ label }}</span>
64
+ @if(count !== '' && count !== null && count !== undefined)
65
+ <span class="{{ countClasses }}">{{ count }}</span>
66
+ @endif
67
+ </a>
68
+ @else
69
+ <button type="button" data-sidebar-item data-label="{{ label }}" class="{{ rowClasses }}" @if(active)data-active="true" aria-current="page"@endif @if(disabled)data-disabled disabled aria-disabled="true"@endif>
70
+ <span class="{{ disclosureClasses }}" @if(expandable)data-sidebar-disclosure role="button" aria-label="Toggle {{ label }}"@endif>
71
+ @if(expandable)
72
+ <span class="{{ chevronClasses }}" aria-hidden="true"></span>
73
+ @endif
74
+ </span>
75
+ @if(image)
76
+ <span class="{{ iconSlotClasses }}"><img src="{{ image }}" alt="" class="{{ imageClasses }}" /></span>
77
+ @elseif(icon)
78
+ <span class="{{ iconSlotClasses }}"><span class="{{ iconClasses }}" @if(iconStyle)style="{{ iconStyle }}"@endif aria-hidden="true"></span></span>
79
+ @endif
80
+ <span class="{{ labelClasses }}">{{ label }}</span>
81
+ @if(count !== '' && count !== null && count !== undefined)
82
+ <span class="{{ countClasses }}">{{ count }}</span>
83
+ @endif
84
+ </button>
85
+ @endif
86
+ </div>