@stacksjs/components 0.2.54 → 0.2.66
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/Accordion-pt8j917s.stx +146 -0
- package/dist/AccordionItem-hbhzx63p.stx +37 -0
- package/dist/{Checkbox-zgdmy3p5.stx → Checkbox-aqmq4cqv.stx} +3 -1
- package/dist/{Dialog-hczpexs4.stx → Dialog-y5rz2vrq.stx} +5 -1
- package/dist/{Drawer-xhevp5cw.stx → Drawer-hrz7qcyp.stx} +4 -1
- package/dist/Notification-03vfh3k6.stx +117 -0
- package/dist/{NumberInput-drnvtqej.stx → NumberInput-g41gmd2c.stx} +2 -1
- package/dist/{Pagination-n3d31cns.stx → Pagination-wtgxmzey.stx} +3 -1
- package/dist/{PasswordInput-n02x6053.stx → PasswordInput-4rsq1gwb.stx} +2 -1
- package/dist/{Progress-yxcntr4x.stx → Progress-9dhakbh4.stx} +2 -1
- package/dist/{Radio-cgjv2hb2.stx → Radio-djtb5dpc.stx} +2 -1
- package/dist/{Select-28y0pg83.stx → Select-1vnd0brz.stx} +2 -1
- package/dist/{Sidebar-73vc78as.stx → Sidebar-0jzasrmt.stx} +116 -22
- package/dist/{SidebarHeader-7v174a9c.stx → SidebarHeader-ywpmx14d.stx} +80 -3
- package/dist/{SidebarItem-3y3e8g2t.stx → SidebarItem-h53sgqzz.stx} +8 -8
- package/dist/{SidebarSection-z5ey6awe.stx → SidebarSection-g195cb4f.stx} +4 -3
- package/dist/{Switch-r5t5w04w.stx → Switch-p1bcr9ks.stx} +3 -1
- package/dist/TabPanel-5htevsq6.stx +25 -0
- package/dist/Tabs-79brys99.stx +202 -0
- package/dist/{TextInput-kf084763.stx → TextInput-g0vaz0pe.stx} +4 -1
- package/dist/{Textarea-xg4rr2xz.stx → Textarea-96kawv5c.stx} +2 -1
- package/dist/{Tooltip-m0tqd72e.stx → Tooltip-y61h04gp.stx} +2 -1
- package/dist/index.js +3909 -15
- package/dist/ui/accordion/Accordion.stx +9 -1
- package/dist/ui/accordion/AccordionItem.stx +7 -0
- package/dist/ui/accordion/index.d.ts +30 -1
- package/dist/ui/input/NumberInput.stx +1 -1
- package/dist/ui/input/TextInput.stx +1 -1
- package/dist/ui/notification/Notification.stx +10 -8
- package/dist/ui/select/Select.stx +1 -1
- package/dist/ui/sidebar/Sidebar.stx +9 -0
- package/dist/ui/sidebar/SidebarHeader.stx +7 -0
- package/dist/ui/sidebar/SidebarItem.stx +1 -1
- package/dist/ui/sidebar/index.d.ts +14 -0
- package/dist/ui/tabs/TabPanel.stx +9 -0
- package/dist/ui/tabs/Tabs.stx +140 -0
- package/dist/ui/tabs/index.d.ts +32 -1
- package/dist/ui/textarea/Textarea.stx +1 -1
- package/package.json +3 -3
- package/src/ui/accordion/Accordion.stx +90 -32
- package/src/ui/accordion/AccordionItem.stx +37 -0
- package/src/ui/accordion/index.ts +32 -1
- package/src/ui/checkbox/Checkbox.stx +3 -1
- package/src/ui/dialog/Dialog.stx +5 -1
- package/src/ui/drawer/Drawer.stx +4 -1
- package/src/ui/input/NumberInput.stx +2 -1
- package/src/ui/input/PasswordInput.stx +2 -1
- package/src/ui/input/TextInput.stx +4 -1
- package/src/ui/notification/Notification.stx +65 -26
- package/src/ui/pagination/Pagination.stx +3 -1
- package/src/ui/progress/Progress.stx +2 -1
- package/src/ui/radio/Radio.stx +2 -1
- package/src/ui/select/Select.stx +2 -1
- package/src/ui/sidebar/Sidebar.stx +116 -22
- package/src/ui/sidebar/SidebarHeader.stx +80 -3
- package/src/ui/sidebar/SidebarItem.stx +8 -8
- package/src/ui/sidebar/SidebarSection.stx +4 -3
- package/src/ui/sidebar/index.ts +30 -0
- package/src/ui/switch/Switch.stx +3 -1
- package/src/ui/tabs/TabPanel.stx +25 -0
- package/src/ui/tabs/Tabs.stx +108 -32
- package/src/ui/tabs/index.ts +35 -1
- package/src/ui/textarea/Textarea.stx +2 -1
- package/src/ui/tooltip/Tooltip.stx +2 -1
- package/src/utils/accessibility.ts +1 -1
- package/dist/Accordion-19x8q4rj.stx +0 -88
- package/dist/Notification-tb9b525s.stx +0 -78
- package/dist/Tabs-bzvvn4ke.stx +0 -126
|
@@ -8,6 +8,8 @@ export const searchPlaceholder = $props.searchPlaceholder || 'Search...'
|
|
|
8
8
|
export const searchValue = $props.searchValue || ''
|
|
9
9
|
export const collapsed = $props.collapsed ?? false
|
|
10
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'
|
|
11
13
|
|
|
12
14
|
const headerVariants = {
|
|
13
15
|
workspace: {
|
|
@@ -19,10 +21,13 @@ const headerVariants = {
|
|
|
19
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',
|
|
20
22
|
},
|
|
21
23
|
desktop: {
|
|
22
|
-
titlebar: 'h-
|
|
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',
|
|
23
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',
|
|
24
|
-
title: 'text-[
|
|
25
|
-
subtitle: 'text-
|
|
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',
|
|
26
31
|
searchWrap: 'px-4 pb-4',
|
|
27
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',
|
|
28
33
|
},
|
|
@@ -38,24 +43,96 @@ const headerVariants = {
|
|
|
38
43
|
|
|
39
44
|
const headerStyle = headerVariants[variant] || headerVariants.default
|
|
40
45
|
export const titlebarClasses = headerStyle.titlebar
|
|
46
|
+
export const chromeClasses = headerStyle.chrome || ''
|
|
47
|
+
export const trafficLightClasses = headerStyle.trafficLight || ''
|
|
48
|
+
export const navButtonClasses = headerStyle.navButton || ''
|
|
41
49
|
export const logoClasses = headerStyle.logo
|
|
42
50
|
export const titleClasses = headerStyle.title
|
|
43
51
|
export const subtitleClasses = headerStyle.subtitle
|
|
44
52
|
export const searchWrapClasses = headerStyle.searchWrap
|
|
45
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'
|
|
46
56
|
</script>
|
|
47
57
|
|
|
48
58
|
<script client>
|
|
49
59
|
const emit = defineEmits()
|
|
50
60
|
|
|
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
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
51
84
|
function onSearchInput(event) {
|
|
52
85
|
emit('search', event.target.value)
|
|
53
86
|
}
|
|
87
|
+
|
|
88
|
+
function goBack() {
|
|
89
|
+
window.history.back()
|
|
90
|
+
emit('back')
|
|
91
|
+
}
|
|
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
|
+
})
|
|
54
108
|
</script>
|
|
55
109
|
|
|
56
110
|
<div class="flex-shrink-0">
|
|
57
111
|
<!-- Title bar area -->
|
|
58
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
|
+
|
|
59
136
|
@if(!collapsed)
|
|
60
137
|
<div class="flex items-center gap-3 min-w-0">
|
|
61
138
|
<!-- Logo -->
|
|
@@ -11,20 +11,20 @@ export const variant = $props.variant || 'tahoe'
|
|
|
11
11
|
|
|
12
12
|
const itemVariants = {
|
|
13
13
|
workspace: {
|
|
14
|
-
base: 'flex w-full items-center gap-3 rounded-[
|
|
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
15
|
indent: '',
|
|
16
16
|
active: 'bg-[#e7e7e5] text-zinc-950',
|
|
17
17
|
inactive: 'text-zinc-700 hover:bg-[#ececea] hover:text-zinc-950 dark:text-zinc-300 dark:hover:bg-white/10',
|
|
18
18
|
icon: active ? 'text-zinc-900 dark:text-zinc-100' : 'text-zinc-500 dark:text-zinc-500',
|
|
19
|
-
badge: 'ml-auto rounded-full bg-white/70 px-
|
|
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
20
|
},
|
|
21
21
|
desktop: {
|
|
22
|
-
base: 'flex w-full items-center gap-3 rounded-[
|
|
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
23
|
indent: '',
|
|
24
|
-
active: 'bg-
|
|
25
|
-
inactive: 'text-zinc-700 hover:bg-white/
|
|
26
|
-
icon: active ? 'text-zinc-900 dark:text-white' : 'text-zinc-
|
|
27
|
-
badge: 'ml-auto rounded-full bg-white/
|
|
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
28
|
},
|
|
29
29
|
default: {
|
|
30
30
|
base: 'flex items-center gap-2.5 px-2.5 py-1.5 rounded-md text-[13px] transition-all duration-150',
|
|
@@ -41,7 +41,7 @@ const baseItemList = [itemStyle.base, itemStyle.indent]
|
|
|
41
41
|
const stateClasses = disabled ? 'opacity-50 cursor-not-allowed' : active ? itemStyle.active : itemStyle.inactive
|
|
42
42
|
|
|
43
43
|
export const itemClasses = [...baseItemList, stateClasses].filter(Boolean).join(' ')
|
|
44
|
-
export const iconClasses = `${icon}
|
|
44
|
+
export const iconClasses = `${icon} h-[18px] w-[18px] flex-shrink-0 ${itemStyle.icon}`
|
|
45
45
|
export const badgeClasses = itemStyle.badge
|
|
46
46
|
</script>
|
|
47
47
|
|
|
@@ -14,7 +14,7 @@ const headerVariants = {
|
|
|
14
14
|
hover: 'hover:text-zinc-600 dark:hover:text-zinc-300 cursor-pointer',
|
|
15
15
|
},
|
|
16
16
|
desktop: {
|
|
17
|
-
base: 'w-full flex items-center gap-2.5 px-3
|
|
17
|
+
base: 'w-full flex items-center gap-2.5 px-3 pb-1.5 pt-3 rounded-[14px] text-left transition-colors duration-150 text-[12px] font-semibold uppercase tracking-[0.10em] text-zinc-500/80 dark:text-zinc-400/80',
|
|
18
18
|
hover: 'hover:bg-white/25 dark:hover:bg-white/10 cursor-pointer',
|
|
19
19
|
},
|
|
20
20
|
default: {
|
|
@@ -26,7 +26,7 @@ const headerVariants = {
|
|
|
26
26
|
const headerStyle = headerVariants[variant] || headerVariants.default
|
|
27
27
|
const headerClasses = [headerStyle.base, collapsible ? headerStyle.hover : ''].filter(Boolean).join(' ')
|
|
28
28
|
export const headerClassesValue = headerClasses
|
|
29
|
-
export const sectionClasses = variant === 'workspace'
|
|
29
|
+
export const sectionClasses = variant === 'workspace' ? 'py-1.5' : variant === 'desktop' ? 'py-1' : 'py-1'
|
|
30
30
|
export const groupClasses = variant === 'workspace' || variant === 'desktop' ? 'space-y-1 overflow-hidden' : 'mt-1 space-y-0.5 overflow-hidden'
|
|
31
31
|
export const labelClasses = variant === 'workspace' || variant === 'desktop'
|
|
32
32
|
? 'flex-1 truncate'
|
|
@@ -37,7 +37,8 @@ export const labelClasses = variant === 'workspace' || variant === 'desktop'
|
|
|
37
37
|
const emit = defineEmits()
|
|
38
38
|
const sectionId = {{ id }}
|
|
39
39
|
const collapsibleProp = {{ collapsible }}
|
|
40
|
-
|
|
40
|
+
// Reactive prop binding — parent can drive `expanded` via signals. See #1704.
|
|
41
|
+
const isExpanded = useReactiveProp('expanded', {{ expanded }})
|
|
41
42
|
|
|
42
43
|
function onHeaderClick() {
|
|
43
44
|
if (!collapsibleProp) return
|
|
@@ -26,7 +26,9 @@ const trackBase = {{ trackBaseClasses }}
|
|
|
26
26
|
const trackDisabled = {{ trackDisabledClasses }}
|
|
27
27
|
const thumbBase = {{ thumbBaseClasses }}
|
|
28
28
|
const extraClass = {{ className }}
|
|
29
|
-
|
|
29
|
+
// Reactive prop binding so the parent can drive `checked` via signals.
|
|
30
|
+
// See stacksjs/stx#1704.
|
|
31
|
+
const isChecked = useReactiveProp('checked', {{ checked }})
|
|
30
32
|
|
|
31
33
|
function toggle() {
|
|
32
34
|
if (isDisabled) return
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
// Slot wrapper that the parent <Tabs> discovers via DOM walk. The parent
|
|
3
|
+
// reads `data-label` (and optional `data-icon`) to build the tab list and
|
|
4
|
+
// toggles the `hidden` attribute as the active tab changes. Hidden by
|
|
5
|
+
// default — the parent flips the active one visible after mount. See
|
|
6
|
+
// stacksjs/stx#1703.
|
|
7
|
+
export const label = $props.label || ''
|
|
8
|
+
export const icon = $props.icon || ''
|
|
9
|
+
export const className = $props.className || ''
|
|
10
|
+
|
|
11
|
+
const baseClasses = 'p-4 focus:outline-none'
|
|
12
|
+
export const panelClasses = `${baseClasses} ${className}`.trim()
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div
|
|
16
|
+
data-stx-tab-panel
|
|
17
|
+
data-label="{{ label }}"
|
|
18
|
+
data-icon="{{ icon }}"
|
|
19
|
+
class="{{ panelClasses }}"
|
|
20
|
+
role="tabpanel"
|
|
21
|
+
tabindex="0"
|
|
22
|
+
hidden
|
|
23
|
+
>
|
|
24
|
+
<slot />
|
|
25
|
+
</div>
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
// Two modes (see stacksjs/stx#1703):
|
|
3
|
+
// 1. Legacy prop API — pass `tabs={[{label, content, icon}, …]}` for
|
|
4
|
+
// simple string-content tabs (kept for backward compatibility with
|
|
5
|
+
// existing visual snapshots and consumers).
|
|
6
|
+
// 2. Slot API — drop `<TabPanel label="…">` children for arbitrary
|
|
7
|
+
// content (components, forms, tables, etc.). The parent enumerates
|
|
8
|
+
// [data-stx-tab-panel] descendants on mount and builds the tab list
|
|
9
|
+
// dynamically.
|
|
10
|
+
export const tabs = $props.tabs || []
|
|
11
|
+
export const defaultTab = $props.defaultTab || 0
|
|
12
|
+
export const orientation = $props.orientation || 'horizontal'
|
|
13
|
+
export const variant = $props.variant || 'line'
|
|
14
|
+
export const className = $props.className || ''
|
|
15
|
+
|
|
16
|
+
const orientationClasses = {
|
|
17
|
+
horizontal: 'flex-col',
|
|
18
|
+
vertical: 'flex-row',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const tabListOrientationClasses = {
|
|
22
|
+
horizontal: 'flex-row border-b',
|
|
23
|
+
vertical: 'flex-col border-r',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const variantClasses = {
|
|
27
|
+
line: 'border-gray-200 dark:border-gray-700',
|
|
28
|
+
pills: 'gap-2 border-0 bg-gray-100 dark:bg-gray-800 p-1 rounded-lg',
|
|
29
|
+
enclosed: 'border-gray-200 dark:border-gray-700',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const containerClasses = `flex ${orientationClasses[orientation]} ${className}`.trim()
|
|
33
|
+
export const tabListClasses = `flex ${tabListOrientationClasses[orientation]} ${variantClasses[variant]}`.trim()
|
|
34
|
+
export const tabPanelClasses = `p-4 focus:outline-none`.trim()
|
|
35
|
+
// True when the parent passes a non-empty `tabs` array — switches to the
|
|
36
|
+
// legacy render path so existing snapshots stay byte-identical.
|
|
37
|
+
export const hasPropTabs = Array.isArray(tabs) && tabs.length > 0
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<script client>
|
|
41
|
+
const emit = defineEmits()
|
|
42
|
+
const tabs = {{ tabs }}
|
|
43
|
+
const variant = {{ variant }}
|
|
44
|
+
const orientation = {{ orientation }}
|
|
45
|
+
const hasPropTabs = {{ hasPropTabs }}
|
|
46
|
+
const containerRef = useRef()
|
|
47
|
+
// In slot mode, `discoveredTabs` is populated from child <TabPanel> data
|
|
48
|
+
// attributes on mount. In prop mode, it mirrors the `tabs` prop so the
|
|
49
|
+
// rendering loop works the same in both modes.
|
|
50
|
+
const discoveredTabs = state(hasPropTabs ? tabs : [])
|
|
51
|
+
const activeTab = state({{ defaultTab }})
|
|
52
|
+
|
|
53
|
+
// Walk only direct descendants of THIS container (not nested Tabs').
|
|
54
|
+
function findOwnPanels() {
|
|
55
|
+
const root = containerRef.value
|
|
56
|
+
if (!root) return []
|
|
57
|
+
// Closest ancestor with data-stx-tabs must be this container's root —
|
|
58
|
+
// protects against nested <Tabs> contamination.
|
|
59
|
+
return Array.from(root.querySelectorAll('[data-stx-tab-panel]'))
|
|
60
|
+
.filter(p => p.closest('[data-stx-tabs]') === root)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
onMount(() => {
|
|
64
|
+
if (hasPropTabs) return // legacy mode: prop array already in discoveredTabs
|
|
65
|
+
const panels = findOwnPanels()
|
|
66
|
+
const list = panels.map(p => ({
|
|
67
|
+
label: p.dataset.label || '',
|
|
68
|
+
icon: p.dataset.icon || '',
|
|
69
|
+
}))
|
|
70
|
+
discoveredTabs.set(list)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// In slot mode, toggle each panel's `hidden` attribute as activeTab changes.
|
|
74
|
+
// Prop mode is handled inline by the @foreach loop's :show binding below.
|
|
75
|
+
effect(() => {
|
|
76
|
+
if (hasPropTabs) return
|
|
77
|
+
const panels = findOwnPanels()
|
|
78
|
+
const idx = activeTab()
|
|
79
|
+
panels.forEach((p, i) => {
|
|
80
|
+
if (i === idx) p.removeAttribute('hidden')
|
|
81
|
+
else p.setAttribute('hidden', '')
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
function selectTab(index) {
|
|
86
|
+
activeTab.set(index)
|
|
87
|
+
const list = discoveredTabs()
|
|
88
|
+
emit('change', { index, tab: list[index] })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isActive(index) {
|
|
92
|
+
return activeTab() === index
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getTabClasses(index) {
|
|
96
|
+
const isActiveTab = isActive(index)
|
|
97
|
+
const base = 'px-4 py-2 font-medium text-sm transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900'
|
|
98
|
+
if (variant === 'line') {
|
|
99
|
+
const border = orientation === 'horizontal' ? 'border-b-2' : 'border-r-2'
|
|
100
|
+
return `${base} ${border} ${isActiveTab ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:border-gray-300 dark:hover:border-gray-600'}`
|
|
101
|
+
}
|
|
102
|
+
if (variant === 'pills') {
|
|
103
|
+
return `${base} rounded-md ${isActiveTab ? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'}`
|
|
104
|
+
}
|
|
105
|
+
if (variant === 'enclosed') {
|
|
106
|
+
const borders = orientation === 'horizontal' ? 'border-t border-l border-r rounded-t-md' : 'border-t border-l border-b rounded-l-md'
|
|
107
|
+
return `${base} ${borders} ${isActiveTab ? 'bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 -mb-px' : 'bg-gray-50 dark:bg-gray-900 border-transparent hover:border-gray-200 dark:hover:border-gray-700'}`
|
|
108
|
+
}
|
|
109
|
+
return base
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function onTabKey(event, index) {
|
|
113
|
+
const tabCount = discoveredTabs().length
|
|
114
|
+
let next = index
|
|
115
|
+
if (orientation === 'horizontal') {
|
|
116
|
+
if (event.key === 'ArrowRight') { event.preventDefault(); next = (index + 1) % tabCount }
|
|
117
|
+
else if (event.key === 'ArrowLeft') { event.preventDefault(); next = (index - 1 + tabCount) % tabCount }
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
if (event.key === 'ArrowDown') { event.preventDefault(); next = (index + 1) % tabCount }
|
|
121
|
+
else if (event.key === 'ArrowUp') { event.preventDefault(); next = (index - 1 + tabCount) % tabCount }
|
|
122
|
+
}
|
|
123
|
+
if (event.key === 'Home') { event.preventDefault(); next = 0 }
|
|
124
|
+
else if (event.key === 'End') { event.preventDefault(); next = tabCount - 1 }
|
|
125
|
+
if (next !== index) {
|
|
126
|
+
selectTab(next)
|
|
127
|
+
document.getElementById(`tab-${next}`)?.focus()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
defineExpose({ activeTab, selectTab })
|
|
132
|
+
</script>
|
|
133
|
+
|
|
134
|
+
<div x-ref="containerRef" data-stx-tabs class="{{ containerClasses }}">
|
|
135
|
+
<div
|
|
136
|
+
class="{{ tabListClasses }}"
|
|
137
|
+
role="tablist"
|
|
138
|
+
aria-orientation="{{ orientation }}"
|
|
139
|
+
>
|
|
140
|
+
@if(hasPropTabs)
|
|
141
|
+
{{-- Legacy mode: server-side iterate the prop tabs array. Buttons --}}
|
|
142
|
+
{{-- get static class names so existing visual snapshots stay --}}
|
|
143
|
+
{{-- byte-identical. --}}
|
|
144
|
+
@foreach(tab in tabs)
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
:class="getTabClasses({{ $loop.index }})"
|
|
148
|
+
role="tab"
|
|
149
|
+
:aria-selected="isActive({{ $loop.index }}) ? 'true' : 'false'"
|
|
150
|
+
aria-controls="tab-panel-{{ $loop.index }}"
|
|
151
|
+
id="tab-{{ $loop.index }}"
|
|
152
|
+
:tabindex="isActive({{ $loop.index }}) ? '0' : '-1'"
|
|
153
|
+
@click="selectTab({{ $loop.index }})"
|
|
154
|
+
@keydown="onTabKey($event, {{ $loop.index }})"
|
|
155
|
+
>
|
|
156
|
+
@if(tab.icon)
|
|
157
|
+
<span class="inline-flex items-center gap-2">
|
|
158
|
+
<span x-html="{!! JSON.stringify(tab.icon) !!}"></span>
|
|
159
|
+
<span>{{ tab.label }}</span>
|
|
160
|
+
</span>
|
|
161
|
+
@else
|
|
162
|
+
{{ tab.label }}
|
|
163
|
+
@endif
|
|
164
|
+
</button>
|
|
165
|
+
@endforeach
|
|
166
|
+
@else
|
|
167
|
+
{{-- Slot mode: render the tab list dynamically from --}}
|
|
168
|
+
{{-- discoveredTabs() which is populated in onMount. --}}
|
|
169
|
+
<button
|
|
170
|
+
:for="(tab, idx) in discoveredTabs()"
|
|
171
|
+
:key="idx"
|
|
172
|
+
type="button"
|
|
173
|
+
x-class="getTabClasses(idx)"
|
|
174
|
+
role="tab"
|
|
175
|
+
:aria-selected="isActive(idx) ? 'true' : 'false'"
|
|
176
|
+
:tabindex="isActive(idx) ? 0 : -1"
|
|
177
|
+
@click="selectTab(idx)"
|
|
178
|
+
@keydown="onTabKey($event, idx)"
|
|
179
|
+
>
|
|
180
|
+
<span :if="tab.icon" class="inline-flex items-center gap-2" x-html="tab.icon"></span>
|
|
181
|
+
<span :text="tab.label"></span>
|
|
182
|
+
</button>
|
|
183
|
+
@endif
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
@if(hasPropTabs)
|
|
187
|
+
@foreach(tab in tabs)
|
|
188
|
+
<div
|
|
189
|
+
:show="isActive({{ $loop.index }})"
|
|
190
|
+
id="tab-panel-{{ $loop.index }}"
|
|
191
|
+
class="{{ tabPanelClasses }}"
|
|
192
|
+
role="tabpanel"
|
|
193
|
+
aria-labelledby="tab-{{ $loop.index }}"
|
|
194
|
+
tabindex="0"
|
|
195
|
+
>
|
|
196
|
+
{!! tab.content !!}
|
|
197
|
+
</div>
|
|
198
|
+
@endforeach
|
|
199
|
+
@else
|
|
200
|
+
<slot />
|
|
201
|
+
@endif
|
|
202
|
+
</div>
|
|
@@ -39,7 +39,10 @@ export const inputId = `input-${label}`
|
|
|
39
39
|
|
|
40
40
|
<script client>
|
|
41
41
|
const emit = defineEmits()
|
|
42
|
-
|
|
42
|
+
// Reactive prop binding so the parent can drive `value` via signals.
|
|
43
|
+
// Parent listens to @input to push user changes back into its signal.
|
|
44
|
+
// See stacksjs/stx#1704.
|
|
45
|
+
const inputValue = useReactiveProp('value', {{ value }}, { parse: (v) => v == null ? '' : String(v) })
|
|
43
46
|
|
|
44
47
|
function onInput(event) {
|
|
45
48
|
inputValue.set(event.target.value)
|
|
@@ -35,7 +35,8 @@ export const helperClasses = `mt-2 text-sm ${error ? 'text-red-600 dark:text-red
|
|
|
35
35
|
const emit = defineEmits()
|
|
36
36
|
const autoResize = {{ autoResize }}
|
|
37
37
|
const maxRows = {{ maxRows }}
|
|
38
|
-
|
|
38
|
+
// Reactive prop binding — parent can drive `value` via signals. See #1704.
|
|
39
|
+
const inputValue = useReactiveProp('value', {{ value }}, { parse: (v) => v == null ? '' : String(v) })
|
|
39
40
|
|
|
40
41
|
function resizeTextarea(el) {
|
|
41
42
|
if (!autoResize) return
|
|
@@ -27,7 +27,8 @@ export const arrowClass = `absolute w-0 h-0 border-4 border-transparent ${arrowC
|
|
|
27
27
|
<script client>
|
|
28
28
|
const delay = {{ delay }}
|
|
29
29
|
const isDisabled = {{ disabled }}
|
|
30
|
-
|
|
30
|
+
// Reactive prop binding — parent can drive `show` via signals. See #1704.
|
|
31
|
+
const isVisible = useReactiveProp('show', {{ show }})
|
|
31
32
|
let timeoutId = null
|
|
32
33
|
|
|
33
34
|
function showTooltip() {
|