@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,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 }
@@ -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 ease;
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), 0 0 0 4px var(--color-accent-300);
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.98);
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.375rem;
447
- color: var(--color-primary-300);
448
- transition: all 0.15s;
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: var(--color-bg-300);
453
- color: var(--color-primary-100);
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: var(--color-primary-700);
458
- color: var(--color-primary-100);
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: 3px solid var(--color-accent);
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 { JSX } from 'solid-js'
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
- icon?: JSX.Element
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
- {props.icon && <span class="mr-1.5">{props.icon}</span>}
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
- actions?: JSX.Element
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">{props.actions}</div>
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
+ }