@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.
Files changed (56) hide show
  1. package/dist/Accordion-pt8j917s.stx +146 -0
  2. package/dist/AccordionItem-hbhzx63p.stx +37 -0
  3. package/dist/{Checkbox-zgdmy3p5.stx → Checkbox-aqmq4cqv.stx} +3 -1
  4. package/dist/{Dialog-hczpexs4.stx → Dialog-y5rz2vrq.stx} +5 -1
  5. package/dist/{Drawer-xhevp5cw.stx → Drawer-hrz7qcyp.stx} +4 -1
  6. package/dist/Notification-03vfh3k6.stx +117 -0
  7. package/dist/{NumberInput-drnvtqej.stx → NumberInput-g41gmd2c.stx} +2 -1
  8. package/dist/{Pagination-n3d31cns.stx → Pagination-wtgxmzey.stx} +3 -1
  9. package/dist/{PasswordInput-n02x6053.stx → PasswordInput-4rsq1gwb.stx} +2 -1
  10. package/dist/{Progress-yxcntr4x.stx → Progress-9dhakbh4.stx} +2 -1
  11. package/dist/{Radio-cgjv2hb2.stx → Radio-djtb5dpc.stx} +2 -1
  12. package/dist/{Select-28y0pg83.stx → Select-1vnd0brz.stx} +2 -1
  13. package/dist/{SidebarSection-npt9h265.stx → SidebarSection-g195cb4f.stx} +2 -1
  14. package/dist/{Switch-r5t5w04w.stx → Switch-p1bcr9ks.stx} +3 -1
  15. package/dist/TabPanel-5htevsq6.stx +25 -0
  16. package/dist/Tabs-79brys99.stx +202 -0
  17. package/dist/{TextInput-kf084763.stx → TextInput-g0vaz0pe.stx} +4 -1
  18. package/dist/{Textarea-xg4rr2xz.stx → Textarea-96kawv5c.stx} +2 -1
  19. package/dist/{Tooltip-m0tqd72e.stx → Tooltip-y61h04gp.stx} +2 -1
  20. package/dist/index.js +3909 -15
  21. package/dist/ui/accordion/Accordion.stx +9 -1
  22. package/dist/ui/accordion/AccordionItem.stx +7 -0
  23. package/dist/ui/accordion/index.d.ts +32 -2
  24. package/dist/ui/input/NumberInput.stx +1 -1
  25. package/dist/ui/input/TextInput.stx +1 -1
  26. package/dist/ui/notification/Notification.stx +10 -8
  27. package/dist/ui/select/Select.stx +1 -1
  28. package/dist/ui/tabs/TabPanel.stx +9 -0
  29. package/dist/ui/tabs/Tabs.stx +140 -0
  30. package/dist/ui/tabs/index.d.ts +32 -1
  31. package/dist/ui/textarea/Textarea.stx +1 -1
  32. package/package.json +3 -3
  33. package/src/ui/accordion/Accordion.stx +90 -32
  34. package/src/ui/accordion/AccordionItem.stx +37 -0
  35. package/src/ui/accordion/index.ts +35 -2
  36. package/src/ui/checkbox/Checkbox.stx +3 -1
  37. package/src/ui/dialog/Dialog.stx +5 -1
  38. package/src/ui/drawer/Drawer.stx +4 -1
  39. package/src/ui/input/NumberInput.stx +2 -1
  40. package/src/ui/input/PasswordInput.stx +2 -1
  41. package/src/ui/input/TextInput.stx +4 -1
  42. package/src/ui/notification/Notification.stx +65 -26
  43. package/src/ui/pagination/Pagination.stx +3 -1
  44. package/src/ui/progress/Progress.stx +2 -1
  45. package/src/ui/radio/Radio.stx +2 -1
  46. package/src/ui/select/Select.stx +2 -1
  47. package/src/ui/sidebar/SidebarSection.stx +2 -1
  48. package/src/ui/switch/Switch.stx +3 -1
  49. package/src/ui/tabs/TabPanel.stx +25 -0
  50. package/src/ui/tabs/Tabs.stx +108 -32
  51. package/src/ui/tabs/index.ts +35 -1
  52. package/src/ui/textarea/Textarea.stx +2 -1
  53. package/src/ui/tooltip/Tooltip.stx +2 -1
  54. package/dist/Accordion-19x8q4rj.stx +0 -88
  55. package/dist/Notification-tb9b525s.stx +0 -78
  56. package/dist/Tabs-bzvvn4ke.stx +0 -126
@@ -39,7 +39,10 @@ export const inputId = `input-${label}`
39
39
 
40
40
  <script client>
41
41
  const emit = defineEmits()
42
- const inputValue = state({{ value }})
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)
@@ -1,11 +1,25 @@
1
1
  <script server>
2
- export const show = $props.show !== false
3
- export const title = $props.title || ''
4
- export const message = $props.message || ''
5
- export const type = $props.type || 'info'
6
- export const position = $props.position || 'top-right'
7
- export const duration = $props.duration || 5000
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)
8
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 }})
9
23
 
10
24
  const typeColors = {
11
25
  info: 'bg-blue-50 text-blue-900 dark:bg-blue-900 dark:text-blue-100',
@@ -24,44 +38,69 @@ const positions = {
24
38
  }
25
39
 
26
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'
27
- export const notificationClasses = `${baseClasses} ${typeColors[type]} ${positions[position]} ${className}`.trim()
28
- </script>
41
+ const extraClassName = {{ JSON.stringify(className) }}
29
42
 
30
- <script client>
31
- const emit = defineEmits()
32
- const duration = {{ duration }}
33
- const isVisible = state({{ show }})
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
+ }
34
63
 
35
64
  function close() {
36
65
  isVisible.set(false)
66
+ clearAutoHide()
37
67
  emit('close')
38
68
  }
39
69
 
40
- onMount(() => {
41
- if (duration > 0 && isVisible()) {
42
- setTimeout(close, duration)
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))
43
77
  }
78
+ isVisible.set(true)
79
+ scheduleAutoHide()
80
+ }
81
+
82
+ onMount(() => {
83
+ if (isVisible()) scheduleAutoHide()
44
84
  })
45
85
 
46
86
  defineExpose({
47
- show: () => isVisible.set(true),
87
+ open,
48
88
  close,
49
89
  isVisible,
90
+ title,
91
+ message,
92
+ type,
93
+ position,
94
+ duration,
50
95
  })
51
96
  </script>
52
97
 
53
- <div :show="isVisible()" class="{{ notificationClasses }}" role="alert">
98
+ <div :if="isVisible()" :class="notificationClasses()" role="alert">
54
99
  <div class="flex items-start">
55
100
  <div class="flex-1">
56
- @if(title)
57
- <p class="text-sm font-medium">{{ title }}</p>
58
- @endif
59
- @if(message)
60
- <p class="mt-1 text-sm {{ title ? 'opacity-90' : '' }}">{{ message }}</p>
61
- @endif
62
- @if(!title && !message)
63
- <slot />
64
- @endif
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()" />
65
104
  </div>
66
105
 
67
106
  <button
@@ -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
- const page = state({{ currentPage }})
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()) {
@@ -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
- const value = state({{ value }})
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
- const isChecked = state({{ checked }})
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
- const selectedValue = state({{ value }})
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
- const isExpanded = state({{ expanded }})
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
- const isChecked = state({{ checked }})
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>
@@ -1,4 +1,12 @@
1
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.
2
10
  export const tabs = $props.tabs || []
3
11
  export const defaultTab = $props.defaultTab || 0
4
12
  export const orientation = $props.orientation || 'horizontal'
@@ -24,6 +32,9 @@ const variantClasses = {
24
32
  export const containerClasses = `flex ${orientationClasses[orientation]} ${className}`.trim()
25
33
  export const tabListClasses = `flex ${tabListOrientationClasses[orientation]} ${variantClasses[variant]}`.trim()
26
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
27
38
  </script>
28
39
 
29
40
  <script client>
@@ -31,11 +42,50 @@ const emit = defineEmits()
31
42
  const tabs = {{ tabs }}
32
43
  const variant = {{ variant }}
33
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 : [])
34
51
  const activeTab = state({{ defaultTab }})
35
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
+
36
85
  function selectTab(index) {
37
86
  activeTab.set(index)
38
- emit('change', { index, tab: tabs[index] })
87
+ const list = discoveredTabs()
88
+ emit('change', { index, tab: list[index] })
39
89
  }
40
90
 
41
91
  function isActive(index) {
@@ -60,7 +110,7 @@ function getTabClasses(index) {
60
110
  }
61
111
 
62
112
  function onTabKey(event, index) {
63
- const tabCount = tabs.length
113
+ const tabCount = discoveredTabs().length
64
114
  let next = index
65
115
  if (orientation === 'horizontal') {
66
116
  if (event.key === 'ArrowRight') { event.preventDefault(); next = (index + 1) % tabCount }
@@ -81,46 +131,72 @@ function onTabKey(event, index) {
81
131
  defineExpose({ activeTab, selectTab })
82
132
  </script>
83
133
 
84
- <div class="{{ containerClasses }}">
134
+ <div x-ref="containerRef" data-stx-tabs class="{{ containerClasses }}">
85
135
  <div
86
136
  class="{{ tabListClasses }}"
87
137
  role="tablist"
88
138
  aria-orientation="{{ orientation }}"
89
139
  >
90
- @foreach(tab in tabs)
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. --}}
91
169
  <button
170
+ :for="(tab, idx) in discoveredTabs()"
171
+ :key="idx"
92
172
  type="button"
93
- :class="getTabClasses({{ $loop.index }})"
173
+ x-class="getTabClasses(idx)"
94
174
  role="tab"
95
- :aria-selected="isActive({{ $loop.index }}) ? 'true' : 'false'"
96
- aria-controls="tab-panel-{{ $loop.index }}"
97
- id="tab-{{ $loop.index }}"
98
- :tabindex="isActive({{ $loop.index }}) ? '0' : '-1'"
99
- @click="selectTab({{ $loop.index }})"
100
- @keydown="onTabKey($event, {{ $loop.index }})"
175
+ :aria-selected="isActive(idx) ? 'true' : 'false'"
176
+ :tabindex="isActive(idx) ? 0 : -1"
177
+ @click="selectTab(idx)"
178
+ @keydown="onTabKey($event, idx)"
101
179
  >
102
- @if(tab.icon)
103
- <span class="inline-flex items-center gap-2">
104
- <span x-html="{!! JSON.stringify(tab.icon) !!}"></span>
105
- <span>{{ tab.label }}</span>
106
- </span>
107
- @else
108
- {{ tab.label }}
109
- @endif
180
+ <span :if="tab.icon" class="inline-flex items-center gap-2" x-html="tab.icon"></span>
181
+ <span :text="tab.label"></span>
110
182
  </button>
111
- @endforeach
183
+ @endif
112
184
  </div>
113
185
 
114
- @foreach(tab in tabs)
115
- <div
116
- :show="isActive({{ $loop.index }})"
117
- id="tab-panel-{{ $loop.index }}"
118
- class="{{ tabPanelClasses }}"
119
- role="tabpanel"
120
- aria-labelledby="tab-{{ $loop.index }}"
121
- tabindex="0"
122
- >
123
- {!! tab.content !!}
124
- </div>
125
- @endforeach
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
126
202
  </div>
@@ -1,5 +1,13 @@
1
+ export { default as TabPanel } from './TabPanel.stx'
1
2
  export { default as Tabs } from './Tabs.stx'
2
3
 
4
+ /**
5
+ * Legacy prop API — pass `tabs` as an array of `{ label, content, icon? }`
6
+ * where `content` is a HTML string. Kept for backward compatibility; prefer
7
+ * the slot API below for any tab whose content needs to be a component tree.
8
+ *
9
+ * See stacksjs/stx#1703.
10
+ */
3
11
  export interface Tab {
4
12
  label: string
5
13
  content: string
@@ -7,10 +15,36 @@ export interface Tab {
7
15
  }
8
16
 
9
17
  export interface TabsProps {
10
- tabs: Tab[]
18
+ /** Legacy: array of tab definitions with string content. */
19
+ tabs?: Tab[]
11
20
  defaultTab?: number
12
21
  orientation?: 'horizontal' | 'vertical'
13
22
  variant?: 'line' | 'pills' | 'enclosed'
14
23
  onChange?: (index: number, tab: Tab) => void
15
24
  className?: string
16
25
  }
26
+
27
+ /**
28
+ * Slot API — wrap each tab's content in a `<TabPanel label="…">`. The parent
29
+ * `<Tabs>` discovers panels on mount via `[data-stx-tab-panel]` data
30
+ * attributes, builds the tab list dynamically, and toggles each panel's
31
+ * `hidden` attribute as the active tab changes.
32
+ *
33
+ * @example
34
+ * ```html
35
+ * <Tabs defaultTab="0" variant="pills">
36
+ * <TabPanel label="Drivers">
37
+ * <DriversTable :drivers="drivers()" />
38
+ * </TabPanel>
39
+ * <TabPanel label="Notifications" icon="bell">
40
+ * <NotificationsTable />
41
+ * </TabPanel>
42
+ * </Tabs>
43
+ * ```
44
+ */
45
+ export interface TabPanelProps {
46
+ label: string
47
+ /** Optional icon HTML (rendered via `x-html`) for the tab button. */
48
+ icon?: string
49
+ className?: string
50
+ }
@@ -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
- const inputValue = state({{ value }})
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
- const isVisible = state({{ show }})
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() {
@@ -1,88 +0,0 @@
1
- <script server>
2
- export const items = $props.items || []
3
- export const allowMultiple = $props.allowMultiple || false
4
- export const defaultOpen = $props.defaultOpen || []
5
- export const className = $props.className || ''
6
-
7
- export const accordionClasses = `divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-lg ${className}`.trim()
8
- 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()
9
- export const iconClasses = `w-5 h-5 text-gray-500 dark:text-gray-400 transition-transform duration-200`.trim()
10
- export const contentClasses = `px-4 py-3 text-gray-700 dark:text-gray-300`.trim()
11
- </script>
12
-
13
- <script client>
14
- const emit = defineEmits()
15
- const allowMultiple = {{ allowMultiple }}
16
- const openItems = state({{ defaultOpen }})
17
-
18
- function toggleItem(index) {
19
- const cur = openItems()
20
- if (allowMultiple) {
21
- openItems.set(cur.includes(index) ? cur.filter(i => i !== index) : [...cur, index])
22
- }
23
- else {
24
- openItems.set(cur.includes(index) ? [] : [index])
25
- }
26
- emit('change', openItems())
27
- }
28
-
29
- function isOpen(index) {
30
- return openItems().includes(index)
31
- }
32
-
33
- function onHeaderKey(event, index) {
34
- const buttons = event.currentTarget.closest('[role="region"]').querySelectorAll('button[data-accordion-header]')
35
- if (event.key === 'ArrowDown') {
36
- event.preventDefault()
37
- buttons[index + 1]?.focus()
38
- }
39
- else if (event.key === 'ArrowUp') {
40
- event.preventDefault()
41
- buttons[index - 1]?.focus()
42
- }
43
- else if (event.key === 'Home') {
44
- event.preventDefault()
45
- buttons[0]?.focus()
46
- }
47
- else if (event.key === 'End') {
48
- event.preventDefault()
49
- buttons[buttons.length - 1]?.focus()
50
- }
51
- }
52
- </script>
53
-
54
- <div class="{{ accordionClasses }}" role="region">
55
- @foreach(item in items)
56
- <div>
57
- <button
58
- type="button"
59
- data-accordion-header
60
- class="{{ headerClasses }}"
61
- @click="toggleItem({{ $loop.index }})"
62
- @keydown="onHeaderKey($event, {{ $loop.index }})"
63
- :aria-expanded="isOpen({{ $loop.index }}) ? 'true' : 'false'"
64
- aria-controls="accordion-content-{{ $loop.index }}"
65
- >
66
- <span>{{ item.title }}</span>
67
- <svg
68
- class="{{ iconClasses }}"
69
- :style="`transform: rotate(${isOpen({{ $loop.index }}) ? 180 : 0}deg)`"
70
- fill="none"
71
- viewBox="0 0 24 24"
72
- stroke="currentColor"
73
- >
74
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
75
- </svg>
76
- </button>
77
-
78
- <div
79
- :show="isOpen({{ $loop.index }})"
80
- id="accordion-content-{{ $loop.index }}"
81
- class="{{ contentClasses }}"
82
- role="region"
83
- >
84
- {!! item.content !!}
85
- </div>
86
- </div>
87
- @endforeach
88
- </div>
@@ -1,78 +0,0 @@
1
- <script server>
2
- export const show = $props.show !== false
3
- export const title = $props.title || ''
4
- export const message = $props.message || ''
5
- export const type = $props.type || 'info'
6
- export const position = $props.position || 'top-right'
7
- export const duration = $props.duration || 5000
8
- export const className = $props.className || ''
9
-
10
- const typeColors = {
11
- info: 'bg-blue-50 text-blue-900 dark:bg-blue-900 dark:text-blue-100',
12
- success: 'bg-green-50 text-green-900 dark:bg-green-900 dark:text-green-100',
13
- warning: 'bg-yellow-50 text-yellow-900 dark:bg-yellow-900 dark:text-yellow-100',
14
- error: 'bg-red-50 text-red-900 dark:bg-red-900 dark:text-red-100',
15
- }
16
-
17
- const positions = {
18
- 'top-left': 'top-0 left-0',
19
- 'top-right': 'top-0 right-0',
20
- 'top-center': 'top-0 left-1/2 -translate-x-1/2',
21
- 'bottom-left': 'bottom-0 left-0',
22
- 'bottom-right': 'bottom-0 right-0',
23
- 'bottom-center': 'bottom-0 left-1/2 -translate-x-1/2',
24
- }
25
-
26
- 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'
27
- export const notificationClasses = `${baseClasses} ${typeColors[type]} ${positions[position]} ${className}`.trim()
28
- </script>
29
-
30
- <script client>
31
- const emit = defineEmits()
32
- const duration = {{ duration }}
33
- const isVisible = state({{ show }})
34
-
35
- function close() {
36
- isVisible.set(false)
37
- emit('close')
38
- }
39
-
40
- onMount(() => {
41
- if (duration > 0 && isVisible()) {
42
- setTimeout(close, duration)
43
- }
44
- })
45
-
46
- defineExpose({
47
- show: () => isVisible.set(true),
48
- close,
49
- isVisible,
50
- })
51
- </script>
52
-
53
- <div :show="isVisible()" class="{{ notificationClasses }}" role="alert">
54
- <div class="flex items-start">
55
- <div class="flex-1">
56
- @if(title)
57
- <p class="text-sm font-medium">{{ title }}</p>
58
- @endif
59
- @if(message)
60
- <p class="mt-1 text-sm {{ title ? 'opacity-90' : '' }}">{{ message }}</p>
61
- @endif
62
- @if(!title && !message)
63
- <slot />
64
- @endif
65
- </div>
66
-
67
- <button
68
- type="button"
69
- @click="close()"
70
- class="ml-4 inline-flex rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2"
71
- >
72
- <span class="sr-only">Close</span>
73
- <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
74
- <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" />
75
- </svg>
76
- </button>
77
- </div>
78
- </div>