@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,59 @@
1
+ import type { JSX } from 'solid-js'
2
+
3
+ export type ProjectCardSize = 'sm' | 'md' | 'lg'
4
+
5
+ export interface ProjectCardProps {
6
+ /** Project name shown in tooltip on hover */
7
+ name: string
8
+ /** Image source for the project logo */
9
+ imageSrc: string
10
+ /** Alt text for the image */
11
+ imageAlt?: string
12
+ /** Optional link to the project */
13
+ href?: string
14
+ /** Size of the card */
15
+ size?: ProjectCardSize
16
+ /** Whether the project is inactive/greyed out */
17
+ inactive?: boolean
18
+ /** Additional CSS class */
19
+ class?: string
20
+ }
21
+
22
+ /**
23
+ * Project card with logo and hover tooltip.
24
+ * Used for showcasing ecosystem projects.
25
+ */
26
+ export function ProjectCard(props: ProjectCardProps): JSX.Element {
27
+ const size = () => props.size ?? 'sm'
28
+ const inactive = () => props.inactive ?? false
29
+
30
+ const cardContent = () => (
31
+ <>
32
+ <div class="vui-project-card__tooltip">
33
+ <span>{props.name}</span>
34
+ </div>
35
+ <img
36
+ class="vui-project-card__image"
37
+ src={props.imageSrc}
38
+ alt={props.imageAlt ?? props.name}
39
+ />
40
+ </>
41
+ )
42
+
43
+ const cardClasses = () =>
44
+ `vui-project-card vui-project-card--${size()} ${inactive() ? 'vui-project-card--inactive' : ''} ${props.class ?? ''}`
45
+
46
+ if (props.href) {
47
+ return (
48
+ <a href={props.href} target="_blank" rel="noopener noreferrer" class={cardClasses()}>
49
+ {cardContent()}
50
+ </a>
51
+ )
52
+ }
53
+
54
+ return (
55
+ <div class={cardClasses()}>
56
+ {cardContent()}
57
+ </div>
58
+ )
59
+ }
@@ -0,0 +1,105 @@
1
+ import type { JSX } from 'solid-js'
2
+ import { Show } from 'solid-js'
3
+ import { Avatar } from '../../avatar'
4
+
5
+ export type TimelineEventType = 'follow' | 'like' | 'comment' | 'event' | 'custom'
6
+
7
+ export interface TimelineItemProps {
8
+ type?: TimelineEventType
9
+ /**
10
+ * Icon to display between the two avatars.
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)
15
+ leftUser?: {
16
+ name: string
17
+ avatar?: string
18
+ }
19
+ rightUser?: {
20
+ name: string
21
+ avatar?: string
22
+ }
23
+ /**
24
+ * Custom message content.
25
+ * Use a function returning JSX for SSR compatibility: `message={() => <span>...</span>}`
26
+ * Or pass a simple string.
27
+ */
28
+ message?: string | (() => JSX.Element)
29
+ class?: string
30
+ }
31
+
32
+ const eventMessages: Record<TimelineEventType, (left: string, right: string) => JSX.Element> = {
33
+ follow: (left, right) => (
34
+ <>
35
+ <span class="font-semibold text-accent-200">{left}</span>
36
+ {' ha empezado a seguir a '}
37
+ <span class="font-semibold text-accent-200">{right}</span>
38
+ </>
39
+ ),
40
+ like: (left, right) => (
41
+ <>
42
+ <span class="font-semibold text-accent-200">{left}</span>
43
+ {' le ha dado like a '}
44
+ <span class="font-semibold text-accent-200">{right}</span>
45
+ </>
46
+ ),
47
+ comment: (left, right) => (
48
+ <>
49
+ <span class="font-semibold text-accent-200">{left}</span>
50
+ {' ha comentado en '}
51
+ <span class="font-semibold text-accent-200">{right}</span>
52
+ </>
53
+ ),
54
+ event: (left, right) => (
55
+ <>
56
+ <span class="font-semibold text-accent-200">{left}</span>
57
+ {' asistirá al evento de '}
58
+ <span class="font-semibold text-accent-200">{right}</span>
59
+ </>
60
+ ),
61
+ custom: () => null,
62
+ }
63
+
64
+ export function TimelineItem(props: TimelineItemProps) {
65
+ const type = () => props.type ?? 'follow'
66
+ const leftName = () => props.leftUser?.name ?? ''
67
+ const rightName = () => props.rightUser?.name ?? ''
68
+
69
+ const renderIcon = () => {
70
+ const icon = props.icon
71
+ if (!icon) return null
72
+ if (typeof icon === 'string') return icon
73
+ return icon()
74
+ }
75
+
76
+ const renderMessage = () => {
77
+ const message = props.message
78
+ if (!message) return null
79
+ if (typeof message === 'string') return message
80
+ return message()
81
+ }
82
+
83
+ return (
84
+ <div class={`inline-flex w-auto flex-col gap-5 rounded-2xl border border-primary-700 bg-bg-200 p-5 hover:bg-bg-300 transition-colors ${props.class ?? ''}`}>
85
+ <div class="flex items-center justify-around gap-3">
86
+ <Show when={props.leftUser}>
87
+ <Avatar src={props.leftUser!.avatar} alt={props.leftUser!.name} />
88
+ </Show>
89
+ <Show when={props.icon}>
90
+ {renderIcon()}
91
+ </Show>
92
+ <Show when={props.rightUser}>
93
+ <Avatar src={props.rightUser!.avatar} alt={props.rightUser!.name} />
94
+ </Show>
95
+ </div>
96
+ <div class="flex items-center justify-center gap-3 text-center">
97
+ <span class="font-light text-primary-300">
98
+ <Show when={props.message} fallback={eventMessages[type()](leftName(), rightName())}>
99
+ {renderMessage()}
100
+ </Show>
101
+ </span>
102
+ </div>
103
+ </div>
104
+ )
105
+ }
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Dialog component for proyecto-viviana-ui
3
+ *
4
+ * Styled dialog component with overlay and backdrop.
5
+ * Follows Spectrum 2 design patterns.
6
+ */
7
+
8
+ import { type JSX, splitProps, Show, createSignal, createContext, useContext, createUniqueId, onMount, onCleanup, createEffect } from 'solid-js'
9
+ import { Portal } from 'solid-js/web'
10
+ import { createInteractOutside } from '@proyecto-viviana/solidaria'
11
+
12
+ // ============================================
13
+ // TYPES
14
+ // ============================================
15
+
16
+ export type DialogSize = 'sm' | 'md' | 'lg' | 'fullscreen'
17
+
18
+ export interface DialogProps {
19
+ /** The size of the dialog. */
20
+ size?: DialogSize
21
+ /** Whether the dialog can be dismissed by clicking the X button. */
22
+ isDismissable?: boolean
23
+ /** Additional CSS class name. */
24
+ class?: string
25
+ /** The title of the dialog. */
26
+ title?: string
27
+ /** The children content. */
28
+ children: JSX.Element
29
+ /** Callback when dialog should close */
30
+ onClose?: () => void
31
+ /** ARIA role - defaults to 'dialog' */
32
+ role?: 'dialog' | 'alertdialog'
33
+ /** ARIA label */
34
+ 'aria-label'?: string
35
+ /** ARIA labelledby */
36
+ 'aria-labelledby'?: string
37
+ }
38
+
39
+ export interface DialogTriggerProps {
40
+ /** Button to trigger the dialog. */
41
+ trigger: JSX.Element
42
+ /** The dialog content - receives close function. */
43
+ content: (close: () => void) => JSX.Element
44
+ /** Whether the dialog is controlled. */
45
+ isOpen?: boolean
46
+ /** Callback when open state changes. */
47
+ onOpenChange?: (isOpen: boolean) => void
48
+ /** Whether clicking outside the dialog closes it. Defaults to true. */
49
+ isDismissable?: boolean
50
+ /** Whether pressing Escape closes the dialog. Defaults to true. */
51
+ isKeyboardDismissDisabled?: boolean
52
+ }
53
+
54
+ // ============================================
55
+ // CONTEXT
56
+ // ============================================
57
+
58
+ interface DialogContextValue {
59
+ close: () => void
60
+ }
61
+
62
+ const DialogContext = createContext<DialogContextValue | null>(null)
63
+
64
+ export function useDialogContext(): DialogContextValue | null {
65
+ return useContext(DialogContext)
66
+ }
67
+
68
+ // ============================================
69
+ // STYLES
70
+ // ============================================
71
+
72
+ const sizeStyles: Record<DialogSize, string> = {
73
+ sm: 'max-w-sm',
74
+ md: 'max-w-md',
75
+ lg: 'max-w-2xl',
76
+ fullscreen: 'max-w-full w-full h-full',
77
+ }
78
+
79
+ // ============================================
80
+ // DIALOG COMPONENT
81
+ // ============================================
82
+
83
+ /**
84
+ * A dialog is an overlay shown above other content in an application.
85
+ */
86
+ export function Dialog(props: DialogProps): JSX.Element {
87
+ const [local, rest] = splitProps(props, [
88
+ 'size',
89
+ 'isDismissable',
90
+ 'class',
91
+ 'title',
92
+ 'onClose',
93
+ 'role',
94
+ 'aria-label',
95
+ 'aria-labelledby',
96
+ ])
97
+
98
+ const size = local.size ?? 'md'
99
+ const customClass = local.class ?? ''
100
+ const role = local.role ?? 'dialog'
101
+
102
+ // Generate a unique ID for the title if one is present
103
+ const titleId = createUniqueId()
104
+ const ariaLabelledBy = local['aria-labelledby'] ?? (local.title ? titleId : undefined)
105
+
106
+ const close = () => local.onClose?.()
107
+
108
+ const baseClass = 'bg-bg-300 rounded-lg shadow-xl border border-primary-700'
109
+ const sizeClass = sizeStyles[size]
110
+ const padding = 'p-6'
111
+ const className = [baseClass, sizeClass, padding, customClass].filter(Boolean).join(' ')
112
+
113
+ return (
114
+ <DialogContext.Provider value={{ close }}>
115
+ <div
116
+ role={role}
117
+ tabIndex={-1}
118
+ aria-label={local['aria-label']}
119
+ aria-labelledby={ariaLabelledBy}
120
+ class={className}
121
+ {...rest}
122
+ >
123
+ <Show when={local.title}>
124
+ <div class="flex items-center justify-between mb-4">
125
+ <h2 id={titleId} class="text-xl font-semibold text-primary-100">
126
+ {local.title}
127
+ </h2>
128
+ <Show when={local.isDismissable}>
129
+ <button
130
+ onClick={close}
131
+ class="text-primary-400 hover:text-primary-200 transition-colors"
132
+ aria-label="Close dialog"
133
+ >
134
+ <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
135
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
136
+ </svg>
137
+ </button>
138
+ </Show>
139
+ </div>
140
+ </Show>
141
+ <div class="text-primary-200">
142
+ {props.children}
143
+ </div>
144
+ </div>
145
+ </DialogContext.Provider>
146
+ )
147
+ }
148
+
149
+ // ============================================
150
+ // DIALOG TRIGGER COMPONENT
151
+ // ============================================
152
+
153
+ /**
154
+ * DialogTrigger wraps a trigger button and dialog content.
155
+ * Handles opening/closing the dialog with overlay and backdrop.
156
+ */
157
+ export function DialogTrigger(props: DialogTriggerProps): JSX.Element {
158
+ const [isOpen, setIsOpen] = createSignal(props.isOpen ?? false)
159
+ let dialogRef: HTMLDivElement | undefined
160
+
161
+ const open = () => {
162
+ setIsOpen(true)
163
+ props.onOpenChange?.(true)
164
+ }
165
+
166
+ const close = () => {
167
+ setIsOpen(false)
168
+ props.onOpenChange?.(false)
169
+ }
170
+
171
+ // Handle controlled state
172
+ const isOpenControlled = () => props.isOpen !== undefined ? props.isOpen : isOpen()
173
+
174
+ // Whether dismissable (defaults to true)
175
+ const isDismissable = () => props.isDismissable !== false
176
+
177
+ // Click outside to close
178
+ createInteractOutside({
179
+ ref: () => dialogRef ?? null,
180
+ onInteractOutside: () => {
181
+ if (isOpenControlled() && isDismissable()) {
182
+ close()
183
+ }
184
+ },
185
+ isDisabled: !isOpenControlled() || !isDismissable(),
186
+ })
187
+
188
+ // Escape key to close
189
+ onMount(() => {
190
+ const handleKeyDown = (e: KeyboardEvent) => {
191
+ if (e.key === 'Escape' && isOpenControlled() && !props.isKeyboardDismissDisabled) {
192
+ e.preventDefault()
193
+ e.stopPropagation()
194
+ close()
195
+ }
196
+ }
197
+ document.addEventListener('keydown', handleKeyDown)
198
+ onCleanup(() => document.removeEventListener('keydown', handleKeyDown))
199
+ })
200
+
201
+ // Prevent background scroll when dialog is open
202
+ createEffect(() => {
203
+ if (!isOpenControlled()) return
204
+
205
+ const prevOverflow = document.documentElement.style.overflow
206
+ document.documentElement.style.overflow = 'hidden'
207
+
208
+ onCleanup(() => {
209
+ document.documentElement.style.overflow = prevOverflow
210
+ })
211
+ })
212
+
213
+ return (
214
+ <>
215
+ <div onClick={open}>
216
+ {props.trigger}
217
+ </div>
218
+
219
+ <Show when={isOpenControlled()}>
220
+ <Portal>
221
+ {/* Backdrop */}
222
+ <div
223
+ class="fixed inset-0 bg-black/50 backdrop-blur-sm z-40"
224
+ aria-hidden="true"
225
+ />
226
+
227
+ {/* Dialog container - pointer-events-none so clicks pass through to backdrop detection */}
228
+ <div class="fixed inset-0 z-50 flex items-center justify-center p-4 pointer-events-none">
229
+ {/* Dialog wrapper - pointer-events-auto to capture clicks on the dialog itself */}
230
+ <div ref={dialogRef} class="pointer-events-auto">
231
+ {props.content(close)}
232
+ </div>
233
+ </div>
234
+ </Portal>
235
+ </Show>
236
+ </>
237
+ )
238
+ }
239
+
240
+ // ============================================
241
+ // DIALOG FOOTER COMPONENT
242
+ // ============================================
243
+
244
+ export interface DialogFooterProps {
245
+ /** Footer content, typically buttons. */
246
+ children: JSX.Element
247
+ /** Additional CSS class. */
248
+ class?: string
249
+ }
250
+
251
+ /**
252
+ * Footer section for dialog actions.
253
+ */
254
+ export function DialogFooter(props: DialogFooterProps): JSX.Element {
255
+ return (
256
+ <div class={`flex gap-3 justify-end mt-6 pt-4 border-t border-primary-700 ${props.class ?? ''}`}>
257
+ {props.children}
258
+ </div>
259
+ )
260
+ }
@@ -0,0 +1,3 @@
1
+ // Export the new standard Dialog component
2
+ export { Dialog, DialogTrigger, DialogFooter } from './Dialog'
3
+ export type { DialogProps, DialogTriggerProps, DialogFooterProps, DialogSize } from './Dialog'