@proyecto-viviana/ui 0.1.7 → 0.2.0

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 (130) hide show
  1. package/README.md +192 -0
  2. package/dist/autocomplete/index.d.ts +89 -0
  3. package/dist/autocomplete/index.d.ts.map +1 -0
  4. package/dist/breadcrumbs/index.d.ts +38 -0
  5. package/dist/breadcrumbs/index.d.ts.map +1 -0
  6. package/dist/button/Button.d.ts.map +1 -1
  7. package/dist/calendar/DateField.d.ts +47 -0
  8. package/dist/calendar/DateField.d.ts.map +1 -0
  9. package/dist/calendar/DatePicker.d.ts +48 -0
  10. package/dist/calendar/DatePicker.d.ts.map +1 -0
  11. package/dist/calendar/RangeCalendar.d.ts +42 -0
  12. package/dist/calendar/RangeCalendar.d.ts.map +1 -0
  13. package/dist/calendar/TimeField.d.ts +44 -0
  14. package/dist/calendar/TimeField.d.ts.map +1 -0
  15. package/dist/calendar/index.d.ts +50 -0
  16. package/dist/calendar/index.d.ts.map +1 -0
  17. package/dist/checkbox/index.d.ts.map +1 -1
  18. package/dist/color/index.d.ts +228 -0
  19. package/dist/color/index.d.ts.map +1 -0
  20. package/dist/combobox/index.d.ts +81 -0
  21. package/dist/combobox/index.d.ts.map +1 -0
  22. package/dist/components.css +116 -14
  23. package/dist/custom/chip/index.d.ts +7 -2
  24. package/dist/custom/chip/index.d.ts.map +1 -1
  25. package/dist/custom/event-card/index.d.ts +5 -1
  26. package/dist/custom/event-card/index.d.ts.map +1 -1
  27. package/dist/custom/header/index.d.ts +16 -0
  28. package/dist/custom/header/index.d.ts.map +1 -0
  29. package/dist/custom/logo/index.d.ts +2 -0
  30. package/dist/custom/logo/index.d.ts.map +1 -1
  31. package/dist/custom/page-layout/index.d.ts +2 -0
  32. package/dist/custom/page-layout/index.d.ts.map +1 -1
  33. package/dist/custom/profile-card/index.d.ts +5 -1
  34. package/dist/custom/profile-card/index.d.ts.map +1 -1
  35. package/dist/custom/timeline-item/index.d.ts +12 -2
  36. package/dist/custom/timeline-item/index.d.ts.map +1 -1
  37. package/dist/dialog/Dialog.d.ts +67 -0
  38. package/dist/dialog/Dialog.d.ts.map +1 -0
  39. package/dist/dialog/index.d.ts +2 -17
  40. package/dist/dialog/index.d.ts.map +1 -1
  41. package/dist/disclosure/index.d.ts +84 -0
  42. package/dist/disclosure/index.d.ts.map +1 -0
  43. package/dist/gridlist/index.d.ts +92 -0
  44. package/dist/gridlist/index.d.ts.map +1 -0
  45. package/dist/index.d.ts +58 -4
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +6984 -783
  48. package/dist/index.js.map +1 -1
  49. package/dist/index.ssr.js +5905 -571
  50. package/dist/index.ssr.js.map +1 -1
  51. package/dist/landmark/index.d.ts +83 -0
  52. package/dist/landmark/index.d.ts.map +1 -0
  53. package/dist/link/index.d.ts.map +1 -1
  54. package/dist/listbox/index.d.ts +47 -0
  55. package/dist/listbox/index.d.ts.map +1 -0
  56. package/dist/menu/index.d.ts +74 -0
  57. package/dist/menu/index.d.ts.map +1 -0
  58. package/dist/meter/index.d.ts +49 -0
  59. package/dist/meter/index.d.ts.map +1 -0
  60. package/dist/numberfield/index.d.ts +50 -0
  61. package/dist/numberfield/index.d.ts.map +1 -0
  62. package/dist/popover/index.d.ts +85 -0
  63. package/dist/popover/index.d.ts.map +1 -0
  64. package/dist/radio/index.d.ts +7 -4
  65. package/dist/radio/index.d.ts.map +1 -1
  66. package/dist/searchfield/index.d.ts +44 -0
  67. package/dist/searchfield/index.d.ts.map +1 -0
  68. package/dist/select/index.d.ts +72 -0
  69. package/dist/select/index.d.ts.map +1 -0
  70. package/dist/slider/index.d.ts +53 -0
  71. package/dist/slider/index.d.ts.map +1 -0
  72. package/dist/switch/ToggleSwitch.d.ts.map +1 -1
  73. package/dist/table/index.d.ts +140 -0
  74. package/dist/table/index.d.ts.map +1 -0
  75. package/dist/tabs/index.d.ts +56 -0
  76. package/dist/tabs/index.d.ts.map +1 -0
  77. package/dist/tag-group/index.d.ts +80 -0
  78. package/dist/tag-group/index.d.ts.map +1 -0
  79. package/dist/toast/index.d.ts +101 -0
  80. package/dist/toast/index.d.ts.map +1 -0
  81. package/dist/toolbar/index.d.ts +42 -0
  82. package/dist/toolbar/index.d.ts.map +1 -0
  83. package/dist/tooltip/index.d.ts +66 -5
  84. package/dist/tooltip/index.d.ts.map +1 -1
  85. package/dist/tree/index.d.ts +99 -0
  86. package/dist/tree/index.d.ts.map +1 -0
  87. package/package.json +66 -58
  88. package/src/autocomplete/index.tsx +313 -0
  89. package/src/breadcrumbs/index.tsx +207 -0
  90. package/src/button/Button.tsx +74 -75
  91. package/src/calendar/DateField.tsx +200 -0
  92. package/src/calendar/DatePicker.tsx +298 -0
  93. package/src/calendar/RangeCalendar.tsx +236 -0
  94. package/src/calendar/TimeField.tsx +196 -0
  95. package/src/calendar/index.tsx +223 -0
  96. package/src/checkbox/index.tsx +3 -4
  97. package/src/color/index.tsx +687 -0
  98. package/src/combobox/index.tsx +383 -0
  99. package/src/components.css +116 -14
  100. package/src/custom/chip/index.tsx +17 -3
  101. package/src/custom/event-card/index.tsx +8 -2
  102. package/src/custom/header/index.tsx +33 -0
  103. package/src/custom/logo/index.tsx +7 -3
  104. package/src/custom/page-layout/index.tsx +12 -3
  105. package/src/custom/profile-card/index.tsx +8 -2
  106. package/src/custom/timeline-item/index.tsx +28 -4
  107. package/src/dialog/Dialog.tsx +260 -0
  108. package/src/dialog/index.tsx +3 -69
  109. package/src/disclosure/index.tsx +307 -0
  110. package/src/gridlist/index.tsx +403 -0
  111. package/src/index.ts +219 -4
  112. package/src/landmark/index.tsx +231 -0
  113. package/src/link/index.tsx +1 -2
  114. package/src/listbox/index.tsx +231 -0
  115. package/src/menu/index.tsx +297 -0
  116. package/src/meter/index.tsx +163 -0
  117. package/src/numberfield/index.tsx +482 -0
  118. package/src/popover/index.tsx +260 -0
  119. package/src/radio/index.tsx +36 -82
  120. package/src/searchfield/index.tsx +453 -0
  121. package/src/select/index.tsx +349 -0
  122. package/src/slider/index.tsx +382 -0
  123. package/src/switch/ToggleSwitch.tsx +1 -2
  124. package/src/table/index.tsx +531 -0
  125. package/src/tabs/index.tsx +273 -0
  126. package/src/tag-group/index.tsx +240 -0
  127. package/src/toast/index.tsx +324 -0
  128. package/src/toolbar/index.tsx +108 -0
  129. package/src/tooltip/index.tsx +171 -5
  130. package/src/tree/index.tsx +494 -0
@@ -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 };