@stacksjs/components 0.2.63 → 0.2.67
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/{SidebarSection-npt9h265.stx → SidebarSection-g195cb4f.stx} +2 -1
- 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 +32 -2
- 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/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 +35 -2
- 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/SidebarSection.stx +2 -1
- 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/dist/Accordion-19x8q4rj.stx +0 -88
- package/dist/Notification-tb9b525s.stx +0 -78
- package/dist/Tabs-bzvvn4ke.stx +0 -126
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
// Two modes (see stacksjs/stx#1703):
|
|
3
|
+
// 1. Legacy prop API — pass `items={[{title, content}, …]}` for simple
|
|
4
|
+
// string-content accordions (kept for backward compatibility with
|
|
5
|
+
// existing visual snapshots and consumers).
|
|
6
|
+
// 2. Slot API — drop `<AccordionItem title="…">` children for arbitrary
|
|
7
|
+
// content. Each AccordionItem renders its own header + content panel;
|
|
8
|
+
// this parent attaches click handlers and toggles content visibility.
|
|
9
|
+
export const items = $props.items || []
|
|
10
|
+
export const allowMultiple = $props.allowMultiple || false
|
|
11
|
+
export const defaultOpen = $props.defaultOpen || []
|
|
12
|
+
export const className = $props.className || ''
|
|
13
|
+
|
|
14
|
+
export const accordionClasses = `divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-lg ${className}`.trim()
|
|
15
|
+
export const headerClasses = `w-full flex items-center justify-between px-4 py-3 text-left font-medium text-gray-900 dark:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900`.trim()
|
|
16
|
+
export const iconClasses = `w-5 h-5 text-gray-500 dark:text-gray-400 transition-transform duration-200`.trim()
|
|
17
|
+
export const contentClasses = `px-4 py-3 text-gray-700 dark:text-gray-300`.trim()
|
|
18
|
+
export const hasPropItems = Array.isArray(items) && items.length > 0
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<script client>
|
|
22
|
+
const emit = defineEmits()
|
|
23
|
+
const allowMultiple = {{ allowMultiple }}
|
|
24
|
+
const hasPropItems = {{ hasPropItems }}
|
|
25
|
+
const containerRef = useRef()
|
|
26
|
+
const openItems = state({{ defaultOpen }})
|
|
27
|
+
|
|
28
|
+
function findOwnItems() {
|
|
29
|
+
const root = containerRef.value
|
|
30
|
+
if (!root) return []
|
|
31
|
+
return Array.from(root.querySelectorAll('[data-stx-accordion-item]'))
|
|
32
|
+
.filter(item => item.closest('[data-stx-accordion]') === root)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
onMount(() => {
|
|
36
|
+
if (hasPropItems) return // legacy mode: server-rendered, no wiring needed
|
|
37
|
+
const items = findOwnItems()
|
|
38
|
+
items.forEach((item, idx) => {
|
|
39
|
+
const header = item.querySelector(':scope > [data-stx-accordion-header]')
|
|
40
|
+
if (header && !header.__stx_wired) {
|
|
41
|
+
header.__stx_wired = true
|
|
42
|
+
header.addEventListener('click', () => toggleItem(idx))
|
|
43
|
+
header.addEventListener('keydown', (e) => onHeaderKey(e, idx))
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// Sync the open state into the DOM: show/hide content panels, update
|
|
49
|
+
// aria-expanded on headers, rotate chevrons.
|
|
50
|
+
effect(() => {
|
|
51
|
+
if (hasPropItems) return
|
|
52
|
+
const items = findOwnItems()
|
|
53
|
+
const open = openItems()
|
|
54
|
+
items.forEach((item, idx) => {
|
|
55
|
+
const isOpenItem = open.includes(idx)
|
|
56
|
+
const header = item.querySelector(':scope > [data-stx-accordion-header]')
|
|
57
|
+
const content = item.querySelector(':scope > [data-stx-accordion-content]')
|
|
58
|
+
const chevron = item.querySelector(':scope > [data-stx-accordion-header] [data-stx-accordion-chevron]')
|
|
59
|
+
if (header) header.setAttribute('aria-expanded', isOpenItem ? 'true' : 'false')
|
|
60
|
+
if (content) {
|
|
61
|
+
if (isOpenItem) content.removeAttribute('hidden')
|
|
62
|
+
else content.setAttribute('hidden', '')
|
|
63
|
+
}
|
|
64
|
+
if (chevron) chevron.style.transform = `rotate(${isOpenItem ? 180 : 0}deg)`
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
function toggleItem(index) {
|
|
69
|
+
const cur = openItems()
|
|
70
|
+
if (allowMultiple) {
|
|
71
|
+
openItems.set(cur.includes(index) ? cur.filter(i => i !== index) : [...cur, index])
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
openItems.set(cur.includes(index) ? [] : [index])
|
|
75
|
+
}
|
|
76
|
+
emit('change', openItems())
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isOpen(index) {
|
|
80
|
+
return openItems().includes(index)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function onHeaderKey(event, index) {
|
|
84
|
+
const root = containerRef.value
|
|
85
|
+
if (!root) return
|
|
86
|
+
const buttons = root.querySelectorAll('button[data-stx-accordion-header], button[data-accordion-header]')
|
|
87
|
+
if (event.key === 'ArrowDown') {
|
|
88
|
+
event.preventDefault()
|
|
89
|
+
buttons[index + 1]?.focus()
|
|
90
|
+
}
|
|
91
|
+
else if (event.key === 'ArrowUp') {
|
|
92
|
+
event.preventDefault()
|
|
93
|
+
buttons[index - 1]?.focus()
|
|
94
|
+
}
|
|
95
|
+
else if (event.key === 'Home') {
|
|
96
|
+
event.preventDefault()
|
|
97
|
+
buttons[0]?.focus()
|
|
98
|
+
}
|
|
99
|
+
else if (event.key === 'End') {
|
|
100
|
+
event.preventDefault()
|
|
101
|
+
buttons[buttons.length - 1]?.focus()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
defineExpose({ openItems, toggleItem })
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
<div x-ref="containerRef" data-stx-accordion class="{{ accordionClasses }}" role="region">
|
|
109
|
+
@if(hasPropItems)
|
|
110
|
+
@foreach(item in items)
|
|
111
|
+
<div>
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
data-accordion-header
|
|
115
|
+
class="{{ headerClasses }}"
|
|
116
|
+
@click="toggleItem({{ $loop.index }})"
|
|
117
|
+
@keydown="onHeaderKey($event, {{ $loop.index }})"
|
|
118
|
+
:aria-expanded="isOpen({{ $loop.index }}) ? 'true' : 'false'"
|
|
119
|
+
aria-controls="accordion-content-{{ $loop.index }}"
|
|
120
|
+
>
|
|
121
|
+
<span>{{ item.title }}</span>
|
|
122
|
+
<svg
|
|
123
|
+
class="{{ iconClasses }}"
|
|
124
|
+
:style="`transform: rotate(${isOpen({{ $loop.index }}) ? 180 : 0}deg)`"
|
|
125
|
+
fill="none"
|
|
126
|
+
viewBox="0 0 24 24"
|
|
127
|
+
stroke="currentColor"
|
|
128
|
+
>
|
|
129
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
130
|
+
</svg>
|
|
131
|
+
</button>
|
|
132
|
+
|
|
133
|
+
<div
|
|
134
|
+
:show="isOpen({{ $loop.index }})"
|
|
135
|
+
id="accordion-content-{{ $loop.index }}"
|
|
136
|
+
class="{{ contentClasses }}"
|
|
137
|
+
role="region"
|
|
138
|
+
>
|
|
139
|
+
{!! item.content !!}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
@endforeach
|
|
143
|
+
@else
|
|
144
|
+
<slot />
|
|
145
|
+
@endif
|
|
146
|
+
</div>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
// Slot wrapper that the parent <Accordion> discovers and wires up on mount.
|
|
3
|
+
// Renders its own header button + content panel; the parent attaches click
|
|
4
|
+
// handlers and toggles the content panel's `hidden` attribute as openItems
|
|
5
|
+
// changes. Hard-coded styling here matches the legacy prop-mode markup so
|
|
6
|
+
// snapshots compare cleanly in both modes. See stacksjs/stx#1703.
|
|
7
|
+
export const title = $props.title || ''
|
|
8
|
+
export const className = $props.className || ''
|
|
9
|
+
|
|
10
|
+
const headerClasses = 'w-full flex items-center justify-between px-4 py-3 text-left font-medium text-gray-900 dark:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900'
|
|
11
|
+
const iconClasses = 'w-5 h-5 text-gray-500 dark:text-gray-400 transition-transform duration-200'
|
|
12
|
+
const contentClasses = 'px-4 py-3 text-gray-700 dark:text-gray-300'
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div data-stx-accordion-item data-title="{{ title }}" class="{{ className }}">
|
|
16
|
+
<button
|
|
17
|
+
type="button"
|
|
18
|
+
data-stx-accordion-header
|
|
19
|
+
data-accordion-header
|
|
20
|
+
class="{{ headerClasses }}"
|
|
21
|
+
aria-expanded="false"
|
|
22
|
+
>
|
|
23
|
+
<span>{{ title }}</span>
|
|
24
|
+
<svg
|
|
25
|
+
data-stx-accordion-chevron
|
|
26
|
+
class="{{ iconClasses }}"
|
|
27
|
+
fill="none"
|
|
28
|
+
viewBox="0 0 24 24"
|
|
29
|
+
stroke="currentColor"
|
|
30
|
+
>
|
|
31
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
32
|
+
</svg>
|
|
33
|
+
</button>
|
|
34
|
+
<div data-stx-accordion-content class="{{ contentClasses }}" role="region" hidden>
|
|
35
|
+
<slot />
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
@@ -26,7 +26,9 @@ export const inputId = `checkbox-${value || label}`
|
|
|
26
26
|
const emit = defineEmits()
|
|
27
27
|
const isDisabled = {{ disabled }}
|
|
28
28
|
const isIndeterminate = {{ indeterminate }}
|
|
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 onInputChange(event) {
|
|
32
34
|
if (isDisabled) {
|
|
@@ -8,7 +8,11 @@ export const dialogClasses = `${baseClasses} ${className}`.trim()
|
|
|
8
8
|
|
|
9
9
|
<script client>
|
|
10
10
|
const emit = defineEmits()
|
|
11
|
-
|
|
11
|
+
// useReactiveProp wires `:open="parentSignal()"` from the parent into our
|
|
12
|
+
// local `isOpen` signal — when the parent's signal changes, bindAttr
|
|
13
|
+
// updates the root's `open` attribute, our MutationObserver picks it up,
|
|
14
|
+
// and isOpen flips. See stacksjs/stx#1704.
|
|
15
|
+
const isOpen = useReactiveProp('open', {{ open }})
|
|
12
16
|
|
|
13
17
|
function close() {
|
|
14
18
|
isOpen.set(false)
|
|
@@ -32,7 +32,10 @@ export const closeButtonClasses = `absolute top-0 ${closeButtonPosition[position
|
|
|
32
32
|
|
|
33
33
|
<script client>
|
|
34
34
|
const emit = defineEmits()
|
|
35
|
-
|
|
35
|
+
// useReactiveProp lets the parent drive `open` via signals
|
|
36
|
+
// (`:open="drawerOpen()"`) instead of the prop being captured once.
|
|
37
|
+
// See stacksjs/stx#1704.
|
|
38
|
+
const isOpen = useReactiveProp('open', {{ open }})
|
|
36
39
|
|
|
37
40
|
function close() {
|
|
38
41
|
isOpen.set(false)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
// Initial defaults captured at render time. The client script wraps these
|
|
3
|
+
// in signals so consumers can mutate them after mount (fetch-then-toast,
|
|
4
|
+
// auto-dismiss, etc.). See stacksjs/stx#1692.
|
|
5
|
+
export const initialShow = $props.show !== false
|
|
6
|
+
export const initialTitle = $props.title || ''
|
|
7
|
+
export const initialMessage = $props.message || ''
|
|
8
|
+
export const initialType = $props.type || 'info'
|
|
9
|
+
export const initialPosition = $props.position || 'top-right'
|
|
10
|
+
export const initialDuration = Number($props.duration ?? 5000)
|
|
11
|
+
export const className = $props.className || ''
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<script client>
|
|
15
|
+
const emit = defineEmits()
|
|
16
|
+
|
|
17
|
+
const isVisible = state({{ initialShow }})
|
|
18
|
+
const title = state({{ initialTitle }})
|
|
19
|
+
const message = state({{ initialMessage }})
|
|
20
|
+
const type = state({{ initialType }})
|
|
21
|
+
const position = state({{ initialPosition }})
|
|
22
|
+
const duration = state({{ initialDuration }})
|
|
23
|
+
|
|
24
|
+
const typeColors = {
|
|
25
|
+
info: 'bg-blue-50 text-blue-900 dark:bg-blue-900 dark:text-blue-100',
|
|
26
|
+
success: 'bg-green-50 text-green-900 dark:bg-green-900 dark:text-green-100',
|
|
27
|
+
warning: 'bg-yellow-50 text-yellow-900 dark:bg-yellow-900 dark:text-yellow-100',
|
|
28
|
+
error: 'bg-red-50 text-red-900 dark:bg-red-900 dark:text-red-100',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const positions = {
|
|
32
|
+
'top-left': 'top-0 left-0',
|
|
33
|
+
'top-right': 'top-0 right-0',
|
|
34
|
+
'top-center': 'top-0 left-1/2 -translate-x-1/2',
|
|
35
|
+
'bottom-left': 'bottom-0 left-0',
|
|
36
|
+
'bottom-right': 'bottom-0 right-0',
|
|
37
|
+
'bottom-center': 'bottom-0 left-1/2 -translate-x-1/2',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const baseClasses = 'fixed z-50 m-4 max-w-md w-full pointer-events-auto rounded-lg p-4 shadow-lg ring-1 ring-black ring-opacity-5 transition-all'
|
|
41
|
+
const extraClassName = {{ JSON.stringify(className) }}
|
|
42
|
+
|
|
43
|
+
const notificationClasses = derived(() => {
|
|
44
|
+
const color = typeColors[type()] || typeColors.info
|
|
45
|
+
const pos = positions[position()] || positions['top-right']
|
|
46
|
+
return `${baseClasses} ${color} ${pos} ${extraClassName}`.trim()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
let autoHideTimer = null
|
|
50
|
+
function clearAutoHide() {
|
|
51
|
+
if (autoHideTimer) {
|
|
52
|
+
clearTimeout(autoHideTimer)
|
|
53
|
+
autoHideTimer = null
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function scheduleAutoHide() {
|
|
57
|
+
clearAutoHide()
|
|
58
|
+
const d = duration()
|
|
59
|
+
if (d > 0 && isVisible()) {
|
|
60
|
+
autoHideTimer = setTimeout(() => close(), d)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function close() {
|
|
65
|
+
isVisible.set(false)
|
|
66
|
+
clearAutoHide()
|
|
67
|
+
emit('close')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function open(opts) {
|
|
71
|
+
if (opts && typeof opts === 'object') {
|
|
72
|
+
if (opts.title !== undefined) title.set(opts.title)
|
|
73
|
+
if (opts.message !== undefined) message.set(opts.message)
|
|
74
|
+
if (opts.type !== undefined) type.set(opts.type)
|
|
75
|
+
if (opts.position !== undefined) position.set(opts.position)
|
|
76
|
+
if (opts.duration !== undefined) duration.set(Number(opts.duration))
|
|
77
|
+
}
|
|
78
|
+
isVisible.set(true)
|
|
79
|
+
scheduleAutoHide()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
onMount(() => {
|
|
83
|
+
if (isVisible()) scheduleAutoHide()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
defineExpose({
|
|
87
|
+
open,
|
|
88
|
+
close,
|
|
89
|
+
isVisible,
|
|
90
|
+
title,
|
|
91
|
+
message,
|
|
92
|
+
type,
|
|
93
|
+
position,
|
|
94
|
+
duration,
|
|
95
|
+
})
|
|
96
|
+
</script>
|
|
97
|
+
|
|
98
|
+
<div :if="isVisible()" :class="notificationClasses()" role="alert">
|
|
99
|
+
<div class="flex items-start">
|
|
100
|
+
<div class="flex-1">
|
|
101
|
+
<p :if="title()" class="text-sm font-medium" :text="title()"></p>
|
|
102
|
+
<p :if="message()" class="mt-1 text-sm" :class="title() ? 'opacity-90' : ''" :text="message()"></p>
|
|
103
|
+
<slot :if="!title() && !message()" />
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
@click="close()"
|
|
109
|
+
class="ml-4 inline-flex rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2"
|
|
110
|
+
>
|
|
111
|
+
<span class="sr-only">Close</span>
|
|
112
|
+
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
113
|
+
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
|
114
|
+
</svg>
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
@@ -37,7 +37,8 @@ const emit = defineEmits()
|
|
|
37
37
|
const stepSize = {{ step }}
|
|
38
38
|
const minVal = {{ min === undefined ? 'undefined' : min }}
|
|
39
39
|
const maxVal = {{ max === undefined ? 'undefined' : max }}
|
|
40
|
-
|
|
40
|
+
// Reactive prop binding — parent can drive value via signals. See #1704.
|
|
41
|
+
const inputValue = useReactiveProp('value', {{ value }}, { parse: (v) => v == null || v === '' ? '' : String(v) })
|
|
41
42
|
|
|
42
43
|
function onInput(event) {
|
|
43
44
|
inputValue.set(event.target.value)
|
|
@@ -17,7 +17,9 @@ export const iconClasses = `w-5 h-5`.trim()
|
|
|
17
17
|
const emit = defineEmits()
|
|
18
18
|
const totalPages = {{ totalPages }}
|
|
19
19
|
const siblingCount = {{ siblingCount }}
|
|
20
|
-
|
|
20
|
+
// Reactive prop binding — parent can drive `current-page` via signals
|
|
21
|
+
// (useful for URL-driven pagination via useSearchParams). See #1704.
|
|
22
|
+
const page = useReactiveProp('current-page', {{ currentPage }})
|
|
21
23
|
|
|
22
24
|
function goToPage(target) {
|
|
23
25
|
if (target >= 1 && target <= totalPages && target !== page()) {
|
|
@@ -34,7 +34,8 @@ export const iconClasses = iconSizeClasses[size]
|
|
|
34
34
|
<script client>
|
|
35
35
|
const emit = defineEmits()
|
|
36
36
|
const showPassword = state(false)
|
|
37
|
-
|
|
37
|
+
// Reactive prop binding — parent can drive value via signals. See #1704.
|
|
38
|
+
const inputValue = useReactiveProp('value', {{ value }}, { parse: (v) => v == null ? '' : String(v) })
|
|
38
39
|
|
|
39
40
|
function onInput(event) {
|
|
40
41
|
inputValue.set(event.target.value)
|
|
@@ -45,7 +45,8 @@ export const circularBarColor = color === 'primary' ? 'stroke-blue-600 dark:stro
|
|
|
45
45
|
const max = {{ max }}
|
|
46
46
|
const indeterminate = {{ indeterminate }}
|
|
47
47
|
const circumference = {{ circumference }}
|
|
48
|
-
|
|
48
|
+
// Reactive prop binding — parent can drive `value` via signals. See #1704.
|
|
49
|
+
const value = useReactiveProp('value', {{ value }})
|
|
49
50
|
|
|
50
51
|
const percentage = derived(() => Math.min(Math.max((value() / max) * 100, 0), 100))
|
|
51
52
|
const percentLabel = derived(() => `${Math.round(percentage())}%`)
|
|
@@ -25,7 +25,8 @@ export const inputId = `radio-${value || label}`
|
|
|
25
25
|
const emit = defineEmits()
|
|
26
26
|
const isDisabled = {{ disabled }}
|
|
27
27
|
const radioValue = {{ value }}
|
|
28
|
-
|
|
28
|
+
// Reactive prop binding — parent can drive `checked` via signals. See #1704.
|
|
29
|
+
const isChecked = useReactiveProp('checked', {{ checked }})
|
|
29
30
|
|
|
30
31
|
function onInputChange(event) {
|
|
31
32
|
if (isDisabled) {
|
|
@@ -27,7 +27,8 @@ export const selectId = `select-${value}`
|
|
|
27
27
|
|
|
28
28
|
<script client>
|
|
29
29
|
const emit = defineEmits()
|
|
30
|
-
|
|
30
|
+
// Reactive prop binding — parent can drive `value` via signals. See #1704.
|
|
31
|
+
const selectedValue = useReactiveProp('value', {{ value }}, { parse: (v) => v == null ? '' : String(v) })
|
|
31
32
|
|
|
32
33
|
function onChange(event) {
|
|
33
34
|
selectedValue.set(event.target.value)
|
|
@@ -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)
|