@stacksjs/components 0.2.54 → 0.2.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Accordion-pt8j917s.stx +146 -0
- package/dist/AccordionItem-hbhzx63p.stx +37 -0
- package/dist/{Checkbox-zgdmy3p5.stx → Checkbox-aqmq4cqv.stx} +3 -1
- package/dist/{Dialog-hczpexs4.stx → Dialog-y5rz2vrq.stx} +5 -1
- package/dist/{Drawer-xhevp5cw.stx → Drawer-hrz7qcyp.stx} +4 -1
- package/dist/Notification-03vfh3k6.stx +117 -0
- package/dist/{NumberInput-drnvtqej.stx → NumberInput-g41gmd2c.stx} +2 -1
- package/dist/{Pagination-n3d31cns.stx → Pagination-wtgxmzey.stx} +3 -1
- package/dist/{PasswordInput-n02x6053.stx → PasswordInput-4rsq1gwb.stx} +2 -1
- package/dist/{Progress-yxcntr4x.stx → Progress-9dhakbh4.stx} +2 -1
- package/dist/{Radio-cgjv2hb2.stx → Radio-djtb5dpc.stx} +2 -1
- package/dist/{Select-28y0pg83.stx → Select-1vnd0brz.stx} +2 -1
- package/dist/{Sidebar-73vc78as.stx → Sidebar-0jzasrmt.stx} +116 -22
- package/dist/{SidebarHeader-7v174a9c.stx → SidebarHeader-ywpmx14d.stx} +80 -3
- package/dist/{SidebarItem-3y3e8g2t.stx → SidebarItem-h53sgqzz.stx} +8 -8
- package/dist/{SidebarSection-z5ey6awe.stx → SidebarSection-g195cb4f.stx} +4 -3
- package/dist/{Switch-r5t5w04w.stx → Switch-p1bcr9ks.stx} +3 -1
- package/dist/TabPanel-5htevsq6.stx +25 -0
- package/dist/Tabs-79brys99.stx +202 -0
- package/dist/{TextInput-kf084763.stx → TextInput-g0vaz0pe.stx} +4 -1
- package/dist/{Textarea-xg4rr2xz.stx → Textarea-96kawv5c.stx} +2 -1
- package/dist/{Tooltip-m0tqd72e.stx → Tooltip-y61h04gp.stx} +2 -1
- package/dist/index.js +3909 -15
- package/dist/ui/accordion/Accordion.stx +9 -1
- package/dist/ui/accordion/AccordionItem.stx +7 -0
- package/dist/ui/accordion/index.d.ts +30 -1
- package/dist/ui/input/NumberInput.stx +1 -1
- package/dist/ui/input/TextInput.stx +1 -1
- package/dist/ui/notification/Notification.stx +10 -8
- package/dist/ui/select/Select.stx +1 -1
- package/dist/ui/sidebar/Sidebar.stx +9 -0
- package/dist/ui/sidebar/SidebarHeader.stx +7 -0
- package/dist/ui/sidebar/SidebarItem.stx +1 -1
- package/dist/ui/sidebar/index.d.ts +14 -0
- package/dist/ui/tabs/TabPanel.stx +9 -0
- package/dist/ui/tabs/Tabs.stx +140 -0
- package/dist/ui/tabs/index.d.ts +32 -1
- package/dist/ui/textarea/Textarea.stx +1 -1
- package/package.json +3 -3
- package/src/ui/accordion/Accordion.stx +90 -32
- package/src/ui/accordion/AccordionItem.stx +37 -0
- package/src/ui/accordion/index.ts +32 -1
- package/src/ui/checkbox/Checkbox.stx +3 -1
- package/src/ui/dialog/Dialog.stx +5 -1
- package/src/ui/drawer/Drawer.stx +4 -1
- package/src/ui/input/NumberInput.stx +2 -1
- package/src/ui/input/PasswordInput.stx +2 -1
- package/src/ui/input/TextInput.stx +4 -1
- package/src/ui/notification/Notification.stx +65 -26
- package/src/ui/pagination/Pagination.stx +3 -1
- package/src/ui/progress/Progress.stx +2 -1
- package/src/ui/radio/Radio.stx +2 -1
- package/src/ui/select/Select.stx +2 -1
- package/src/ui/sidebar/Sidebar.stx +116 -22
- package/src/ui/sidebar/SidebarHeader.stx +80 -3
- package/src/ui/sidebar/SidebarItem.stx +8 -8
- package/src/ui/sidebar/SidebarSection.stx +4 -3
- package/src/ui/sidebar/index.ts +30 -0
- package/src/ui/switch/Switch.stx +3 -1
- package/src/ui/tabs/TabPanel.stx +25 -0
- package/src/ui/tabs/Tabs.stx +108 -32
- package/src/ui/tabs/index.ts +35 -1
- package/src/ui/textarea/Textarea.stx +2 -1
- package/src/ui/tooltip/Tooltip.stx +2 -1
- package/src/utils/accessibility.ts +1 -1
- package/dist/Accordion-19x8q4rj.stx +0 -88
- package/dist/Notification-tb9b525s.stx +0 -78
- package/dist/Tabs-bzvvn4ke.stx +0 -126
|
@@ -11,20 +11,20 @@ export const variant = $props.variant || 'tahoe'
|
|
|
11
11
|
|
|
12
12
|
const itemVariants = {
|
|
13
13
|
workspace: {
|
|
14
|
-
base: 'flex w-full items-center gap-3 rounded-[
|
|
14
|
+
base: 'flex w-full items-center gap-3 rounded-[16px] px-3.5 py-2 text-[14px] leading-tight transition-colors duration-150',
|
|
15
15
|
indent: '',
|
|
16
16
|
active: 'bg-[#e7e7e5] text-zinc-950',
|
|
17
17
|
inactive: 'text-zinc-700 hover:bg-[#ececea] hover:text-zinc-950 dark:text-zinc-300 dark:hover:bg-white/10',
|
|
18
18
|
icon: active ? 'text-zinc-900 dark:text-zinc-100' : 'text-zinc-500 dark:text-zinc-500',
|
|
19
|
-
badge: 'ml-auto rounded-full bg-white/70 px-
|
|
19
|
+
badge: 'ml-auto grid h-5 min-w-5 place-items-center rounded-full bg-white/70 px-1.5 text-[11px] font-medium text-zinc-500 shadow-sm dark:bg-white/10 dark:text-zinc-400',
|
|
20
20
|
},
|
|
21
21
|
desktop: {
|
|
22
|
-
base: 'flex w-full items-center gap-3 rounded-[
|
|
22
|
+
base: 'flex w-full items-center gap-3 rounded-[14px] px-3 py-1.5 text-[13.5px] leading-tight transition-colors duration-150',
|
|
23
23
|
indent: '',
|
|
24
|
-
active: 'bg-
|
|
25
|
-
inactive: 'text-zinc-700 hover:bg-white/
|
|
26
|
-
icon: active ? 'text-zinc-900 dark:text-white' : 'text-zinc-
|
|
27
|
-
badge: 'ml-auto rounded-full bg-white/
|
|
24
|
+
active: 'bg-[#e8e8e6]/90 text-zinc-950 dark:bg-white/12 dark:text-white',
|
|
25
|
+
inactive: 'text-zinc-700 hover:bg-white/38 hover:text-zinc-950 dark:text-zinc-300 dark:hover:bg-white/10',
|
|
26
|
+
icon: active ? 'text-zinc-900 dark:text-white' : 'text-zinc-600 dark:text-zinc-400',
|
|
27
|
+
badge: 'ml-auto grid h-5 min-w-5 place-items-center rounded-full bg-white/72 px-1.5 text-[11px] font-medium text-zinc-500 shadow-sm dark:bg-white/10 dark:text-zinc-400',
|
|
28
28
|
},
|
|
29
29
|
default: {
|
|
30
30
|
base: 'flex items-center gap-2.5 px-2.5 py-1.5 rounded-md text-[13px] transition-all duration-150',
|
|
@@ -41,7 +41,7 @@ const baseItemList = [itemStyle.base, itemStyle.indent]
|
|
|
41
41
|
const stateClasses = disabled ? 'opacity-50 cursor-not-allowed' : active ? itemStyle.active : itemStyle.inactive
|
|
42
42
|
|
|
43
43
|
export const itemClasses = [...baseItemList, stateClasses].filter(Boolean).join(' ')
|
|
44
|
-
export const iconClasses = `${icon}
|
|
44
|
+
export const iconClasses = `${icon} h-[18px] w-[18px] flex-shrink-0 ${itemStyle.icon}`
|
|
45
45
|
export const badgeClasses = itemStyle.badge
|
|
46
46
|
</script>
|
|
47
47
|
|
|
@@ -14,7 +14,7 @@ const headerVariants = {
|
|
|
14
14
|
hover: 'hover:text-zinc-600 dark:hover:text-zinc-300 cursor-pointer',
|
|
15
15
|
},
|
|
16
16
|
desktop: {
|
|
17
|
-
base: 'w-full flex items-center gap-2.5 px-3
|
|
17
|
+
base: 'w-full flex items-center gap-2.5 px-3 pb-1.5 pt-3 rounded-[14px] text-left transition-colors duration-150 text-[12px] font-semibold uppercase tracking-[0.10em] text-zinc-500/80 dark:text-zinc-400/80',
|
|
18
18
|
hover: 'hover:bg-white/25 dark:hover:bg-white/10 cursor-pointer',
|
|
19
19
|
},
|
|
20
20
|
default: {
|
|
@@ -26,7 +26,7 @@ const headerVariants = {
|
|
|
26
26
|
const headerStyle = headerVariants[variant] || headerVariants.default
|
|
27
27
|
const headerClasses = [headerStyle.base, collapsible ? headerStyle.hover : ''].filter(Boolean).join(' ')
|
|
28
28
|
export const headerClassesValue = headerClasses
|
|
29
|
-
export const sectionClasses = variant === 'workspace'
|
|
29
|
+
export const sectionClasses = variant === 'workspace' ? 'py-1.5' : variant === 'desktop' ? 'py-1' : 'py-1'
|
|
30
30
|
export const groupClasses = variant === 'workspace' || variant === 'desktop' ? 'space-y-1 overflow-hidden' : 'mt-1 space-y-0.5 overflow-hidden'
|
|
31
31
|
export const labelClasses = variant === 'workspace' || variant === 'desktop'
|
|
32
32
|
? 'flex-1 truncate'
|
|
@@ -37,7 +37,8 @@ export const labelClasses = variant === 'workspace' || variant === 'desktop'
|
|
|
37
37
|
const emit = defineEmits()
|
|
38
38
|
const sectionId = {{ id }}
|
|
39
39
|
const collapsibleProp = {{ collapsible }}
|
|
40
|
-
|
|
40
|
+
// Reactive prop binding — parent can drive `expanded` via signals. See #1704.
|
|
41
|
+
const isExpanded = useReactiveProp('expanded', {{ expanded }})
|
|
41
42
|
|
|
42
43
|
function onHeaderClick() {
|
|
43
44
|
if (!collapsibleProp) return
|
package/src/ui/sidebar/index.ts
CHANGED
|
@@ -35,6 +35,8 @@ export interface SidebarProps {
|
|
|
35
35
|
width?: number
|
|
36
36
|
/** Minimum width when collapsed in pixels */
|
|
37
37
|
minWidth?: number
|
|
38
|
+
/** Collapse behavior: keep a compact rail or hide the sidebar entirely */
|
|
39
|
+
collapseMode?: 'rail' | 'hidden'
|
|
38
40
|
/** Visual style variant */
|
|
39
41
|
variant?: SidebarVariant
|
|
40
42
|
/** Layout placement for app shells and native-like sidebars */
|
|
@@ -43,6 +45,22 @@ export interface SidebarProps {
|
|
|
43
45
|
position?: 'left' | 'right'
|
|
44
46
|
/** Whether to show border */
|
|
45
47
|
bordered?: boolean
|
|
48
|
+
/** Light-mode tint opacity over sidebar material. Higher values reduce vibrancy. */
|
|
49
|
+
materialOpacity?: number
|
|
50
|
+
/** Dark-mode tint opacity over sidebar material. Higher values reduce vibrancy. */
|
|
51
|
+
materialDarkOpacity?: number
|
|
52
|
+
/** Force native/sidebar material appearance instead of following system mode */
|
|
53
|
+
materialScheme?: 'system' | 'light' | 'dark'
|
|
54
|
+
/** localStorage key used to persist collapse state */
|
|
55
|
+
persistKey?: string
|
|
56
|
+
/** Optional app shell selector whose sidebar width CSS variable should be synced */
|
|
57
|
+
shellSelector?: string
|
|
58
|
+
/** CSS variable written on shellSelector when the sidebar collapses or expands */
|
|
59
|
+
widthVar?: string
|
|
60
|
+
/** Optional class toggled on the document element while collapsed */
|
|
61
|
+
collapsedClass?: string
|
|
62
|
+
/** Whether hidden desktop collapse should notify Craft's web sidebar material */
|
|
63
|
+
nativeMaterialCollapse?: boolean
|
|
46
64
|
/** Additional CSS classes */
|
|
47
65
|
className?: string
|
|
48
66
|
/** Callback when collapse state changes */
|
|
@@ -84,6 +102,12 @@ export interface SidebarHeaderProps {
|
|
|
84
102
|
subtitle?: string
|
|
85
103
|
logo?: string
|
|
86
104
|
logoIcon?: string
|
|
105
|
+
/**
|
|
106
|
+
* Show macOS-style window controls. `auto` only renders them when Craft
|
|
107
|
+
* exposes a custom-window-controls marker, avoiding duplicate native chrome.
|
|
108
|
+
*/
|
|
109
|
+
showWindowControls?: boolean | 'auto'
|
|
110
|
+
showNavigationControls?: boolean
|
|
87
111
|
showSearch?: boolean
|
|
88
112
|
searchPlaceholder?: string
|
|
89
113
|
searchValue?: string
|
|
@@ -129,6 +153,12 @@ export interface NativeSidebarConfig {
|
|
|
129
153
|
backgroundEffect?: SidebarBackgroundEffect
|
|
130
154
|
/** Whether the native sidebar should let the window background show through */
|
|
131
155
|
allowsVibrancy?: boolean
|
|
156
|
+
/** Light-mode tint opacity over sidebar material */
|
|
157
|
+
materialOpacity?: number
|
|
158
|
+
/** Dark-mode tint opacity over sidebar material */
|
|
159
|
+
materialDarkOpacity?: number
|
|
160
|
+
/** Force native/sidebar material appearance instead of following system mode */
|
|
161
|
+
materialScheme?: 'system' | 'light' | 'dark'
|
|
132
162
|
/** Header configuration */
|
|
133
163
|
header?: {
|
|
134
164
|
title?: string
|
package/src/ui/switch/Switch.stx
CHANGED
|
@@ -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>
|
package/src/ui/tabs/Tabs.stx
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
@
|
|
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
|
-
|
|
173
|
+
x-class="getTabClasses(idx)"
|
|
94
174
|
role="tab"
|
|
95
|
-
:aria-selected="isActive(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
@
|
|
183
|
+
@endif
|
|
112
184
|
</div>
|
|
113
185
|
|
|
114
|
-
@
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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>
|
package/src/ui/tabs/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
38
|
+
// Reactive prop binding — parent can drive `value` via signals. See #1704.
|
|
39
|
+
const inputValue = useReactiveProp('value', {{ value }}, { parse: (v) => v == null ? '' : String(v) })
|
|
39
40
|
|
|
40
41
|
function resizeTextarea(el) {
|
|
41
42
|
if (!autoResize) return
|
|
@@ -27,7 +27,8 @@ export const arrowClass = `absolute w-0 h-0 border-4 border-transparent ${arrowC
|
|
|
27
27
|
<script client>
|
|
28
28
|
const delay = {{ delay }}
|
|
29
29
|
const isDisabled = {{ disabled }}
|
|
30
|
-
|
|
30
|
+
// Reactive prop binding — parent can drive `show` via signals. See #1704.
|
|
31
|
+
const isVisible = useReactiveProp('show', {{ show }})
|
|
31
32
|
let timeoutId = null
|
|
32
33
|
|
|
33
34
|
function showTooltip() {
|
|
@@ -265,7 +265,7 @@ export function announceToScreenReader(message: string, priority: AriaLive = 'po
|
|
|
265
265
|
|
|
266
266
|
// Clean up after announcement
|
|
267
267
|
setTimeout(() => {
|
|
268
|
-
|
|
268
|
+
announcement.parentNode?.removeChild(announcement)
|
|
269
269
|
}, 1000)
|
|
270
270
|
}
|
|
271
271
|
|
|
@@ -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>
|