@proyecto-viviana/ui 0.3.2 → 0.3.3
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/components.css +1077 -1077
- package/dist/index.js +236 -249
- package/dist/index.js.map +3 -3
- package/dist/index.ssr.js +78 -81
- package/dist/index.ssr.js.map +3 -3
- package/dist/radio/index.d.ts +12 -27
- package/dist/radio/index.d.ts.map +1 -1
- package/dist/test-utils/index.d.ts +2 -2
- package/dist/test-utils/index.d.ts.map +1 -1
- package/package.json +13 -12
- package/src/alert/index.tsx +48 -0
- package/src/assets/favicon.png +0 -0
- package/src/assets/fire.gif +0 -0
- package/src/autocomplete/index.tsx +313 -0
- package/src/avatar/index.tsx +75 -0
- package/src/badge/index.tsx +43 -0
- package/src/breadcrumbs/index.tsx +207 -0
- package/src/button/Button.tsx +74 -0
- package/src/button/index.ts +2 -0
- package/src/button/types.ts +24 -0
- package/src/calendar/DateField.tsx +200 -0
- package/src/calendar/DatePicker.tsx +298 -0
- package/src/calendar/RangeCalendar.tsx +236 -0
- package/src/calendar/TimeField.tsx +196 -0
- package/src/calendar/index.tsx +223 -0
- package/src/checkbox/index.tsx +257 -0
- package/src/color/index.tsx +687 -0
- package/src/combobox/index.tsx +383 -0
- package/src/components.css +1077 -0
- package/src/custom/calendar-card/index.tsx +66 -0
- package/src/custom/chip/index.tsx +46 -0
- package/src/custom/conversation/index.tsx +105 -0
- package/src/custom/event-card/index.tsx +132 -0
- package/src/custom/header/index.tsx +33 -0
- package/src/custom/lateral-nav/index.tsx +88 -0
- package/src/custom/logo/index.tsx +58 -0
- package/src/custom/nav-header/index.tsx +42 -0
- package/src/custom/page-layout/index.tsx +29 -0
- package/src/custom/profile-card/index.tsx +64 -0
- package/src/custom/project-card/index.tsx +59 -0
- package/src/custom/timeline-item/index.tsx +105 -0
- package/src/dialog/Dialog.tsx +260 -0
- package/src/dialog/index.tsx +3 -0
- package/src/disclosure/index.tsx +307 -0
- package/src/gridlist/index.tsx +403 -0
- package/src/icon/icons/GitHubIcon.tsx +20 -0
- package/src/icon/index.tsx +48 -0
- package/src/index.ts +322 -0
- package/src/landmark/index.tsx +231 -0
- package/src/link/index.tsx +130 -0
- package/src/listbox/index.tsx +231 -0
- package/src/menu/index.tsx +297 -0
- package/src/meter/index.tsx +163 -0
- package/src/numberfield/index.tsx +482 -0
- package/src/popover/index.tsx +260 -0
- package/src/progress-bar/index.tsx +169 -0
- package/src/radio/index.tsx +173 -0
- package/src/searchfield/index.tsx +453 -0
- package/src/select/index.tsx +349 -0
- package/src/separator/index.tsx +141 -0
- package/src/slider/index.tsx +382 -0
- package/src/styles.css +450 -0
- package/src/switch/ToggleSwitch.tsx +112 -0
- package/src/switch/index.tsx +90 -0
- package/src/table/index.tsx +531 -0
- package/src/tabs/index.tsx +273 -0
- package/src/tag-group/index.tsx +240 -0
- package/src/test-utils/index.ts +40 -0
- package/src/textfield/index.tsx +211 -0
- package/src/theme.css +101 -0
- package/src/toast/index.tsx +324 -0
- package/src/toolbar/index.tsx +108 -0
- package/src/tooltip/index.tsx +197 -0
- package/src/tree/index.tsx +494 -0
- package/dist/index.jsx +0 -6658
- package/dist/index.jsx.map +0 -7
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tabs component for proyecto-viviana-ui
|
|
3
|
+
*
|
|
4
|
+
* Styled tabs component built on top of solidaria-components.
|
|
5
|
+
* Inspired by Spectrum 2's Tabs component patterns.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type JSX, splitProps, createContext, useContext } from 'solid-js'
|
|
9
|
+
import {
|
|
10
|
+
Tabs as HeadlessTabs,
|
|
11
|
+
TabList as HeadlessTabList,
|
|
12
|
+
Tab as HeadlessTab,
|
|
13
|
+
TabPanel as HeadlessTabPanel,
|
|
14
|
+
type TabsProps as HeadlessTabsProps,
|
|
15
|
+
type TabListProps as HeadlessTabListProps,
|
|
16
|
+
type TabProps as HeadlessTabProps,
|
|
17
|
+
type TabPanelProps as HeadlessTabPanelProps,
|
|
18
|
+
type TabsRenderProps,
|
|
19
|
+
type TabListRenderProps,
|
|
20
|
+
type TabRenderProps,
|
|
21
|
+
type TabPanelRenderProps,
|
|
22
|
+
} from '@proyecto-viviana/solidaria-components'
|
|
23
|
+
import type { Key, TabOrientation } from '@proyecto-viviana/solid-stately'
|
|
24
|
+
|
|
25
|
+
// ============================================
|
|
26
|
+
// SIZE CONTEXT
|
|
27
|
+
// ============================================
|
|
28
|
+
|
|
29
|
+
export type TabsSize = 'sm' | 'md' | 'lg'
|
|
30
|
+
export type TabsVariant = 'underline' | 'pill' | 'boxed'
|
|
31
|
+
|
|
32
|
+
interface TabsContextValue {
|
|
33
|
+
size: TabsSize
|
|
34
|
+
variant: TabsVariant
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const TabsSizeContext = createContext<TabsContextValue>({ size: 'md', variant: 'underline' })
|
|
38
|
+
|
|
39
|
+
// ============================================
|
|
40
|
+
// TYPES
|
|
41
|
+
// ============================================
|
|
42
|
+
|
|
43
|
+
export interface TabsProps<T> extends Omit<HeadlessTabsProps<T>, 'class' | 'style'> {
|
|
44
|
+
/** The size of the tabs. */
|
|
45
|
+
size?: TabsSize
|
|
46
|
+
/** The visual variant of the tabs. */
|
|
47
|
+
variant?: TabsVariant
|
|
48
|
+
/** Additional CSS class name. */
|
|
49
|
+
class?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface TabListProps<T> extends Omit<HeadlessTabListProps<T>, 'class' | 'style'> {
|
|
53
|
+
/** Additional CSS class name. */
|
|
54
|
+
class?: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface TabProps extends Omit<HeadlessTabProps, 'class' | 'style'> {
|
|
58
|
+
/** Additional CSS class name. */
|
|
59
|
+
class?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface TabPanelProps extends Omit<HeadlessTabPanelProps, 'class' | 'style'> {
|
|
63
|
+
/** Additional CSS class name. */
|
|
64
|
+
class?: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================
|
|
68
|
+
// STYLES
|
|
69
|
+
// ============================================
|
|
70
|
+
|
|
71
|
+
const sizeStyles = {
|
|
72
|
+
sm: {
|
|
73
|
+
tab: 'text-sm px-3 py-1.5',
|
|
74
|
+
tabList: 'gap-1',
|
|
75
|
+
panel: 'text-sm p-3',
|
|
76
|
+
},
|
|
77
|
+
md: {
|
|
78
|
+
tab: 'text-base px-4 py-2',
|
|
79
|
+
tabList: 'gap-2',
|
|
80
|
+
panel: 'text-base p-4',
|
|
81
|
+
},
|
|
82
|
+
lg: {
|
|
83
|
+
tab: 'text-lg px-5 py-2.5',
|
|
84
|
+
tabList: 'gap-3',
|
|
85
|
+
panel: 'text-lg p-5',
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const variantStyles = {
|
|
90
|
+
underline: {
|
|
91
|
+
tabList: 'border-b-2 border-primary-600',
|
|
92
|
+
tab: {
|
|
93
|
+
base: 'relative border-b-2 -mb-0.5 transition-colors duration-200',
|
|
94
|
+
default: 'border-transparent text-primary-400 hover:text-primary-200 hover:border-primary-400',
|
|
95
|
+
selected: 'border-accent text-accent',
|
|
96
|
+
disabled: 'border-transparent text-primary-600 cursor-not-allowed',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
pill: {
|
|
100
|
+
tabList: 'bg-bg-300 rounded-lg p-1',
|
|
101
|
+
tab: {
|
|
102
|
+
base: 'rounded-md transition-all duration-200',
|
|
103
|
+
default: 'text-primary-400 hover:text-primary-200 hover:bg-bg-400',
|
|
104
|
+
selected: 'bg-accent text-primary-100 shadow-sm',
|
|
105
|
+
disabled: 'text-primary-600 cursor-not-allowed',
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
boxed: {
|
|
109
|
+
tabList: 'border-2 border-primary-600 rounded-lg overflow-hidden',
|
|
110
|
+
tab: {
|
|
111
|
+
base: 'border-r-2 border-primary-600 last:border-r-0 transition-colors duration-200',
|
|
112
|
+
default: 'text-primary-400 bg-bg-400 hover:text-primary-200 hover:bg-bg-300',
|
|
113
|
+
selected: 'bg-accent/20 text-accent',
|
|
114
|
+
disabled: 'text-primary-600 bg-bg-300 cursor-not-allowed',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ============================================
|
|
120
|
+
// TABS COMPONENT
|
|
121
|
+
// ============================================
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Tabs organize content into multiple sections and allow users to navigate between them.
|
|
125
|
+
*
|
|
126
|
+
* Built on solidaria-components Tabs for full accessibility support.
|
|
127
|
+
*/
|
|
128
|
+
export function Tabs<T>(props: TabsProps<T>): JSX.Element {
|
|
129
|
+
const [local, headlessProps] = splitProps(props, [
|
|
130
|
+
'size',
|
|
131
|
+
'variant',
|
|
132
|
+
'class',
|
|
133
|
+
])
|
|
134
|
+
|
|
135
|
+
const size = local.size ?? 'md'
|
|
136
|
+
const variant = local.variant ?? 'underline'
|
|
137
|
+
const customClass = local.class ?? ''
|
|
138
|
+
|
|
139
|
+
const getClassName = (renderProps: TabsRenderProps): string => {
|
|
140
|
+
const base = 'flex flex-col'
|
|
141
|
+
const orientationClass = renderProps.orientation === 'vertical' ? 'flex-row' : 'flex-col'
|
|
142
|
+
const disabledClass = renderProps.isDisabled ? 'opacity-50' : ''
|
|
143
|
+
return [base, orientationClass, disabledClass, customClass].filter(Boolean).join(' ')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<TabsSizeContext.Provider value={{ size, variant }}>
|
|
148
|
+
<HeadlessTabs
|
|
149
|
+
{...headlessProps}
|
|
150
|
+
class={getClassName}
|
|
151
|
+
children={props.children}
|
|
152
|
+
/>
|
|
153
|
+
</TabsSizeContext.Provider>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ============================================
|
|
158
|
+
// TAB LIST COMPONENT
|
|
159
|
+
// ============================================
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* A TabList contains Tab elements that represent the available tabs.
|
|
163
|
+
*/
|
|
164
|
+
export function TabList<T>(props: TabListProps<T>): JSX.Element {
|
|
165
|
+
const [local, headlessProps] = splitProps(props, ['class'])
|
|
166
|
+
const ctx = useContext(TabsSizeContext)
|
|
167
|
+
const customClass = local.class ?? ''
|
|
168
|
+
|
|
169
|
+
const getClassName = (renderProps: TabListRenderProps): string => {
|
|
170
|
+
const base = 'flex'
|
|
171
|
+
const orientationClass = renderProps.orientation === 'vertical' ? 'flex-col' : 'flex-row'
|
|
172
|
+
const sizeClass = sizeStyles[ctx.size].tabList
|
|
173
|
+
const variantClass = variantStyles[ctx.variant].tabList
|
|
174
|
+
|
|
175
|
+
const focusClass = renderProps.isFocusVisible
|
|
176
|
+
? 'ring-2 ring-accent-300 ring-offset-2 ring-offset-bg-400'
|
|
177
|
+
: ''
|
|
178
|
+
|
|
179
|
+
return [base, orientationClass, sizeClass, variantClass, focusClass, customClass].filter(Boolean).join(' ')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<HeadlessTabList
|
|
184
|
+
{...headlessProps}
|
|
185
|
+
class={getClassName}
|
|
186
|
+
children={props.children}
|
|
187
|
+
/>
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ============================================
|
|
192
|
+
// TAB COMPONENT
|
|
193
|
+
// ============================================
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* A Tab represents an individual tab in a TabList.
|
|
197
|
+
*/
|
|
198
|
+
export function Tab(props: TabProps): JSX.Element {
|
|
199
|
+
const [local, headlessProps] = splitProps(props, ['class'])
|
|
200
|
+
const ctx = useContext(TabsSizeContext)
|
|
201
|
+
const customClass = local.class ?? ''
|
|
202
|
+
|
|
203
|
+
const getClassName = (renderProps: TabRenderProps): string => {
|
|
204
|
+
const sizeClass = sizeStyles[ctx.size].tab
|
|
205
|
+
const variantBase = variantStyles[ctx.variant].tab.base
|
|
206
|
+
|
|
207
|
+
let stateClass: string
|
|
208
|
+
if (renderProps.isDisabled) {
|
|
209
|
+
stateClass = variantStyles[ctx.variant].tab.disabled
|
|
210
|
+
} else if (renderProps.isSelected) {
|
|
211
|
+
stateClass = variantStyles[ctx.variant].tab.selected
|
|
212
|
+
} else {
|
|
213
|
+
stateClass = variantStyles[ctx.variant].tab.default
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const focusClass = renderProps.isFocusVisible
|
|
217
|
+
? 'ring-2 ring-accent-300 ring-offset-1 ring-offset-bg-400 outline-none'
|
|
218
|
+
: ''
|
|
219
|
+
|
|
220
|
+
const pressedClass = renderProps.isPressed ? 'scale-95' : ''
|
|
221
|
+
const cursorClass = renderProps.isDisabled ? '' : 'cursor-pointer'
|
|
222
|
+
|
|
223
|
+
return [variantBase, sizeClass, stateClass, focusClass, pressedClass, cursorClass, customClass].filter(Boolean).join(' ')
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<HeadlessTab
|
|
228
|
+
{...headlessProps}
|
|
229
|
+
class={getClassName}
|
|
230
|
+
children={props.children}
|
|
231
|
+
/>
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ============================================
|
|
236
|
+
// TAB PANEL COMPONENT
|
|
237
|
+
// ============================================
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* A TabPanel displays the content for a selected Tab.
|
|
241
|
+
*/
|
|
242
|
+
export function TabPanel(props: TabPanelProps): JSX.Element {
|
|
243
|
+
const [local, headlessProps] = splitProps(props, ['class'])
|
|
244
|
+
const ctx = useContext(TabsSizeContext)
|
|
245
|
+
const customClass = local.class ?? ''
|
|
246
|
+
|
|
247
|
+
const getClassName = (renderProps: TabPanelRenderProps): string => {
|
|
248
|
+
const base = 'outline-none'
|
|
249
|
+
const sizeClass = sizeStyles[ctx.size].panel
|
|
250
|
+
|
|
251
|
+
const focusClass = renderProps.isFocusVisible
|
|
252
|
+
? 'ring-2 ring-accent-300 ring-offset-2 ring-offset-bg-400'
|
|
253
|
+
: ''
|
|
254
|
+
|
|
255
|
+
return [base, sizeClass, focusClass, customClass].filter(Boolean).join(' ')
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<HeadlessTabPanel
|
|
260
|
+
{...headlessProps}
|
|
261
|
+
class={getClassName}
|
|
262
|
+
children={props.children}
|
|
263
|
+
/>
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Attach sub-components for convenience
|
|
268
|
+
Tabs.List = TabList
|
|
269
|
+
Tabs.Tab = Tab
|
|
270
|
+
Tabs.Panel = TabPanel
|
|
271
|
+
|
|
272
|
+
// Re-export types for convenience
|
|
273
|
+
export type { Key, TabOrientation }
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TagGroup component for proyecto-viviana-ui
|
|
3
|
+
*
|
|
4
|
+
* Styled tag group component built on top of solidaria-components.
|
|
5
|
+
* A tag group displays a collection of tags that can be selected and/or removed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type JSX, splitProps, Show } from 'solid-js';
|
|
9
|
+
import {
|
|
10
|
+
TagList as HeadlessTagList,
|
|
11
|
+
Tag as HeadlessTag,
|
|
12
|
+
} from '@proyecto-viviana/solidaria-components';
|
|
13
|
+
import type { Key, SelectionMode } from '@proyecto-viviana/solid-stately';
|
|
14
|
+
|
|
15
|
+
// ============================================
|
|
16
|
+
// TYPES
|
|
17
|
+
// ============================================
|
|
18
|
+
|
|
19
|
+
export type TagGroupSize = 'sm' | 'md' | 'lg';
|
|
20
|
+
export type TagGroupVariant = 'default' | 'outline' | 'solid';
|
|
21
|
+
|
|
22
|
+
export interface TagGroupProps<T> {
|
|
23
|
+
/** The label for the tag group. */
|
|
24
|
+
label?: string;
|
|
25
|
+
/** The items to display as tags. */
|
|
26
|
+
items: T[];
|
|
27
|
+
/** Function to render the content of each tag. */
|
|
28
|
+
children: (item: T) => JSX.Element;
|
|
29
|
+
/** Function to get a unique key from an item. */
|
|
30
|
+
getKey?: (item: T) => Key;
|
|
31
|
+
/** Handler called when tags are removed. */
|
|
32
|
+
onRemove?: (keys: Set<Key>) => void;
|
|
33
|
+
/** The size of the tags. @default 'md' */
|
|
34
|
+
size?: TagGroupSize;
|
|
35
|
+
/** The visual variant of the tags. @default 'default' */
|
|
36
|
+
variant?: TagGroupVariant;
|
|
37
|
+
/** The selection mode. @default 'none' */
|
|
38
|
+
selectionMode?: SelectionMode;
|
|
39
|
+
/** The currently selected keys (controlled). */
|
|
40
|
+
selectedKeys?: Iterable<Key>;
|
|
41
|
+
/** Handler called when selection changes. */
|
|
42
|
+
onSelectionChange?: (keys: 'all' | Set<Key>) => void;
|
|
43
|
+
/** Keys that are disabled. */
|
|
44
|
+
disabledKeys?: Iterable<Key>;
|
|
45
|
+
/** Additional CSS class name. */
|
|
46
|
+
class?: string;
|
|
47
|
+
/** Content to render when empty. */
|
|
48
|
+
renderEmptyState?: () => JSX.Element;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface TagProps {
|
|
52
|
+
/** A unique key for this tag. */
|
|
53
|
+
id: Key;
|
|
54
|
+
/** The content of the tag. */
|
|
55
|
+
children: JSX.Element;
|
|
56
|
+
/** Whether the tag is disabled. */
|
|
57
|
+
isDisabled?: boolean;
|
|
58
|
+
/** Additional CSS class name. */
|
|
59
|
+
class?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ============================================
|
|
63
|
+
// STYLES
|
|
64
|
+
// ============================================
|
|
65
|
+
|
|
66
|
+
const sizeStyles = {
|
|
67
|
+
sm: {
|
|
68
|
+
tag: 'text-xs px-2 py-0.5 gap-1',
|
|
69
|
+
removeButton: 'w-3 h-3',
|
|
70
|
+
label: 'text-xs',
|
|
71
|
+
},
|
|
72
|
+
md: {
|
|
73
|
+
tag: 'text-sm px-2.5 py-1 gap-1.5',
|
|
74
|
+
removeButton: 'w-4 h-4',
|
|
75
|
+
label: 'text-sm',
|
|
76
|
+
},
|
|
77
|
+
lg: {
|
|
78
|
+
tag: 'text-base px-3 py-1.5 gap-2',
|
|
79
|
+
removeButton: 'w-5 h-5',
|
|
80
|
+
label: 'text-base',
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const variantStyles = {
|
|
85
|
+
default: {
|
|
86
|
+
tag: 'bg-bg-400 text-primary-200 hover:bg-bg-300',
|
|
87
|
+
selected: 'bg-accent text-white',
|
|
88
|
+
disabled: 'opacity-50 cursor-not-allowed',
|
|
89
|
+
},
|
|
90
|
+
outline: {
|
|
91
|
+
tag: 'border border-primary-600 text-primary-200 hover:border-primary-500 hover:bg-bg-400/50',
|
|
92
|
+
selected: 'border-accent bg-accent/10 text-accent',
|
|
93
|
+
disabled: 'opacity-50 cursor-not-allowed',
|
|
94
|
+
},
|
|
95
|
+
solid: {
|
|
96
|
+
tag: 'bg-primary-600 text-primary-100 hover:bg-primary-500',
|
|
97
|
+
selected: 'bg-accent text-white',
|
|
98
|
+
disabled: 'opacity-50 cursor-not-allowed',
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// ============================================
|
|
103
|
+
// TAG GROUP COMPONENT
|
|
104
|
+
// ============================================
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A tag group displays a collection of tags that can be selected and/or removed.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```tsx
|
|
111
|
+
* // Simple tag group
|
|
112
|
+
* <TagGroup
|
|
113
|
+
* label="Categories"
|
|
114
|
+
* items={categories}
|
|
115
|
+
* onRemove={(keys) => removeCategories(keys)}
|
|
116
|
+
* >
|
|
117
|
+
* {(item) => item.name}
|
|
118
|
+
* </TagGroup>
|
|
119
|
+
*
|
|
120
|
+
* // With selection
|
|
121
|
+
* <TagGroup
|
|
122
|
+
* label="Filters"
|
|
123
|
+
* items={filters}
|
|
124
|
+
* selectionMode="multiple"
|
|
125
|
+
* selectedKeys={selectedFilters}
|
|
126
|
+
* onSelectionChange={setSelectedFilters}
|
|
127
|
+
* >
|
|
128
|
+
* {(item) => item.label}
|
|
129
|
+
* </TagGroup>
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export function TagGroup<T extends { id?: Key; key?: Key }>(props: TagGroupProps<T>): JSX.Element {
|
|
133
|
+
const [local] = splitProps(props, [
|
|
134
|
+
'label',
|
|
135
|
+
'items',
|
|
136
|
+
'getKey',
|
|
137
|
+
'onRemove',
|
|
138
|
+
'size',
|
|
139
|
+
'variant',
|
|
140
|
+
'selectionMode',
|
|
141
|
+
'selectedKeys',
|
|
142
|
+
'onSelectionChange',
|
|
143
|
+
'disabledKeys',
|
|
144
|
+
'class',
|
|
145
|
+
'renderEmptyState',
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
const size = () => local.size ?? 'md';
|
|
149
|
+
const variant = () => local.variant ?? 'default';
|
|
150
|
+
const sizeConfig = () => sizeStyles[size()];
|
|
151
|
+
const variantConfig = () => variantStyles[variant()];
|
|
152
|
+
|
|
153
|
+
// Default getKey function
|
|
154
|
+
const getKey = (item: T): Key => {
|
|
155
|
+
if (local.getKey) return local.getKey(item);
|
|
156
|
+
if (item.id !== undefined) return item.id;
|
|
157
|
+
if (item.key !== undefined) return item.key;
|
|
158
|
+
return String(item);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div class={`flex flex-col gap-2 ${local.class ?? ''}`}>
|
|
163
|
+
<Show when={local.label}>
|
|
164
|
+
<span class={`font-medium text-primary-200 ${sizeConfig().label}`}>
|
|
165
|
+
{local.label}
|
|
166
|
+
</span>
|
|
167
|
+
</Show>
|
|
168
|
+
<HeadlessTagList
|
|
169
|
+
items={local.items}
|
|
170
|
+
getKey={getKey}
|
|
171
|
+
onRemove={local.onRemove}
|
|
172
|
+
selectionMode={local.selectionMode}
|
|
173
|
+
selectedKeys={local.selectedKeys}
|
|
174
|
+
onSelectionChange={local.onSelectionChange}
|
|
175
|
+
disabledKeys={local.disabledKeys}
|
|
176
|
+
class="flex flex-wrap gap-2"
|
|
177
|
+
renderEmptyState={local.renderEmptyState ?? (() => (
|
|
178
|
+
<span class="text-primary-400 text-sm italic">No items</span>
|
|
179
|
+
))}
|
|
180
|
+
>
|
|
181
|
+
{(item) => (
|
|
182
|
+
<HeadlessTag
|
|
183
|
+
id={getKey(item)}
|
|
184
|
+
class={({ isSelected, isDisabled }) => {
|
|
185
|
+
const base = `
|
|
186
|
+
inline-flex items-center rounded-full
|
|
187
|
+
transition-colors duration-150 cursor-pointer
|
|
188
|
+
focus:outline-none focus:ring-2 focus:ring-accent/50
|
|
189
|
+
${sizeConfig().tag}
|
|
190
|
+
`;
|
|
191
|
+
const variantClass = isSelected
|
|
192
|
+
? variantConfig().selected
|
|
193
|
+
: variantConfig().tag;
|
|
194
|
+
const disabledClass = isDisabled ? variantConfig().disabled : '';
|
|
195
|
+
return `${base} ${variantClass} ${disabledClass}`.trim();
|
|
196
|
+
}}
|
|
197
|
+
>
|
|
198
|
+
{(renderProps) => (
|
|
199
|
+
<>
|
|
200
|
+
<span>{props.children(item)}</span>
|
|
201
|
+
<Show when={renderProps.allowsRemoving}>
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
class={`
|
|
205
|
+
${sizeConfig().removeButton}
|
|
206
|
+
rounded-full flex items-center justify-center
|
|
207
|
+
hover:bg-black/20 transition-colors
|
|
208
|
+
focus:outline-none
|
|
209
|
+
`}
|
|
210
|
+
onClick={(e) => {
|
|
211
|
+
e.stopPropagation();
|
|
212
|
+
local.onRemove?.(new Set([getKey(item)]));
|
|
213
|
+
}}
|
|
214
|
+
aria-label="Remove"
|
|
215
|
+
>
|
|
216
|
+
<svg
|
|
217
|
+
viewBox="0 0 24 24"
|
|
218
|
+
fill="none"
|
|
219
|
+
stroke="currentColor"
|
|
220
|
+
stroke-width="2"
|
|
221
|
+
stroke-linecap="round"
|
|
222
|
+
stroke-linejoin="round"
|
|
223
|
+
class="w-full h-full"
|
|
224
|
+
>
|
|
225
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
226
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
227
|
+
</svg>
|
|
228
|
+
</button>
|
|
229
|
+
</Show>
|
|
230
|
+
</>
|
|
231
|
+
)}
|
|
232
|
+
</HeadlessTag>
|
|
233
|
+
)}
|
|
234
|
+
</HeadlessTagList>
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Re-export types
|
|
240
|
+
export type { Key, SelectionMode };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import userEvent, { type UserEvent, PointerEventsCheckLevel } from '@testing-library/user-event';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pointer map matching react-spectrum's test setup.
|
|
5
|
+
* Ensures pointer events have realistic dimensions so they aren't mistaken for virtual clicks.
|
|
6
|
+
*/
|
|
7
|
+
export const pointerMap = [
|
|
8
|
+
{ name: 'MouseLeft', pointerType: 'mouse', button: 'primary', height: 1, width: 1, pressure: 0.5 },
|
|
9
|
+
{ name: 'MouseRight', pointerType: 'mouse', button: 'secondary' },
|
|
10
|
+
{ name: 'MouseMiddle', pointerType: 'mouse', button: 'auxiliary' },
|
|
11
|
+
{ name: 'TouchA', pointerType: 'touch', height: 1, width: 1 },
|
|
12
|
+
{ name: 'TouchB', pointerType: 'touch' },
|
|
13
|
+
{ name: 'TouchC', pointerType: 'touch' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
// Type for userEvent v14+ setup function
|
|
17
|
+
type UserEventSetup = (options?: {
|
|
18
|
+
delay?: number | null;
|
|
19
|
+
pointerMap?: readonly unknown[];
|
|
20
|
+
pointerEventsCheck?: PointerEventsCheckLevel;
|
|
21
|
+
}) => UserEvent;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Set up userEvent with react-spectrum's configuration.
|
|
25
|
+
* - delay: null - No artificial delays between actions
|
|
26
|
+
* - pointerMap - Realistic pointer event dimensions
|
|
27
|
+
* - pointerEventsCheck: Never - Skip pointer-events CSS check (jsdom doesn't handle this well)
|
|
28
|
+
*/
|
|
29
|
+
export function setupUser(): UserEvent {
|
|
30
|
+
// userEvent.setup exists in v14+ but types may not expose it correctly
|
|
31
|
+
const setup = (userEvent as unknown as { setup: UserEventSetup }).setup;
|
|
32
|
+
return setup({
|
|
33
|
+
delay: null,
|
|
34
|
+
pointerMap: pointerMap,
|
|
35
|
+
pointerEventsCheck: PointerEventsCheckLevel.Never,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { render, screen, fireEvent, cleanup } from '@solidjs/testing-library';
|
|
40
|
+
export { userEvent };
|