@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.
- package/README.md +192 -0
- package/dist/autocomplete/index.d.ts +89 -0
- package/dist/autocomplete/index.d.ts.map +1 -0
- package/dist/breadcrumbs/index.d.ts +38 -0
- package/dist/breadcrumbs/index.d.ts.map +1 -0
- package/dist/button/Button.d.ts.map +1 -1
- package/dist/calendar/DateField.d.ts +47 -0
- package/dist/calendar/DateField.d.ts.map +1 -0
- package/dist/calendar/DatePicker.d.ts +48 -0
- package/dist/calendar/DatePicker.d.ts.map +1 -0
- package/dist/calendar/RangeCalendar.d.ts +42 -0
- package/dist/calendar/RangeCalendar.d.ts.map +1 -0
- package/dist/calendar/TimeField.d.ts +44 -0
- package/dist/calendar/TimeField.d.ts.map +1 -0
- package/dist/calendar/index.d.ts +50 -0
- package/dist/calendar/index.d.ts.map +1 -0
- package/dist/checkbox/index.d.ts.map +1 -1
- package/dist/color/index.d.ts +228 -0
- package/dist/color/index.d.ts.map +1 -0
- package/dist/combobox/index.d.ts +81 -0
- package/dist/combobox/index.d.ts.map +1 -0
- package/dist/components.css +116 -14
- package/dist/custom/chip/index.d.ts +7 -2
- package/dist/custom/chip/index.d.ts.map +1 -1
- package/dist/custom/event-card/index.d.ts +5 -1
- package/dist/custom/event-card/index.d.ts.map +1 -1
- package/dist/custom/header/index.d.ts +16 -0
- package/dist/custom/header/index.d.ts.map +1 -0
- package/dist/custom/logo/index.d.ts +2 -0
- package/dist/custom/logo/index.d.ts.map +1 -1
- package/dist/custom/page-layout/index.d.ts +2 -0
- package/dist/custom/page-layout/index.d.ts.map +1 -1
- package/dist/custom/profile-card/index.d.ts +5 -1
- package/dist/custom/profile-card/index.d.ts.map +1 -1
- package/dist/custom/timeline-item/index.d.ts +12 -2
- package/dist/custom/timeline-item/index.d.ts.map +1 -1
- package/dist/dialog/Dialog.d.ts +67 -0
- package/dist/dialog/Dialog.d.ts.map +1 -0
- package/dist/dialog/index.d.ts +2 -17
- package/dist/dialog/index.d.ts.map +1 -1
- package/dist/disclosure/index.d.ts +84 -0
- package/dist/disclosure/index.d.ts.map +1 -0
- package/dist/gridlist/index.d.ts +92 -0
- package/dist/gridlist/index.d.ts.map +1 -0
- package/dist/index.d.ts +58 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6984 -783
- package/dist/index.js.map +1 -1
- package/dist/index.ssr.js +5905 -571
- package/dist/index.ssr.js.map +1 -1
- package/dist/landmark/index.d.ts +83 -0
- package/dist/landmark/index.d.ts.map +1 -0
- package/dist/link/index.d.ts.map +1 -1
- package/dist/listbox/index.d.ts +47 -0
- package/dist/listbox/index.d.ts.map +1 -0
- package/dist/menu/index.d.ts +74 -0
- package/dist/menu/index.d.ts.map +1 -0
- package/dist/meter/index.d.ts +49 -0
- package/dist/meter/index.d.ts.map +1 -0
- package/dist/numberfield/index.d.ts +50 -0
- package/dist/numberfield/index.d.ts.map +1 -0
- package/dist/popover/index.d.ts +85 -0
- package/dist/popover/index.d.ts.map +1 -0
- package/dist/radio/index.d.ts +7 -4
- package/dist/radio/index.d.ts.map +1 -1
- package/dist/searchfield/index.d.ts +44 -0
- package/dist/searchfield/index.d.ts.map +1 -0
- package/dist/select/index.d.ts +72 -0
- package/dist/select/index.d.ts.map +1 -0
- package/dist/slider/index.d.ts +53 -0
- package/dist/slider/index.d.ts.map +1 -0
- package/dist/switch/ToggleSwitch.d.ts.map +1 -1
- package/dist/table/index.d.ts +140 -0
- package/dist/table/index.d.ts.map +1 -0
- package/dist/tabs/index.d.ts +56 -0
- package/dist/tabs/index.d.ts.map +1 -0
- package/dist/tag-group/index.d.ts +80 -0
- package/dist/tag-group/index.d.ts.map +1 -0
- package/dist/toast/index.d.ts +101 -0
- package/dist/toast/index.d.ts.map +1 -0
- package/dist/toolbar/index.d.ts +42 -0
- package/dist/toolbar/index.d.ts.map +1 -0
- package/dist/tooltip/index.d.ts +66 -5
- package/dist/tooltip/index.d.ts.map +1 -1
- package/dist/tree/index.d.ts +99 -0
- package/dist/tree/index.d.ts.map +1 -0
- package/package.json +66 -58
- package/src/autocomplete/index.tsx +313 -0
- package/src/breadcrumbs/index.tsx +207 -0
- package/src/button/Button.tsx +74 -75
- 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 +3 -4
- package/src/color/index.tsx +687 -0
- package/src/combobox/index.tsx +383 -0
- package/src/components.css +116 -14
- package/src/custom/chip/index.tsx +17 -3
- package/src/custom/event-card/index.tsx +8 -2
- package/src/custom/header/index.tsx +33 -0
- package/src/custom/logo/index.tsx +7 -3
- package/src/custom/page-layout/index.tsx +12 -3
- package/src/custom/profile-card/index.tsx +8 -2
- package/src/custom/timeline-item/index.tsx +28 -4
- package/src/dialog/Dialog.tsx +260 -0
- package/src/dialog/index.tsx +3 -69
- package/src/disclosure/index.tsx +307 -0
- package/src/gridlist/index.tsx +403 -0
- package/src/index.ts +219 -4
- package/src/landmark/index.tsx +231 -0
- package/src/link/index.tsx +1 -2
- 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/radio/index.tsx +36 -82
- package/src/searchfield/index.tsx +453 -0
- package/src/select/index.tsx +349 -0
- package/src/slider/index.tsx +382 -0
- package/src/switch/ToggleSwitch.tsx +1 -2
- package/src/table/index.tsx +531 -0
- package/src/tabs/index.tsx +273 -0
- package/src/tag-group/index.tsx +240 -0
- package/src/toast/index.tsx +324 -0
- package/src/toolbar/index.tsx +108 -0
- package/src/tooltip/index.tsx +171 -5
- package/src/tree/index.tsx +494 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ComboBox component for proyecto-viviana-ui
|
|
3
|
+
*
|
|
4
|
+
* Styled combobox component built on top of solidaria-components.
|
|
5
|
+
* Inspired by Spectrum 2's ComboBox component patterns.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type JSX, splitProps, createContext, useContext, Show } from 'solid-js'
|
|
9
|
+
import {
|
|
10
|
+
ComboBox as HeadlessComboBox,
|
|
11
|
+
ComboBoxInput as HeadlessComboBoxInput,
|
|
12
|
+
ComboBoxButton as HeadlessComboBoxButton,
|
|
13
|
+
ComboBoxListBox as HeadlessComboBoxListBox,
|
|
14
|
+
ComboBoxOption as HeadlessComboBoxOption,
|
|
15
|
+
defaultContainsFilter,
|
|
16
|
+
type ComboBoxProps as HeadlessComboBoxProps,
|
|
17
|
+
type ComboBoxInputProps as HeadlessComboBoxInputProps,
|
|
18
|
+
type ComboBoxButtonProps as HeadlessComboBoxButtonProps,
|
|
19
|
+
type ComboBoxListBoxProps as HeadlessComboBoxListBoxProps,
|
|
20
|
+
type ComboBoxOptionProps as HeadlessComboBoxOptionProps,
|
|
21
|
+
type ComboBoxRenderProps,
|
|
22
|
+
type ComboBoxInputRenderProps,
|
|
23
|
+
type ComboBoxButtonRenderProps,
|
|
24
|
+
type ComboBoxListBoxRenderProps,
|
|
25
|
+
type ComboBoxOptionRenderProps,
|
|
26
|
+
} from '@proyecto-viviana/solidaria-components'
|
|
27
|
+
import type { Key, FilterFn, MenuTriggerAction } from '@proyecto-viviana/solid-stately'
|
|
28
|
+
|
|
29
|
+
// ============================================
|
|
30
|
+
// SIZE CONTEXT
|
|
31
|
+
// ============================================
|
|
32
|
+
|
|
33
|
+
export type ComboBoxSize = 'sm' | 'md' | 'lg'
|
|
34
|
+
|
|
35
|
+
const ComboBoxSizeContext = createContext<ComboBoxSize>('md')
|
|
36
|
+
|
|
37
|
+
// ============================================
|
|
38
|
+
// TYPES
|
|
39
|
+
// ============================================
|
|
40
|
+
|
|
41
|
+
export interface ComboBoxProps<T> extends Omit<HeadlessComboBoxProps<T>, 'class' | 'style'> {
|
|
42
|
+
/** The size of the combobox. */
|
|
43
|
+
size?: ComboBoxSize
|
|
44
|
+
/** Additional CSS class name. */
|
|
45
|
+
class?: string
|
|
46
|
+
/** Label for the combobox. */
|
|
47
|
+
label?: string
|
|
48
|
+
/** Description for the combobox. */
|
|
49
|
+
description?: string
|
|
50
|
+
/** Error message when invalid. */
|
|
51
|
+
errorMessage?: string
|
|
52
|
+
/** Whether the combobox is invalid. */
|
|
53
|
+
isInvalid?: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ComboBoxInputProps extends Omit<HeadlessComboBoxInputProps, 'class' | 'style'> {
|
|
57
|
+
/** Additional CSS class name. */
|
|
58
|
+
class?: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ComboBoxButtonProps extends Omit<HeadlessComboBoxButtonProps, 'class' | 'style'> {
|
|
62
|
+
/** Additional CSS class name. */
|
|
63
|
+
class?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ComboBoxListBoxProps<T> extends Omit<HeadlessComboBoxListBoxProps<T>, 'class' | 'style'> {
|
|
67
|
+
/** Additional CSS class name. */
|
|
68
|
+
class?: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ComboBoxOptionProps<T> extends Omit<HeadlessComboBoxOptionProps<T>, 'class' | 'style'> {
|
|
72
|
+
/** Additional CSS class name. */
|
|
73
|
+
class?: string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ============================================
|
|
77
|
+
// STYLES
|
|
78
|
+
// ============================================
|
|
79
|
+
|
|
80
|
+
const sizeStyles = {
|
|
81
|
+
sm: {
|
|
82
|
+
wrapper: 'h-8',
|
|
83
|
+
input: 'h-8 text-sm pl-3 pr-8',
|
|
84
|
+
button: 'h-8 w-8',
|
|
85
|
+
label: 'text-sm',
|
|
86
|
+
option: 'text-sm py-1.5 px-3',
|
|
87
|
+
icon: 'h-4 w-4',
|
|
88
|
+
},
|
|
89
|
+
md: {
|
|
90
|
+
wrapper: 'h-10',
|
|
91
|
+
input: 'h-10 text-base pl-4 pr-10',
|
|
92
|
+
button: 'h-10 w-10',
|
|
93
|
+
label: 'text-base',
|
|
94
|
+
option: 'text-base py-2 px-4',
|
|
95
|
+
icon: 'h-5 w-5',
|
|
96
|
+
},
|
|
97
|
+
lg: {
|
|
98
|
+
wrapper: 'h-12',
|
|
99
|
+
input: 'h-12 text-lg pl-5 pr-12',
|
|
100
|
+
button: 'h-12 w-12',
|
|
101
|
+
label: 'text-lg',
|
|
102
|
+
option: 'text-lg py-2.5 px-5',
|
|
103
|
+
icon: 'h-6 w-6',
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ============================================
|
|
108
|
+
// COMBOBOX COMPONENT
|
|
109
|
+
// ============================================
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* A combobox combines a text input with a listbox, allowing users to filter a list of options.
|
|
113
|
+
*
|
|
114
|
+
* Built on solidaria-components ComboBox for full accessibility support.
|
|
115
|
+
*/
|
|
116
|
+
export function ComboBox<T>(props: ComboBoxProps<T>): JSX.Element {
|
|
117
|
+
const [local, headlessProps] = splitProps(props, [
|
|
118
|
+
'size',
|
|
119
|
+
'class',
|
|
120
|
+
'label',
|
|
121
|
+
'description',
|
|
122
|
+
'errorMessage',
|
|
123
|
+
'isInvalid',
|
|
124
|
+
])
|
|
125
|
+
|
|
126
|
+
const size = local.size ?? 'md'
|
|
127
|
+
const customClass = local.class ?? ''
|
|
128
|
+
|
|
129
|
+
const getClassName = (renderProps: ComboBoxRenderProps): string => {
|
|
130
|
+
const base = 'relative inline-flex flex-col gap-1.5'
|
|
131
|
+
const disabledClass = renderProps.isDisabled ? 'opacity-50' : ''
|
|
132
|
+
return [base, disabledClass, customClass].filter(Boolean).join(' ')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<ComboBoxSizeContext.Provider value={size}>
|
|
137
|
+
<HeadlessComboBox
|
|
138
|
+
{...headlessProps}
|
|
139
|
+
class={getClassName}
|
|
140
|
+
>
|
|
141
|
+
<Show when={local.label}>
|
|
142
|
+
<label class={`text-primary-200 font-medium ${sizeStyles[size].label}`}>
|
|
143
|
+
{local.label}
|
|
144
|
+
</label>
|
|
145
|
+
</Show>
|
|
146
|
+
{props.children}
|
|
147
|
+
<Show when={local.description && !local.isInvalid}>
|
|
148
|
+
<span class="text-primary-400 text-sm">{local.description}</span>
|
|
149
|
+
</Show>
|
|
150
|
+
<Show when={local.errorMessage && local.isInvalid}>
|
|
151
|
+
<span class="text-danger-400 text-sm">{local.errorMessage}</span>
|
|
152
|
+
</Show>
|
|
153
|
+
</HeadlessComboBox>
|
|
154
|
+
</ComboBoxSizeContext.Provider>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ============================================
|
|
159
|
+
// COMBOBOX INPUT GROUP COMPONENT
|
|
160
|
+
// ============================================
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* A wrapper for the input and button that provides proper styling.
|
|
164
|
+
*/
|
|
165
|
+
export function ComboBoxInputGroup(props: { children: JSX.Element; class?: string }): JSX.Element {
|
|
166
|
+
const size = useContext(ComboBoxSizeContext)
|
|
167
|
+
const styles = () => sizeStyles[size]
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div class={`relative flex items-center ${styles().wrapper} ${props.class ?? ''}`}>
|
|
171
|
+
{props.children}
|
|
172
|
+
</div>
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ============================================
|
|
177
|
+
// COMBOBOX INPUT COMPONENT
|
|
178
|
+
// ============================================
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The text input for a combobox.
|
|
182
|
+
*/
|
|
183
|
+
export function ComboBoxInput(props: ComboBoxInputProps): JSX.Element {
|
|
184
|
+
const [local, headlessProps] = splitProps(props, ['class'])
|
|
185
|
+
const size = useContext(ComboBoxSizeContext)
|
|
186
|
+
const styles = () => sizeStyles[size]
|
|
187
|
+
const customClass = local.class ?? ''
|
|
188
|
+
|
|
189
|
+
const getClassName = (renderProps: ComboBoxInputRenderProps): string => {
|
|
190
|
+
const base = 'w-full rounded-lg border-2 transition-all duration-200 outline-none'
|
|
191
|
+
const sizeClass = styles().input
|
|
192
|
+
|
|
193
|
+
let colorClass: string
|
|
194
|
+
if (renderProps.isDisabled) {
|
|
195
|
+
colorClass = 'border-bg-300 bg-bg-200 text-primary-500 cursor-not-allowed'
|
|
196
|
+
} else if (renderProps.isOpen) {
|
|
197
|
+
colorClass = 'border-accent bg-bg-300 text-primary-100'
|
|
198
|
+
} else if (renderProps.isHovered) {
|
|
199
|
+
colorClass = 'border-accent-300 bg-bg-300 text-primary-100'
|
|
200
|
+
} else {
|
|
201
|
+
colorClass = 'border-primary-600 bg-bg-400 text-primary-200'
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const focusClass = renderProps.isFocusVisible
|
|
205
|
+
? 'ring-2 ring-accent-300 ring-offset-2 ring-offset-bg-400'
|
|
206
|
+
: ''
|
|
207
|
+
|
|
208
|
+
return [base, sizeClass, colorClass, focusClass, customClass].filter(Boolean).join(' ')
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<HeadlessComboBoxInput
|
|
213
|
+
{...headlessProps}
|
|
214
|
+
class={getClassName}
|
|
215
|
+
/>
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ============================================
|
|
220
|
+
// COMBOBOX BUTTON COMPONENT
|
|
221
|
+
// ============================================
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The trigger button for a combobox.
|
|
225
|
+
* SSR-compatible - renders children or chevron icon directly without render props.
|
|
226
|
+
*/
|
|
227
|
+
export function ComboBoxButton(props: ComboBoxButtonProps): JSX.Element {
|
|
228
|
+
const [local, headlessProps] = splitProps(props, ['class'])
|
|
229
|
+
const size = useContext(ComboBoxSizeContext)
|
|
230
|
+
const sizeStyle = sizeStyles[size]
|
|
231
|
+
const customClass = local.class ?? ''
|
|
232
|
+
|
|
233
|
+
const getClassName = (renderProps: ComboBoxButtonRenderProps): string => {
|
|
234
|
+
const base = 'absolute right-0 top-0 flex items-center justify-center transition-all duration-200 rounded-r-lg'
|
|
235
|
+
const sizeClass = sizeStyle.button
|
|
236
|
+
|
|
237
|
+
let colorClass: string
|
|
238
|
+
if (renderProps.isDisabled) {
|
|
239
|
+
colorClass = 'text-primary-500 cursor-not-allowed'
|
|
240
|
+
} else if (renderProps.isOpen) {
|
|
241
|
+
colorClass = 'text-accent'
|
|
242
|
+
} else if (renderProps.isHovered) {
|
|
243
|
+
colorClass = 'text-accent-300 cursor-pointer'
|
|
244
|
+
} else {
|
|
245
|
+
colorClass = 'text-primary-400 cursor-pointer hover:text-primary-200'
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return [base, sizeClass, colorClass, customClass].filter(Boolean).join(' ')
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<HeadlessComboBoxButton
|
|
253
|
+
{...headlessProps}
|
|
254
|
+
class={getClassName}
|
|
255
|
+
>
|
|
256
|
+
{props.children || <ChevronIcon class={`${sizeStyle.icon} transition-transform duration-200 data-open:rotate-180`} />}
|
|
257
|
+
</HeadlessComboBoxButton>
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ============================================
|
|
262
|
+
// COMBOBOX LISTBOX COMPONENT
|
|
263
|
+
// ============================================
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The listbox popup for a combobox.
|
|
267
|
+
*/
|
|
268
|
+
export function ComboBoxListBox<T>(props: ComboBoxListBoxProps<T>): JSX.Element {
|
|
269
|
+
const [local, headlessProps] = splitProps(props, ['class'])
|
|
270
|
+
const customClass = local.class ?? ''
|
|
271
|
+
|
|
272
|
+
const getClassName = (_renderProps: ComboBoxListBoxRenderProps): string => {
|
|
273
|
+
const base = 'absolute z-50 mt-1 w-full rounded-lg border-2 border-primary-600 bg-bg-400 py-1 shadow-lg max-h-60 overflow-auto'
|
|
274
|
+
return [base, customClass].filter(Boolean).join(' ')
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<HeadlessComboBoxListBox
|
|
279
|
+
{...headlessProps}
|
|
280
|
+
class={getClassName}
|
|
281
|
+
children={props.children}
|
|
282
|
+
/>
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ============================================
|
|
287
|
+
// COMBOBOX OPTION COMPONENT
|
|
288
|
+
// ============================================
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* An option in a combobox listbox.
|
|
292
|
+
* SSR-compatible - renders check icon and content directly without render props.
|
|
293
|
+
*/
|
|
294
|
+
export function ComboBoxOption<T>(props: ComboBoxOptionProps<T>): JSX.Element {
|
|
295
|
+
const [local, headlessProps] = splitProps(props, ['class'])
|
|
296
|
+
const size = useContext(ComboBoxSizeContext)
|
|
297
|
+
const sizeStyle = sizeStyles[size]
|
|
298
|
+
const customClass = local.class ?? ''
|
|
299
|
+
|
|
300
|
+
const getClassName = (renderProps: ComboBoxOptionRenderProps): string => {
|
|
301
|
+
const base = 'flex items-center gap-2 cursor-pointer transition-colors duration-150'
|
|
302
|
+
const sizeClass = sizeStyle.option
|
|
303
|
+
|
|
304
|
+
let colorClass: string
|
|
305
|
+
if (renderProps.isDisabled) {
|
|
306
|
+
colorClass = 'text-primary-500 cursor-not-allowed'
|
|
307
|
+
} else if (renderProps.isSelected) {
|
|
308
|
+
colorClass = 'bg-accent/20 text-accent'
|
|
309
|
+
} else if (renderProps.isFocused || renderProps.isHovered) {
|
|
310
|
+
colorClass = 'bg-bg-300 text-primary-100'
|
|
311
|
+
} else {
|
|
312
|
+
colorClass = 'text-primary-200'
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const focusClass = renderProps.isFocusVisible
|
|
316
|
+
? 'ring-2 ring-inset ring-accent-300'
|
|
317
|
+
: ''
|
|
318
|
+
|
|
319
|
+
return [base, sizeClass, colorClass, focusClass, customClass].filter(Boolean).join(' ')
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Compute padding for non-selected items to align with check icon
|
|
323
|
+
const iconPadding: Record<ComboBoxSize, string> = {
|
|
324
|
+
sm: 'pl-6', // h-4 icon + gap
|
|
325
|
+
md: 'pl-7', // h-5 icon + gap
|
|
326
|
+
lg: 'pl-8', // h-6 icon + gap
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return (
|
|
330
|
+
<HeadlessComboBoxOption
|
|
331
|
+
{...headlessProps}
|
|
332
|
+
class={getClassName}
|
|
333
|
+
>
|
|
334
|
+
<CheckIcon class={`${sizeStyle.icon} text-accent shrink-0 hidden data-selected:block`} />
|
|
335
|
+
<span class={`flex-1 data-selected:pl-0 ${iconPadding[size]}`}>
|
|
336
|
+
{props.children as JSX.Element}
|
|
337
|
+
</span>
|
|
338
|
+
</HeadlessComboBoxOption>
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ============================================
|
|
343
|
+
// ICONS
|
|
344
|
+
// ============================================
|
|
345
|
+
|
|
346
|
+
function ChevronIcon(props: { class?: string }): JSX.Element {
|
|
347
|
+
return (
|
|
348
|
+
<svg
|
|
349
|
+
class={props.class}
|
|
350
|
+
fill="none"
|
|
351
|
+
viewBox="0 0 24 24"
|
|
352
|
+
stroke="currentColor"
|
|
353
|
+
stroke-width="2"
|
|
354
|
+
>
|
|
355
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
|
|
356
|
+
</svg>
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function CheckIcon(props: { class?: string }): JSX.Element {
|
|
361
|
+
return (
|
|
362
|
+
<svg
|
|
363
|
+
class={props.class}
|
|
364
|
+
fill="none"
|
|
365
|
+
viewBox="0 0 24 24"
|
|
366
|
+
stroke="currentColor"
|
|
367
|
+
stroke-width="2"
|
|
368
|
+
>
|
|
369
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
|
370
|
+
</svg>
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Attach sub-components for convenience
|
|
375
|
+
ComboBox.InputGroup = ComboBoxInputGroup
|
|
376
|
+
ComboBox.Input = ComboBoxInput
|
|
377
|
+
ComboBox.Button = ComboBoxButton
|
|
378
|
+
ComboBox.ListBox = ComboBoxListBox
|
|
379
|
+
ComboBox.Option = ComboBoxOption
|
|
380
|
+
|
|
381
|
+
// Re-export types and utilities for convenience
|
|
382
|
+
export type { Key, FilterFn, MenuTriggerAction }
|
|
383
|
+
export { defaultContainsFilter }
|
package/src/components.css
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
|
|
40
40
|
/* ===== BUTTON COMPONENT ===== */
|
|
41
41
|
.vui-button {
|
|
42
|
+
position: relative;
|
|
42
43
|
font-family: 'Jost', sans-serif;
|
|
43
44
|
display: inline-flex;
|
|
44
45
|
align-items: center;
|
|
@@ -48,15 +49,37 @@
|
|
|
48
49
|
font-weight: 500;
|
|
49
50
|
text-transform: uppercase;
|
|
50
51
|
border-radius: 0.5rem;
|
|
51
|
-
transition: all 0.2s
|
|
52
|
+
transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
52
53
|
cursor: pointer;
|
|
53
54
|
border: none;
|
|
54
55
|
outline: none;
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Shimmer overlay for hover effect */
|
|
60
|
+
.vui-button::before {
|
|
61
|
+
content: '';
|
|
62
|
+
position: absolute;
|
|
63
|
+
inset: 0;
|
|
64
|
+
background: linear-gradient(
|
|
65
|
+
90deg,
|
|
66
|
+
transparent 0%,
|
|
67
|
+
rgba(255, 255, 255, 0.1) 50%,
|
|
68
|
+
transparent 100%
|
|
69
|
+
);
|
|
70
|
+
transform: translateX(-100%);
|
|
71
|
+
transition: transform 0.5s ease;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.vui-button:hover::before {
|
|
75
|
+
transform: translateX(100%);
|
|
55
76
|
}
|
|
56
77
|
|
|
57
78
|
.vui-button:focus-visible {
|
|
58
79
|
outline: none;
|
|
59
|
-
box-shadow: 0 0 0 2px var(--color-bg-400),
|
|
80
|
+
box-shadow: 0 0 0 2px var(--color-bg-400),
|
|
81
|
+
0 0 0 4px var(--color-accent-300),
|
|
82
|
+
0 0 20px rgba(223, 92, 154, 0.3);
|
|
60
83
|
}
|
|
61
84
|
|
|
62
85
|
.vui-button:disabled {
|
|
@@ -64,8 +87,12 @@
|
|
|
64
87
|
cursor: not-allowed;
|
|
65
88
|
}
|
|
66
89
|
|
|
90
|
+
.vui-button:disabled::before {
|
|
91
|
+
display: none;
|
|
92
|
+
}
|
|
93
|
+
|
|
67
94
|
.vui-button.is-pressed {
|
|
68
|
-
transform: scale(0.
|
|
95
|
+
transform: scale(0.97);
|
|
69
96
|
}
|
|
70
97
|
|
|
71
98
|
/* Size variants */
|
|
@@ -110,6 +137,11 @@
|
|
|
110
137
|
color: white;
|
|
111
138
|
}
|
|
112
139
|
|
|
140
|
+
.vui-button--fill.vui-button--accent:hover:not(:disabled) {
|
|
141
|
+
box-shadow: 0 0 20px rgba(223, 92, 154, 0.4),
|
|
142
|
+
0 4px 12px rgba(223, 92, 154, 0.3);
|
|
143
|
+
}
|
|
144
|
+
|
|
113
145
|
/* Positive = bg-success-600, text-success-100, border-success-400 */
|
|
114
146
|
.vui-button--fill.vui-button--positive {
|
|
115
147
|
background: var(--color-success-600);
|
|
@@ -441,22 +473,48 @@
|
|
|
441
473
|
|
|
442
474
|
/* ===== SIDEBAR ===== */
|
|
443
475
|
.sidebar-link {
|
|
476
|
+
position: relative;
|
|
444
477
|
display: block;
|
|
445
478
|
padding: 0.5rem 1rem;
|
|
446
|
-
border-radius: 0.
|
|
447
|
-
color: var(--color-primary-
|
|
448
|
-
|
|
479
|
+
border-radius: 0.5rem;
|
|
480
|
+
color: var(--color-primary-400);
|
|
481
|
+
font-size: 0.875rem;
|
|
482
|
+
transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.sidebar-link::before {
|
|
486
|
+
content: '';
|
|
487
|
+
position: absolute;
|
|
488
|
+
left: 0;
|
|
489
|
+
top: 50%;
|
|
490
|
+
transform: translateY(-50%);
|
|
491
|
+
width: 3px;
|
|
492
|
+
height: 0;
|
|
493
|
+
background: var(--color-accent);
|
|
494
|
+
border-radius: 0 2px 2px 0;
|
|
495
|
+
transition: height 0.2s ease;
|
|
449
496
|
}
|
|
450
497
|
|
|
451
498
|
.sidebar-link:hover {
|
|
452
|
-
background:
|
|
453
|
-
color: var(--color-primary-
|
|
499
|
+
background: rgba(117, 171, 199, 0.08);
|
|
500
|
+
color: var(--color-primary-200);
|
|
501
|
+
padding-left: 1.25rem;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.sidebar-link:hover::before {
|
|
505
|
+
height: 60%;
|
|
454
506
|
}
|
|
455
507
|
|
|
456
508
|
.sidebar-link.active {
|
|
457
|
-
background:
|
|
458
|
-
color: var(--color-
|
|
509
|
+
background: rgba(223, 92, 154, 0.1);
|
|
510
|
+
color: var(--color-accent-200);
|
|
459
511
|
font-weight: 500;
|
|
512
|
+
padding-left: 1.25rem;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.sidebar-link.active::before {
|
|
516
|
+
height: 70%;
|
|
517
|
+
box-shadow: 0 0 8px rgba(223, 92, 154, 0.5);
|
|
460
518
|
}
|
|
461
519
|
|
|
462
520
|
/* ===== LOGO COMPONENT ===== */
|
|
@@ -517,6 +575,46 @@
|
|
|
517
575
|
text-shadow: 6px 5px 0 var(--color-accent);
|
|
518
576
|
}
|
|
519
577
|
|
|
578
|
+
/* Inverted variant - first word gets the 3D effect, second word is muted */
|
|
579
|
+
.vui-logo.vui-logo--inverted .vui-logo__first {
|
|
580
|
+
position: relative;
|
|
581
|
+
color: var(--color-primary-500);
|
|
582
|
+
font-weight: 900;
|
|
583
|
+
-webkit-text-stroke: 2px rgba(255, 255, 255, 0.9);
|
|
584
|
+
paint-order: stroke fill;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
.vui-logo.vui-logo--inverted .vui-logo__first::before {
|
|
588
|
+
content: attr(data-text);
|
|
589
|
+
position: absolute;
|
|
590
|
+
left: 0;
|
|
591
|
+
top: 0;
|
|
592
|
+
z-index: -1;
|
|
593
|
+
color: transparent;
|
|
594
|
+
-webkit-text-stroke: 0;
|
|
595
|
+
text-shadow: 4px 3px 0 var(--color-accent);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.vui-logo.vui-logo--inverted .vui-logo__second {
|
|
599
|
+
position: static;
|
|
600
|
+
color: var(--color-primary-800);
|
|
601
|
+
font-weight: 300;
|
|
602
|
+
-webkit-text-stroke: 0;
|
|
603
|
+
paint-order: normal;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
.vui-logo.vui-logo--inverted .vui-logo__second::before {
|
|
607
|
+
content: none;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.vui-logo.vui-logo--inverted.vui-logo--lg .vui-logo__first::before {
|
|
611
|
+
text-shadow: 5px 4px 0 var(--color-accent);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.vui-logo.vui-logo--inverted.vui-logo--xl .vui-logo__first::before {
|
|
615
|
+
text-shadow: 6px 5px 0 var(--color-accent);
|
|
616
|
+
}
|
|
617
|
+
|
|
520
618
|
/* ===== HEADER COMPONENT ===== */
|
|
521
619
|
.vui-header {
|
|
522
620
|
position: fixed;
|
|
@@ -524,17 +622,16 @@
|
|
|
524
622
|
left: 0;
|
|
525
623
|
right: 0;
|
|
526
624
|
z-index: 50;
|
|
527
|
-
border-bottom:
|
|
625
|
+
border-bottom: 4px solid var(--color-accent);
|
|
528
626
|
background: color-mix(in srgb, var(--color-bg-400) 80%, transparent);
|
|
529
627
|
backdrop-filter: blur(8px);
|
|
530
628
|
-webkit-backdrop-filter: blur(8px);
|
|
531
629
|
}
|
|
532
630
|
|
|
533
631
|
.vui-header__container {
|
|
534
|
-
margin-left: auto;
|
|
535
|
-
margin-right: auto;
|
|
536
|
-
max-width: 72rem;
|
|
537
632
|
height: 70px;
|
|
633
|
+
padding-left: 32px;
|
|
634
|
+
padding-right: 32px;
|
|
538
635
|
display: flex;
|
|
539
636
|
align-items: center;
|
|
540
637
|
justify-content: space-between;
|
|
@@ -575,6 +672,11 @@
|
|
|
575
672
|
font-family: 'Sen', sans-serif;
|
|
576
673
|
}
|
|
577
674
|
|
|
675
|
+
/* Use this modifier for pages with fixed header where content shouldn't go behind it */
|
|
676
|
+
.vui-page--with-header {
|
|
677
|
+
padding-top: 4rem; /* 64px - matches h-16 header */
|
|
678
|
+
}
|
|
679
|
+
|
|
578
680
|
.vui-page h1,
|
|
579
681
|
.vui-page h2,
|
|
580
682
|
.vui-page h3,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type JSX, Show } from 'solid-js'
|
|
2
2
|
|
|
3
3
|
export type ChipVariant = 'primary' | 'secondary' | 'accent' | 'outline'
|
|
4
4
|
|
|
@@ -6,7 +6,12 @@ export interface ChipProps {
|
|
|
6
6
|
text: string
|
|
7
7
|
variant?: ChipVariant
|
|
8
8
|
onClick?: () => void
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Icon to display before the text.
|
|
11
|
+
* Use a function returning JSX for SSR compatibility: `icon={() => <MyIcon />}`
|
|
12
|
+
* Or pass a simple string for text-based icons: `icon="★"`
|
|
13
|
+
*/
|
|
14
|
+
icon?: string | (() => JSX.Element)
|
|
10
15
|
class?: string
|
|
11
16
|
}
|
|
12
17
|
|
|
@@ -20,12 +25,21 @@ const variantStyles: Record<ChipVariant, string> = {
|
|
|
20
25
|
export function Chip(props: ChipProps) {
|
|
21
26
|
const variant = () => props.variant ?? 'primary'
|
|
22
27
|
|
|
28
|
+
const renderIcon = () => {
|
|
29
|
+
const icon = props.icon
|
|
30
|
+
if (!icon) return null
|
|
31
|
+
if (typeof icon === 'string') return icon
|
|
32
|
+
return icon()
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
return (
|
|
24
36
|
<button
|
|
25
37
|
class={`flex justify-center items-center h-6 w-auto rounded-full px-4 py-1 font-medium text-sm tracking-wide transition-colors ${variantStyles[variant()]} ${props.class ?? ''}`}
|
|
26
38
|
onClick={props.onClick}
|
|
27
39
|
>
|
|
28
|
-
|
|
40
|
+
<Show when={props.icon}>
|
|
41
|
+
<span class="mr-1.5">{renderIcon()}</span>
|
|
42
|
+
</Show>
|
|
29
43
|
<span>{props.text}</span>
|
|
30
44
|
</button>
|
|
31
45
|
)
|
|
@@ -11,7 +11,11 @@ export interface EventCardProps {
|
|
|
11
11
|
attendees?: { avatar?: string; name: string }[]
|
|
12
12
|
attendeeCount?: number
|
|
13
13
|
decorationImage?: string
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Actions to display below the event.
|
|
16
|
+
* Use a function returning JSX for SSR compatibility: `actions={() => <Button>...</Button>}`
|
|
17
|
+
*/
|
|
18
|
+
actions?: JSX.Element | (() => JSX.Element)
|
|
15
19
|
class?: string
|
|
16
20
|
}
|
|
17
21
|
|
|
@@ -85,7 +89,9 @@ export function EventCard(props: EventCardProps) {
|
|
|
85
89
|
</Show>
|
|
86
90
|
|
|
87
91
|
<Show when={props.actions}>
|
|
88
|
-
<div class="mt-4 flex gap-2">
|
|
92
|
+
<div class="mt-4 flex gap-2">
|
|
93
|
+
{typeof props.actions === 'function' ? props.actions() : props.actions}
|
|
94
|
+
</div>
|
|
89
95
|
</Show>
|
|
90
96
|
</div>
|
|
91
97
|
</div>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { JSX } from 'solid-js'
|
|
2
|
+
import { Logo, type LogoProps } from '../logo'
|
|
3
|
+
|
|
4
|
+
export interface HeaderProps {
|
|
5
|
+
/** Image element to show to the left of the text logo */
|
|
6
|
+
logoImage?: JSX.Element
|
|
7
|
+
/** Props to pass to the Logo component (firstWord, secondWord, size, inverted). Pass null to hide the text logo. */
|
|
8
|
+
logoProps?: LogoProps | null
|
|
9
|
+
/** Custom logo element - replaces the default Logo component entirely */
|
|
10
|
+
logo?: JSX.Element
|
|
11
|
+
/** Navigation items to display on the right side */
|
|
12
|
+
children?: JSX.Element
|
|
13
|
+
/** Additional CSS classes */
|
|
14
|
+
class?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function Header(props: HeaderProps) {
|
|
18
|
+
const showTextLogo = () => props.logo !== undefined || props.logoProps !== null
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<header class={`vui-header ${props.class ?? ''}`}>
|
|
22
|
+
<div class="vui-header__container">
|
|
23
|
+
<div class="flex items-center gap-3">
|
|
24
|
+
{props.logoImage}
|
|
25
|
+
{showTextLogo() && (props.logo ?? <Logo size="lg" {...(props.logoProps ?? {})} />)}
|
|
26
|
+
</div>
|
|
27
|
+
<nav class="vui-header__nav">
|
|
28
|
+
{props.children}
|
|
29
|
+
</nav>
|
|
30
|
+
</div>
|
|
31
|
+
</header>
|
|
32
|
+
)
|
|
33
|
+
}
|