@stacksjs/components 0.2.63 → 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/{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 +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/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/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
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
// Two modes (see stacksjs/stx#1703):
|
|
2
|
+
// 1. Legacy prop API — pass `items={[{title, content}, …]}` for simple
|
|
3
|
+
// string-content accordions (kept for backward compatibility with
|
|
4
|
+
// existing visual snapshots and consumers).
|
|
5
|
+
// 2. Slot API — drop `<AccordionItem title="…">` children for arbitrary
|
|
6
|
+
// content. Each AccordionItem renders its own header + content panel;
|
|
7
|
+
// this parent attaches click handlers and toggles content visibility.
|
|
1
8
|
export declare const items: unknown;
|
|
2
9
|
export declare const allowMultiple: unknown;
|
|
3
10
|
export declare const defaultOpen: unknown;
|
|
@@ -5,4 +12,5 @@ export declare const className: unknown;
|
|
|
5
12
|
export declare const accordionClasses: unknown;
|
|
6
13
|
export declare const headerClasses: unknown;
|
|
7
14
|
export declare const iconClasses: unknown;
|
|
8
|
-
export declare const contentClasses:
|
|
15
|
+
export declare const contentClasses: unknown;
|
|
16
|
+
export declare const hasPropItems: (...args: any[]) => any;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Slot wrapper that the parent <Accordion> discovers and wires up on mount.
|
|
2
|
+
// Renders its own header button + content panel; the parent attaches click
|
|
3
|
+
// handlers and toggles the content panel's `hidden` attribute as openItems
|
|
4
|
+
// changes. Hard-coded styling here matches the legacy prop-mode markup so
|
|
5
|
+
// snapshots compare cleanly in both modes. See stacksjs/stx#1703.
|
|
6
|
+
export declare const title: unknown;
|
|
7
|
+
export declare const className: unknown;
|
|
@@ -1,12 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legacy prop API — pass `items` as an array of `{ title, content }` where
|
|
3
|
+
* `content` is a HTML string. Kept for backward compatibility; prefer the
|
|
4
|
+
* slot API below for any accordion whose content needs to be a component tree.
|
|
5
|
+
*
|
|
6
|
+
* See stacksjs/stx#1703.
|
|
7
|
+
*/
|
|
1
8
|
export declare interface AccordionItem {
|
|
2
9
|
title: string
|
|
3
10
|
content: string
|
|
4
11
|
}
|
|
5
12
|
export declare interface AccordionProps {
|
|
6
|
-
items
|
|
13
|
+
items?: AccordionItem[]
|
|
7
14
|
allowMultiple?: boolean
|
|
8
15
|
defaultOpen?: number[]
|
|
9
16
|
onChange?: (openItems: number[]) => void
|
|
10
17
|
className?: string
|
|
11
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Slot API — wrap each section's content in `<AccordionItem title="…">`.
|
|
21
|
+
* Each `<AccordionItem>` renders its own header + content panel; the parent
|
|
22
|
+
* `<Accordion>` attaches click handlers and toggles the content visibility.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```html
|
|
26
|
+
* <Accordion allowMultiple>
|
|
27
|
+
* <AccordionItem title="Profile">
|
|
28
|
+
* <ProfileEditor :user="user()" />
|
|
29
|
+
* </AccordionItem>
|
|
30
|
+
* <AccordionItem title="Zones">
|
|
31
|
+
* <ZonesEditor />
|
|
32
|
+
* </AccordionItem>
|
|
33
|
+
* </Accordion>
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare interface AccordionItemProps {
|
|
37
|
+
title: string
|
|
38
|
+
className?: string
|
|
39
|
+
}
|
|
12
40
|
export { default as Accordion } from './Accordion.stx';
|
|
41
|
+
export { default as AccordionItem } from './AccordionItem.stx';
|
|
@@ -15,4 +15,4 @@ export declare const className: unknown;
|
|
|
15
15
|
export declare const inputClassName: unknown;
|
|
16
16
|
export declare const inputClasses: unknown;
|
|
17
17
|
export declare const labelClasses: unknown;
|
|
18
|
-
export declare const helperClasses: unknown;
|
|
18
|
+
export declare const helperClasses: () => unknown;
|
|
@@ -18,4 +18,4 @@ export declare const inputClasses: unknown;
|
|
|
18
18
|
export declare const labelClasses: unknown;
|
|
19
19
|
export declare const helperClasses: unknown;
|
|
20
20
|
export declare const iconClasses: unknown;
|
|
21
|
-
export declare const inputId: () =>
|
|
21
|
+
export declare const inputId: (...args: any[]) => any;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export declare const
|
|
5
|
-
export declare const
|
|
6
|
-
export declare const
|
|
7
|
-
export declare const
|
|
8
|
-
export declare const
|
|
1
|
+
// Initial defaults captured at render time. The client script wraps these
|
|
2
|
+
// in signals so consumers can mutate them after mount (fetch-then-toast,
|
|
3
|
+
// auto-dismiss, etc.). See stacksjs/stx#1692.
|
|
4
|
+
export declare const initialShow: unknown;
|
|
5
|
+
export declare const initialTitle: unknown;
|
|
6
|
+
export declare const initialMessage: unknown;
|
|
7
|
+
export declare const initialType: unknown;
|
|
8
|
+
export declare const initialPosition: unknown;
|
|
9
|
+
export declare const initialDuration: unknown;
|
|
10
|
+
export declare const className: (...args: any[]) => any;
|
|
@@ -11,4 +11,4 @@ export declare const className: unknown;
|
|
|
11
11
|
export declare const selectClasses: unknown;
|
|
12
12
|
export declare const labelClasses: unknown;
|
|
13
13
|
export declare const helperClasses: unknown;
|
|
14
|
-
export declare const selectId: unknown;
|
|
14
|
+
export declare const selectId: () => unknown;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Slot wrapper that the parent <Tabs> discovers via DOM walk. The parent
|
|
2
|
+
// reads `data-label` (and optional `data-icon`) to build the tab list and
|
|
3
|
+
// toggles the `hidden` attribute as the active tab changes. Hidden by
|
|
4
|
+
// default — the parent flips the active one visible after mount. See
|
|
5
|
+
// stacksjs/stx#1703.
|
|
6
|
+
export declare const label: unknown;
|
|
7
|
+
export declare const icon: unknown;
|
|
8
|
+
export declare const className: unknown;
|
|
9
|
+
export declare const panelClasses: unknown;
|
package/dist/ui/tabs/Tabs.stx
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
// Two modes (see stacksjs/stx#1703):
|
|
2
|
+
// 1. Legacy prop API — pass `tabs={[{label, content, icon}, …]}` for
|
|
3
|
+
// simple string-content tabs (kept for backward compatibility with
|
|
4
|
+
// existing visual snapshots and consumers).
|
|
5
|
+
// 2. Slot API — drop `<TabPanel label="…">` children for arbitrary
|
|
6
|
+
// content (components, forms, tables, etc.). The parent enumerates
|
|
7
|
+
// [data-stx-tab-panel] descendants on mount and builds the tab list
|
|
8
|
+
// dynamically.
|
|
1
9
|
export declare const tabs: unknown;
|
|
2
10
|
export declare const defaultTab: unknown;
|
|
3
11
|
export declare const orientation: unknown;
|
|
@@ -6,3 +14,135 @@ export declare const className: unknown;
|
|
|
6
14
|
export declare const containerClasses: unknown;
|
|
7
15
|
export declare const tabListClasses: unknown;
|
|
8
16
|
export declare const tabPanelClasses: unknown;
|
|
17
|
+
// True when the parent passes a non-empty `tabs` array — switches to the
|
|
18
|
+
// legacy render path so existing snapshots stay byte-identical.
|
|
19
|
+
export declare const hasPropTabs: activeTab changes.
|
|
20
|
+
// Prop mode is handled inline by the @foreach loop's :show binding below.
|
|
21
|
+
effect(() => {
|
|
22
|
+
if (hasPropTabs) return
|
|
23
|
+
const panels = findOwnPanels()
|
|
24
|
+
const idx = activeTab()
|
|
25
|
+
panels.forEach((p, i) => {
|
|
26
|
+
if (i === idx) p.removeAttribute('hidden')
|
|
27
|
+
else p.setAttribute('hidden', '')
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
function selectTab(index) {
|
|
32
|
+
activeTab.set(index)
|
|
33
|
+
const list = discoveredTabs()
|
|
34
|
+
emit('change', { index, tab: list[index] })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isActive(index) {
|
|
38
|
+
return activeTab() === index
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getTabClasses(index) {
|
|
42
|
+
const isActiveTab = isActive(index)
|
|
43
|
+
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'
|
|
44
|
+
if (variant === 'line') {
|
|
45
|
+
const border = orientation === 'horizontal' ? 'border-b-2' : 'border-r-2'
|
|
46
|
+
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'}`
|
|
47
|
+
}
|
|
48
|
+
if (variant === 'pills') {
|
|
49
|
+
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'}`
|
|
50
|
+
}
|
|
51
|
+
if (variant === 'enclosed') {
|
|
52
|
+
const borders = orientation === 'horizontal' ? 'border-t border-l border-r rounded-t-md' : 'border-t border-l border-b rounded-l-md'
|
|
53
|
+
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'}`
|
|
54
|
+
}
|
|
55
|
+
return base
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function onTabKey(event, index) {
|
|
59
|
+
const tabCount = discoveredTabs().length
|
|
60
|
+
let next = index
|
|
61
|
+
if (orientation === 'horizontal') {
|
|
62
|
+
if (event.key === 'ArrowRight') { event.preventDefault(); next = (index + 1) % tabCount }
|
|
63
|
+
else if (event.key === 'ArrowLeft') { event.preventDefault(); next = (index - 1 + tabCount) % tabCount }
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
if (event.key === 'ArrowDown') { event.preventDefault(); next = (index + 1) % tabCount }
|
|
67
|
+
else if (event.key === 'ArrowUp') { event.preventDefault(); next = (index - 1 + tabCount) % tabCount }
|
|
68
|
+
}
|
|
69
|
+
if (event.key === 'Home') { event.preventDefault(); next = 0 }
|
|
70
|
+
else if (event.key === 'End') { event.preventDefault(); next = tabCount - 1 }
|
|
71
|
+
if (next !== index) {
|
|
72
|
+
selectTab(next)
|
|
73
|
+
document.getElementById(`tab-${next}`)?.focus()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
defineExpose({ activeTab, selectTab })
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<div x-ref="containerRef" data-stx-tabs class="{{ containerClasses }}">
|
|
81
|
+
<div
|
|
82
|
+
class="{{ tabListClasses }}"
|
|
83
|
+
role="tablist"
|
|
84
|
+
aria-orientation="{{ orientation }}"
|
|
85
|
+
>
|
|
86
|
+
@if(hasPropTabs)
|
|
87
|
+
{{-- Legacy mode: server-side iterate the prop tabs array. Buttons --}}
|
|
88
|
+
{{-- get static class names so existing visual snapshots stay --}}
|
|
89
|
+
{{-- byte-identical. --}}
|
|
90
|
+
@foreach(tab in tabs)
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
:class="getTabClasses({{ $loop.index }})"
|
|
94
|
+
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 }})"
|
|
101
|
+
>
|
|
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
|
|
110
|
+
</button>
|
|
111
|
+
@endforeach
|
|
112
|
+
@else
|
|
113
|
+
{{-- Slot mode: render the tab list dynamically from --}}
|
|
114
|
+
{{-- discoveredTabs() which is populated in onMount. --}}
|
|
115
|
+
<button
|
|
116
|
+
:for="(tab, idx) in discoveredTabs()"
|
|
117
|
+
:key="idx"
|
|
118
|
+
type="button"
|
|
119
|
+
x-class="getTabClasses(idx)"
|
|
120
|
+
role="tab"
|
|
121
|
+
:aria-selected="isActive(idx) ? 'true' : 'false'"
|
|
122
|
+
:tabindex="isActive(idx) ? 0 : -1"
|
|
123
|
+
@click="selectTab(idx)"
|
|
124
|
+
@keydown="onTabKey($event, idx)"
|
|
125
|
+
>
|
|
126
|
+
<span :if="tab.icon" class="inline-flex items-center gap-2" x-html="tab.icon"></span>
|
|
127
|
+
<span :text="tab.label"></span>
|
|
128
|
+
</button>
|
|
129
|
+
@endif
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
@if(hasPropTabs)
|
|
133
|
+
@foreach(tab in tabs)
|
|
134
|
+
<div
|
|
135
|
+
:show="isActive({{ $loop.index }})"
|
|
136
|
+
id="tab-panel-{{ $loop.index }}"
|
|
137
|
+
class="{{ tabPanelClasses }}"
|
|
138
|
+
role="tabpanel"
|
|
139
|
+
aria-labelledby="tab-{{ $loop.index }}"
|
|
140
|
+
tabindex="0"
|
|
141
|
+
>
|
|
142
|
+
{!! tab.content !!}
|
|
143
|
+
</div>
|
|
144
|
+
@endforeach
|
|
145
|
+
@else
|
|
146
|
+
<slot />
|
|
147
|
+
@endif
|
|
148
|
+
</div>;
|
package/dist/ui/tabs/index.d.ts
CHANGED
|
@@ -1,14 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legacy prop API — pass `tabs` as an array of `{ label, content, icon? }`
|
|
3
|
+
* where `content` is a HTML string. Kept for backward compatibility; prefer
|
|
4
|
+
* the slot API below for any tab whose content needs to be a component tree.
|
|
5
|
+
*
|
|
6
|
+
* See stacksjs/stx#1703.
|
|
7
|
+
*/
|
|
1
8
|
export declare interface Tab {
|
|
2
9
|
label: string
|
|
3
10
|
content: string
|
|
4
11
|
icon?: string
|
|
5
12
|
}
|
|
6
13
|
export declare interface TabsProps {
|
|
7
|
-
tabs
|
|
14
|
+
tabs?: Tab[]
|
|
8
15
|
defaultTab?: number
|
|
9
16
|
orientation?: 'horizontal' | 'vertical'
|
|
10
17
|
variant?: 'line' | 'pills' | 'enclosed'
|
|
11
18
|
onChange?: (index: number, tab: Tab) => void
|
|
12
19
|
className?: string
|
|
13
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Slot API — wrap each tab's content in a `<TabPanel label="…">`. The parent
|
|
23
|
+
* `<Tabs>` discovers panels on mount via `[data-stx-tab-panel]` data
|
|
24
|
+
* attributes, builds the tab list dynamically, and toggles each panel's
|
|
25
|
+
* `hidden` attribute as the active tab changes.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```html
|
|
29
|
+
* <Tabs defaultTab="0" variant="pills">
|
|
30
|
+
* <TabPanel label="Drivers">
|
|
31
|
+
* <DriversTable :drivers="drivers()" />
|
|
32
|
+
* </TabPanel>
|
|
33
|
+
* <TabPanel label="Notifications" icon="bell">
|
|
34
|
+
* <NotificationsTable />
|
|
35
|
+
* </TabPanel>
|
|
36
|
+
* </Tabs>
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare interface TabPanelProps {
|
|
40
|
+
label: string
|
|
41
|
+
icon?: string
|
|
42
|
+
className?: string
|
|
43
|
+
}
|
|
44
|
+
export { default as TabPanel } from './TabPanel.stx';
|
|
14
45
|
export { default as Tabs } from './Tabs.stx';
|
|
@@ -15,4 +15,4 @@ export declare const resize: unknown;
|
|
|
15
15
|
export declare const className: unknown;
|
|
16
16
|
export declare const textareaClasses: unknown;
|
|
17
17
|
export declare const labelClasses: unknown;
|
|
18
|
-
export declare const helperClasses: () =>
|
|
18
|
+
export declare const helperClasses: (...args: any[]) => any;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/components",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.66",
|
|
5
5
|
"description": "A modern component library built with STX, featuring headwind utility classes and TypeScript syntax highlighting.",
|
|
6
6
|
"author": "Chris Breuer <chris@stacksjs.org>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@cwcss/crosswind": "^0.2.0",
|
|
61
|
-
"@stacksjs/stx": "0.2.
|
|
62
|
-
"bun-plugin-stx": "0.2.
|
|
61
|
+
"@stacksjs/stx": "0.2.66",
|
|
62
|
+
"bun-plugin-stx": "0.2.66",
|
|
63
63
|
"ts-syntax-highlighter": "^0.2.1"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
@@ -1,4 +1,11 @@
|
|
|
1
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.
|
|
2
9
|
export const items = $props.items || []
|
|
3
10
|
export const allowMultiple = $props.allowMultiple || false
|
|
4
11
|
export const defaultOpen = $props.defaultOpen || []
|
|
@@ -8,13 +15,56 @@ export const accordionClasses = `divide-y divide-gray-200 dark:divide-gray-700 b
|
|
|
8
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()
|
|
9
16
|
export const iconClasses = `w-5 h-5 text-gray-500 dark:text-gray-400 transition-transform duration-200`.trim()
|
|
10
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
|
|
11
19
|
</script>
|
|
12
20
|
|
|
13
21
|
<script client>
|
|
14
22
|
const emit = defineEmits()
|
|
15
23
|
const allowMultiple = {{ allowMultiple }}
|
|
24
|
+
const hasPropItems = {{ hasPropItems }}
|
|
25
|
+
const containerRef = useRef()
|
|
16
26
|
const openItems = state({{ defaultOpen }})
|
|
17
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
|
+
|
|
18
68
|
function toggleItem(index) {
|
|
19
69
|
const cur = openItems()
|
|
20
70
|
if (allowMultiple) {
|
|
@@ -31,7 +81,9 @@ function isOpen(index) {
|
|
|
31
81
|
}
|
|
32
82
|
|
|
33
83
|
function onHeaderKey(event, index) {
|
|
34
|
-
const
|
|
84
|
+
const root = containerRef.value
|
|
85
|
+
if (!root) return
|
|
86
|
+
const buttons = root.querySelectorAll('button[data-stx-accordion-header], button[data-accordion-header]')
|
|
35
87
|
if (event.key === 'ArrowDown') {
|
|
36
88
|
event.preventDefault()
|
|
37
89
|
buttons[index + 1]?.focus()
|
|
@@ -49,40 +101,46 @@ function onHeaderKey(event, index) {
|
|
|
49
101
|
buttons[buttons.length - 1]?.focus()
|
|
50
102
|
}
|
|
51
103
|
}
|
|
104
|
+
|
|
105
|
+
defineExpose({ openItems, toggleItem })
|
|
52
106
|
</script>
|
|
53
107
|
|
|
54
|
-
<div class="{{ accordionClasses }}" role="region">
|
|
55
|
-
@
|
|
56
|
-
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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"
|
|
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 }}"
|
|
73
120
|
>
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
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>
|
|
77
132
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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>
|
|
85
141
|
</div>
|
|
86
|
-
|
|
87
|
-
@
|
|
142
|
+
@endforeach
|
|
143
|
+
@else
|
|
144
|
+
<slot />
|
|
145
|
+
@endif
|
|
88
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>
|
|
@@ -1,14 +1,45 @@
|
|
|
1
1
|
export { default as Accordion } from './Accordion.stx'
|
|
2
|
+
export { default as AccordionItem } from './AccordionItem.stx'
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Legacy prop API — pass `items` as an array of `{ title, content }` where
|
|
6
|
+
* `content` is a HTML string. Kept for backward compatibility; prefer the
|
|
7
|
+
* slot API below for any accordion whose content needs to be a component tree.
|
|
8
|
+
*
|
|
9
|
+
* See stacksjs/stx#1703.
|
|
10
|
+
*/
|
|
3
11
|
export interface AccordionItem {
|
|
4
12
|
title: string
|
|
5
13
|
content: string
|
|
6
14
|
}
|
|
7
15
|
|
|
8
16
|
export interface AccordionProps {
|
|
9
|
-
|
|
17
|
+
/** Legacy: array of item definitions with string content. */
|
|
18
|
+
items?: AccordionItem[]
|
|
10
19
|
allowMultiple?: boolean
|
|
11
20
|
defaultOpen?: number[]
|
|
12
21
|
onChange?: (openItems: number[]) => void
|
|
13
22
|
className?: string
|
|
14
23
|
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Slot API — wrap each section's content in `<AccordionItem title="…">`.
|
|
27
|
+
* Each `<AccordionItem>` renders its own header + content panel; the parent
|
|
28
|
+
* `<Accordion>` attaches click handlers and toggles the content visibility.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```html
|
|
32
|
+
* <Accordion allowMultiple>
|
|
33
|
+
* <AccordionItem title="Profile">
|
|
34
|
+
* <ProfileEditor :user="user()" />
|
|
35
|
+
* </AccordionItem>
|
|
36
|
+
* <AccordionItem title="Zones">
|
|
37
|
+
* <ZonesEditor />
|
|
38
|
+
* </AccordionItem>
|
|
39
|
+
* </Accordion>
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export interface AccordionItemProps {
|
|
43
|
+
title: string
|
|
44
|
+
className?: string
|
|
45
|
+
}
|
|
@@ -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) {
|
package/src/ui/dialog/Dialog.stx
CHANGED
|
@@ -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)
|
package/src/ui/drawer/Drawer.stx
CHANGED
|
@@ -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)
|
|
@@ -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)
|
|
@@ -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)
|
|
@@ -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)
|