@proyecto-viviana/ui 0.3.2 → 0.3.4

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 (76) hide show
  1. package/dist/components.css +1077 -1077
  2. package/dist/index.js +236 -249
  3. package/dist/index.js.map +3 -3
  4. package/dist/index.ssr.js +78 -81
  5. package/dist/index.ssr.js.map +3 -3
  6. package/dist/radio/index.d.ts +12 -27
  7. package/dist/radio/index.d.ts.map +1 -1
  8. package/dist/test-utils/index.d.ts +2 -2
  9. package/dist/test-utils/index.d.ts.map +1 -1
  10. package/package.json +13 -12
  11. package/src/alert/index.tsx +48 -0
  12. package/src/assets/favicon.png +0 -0
  13. package/src/assets/fire.gif +0 -0
  14. package/src/autocomplete/index.tsx +313 -0
  15. package/src/avatar/index.tsx +75 -0
  16. package/src/badge/index.tsx +43 -0
  17. package/src/breadcrumbs/index.tsx +207 -0
  18. package/src/button/Button.tsx +74 -0
  19. package/src/button/index.ts +2 -0
  20. package/src/button/types.ts +24 -0
  21. package/src/calendar/DateField.tsx +200 -0
  22. package/src/calendar/DatePicker.tsx +298 -0
  23. package/src/calendar/RangeCalendar.tsx +236 -0
  24. package/src/calendar/TimeField.tsx +196 -0
  25. package/src/calendar/index.tsx +223 -0
  26. package/src/checkbox/index.tsx +257 -0
  27. package/src/color/index.tsx +687 -0
  28. package/src/combobox/index.tsx +383 -0
  29. package/src/components.css +1077 -0
  30. package/src/custom/calendar-card/index.tsx +66 -0
  31. package/src/custom/chip/index.tsx +46 -0
  32. package/src/custom/conversation/index.tsx +105 -0
  33. package/src/custom/event-card/index.tsx +132 -0
  34. package/src/custom/header/index.tsx +33 -0
  35. package/src/custom/lateral-nav/index.tsx +88 -0
  36. package/src/custom/logo/index.tsx +58 -0
  37. package/src/custom/nav-header/index.tsx +42 -0
  38. package/src/custom/page-layout/index.tsx +29 -0
  39. package/src/custom/profile-card/index.tsx +64 -0
  40. package/src/custom/project-card/index.tsx +59 -0
  41. package/src/custom/timeline-item/index.tsx +105 -0
  42. package/src/dialog/Dialog.tsx +260 -0
  43. package/src/dialog/index.tsx +3 -0
  44. package/src/disclosure/index.tsx +307 -0
  45. package/src/gridlist/index.tsx +403 -0
  46. package/src/icon/icons/GitHubIcon.tsx +20 -0
  47. package/src/icon/index.tsx +48 -0
  48. package/src/index.ts +322 -0
  49. package/src/landmark/index.tsx +231 -0
  50. package/src/link/index.tsx +130 -0
  51. package/src/listbox/index.tsx +231 -0
  52. package/src/menu/index.tsx +297 -0
  53. package/src/meter/index.tsx +163 -0
  54. package/src/numberfield/index.tsx +482 -0
  55. package/src/popover/index.tsx +260 -0
  56. package/src/progress-bar/index.tsx +169 -0
  57. package/src/radio/index.tsx +173 -0
  58. package/src/searchfield/index.tsx +453 -0
  59. package/src/select/index.tsx +349 -0
  60. package/src/separator/index.tsx +141 -0
  61. package/src/slider/index.tsx +382 -0
  62. package/src/styles.css +450 -0
  63. package/src/switch/ToggleSwitch.tsx +112 -0
  64. package/src/switch/index.tsx +90 -0
  65. package/src/table/index.tsx +531 -0
  66. package/src/tabs/index.tsx +273 -0
  67. package/src/tag-group/index.tsx +240 -0
  68. package/src/test-utils/index.ts +40 -0
  69. package/src/textfield/index.tsx +211 -0
  70. package/src/theme.css +101 -0
  71. package/src/toast/index.tsx +324 -0
  72. package/src/toolbar/index.tsx +108 -0
  73. package/src/tooltip/index.tsx +197 -0
  74. package/src/tree/index.tsx +494 -0
  75. package/dist/index.jsx +0 -6658
  76. package/dist/index.jsx.map +0 -7
@@ -0,0 +1,494 @@
1
+ /**
2
+ * Tree component for proyecto-viviana-ui
3
+ *
4
+ * Styled tree component built on top of solidaria-components.
5
+ * Inspired by Spectrum 2's Tree component patterns.
6
+ *
7
+ * Tree displays hierarchical data with expandable/collapsible nodes,
8
+ * supporting keyboard navigation and selection.
9
+ */
10
+
11
+ import { type JSX, splitProps, createContext, useContext, Show } from 'solid-js'
12
+ import {
13
+ Tree as HeadlessTree,
14
+ TreeItem as HeadlessTreeItem,
15
+ TreeExpandButton as HeadlessTreeExpandButton,
16
+ TreeSelectionCheckbox as HeadlessTreeSelectionCheckbox,
17
+ type TreeProps as HeadlessTreeProps,
18
+ type TreeItemProps as HeadlessTreeItemProps,
19
+ type TreeExpandButtonProps as HeadlessTreeExpandButtonProps,
20
+ type TreeRenderProps,
21
+ type TreeItemRenderProps,
22
+ type TreeRenderItemState,
23
+ } from '@proyecto-viviana/solidaria-components'
24
+ import type { Key, TreeItemData } from '@proyecto-viviana/solid-stately'
25
+
26
+ // ============================================
27
+ // SIZE CONTEXT
28
+ // ============================================
29
+
30
+ export type TreeSize = 'sm' | 'md' | 'lg'
31
+ export type TreeVariant = 'default' | 'bordered' | 'quiet'
32
+
33
+ interface TreeContextValue {
34
+ size: TreeSize
35
+ variant: TreeVariant
36
+ }
37
+
38
+ const TreeSizeContext = createContext<TreeContextValue>({ size: 'md', variant: 'default' })
39
+
40
+ // ============================================
41
+ // TYPES
42
+ // ============================================
43
+
44
+ export interface TreeProps<T extends object>
45
+ extends Omit<HeadlessTreeProps<T>, 'class' | 'style'> {
46
+ /** The size of the tree. */
47
+ size?: TreeSize
48
+ /** The visual variant of the tree. */
49
+ variant?: TreeVariant
50
+ /** Additional CSS class name. */
51
+ class?: string
52
+ /** Label for the tree. */
53
+ label?: string
54
+ /** Description for the tree. */
55
+ description?: string
56
+ }
57
+
58
+ export interface TreeItemProps<T extends object>
59
+ extends Omit<HeadlessTreeItemProps<T>, 'class' | 'style'> {
60
+ /** Additional CSS class name. */
61
+ class?: string
62
+ /** Optional description text. */
63
+ description?: string
64
+ /**
65
+ * Optional icon to display before the content.
66
+ * Use a function returning JSX for SSR compatibility: `icon={() => <FolderIcon />}`
67
+ */
68
+ icon?: () => JSX.Element
69
+ }
70
+
71
+ export interface TreeExpandButtonProps extends Omit<HeadlessTreeExpandButtonProps, 'class' | 'style'> {
72
+ /** Additional CSS class name. */
73
+ class?: string
74
+ }
75
+
76
+ // ============================================
77
+ // STYLES
78
+ // ============================================
79
+
80
+ const sizeStyles = {
81
+ sm: {
82
+ tree: 'text-sm',
83
+ item: 'py-1 px-2 gap-1',
84
+ indent: 16,
85
+ icon: 'h-4 w-4',
86
+ expandButton: 'h-4 w-4',
87
+ label: 'text-sm',
88
+ description: 'text-xs',
89
+ checkbox: 'w-4 h-4',
90
+ },
91
+ md: {
92
+ tree: 'text-base',
93
+ item: 'py-1.5 px-3 gap-2',
94
+ indent: 20,
95
+ icon: 'h-5 w-5',
96
+ expandButton: 'h-5 w-5',
97
+ label: 'text-base',
98
+ description: 'text-sm',
99
+ checkbox: 'w-5 h-5',
100
+ },
101
+ lg: {
102
+ tree: 'text-lg',
103
+ item: 'py-2 px-4 gap-2',
104
+ indent: 24,
105
+ icon: 'h-6 w-6',
106
+ expandButton: 'h-6 w-6',
107
+ label: 'text-lg',
108
+ description: 'text-base',
109
+ checkbox: 'w-6 h-6',
110
+ },
111
+ }
112
+
113
+ const variantStyles = {
114
+ default: {
115
+ tree: 'bg-bg-400 rounded-lg border border-bg-300 p-2',
116
+ item: 'rounded-md',
117
+ itemHover: 'hover:bg-bg-200/50',
118
+ itemSelected: 'bg-accent/10 text-accent',
119
+ },
120
+ bordered: {
121
+ tree: 'bg-bg-400 rounded-lg border-2 border-bg-400 p-2',
122
+ item: 'border-b border-bg-300/30 last:border-b-0',
123
+ itemHover: 'hover:bg-bg-200/50',
124
+ itemSelected: 'bg-accent/10 text-accent',
125
+ },
126
+ quiet: {
127
+ tree: 'bg-transparent',
128
+ item: 'rounded-md',
129
+ itemHover: 'hover:bg-bg-300/50',
130
+ itemSelected: 'bg-accent/10 text-accent',
131
+ },
132
+ }
133
+
134
+ // ============================================
135
+ // TREE COMPONENT
136
+ // ============================================
137
+
138
+ /**
139
+ * A tree displays hierarchical data with expandable/collapsible nodes,
140
+ * supporting keyboard navigation and selection.
141
+ *
142
+ * Built on solidaria-components Tree for full accessibility support.
143
+ *
144
+ * @example
145
+ * ```tsx
146
+ * const items = [
147
+ * {
148
+ * key: 'documents',
149
+ * value: { name: 'Documents' },
150
+ * children: [
151
+ * { key: 'doc1', value: { name: 'Report.pdf' } },
152
+ * { key: 'doc2', value: { name: 'Notes.txt' } },
153
+ * ],
154
+ * },
155
+ * {
156
+ * key: 'images',
157
+ * value: { name: 'Images' },
158
+ * children: [
159
+ * { key: 'img1', value: { name: 'Photo.jpg' } },
160
+ * ],
161
+ * },
162
+ * ]
163
+ *
164
+ * <Tree items={items} defaultExpandedKeys={['documents']}>
165
+ * {(item, state) => (
166
+ * <TreeItem id={item.key} icon={() => <FolderIcon />}>
167
+ * {item.value.name}
168
+ * </TreeItem>
169
+ * )}
170
+ * </Tree>
171
+ * ```
172
+ */
173
+ export function Tree<T extends object>(props: TreeProps<T>): JSX.Element {
174
+ const [local, headlessProps] = splitProps(props, [
175
+ 'size',
176
+ 'variant',
177
+ 'class',
178
+ 'label',
179
+ 'description',
180
+ ])
181
+
182
+ const size = () => local.size ?? 'md'
183
+ const variant = () => local.variant ?? 'default'
184
+ const styles = () => sizeStyles[size()]
185
+ const variantStyle = () => variantStyles[variant()]
186
+ const customClass = local.class ?? ''
187
+
188
+ const getClassName = (renderProps: TreeRenderProps): string => {
189
+ const base = 'overflow-auto focus:outline-none'
190
+ const sizeClass = styles().tree
191
+ const variantClass = variantStyle().tree
192
+
193
+ let stateClass = ''
194
+ if (renderProps.isDisabled) {
195
+ stateClass = 'opacity-50'
196
+ }
197
+
198
+ const focusClass = renderProps.isFocusVisible
199
+ ? 'ring-2 ring-accent-300 ring-offset-2 ring-offset-bg-400'
200
+ : ''
201
+
202
+ return [base, sizeClass, variantClass, stateClass, focusClass, customClass]
203
+ .filter(Boolean)
204
+ .join(' ')
205
+ }
206
+
207
+ const defaultEmptyState = () => (
208
+ <div class="py-8 text-center text-primary-400">
209
+ <div class="flex flex-col items-center gap-2">
210
+ <EmptyTreeIcon class="w-12 h-12 text-primary-500" />
211
+ <span>No items</span>
212
+ </div>
213
+ </div>
214
+ )
215
+
216
+ const contextValue = () => ({ size: size(), variant: variant() })
217
+
218
+ return (
219
+ <TreeSizeContext.Provider value={contextValue()}>
220
+ <div class="flex flex-col gap-2">
221
+ <Show when={local.label}>
222
+ <label class={`text-primary-200 font-medium ${styles().label}`}>
223
+ {local.label}
224
+ </label>
225
+ </Show>
226
+ <HeadlessTree
227
+ {...headlessProps}
228
+ class={getClassName}
229
+ renderEmptyState={headlessProps.renderEmptyState ?? defaultEmptyState}
230
+ />
231
+ <Show when={local.description}>
232
+ <span class="text-primary-400 text-sm">{local.description}</span>
233
+ </Show>
234
+ </div>
235
+ </TreeSizeContext.Provider>
236
+ )
237
+ }
238
+
239
+ // ============================================
240
+ // TREE ITEM COMPONENT
241
+ // ============================================
242
+
243
+ /**
244
+ * An item in a tree.
245
+ */
246
+ export function TreeItem<T extends object>(props: TreeItemProps<T>): JSX.Element {
247
+ const [local, headlessProps] = splitProps(props, [
248
+ 'class',
249
+ 'description',
250
+ 'icon',
251
+ ])
252
+
253
+ const context = useContext(TreeSizeContext)
254
+ const sizeStyle = sizeStyles[context.size]
255
+ const variantStyle = variantStyles[context.variant]
256
+ const customClass = local.class ?? ''
257
+
258
+ const getClassName = (renderProps: TreeItemRenderProps): string => {
259
+ const base = 'flex items-center cursor-pointer transition-all duration-150 outline-none'
260
+ const sizeClass = sizeStyle.item
261
+ const variantClass = variantStyle.item
262
+
263
+ let stateClass = ''
264
+ if (renderProps.isDisabled) {
265
+ stateClass = 'opacity-50 cursor-not-allowed'
266
+ } else if (renderProps.isSelected) {
267
+ stateClass = variantStyle.itemSelected
268
+ } else if (renderProps.isHovered) {
269
+ stateClass = variantStyle.itemHover
270
+ }
271
+
272
+ let textClass = ''
273
+ if (!renderProps.isDisabled && !renderProps.isSelected) {
274
+ textClass = 'text-primary-200'
275
+ }
276
+
277
+ const focusClass = renderProps.isFocusVisible
278
+ ? 'ring-2 ring-inset ring-accent-300'
279
+ : ''
280
+
281
+ const pressedClass = renderProps.isPressed ? 'scale-[0.99]' : ''
282
+
283
+ return [base, sizeClass, variantClass, stateClass, textClass, focusClass, pressedClass, customClass]
284
+ .filter(Boolean)
285
+ .join(' ')
286
+ }
287
+
288
+ const getStyle = (renderProps: TreeItemRenderProps): JSX.CSSProperties => ({
289
+ 'padding-left': `${renderProps.level * sizeStyle.indent + 8}px`,
290
+ })
291
+
292
+ return (
293
+ <HeadlessTreeItem {...headlessProps} class={getClassName} style={getStyle}>
294
+ {(renderProps: TreeItemRenderProps) => (
295
+ <>
296
+ {/* Expand button */}
297
+ <TreeExpandButton class={`${sizeStyle.expandButton} shrink-0`} />
298
+
299
+ {/* Icon */}
300
+ <Show when={local.icon}>
301
+ <span class={`shrink-0 ${sizeStyle.icon}`}>
302
+ {local.icon!()}
303
+ </span>
304
+ </Show>
305
+
306
+ {/* Default folder/file icon if no custom icon */}
307
+ <Show when={!local.icon}>
308
+ {renderProps.isExpandable ? (
309
+ <FolderIcon class={`shrink-0 ${sizeStyle.icon} text-accent-300`} isOpen={renderProps.isExpanded} />
310
+ ) : (
311
+ <FileIcon class={`shrink-0 ${sizeStyle.icon} text-primary-400`} />
312
+ )}
313
+ </Show>
314
+
315
+ {/* Content */}
316
+ <div class="flex flex-col flex-1 min-w-0">
317
+ <span class="truncate">
318
+ {typeof props.children === 'function'
319
+ ? props.children(renderProps)
320
+ : props.children}
321
+ </span>
322
+ <Show when={local.description}>
323
+ <span class={`text-primary-400 truncate ${sizeStyle.description}`}>
324
+ {local.description}
325
+ </span>
326
+ </Show>
327
+ </div>
328
+
329
+ {/* Selection indicator */}
330
+ <Show when={renderProps.isSelected}>
331
+ <CheckIcon class={`shrink-0 ${sizeStyle.icon} text-accent`} />
332
+ </Show>
333
+ </>
334
+ )}
335
+ </HeadlessTreeItem>
336
+ )
337
+ }
338
+
339
+ // ============================================
340
+ // TREE EXPAND BUTTON COMPONENT
341
+ // ============================================
342
+
343
+ /**
344
+ * A button to expand/collapse a tree item.
345
+ */
346
+ export function TreeExpandButton(props: TreeExpandButtonProps): JSX.Element {
347
+ const [local, headlessProps] = splitProps(props, ['class'])
348
+ const context = useContext(TreeSizeContext)
349
+ const sizeStyle = sizeStyles[context.size]
350
+ const customClass = local.class ?? ''
351
+
352
+ const className = [
353
+ 'flex items-center justify-center transition-transform duration-150 text-primary-400 hover:text-primary-200',
354
+ customClass,
355
+ ]
356
+ .filter(Boolean)
357
+ .join(' ')
358
+
359
+ return (
360
+ <HeadlessTreeExpandButton
361
+ {...headlessProps}
362
+ class={className}
363
+ >
364
+ {props.children ?? (({ isExpanded }: { isExpanded: boolean }) => (
365
+ <ChevronIcon
366
+ class={`${sizeStyle.expandButton} transition-transform duration-150 ${
367
+ isExpanded ? 'rotate-90' : ''
368
+ }`}
369
+ />
370
+ ))}
371
+ </HeadlessTreeExpandButton>
372
+ )
373
+ }
374
+
375
+ // ============================================
376
+ // TREE SELECTION CHECKBOX COMPONENT
377
+ // ============================================
378
+
379
+ /**
380
+ * A styled checkbox for item selection in a tree.
381
+ */
382
+ export function TreeSelectionCheckbox(props: { itemKey: Key; class?: string }): JSX.Element {
383
+ const context = useContext(TreeSizeContext)
384
+ const sizeStyle = sizeStyles[context.size]
385
+ const className = `${sizeStyle.checkbox} rounded border-2 border-primary-500 bg-bg-400 text-accent cursor-pointer checked:bg-accent checked:border-accent focus:ring-2 focus:ring-accent-300 focus:ring-offset-1 focus:ring-offset-bg-400 ${props.class ?? ''}`
386
+
387
+ return (
388
+ <span class={className}>
389
+ <HeadlessTreeSelectionCheckbox itemKey={props.itemKey} />
390
+ </span>
391
+ )
392
+ }
393
+
394
+ // ============================================
395
+ // ICONS
396
+ // ============================================
397
+
398
+ function ChevronIcon(props: { class?: string }): JSX.Element {
399
+ return (
400
+ <svg
401
+ class={props.class}
402
+ fill="none"
403
+ viewBox="0 0 24 24"
404
+ stroke="currentColor"
405
+ stroke-width="2"
406
+ >
407
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
408
+ </svg>
409
+ )
410
+ }
411
+
412
+ function FolderIcon(props: { class?: string; isOpen?: boolean }): JSX.Element {
413
+ return (
414
+ <svg
415
+ class={props.class}
416
+ fill="none"
417
+ viewBox="0 0 24 24"
418
+ stroke="currentColor"
419
+ stroke-width="1.5"
420
+ >
421
+ {props.isOpen ? (
422
+ <path
423
+ stroke-linecap="round"
424
+ stroke-linejoin="round"
425
+ d="M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026m-16.5 0a2.25 2.25 0 00-1.883 2.542l.857 6a2.25 2.25 0 002.227 1.932H19.05a2.25 2.25 0 002.227-1.932l.857-6a2.25 2.25 0 00-1.883-2.542m-16.5 0V6A2.25 2.25 0 016 3.75h3.879a1.5 1.5 0 011.06.44l2.122 2.12a1.5 1.5 0 001.06.44H18A2.25 2.25 0 0120.25 9v.776"
426
+ />
427
+ ) : (
428
+ <path
429
+ stroke-linecap="round"
430
+ stroke-linejoin="round"
431
+ d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z"
432
+ />
433
+ )}
434
+ </svg>
435
+ )
436
+ }
437
+
438
+ function FileIcon(props: { class?: string }): JSX.Element {
439
+ return (
440
+ <svg
441
+ class={props.class}
442
+ fill="none"
443
+ viewBox="0 0 24 24"
444
+ stroke="currentColor"
445
+ stroke-width="1.5"
446
+ >
447
+ <path
448
+ stroke-linecap="round"
449
+ stroke-linejoin="round"
450
+ d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
451
+ />
452
+ </svg>
453
+ )
454
+ }
455
+
456
+ function CheckIcon(props: { class?: string }): JSX.Element {
457
+ return (
458
+ <svg
459
+ class={props.class}
460
+ fill="none"
461
+ viewBox="0 0 24 24"
462
+ stroke="currentColor"
463
+ stroke-width="2"
464
+ >
465
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
466
+ </svg>
467
+ )
468
+ }
469
+
470
+ function EmptyTreeIcon(props: { class?: string }): JSX.Element {
471
+ return (
472
+ <svg
473
+ class={props.class}
474
+ fill="none"
475
+ viewBox="0 0 24 24"
476
+ stroke="currentColor"
477
+ stroke-width="1.5"
478
+ >
479
+ <path
480
+ stroke-linecap="round"
481
+ stroke-linejoin="round"
482
+ d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6z"
483
+ />
484
+ </svg>
485
+ )
486
+ }
487
+
488
+ // Attach sub-components for convenience
489
+ Tree.Item = TreeItem
490
+ Tree.ExpandButton = TreeExpandButton
491
+ Tree.SelectionCheckbox = TreeSelectionCheckbox
492
+
493
+ // Re-export types for convenience
494
+ export type { Key, TreeItemData, TreeRenderItemState }