@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,231 @@
1
+ /**
2
+ * ListBox component for proyecto-viviana-ui
3
+ *
4
+ * Styled listbox component built on top of solidaria-components.
5
+ * Inspired by Spectrum 2's ListBox component patterns.
6
+ */
7
+
8
+ import { type JSX, splitProps, createContext, useContext, Show } from 'solid-js'
9
+ import {
10
+ ListBox as HeadlessListBox,
11
+ ListBoxOption as HeadlessListBoxOption,
12
+ type ListBoxProps as HeadlessListBoxProps,
13
+ type ListBoxOptionProps as HeadlessListBoxOptionProps,
14
+ type ListBoxRenderProps,
15
+ type ListBoxOptionRenderProps,
16
+ } from '@proyecto-viviana/solidaria-components'
17
+ import type { Key } from '@proyecto-viviana/solid-stately'
18
+
19
+ // ============================================
20
+ // SIZE CONTEXT
21
+ // ============================================
22
+
23
+ export type ListBoxSize = 'sm' | 'md' | 'lg'
24
+
25
+ const ListBoxSizeContext = createContext<ListBoxSize>('md')
26
+
27
+ // ============================================
28
+ // TYPES
29
+ // ============================================
30
+
31
+ export interface ListBoxProps<T> extends Omit<HeadlessListBoxProps<T>, 'class' | 'style'> {
32
+ /** The size of the listbox. */
33
+ size?: ListBoxSize
34
+ /** Additional CSS class name. */
35
+ class?: string
36
+ /** Label for the listbox. */
37
+ label?: string
38
+ /** Description for the listbox. */
39
+ description?: string
40
+ }
41
+
42
+ export interface ListBoxOptionProps<T> extends Omit<HeadlessListBoxOptionProps<T>, 'class' | 'style'> {
43
+ /** Additional CSS class name. */
44
+ class?: string
45
+ /** Optional description text. */
46
+ description?: string
47
+ /**
48
+ * Optional icon to display before the label.
49
+ * Use a function returning JSX for SSR compatibility: `icon={() => <MyIcon />}`
50
+ */
51
+ icon?: () => JSX.Element
52
+ }
53
+
54
+ // ============================================
55
+ // STYLES
56
+ // ============================================
57
+
58
+ const sizeStyles = {
59
+ sm: {
60
+ list: 'py-1',
61
+ option: 'text-sm py-1.5 px-3 gap-2',
62
+ icon: 'h-4 w-4',
63
+ label: 'text-sm',
64
+ description: 'text-xs',
65
+ },
66
+ md: {
67
+ list: 'py-1.5',
68
+ option: 'text-base py-2 px-4 gap-3',
69
+ icon: 'h-5 w-5',
70
+ label: 'text-base',
71
+ description: 'text-sm',
72
+ },
73
+ lg: {
74
+ list: 'py-2',
75
+ option: 'text-lg py-2.5 px-5 gap-3',
76
+ icon: 'h-6 w-6',
77
+ label: 'text-lg',
78
+ description: 'text-base',
79
+ },
80
+ }
81
+
82
+ // ============================================
83
+ // LISTBOX COMPONENT
84
+ // ============================================
85
+
86
+ /**
87
+ * A listbox displays a list of options and allows a user to select one or more of them.
88
+ *
89
+ * Built on solidaria-components ListBox for full accessibility support.
90
+ */
91
+ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
92
+ const [local, headlessProps] = splitProps(props, [
93
+ 'size',
94
+ 'class',
95
+ 'label',
96
+ 'description',
97
+ 'renderEmptyState',
98
+ ])
99
+
100
+ const size = local.size ?? 'md'
101
+ const styles = sizeStyles[size]
102
+ const customClass = local.class ?? ''
103
+
104
+ const getClassName = (renderProps: ListBoxRenderProps): string => {
105
+ const base = 'rounded-lg border-2 border-primary-600 bg-bg-400 overflow-auto focus:outline-none'
106
+ const sizeClass = styles.list
107
+
108
+ let stateClass: string
109
+ if (renderProps.isDisabled) {
110
+ stateClass = 'opacity-50'
111
+ } else {
112
+ stateClass = ''
113
+ }
114
+
115
+ const focusClass = renderProps.isFocusVisible
116
+ ? 'ring-2 ring-accent-300 ring-offset-2 ring-offset-bg-400'
117
+ : ''
118
+
119
+ return [base, sizeClass, stateClass, focusClass, customClass].filter(Boolean).join(' ')
120
+ }
121
+
122
+ const defaultEmptyState = () => (
123
+ <li class="py-4 px-4 text-center text-primary-500">
124
+ No items
125
+ </li>
126
+ )
127
+
128
+ return (
129
+ <ListBoxSizeContext.Provider value={size}>
130
+ <div class="flex flex-col gap-1.5">
131
+ <Show when={local.label}>
132
+ <label class={`text-primary-200 font-medium ${styles.label}`}>
133
+ {local.label}
134
+ </label>
135
+ </Show>
136
+ <HeadlessListBox
137
+ {...headlessProps}
138
+ class={getClassName}
139
+ renderEmptyState={local.renderEmptyState ?? defaultEmptyState}
140
+ children={props.children}
141
+ />
142
+ <Show when={local.description}>
143
+ <span class="text-primary-400 text-sm">{local.description}</span>
144
+ </Show>
145
+ </div>
146
+ </ListBoxSizeContext.Provider>
147
+ )
148
+ }
149
+
150
+ // ============================================
151
+ // LISTBOX OPTION COMPONENT
152
+ // ============================================
153
+
154
+ /**
155
+ * An option in a listbox.
156
+ * SSR-compatible - renders icon, check, content, and description directly without render props.
157
+ */
158
+ export function ListBoxOption<T>(props: ListBoxOptionProps<T>): JSX.Element {
159
+ const [local, headlessProps] = splitProps(props, ['class', 'description', 'icon'])
160
+ const size = useContext(ListBoxSizeContext)
161
+ const sizeStyle = sizeStyles[size]
162
+ const customClass = local.class ?? ''
163
+
164
+ const getClassName = (renderProps: ListBoxOptionRenderProps): string => {
165
+ const base = 'flex items-center cursor-pointer transition-colors duration-150 outline-none'
166
+ const sizeClass = sizeStyle.option
167
+
168
+ let colorClass: string
169
+ if (renderProps.isDisabled) {
170
+ colorClass = 'text-primary-500 cursor-not-allowed'
171
+ } else if (renderProps.isSelected) {
172
+ if (renderProps.isFocused || renderProps.isHovered) {
173
+ colorClass = 'bg-accent/30 text-accent'
174
+ } else {
175
+ colorClass = 'bg-accent/20 text-accent'
176
+ }
177
+ } else if (renderProps.isFocused || renderProps.isHovered) {
178
+ colorClass = 'bg-bg-300 text-primary-100'
179
+ } else {
180
+ colorClass = 'text-primary-200'
181
+ }
182
+
183
+ const focusClass = renderProps.isFocusVisible
184
+ ? 'ring-2 ring-inset ring-accent-300'
185
+ : ''
186
+
187
+ return [base, sizeClass, colorClass, focusClass, customClass].filter(Boolean).join(' ')
188
+ }
189
+
190
+ return (
191
+ <HeadlessListBoxOption
192
+ {...headlessProps}
193
+ class={getClassName}
194
+ >
195
+ {local.icon && <span class={`shrink-0 ${sizeStyle.icon}`}>{local.icon()}</span>}
196
+ <CheckIcon class={`shrink-0 ${sizeStyle.icon} text-accent hidden data-selected:block`} />
197
+ <div class="flex flex-col flex-1 min-w-0">
198
+ <span class="truncate">{props.children as JSX.Element}</span>
199
+ {local.description && (
200
+ <span class={`text-primary-400 truncate ${sizeStyle.description}`}>
201
+ {local.description}
202
+ </span>
203
+ )}
204
+ </div>
205
+ </HeadlessListBoxOption>
206
+ )
207
+ }
208
+
209
+ // ============================================
210
+ // ICONS
211
+ // ============================================
212
+
213
+ function CheckIcon(props: { class?: string }): JSX.Element {
214
+ return (
215
+ <svg
216
+ class={props.class}
217
+ fill="none"
218
+ viewBox="0 0 24 24"
219
+ stroke="currentColor"
220
+ stroke-width="2"
221
+ >
222
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
223
+ </svg>
224
+ )
225
+ }
226
+
227
+ // Attach sub-components for convenience
228
+ ListBox.Option = ListBoxOption
229
+
230
+ // Re-export Key type for convenience
231
+ export type { Key }
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Menu component for proyecto-viviana-ui
3
+ *
4
+ * Styled menu component built on top of solidaria-components.
5
+ * Inspired by Spectrum 2's Menu component patterns.
6
+ */
7
+
8
+ import { type JSX, splitProps, createContext, useContext } from 'solid-js'
9
+ import {
10
+ Menu as HeadlessMenu,
11
+ MenuItem as HeadlessMenuItem,
12
+ MenuTrigger as HeadlessMenuTrigger,
13
+ MenuButton as HeadlessMenuButton,
14
+ type MenuProps as HeadlessMenuProps,
15
+ type MenuItemProps as HeadlessMenuItemProps,
16
+ type MenuTriggerProps as HeadlessMenuTriggerProps,
17
+ type MenuButtonProps as HeadlessMenuButtonProps,
18
+ type MenuRenderProps,
19
+ type MenuItemRenderProps,
20
+ type MenuTriggerRenderProps,
21
+ } from '@proyecto-viviana/solidaria-components'
22
+ import type { Key } from '@proyecto-viviana/solid-stately'
23
+
24
+ // ============================================
25
+ // SIZE CONTEXT
26
+ // ============================================
27
+
28
+ export type MenuSize = 'sm' | 'md' | 'lg'
29
+
30
+ const MenuSizeContext = createContext<MenuSize>('md')
31
+
32
+ // ============================================
33
+ // TYPES
34
+ // ============================================
35
+
36
+ export interface MenuTriggerProps extends Omit<HeadlessMenuTriggerProps, 'class' | 'style'> {
37
+ /** The size of the menu. */
38
+ size?: MenuSize
39
+ /** Additional CSS class name. */
40
+ class?: string
41
+ }
42
+
43
+ export interface MenuButtonProps extends Omit<HeadlessMenuButtonProps, 'class' | 'style'> {
44
+ /** Additional CSS class name. */
45
+ class?: string
46
+ /** Visual variant of the button. */
47
+ variant?: 'primary' | 'secondary' | 'quiet'
48
+ }
49
+
50
+ export interface MenuProps<T> extends Omit<HeadlessMenuProps<T>, 'class' | 'style'> {
51
+ /** Additional CSS class name. */
52
+ class?: string
53
+ }
54
+
55
+ export interface MenuItemProps<T> extends Omit<HeadlessMenuItemProps<T>, 'class' | 'style'> {
56
+ /** Additional CSS class name. */
57
+ class?: string
58
+ /**
59
+ * Optional icon to display before the label.
60
+ * Use a function returning JSX for SSR compatibility: `icon={() => <MyIcon />}`
61
+ */
62
+ icon?: () => JSX.Element
63
+ /** Optional keyboard shortcut to display. */
64
+ shortcut?: string
65
+ /** Whether this is a destructive action. */
66
+ isDestructive?: boolean
67
+ }
68
+
69
+ // ============================================
70
+ // STYLES
71
+ // ============================================
72
+
73
+ const sizeStyles = {
74
+ sm: {
75
+ button: 'h-8 text-sm px-3 gap-2',
76
+ menu: 'py-1',
77
+ item: 'text-sm py-1.5 px-3 gap-2',
78
+ icon: 'h-4 w-4',
79
+ },
80
+ md: {
81
+ button: 'h-10 text-base px-4 gap-2',
82
+ menu: 'py-1.5',
83
+ item: 'text-base py-2 px-4 gap-3',
84
+ icon: 'h-5 w-5',
85
+ },
86
+ lg: {
87
+ button: 'h-12 text-lg px-5 gap-3',
88
+ menu: 'py-2',
89
+ item: 'text-lg py-2.5 px-5 gap-3',
90
+ icon: 'h-6 w-6',
91
+ },
92
+ }
93
+
94
+ const buttonVariants = {
95
+ primary: 'bg-accent text-bg-500 border-accent hover:bg-accent-300 hover:border-accent-300',
96
+ secondary: 'bg-bg-400 text-primary-200 border-primary-600 hover:bg-bg-300 hover:border-accent-300',
97
+ quiet: 'bg-transparent text-primary-200 border-transparent hover:bg-bg-300',
98
+ }
99
+
100
+ // ============================================
101
+ // MENU TRIGGER COMPONENT
102
+ // ============================================
103
+
104
+ /**
105
+ * A menu trigger wraps a button and menu, handling the open/close state.
106
+ */
107
+ export function MenuTrigger(props: MenuTriggerProps): JSX.Element {
108
+ const [local, headlessProps] = splitProps(props, ['size', 'class'])
109
+ const size = local.size ?? 'md'
110
+
111
+ return (
112
+ <MenuSizeContext.Provider value={size}>
113
+ <div class={`relative inline-block ${local.class ?? ''}`}>
114
+ <HeadlessMenuTrigger {...headlessProps}>
115
+ {props.children}
116
+ </HeadlessMenuTrigger>
117
+ </div>
118
+ </MenuSizeContext.Provider>
119
+ )
120
+ }
121
+
122
+ // ============================================
123
+ // MENU BUTTON COMPONENT
124
+ // ============================================
125
+
126
+ /**
127
+ * A button that opens a menu.
128
+ * SSR-compatible - renders children and chevron icon directly without render props.
129
+ */
130
+ export function MenuButton(props: MenuButtonProps): JSX.Element {
131
+ const [local, headlessProps] = splitProps(props, ['class', 'variant'])
132
+ const size = useContext(MenuSizeContext)
133
+ const sizeStyle = sizeStyles[size]
134
+ const variant = local.variant ?? 'secondary'
135
+ const customClass = local.class ?? ''
136
+
137
+ const getClassName = (renderProps: MenuTriggerRenderProps): string => {
138
+ const base = 'inline-flex items-center justify-center rounded-lg border-2 font-medium transition-all duration-200'
139
+ const sizeClass = sizeStyle.button
140
+ const variantClass = buttonVariants[variant]
141
+
142
+ let stateClass: string
143
+ if (renderProps.isDisabled) {
144
+ stateClass = 'opacity-50 cursor-not-allowed'
145
+ } else if (renderProps.isPressed) {
146
+ stateClass = 'scale-95'
147
+ } else {
148
+ stateClass = 'cursor-pointer'
149
+ }
150
+
151
+ const focusClass = renderProps.isFocusVisible
152
+ ? 'ring-2 ring-accent-300 ring-offset-2 ring-offset-bg-400'
153
+ : ''
154
+
155
+ return [base, sizeClass, variantClass, stateClass, focusClass, customClass].filter(Boolean).join(' ')
156
+ }
157
+
158
+ return (
159
+ <HeadlessMenuButton
160
+ {...headlessProps}
161
+ class={getClassName}
162
+ >
163
+ {props.children as JSX.Element}
164
+ {/* Chevron rotates via CSS based on data-open attribute */}
165
+ <ChevronIcon class={`${sizeStyle.icon} transition-transform duration-200 data-open:rotate-180`} />
166
+ </HeadlessMenuButton>
167
+ )
168
+ }
169
+
170
+ // ============================================
171
+ // MENU COMPONENT
172
+ // ============================================
173
+
174
+ /**
175
+ * A menu displays a list of actions or options for the user to choose from.
176
+ */
177
+ export function Menu<T>(props: MenuProps<T>): JSX.Element {
178
+ const [local, headlessProps] = splitProps(props, ['class'])
179
+ const size = useContext(MenuSizeContext)
180
+ const styles = () => sizeStyles[size]
181
+ const customClass = local.class ?? ''
182
+
183
+ const getClassName = (_renderProps: MenuRenderProps): string => {
184
+ const base = 'absolute z-50 mt-1 min-w-[12rem] rounded-lg border-2 border-primary-600 bg-bg-400 shadow-lg overflow-hidden'
185
+ const sizeClass = styles().menu
186
+ return [base, sizeClass, customClass].filter(Boolean).join(' ')
187
+ }
188
+
189
+ return (
190
+ <HeadlessMenu
191
+ {...headlessProps}
192
+ class={getClassName}
193
+ children={props.children}
194
+ />
195
+ )
196
+ }
197
+
198
+ // ============================================
199
+ // MENU ITEM COMPONENT
200
+ // ============================================
201
+
202
+ /**
203
+ * An item in a menu.
204
+ * SSR-compatible - renders icon, content, and shortcut directly without render props.
205
+ */
206
+ export function MenuItem<T>(props: MenuItemProps<T>): JSX.Element {
207
+ const [local, headlessProps] = splitProps(props, ['class', 'icon', 'shortcut', 'isDestructive'])
208
+ const size = useContext(MenuSizeContext)
209
+ const sizeStyle = sizeStyles[size]
210
+ const customClass = local.class ?? ''
211
+
212
+ const getClassName = (renderProps: MenuItemRenderProps): string => {
213
+ const base = 'flex items-center cursor-pointer transition-colors duration-150 outline-none'
214
+ const sizeClass = sizeStyle.item
215
+
216
+ let colorClass: string
217
+ if (renderProps.isDisabled) {
218
+ colorClass = 'text-primary-500 cursor-not-allowed'
219
+ } else if (local.isDestructive) {
220
+ if (renderProps.isFocused || renderProps.isHovered) {
221
+ colorClass = 'bg-danger-400/20 text-danger-400'
222
+ } else {
223
+ colorClass = 'text-danger-400'
224
+ }
225
+ } else if (renderProps.isFocused || renderProps.isHovered) {
226
+ colorClass = 'bg-bg-300 text-primary-100'
227
+ } else {
228
+ colorClass = 'text-primary-200'
229
+ }
230
+
231
+ const pressedClass = renderProps.isPressed ? 'bg-bg-200' : ''
232
+
233
+ const focusClass = renderProps.isFocusVisible
234
+ ? 'ring-2 ring-inset ring-accent-300'
235
+ : ''
236
+
237
+ return [base, sizeClass, colorClass, pressedClass, focusClass, customClass].filter(Boolean).join(' ')
238
+ }
239
+
240
+ return (
241
+ <HeadlessMenuItem
242
+ {...headlessProps}
243
+ class={getClassName}
244
+ >
245
+ {local.icon && <span class={`shrink-0 ${sizeStyle.icon}`}>{local.icon()}</span>}
246
+ <span class="flex-1">{props.children as JSX.Element}</span>
247
+ {local.shortcut && <span class="text-primary-500 text-sm ml-auto">{local.shortcut}</span>}
248
+ </HeadlessMenuItem>
249
+ )
250
+ }
251
+
252
+ // ============================================
253
+ // MENU SEPARATOR COMPONENT
254
+ // ============================================
255
+
256
+ export interface MenuSeparatorProps {
257
+ /** Additional CSS class name. */
258
+ class?: string
259
+ }
260
+
261
+ /**
262
+ * A visual separator between menu items.
263
+ */
264
+ export function MenuSeparator(props: MenuSeparatorProps): JSX.Element {
265
+ return (
266
+ <li
267
+ role="separator"
268
+ class={`my-1 border-t border-primary-600 ${props.class ?? ''}`}
269
+ />
270
+ )
271
+ }
272
+
273
+ // ============================================
274
+ // ICONS
275
+ // ============================================
276
+
277
+ function ChevronIcon(props: { class?: string }): JSX.Element {
278
+ return (
279
+ <svg
280
+ class={props.class}
281
+ fill="none"
282
+ viewBox="0 0 24 24"
283
+ stroke="currentColor"
284
+ stroke-width="2"
285
+ >
286
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
287
+ </svg>
288
+ )
289
+ }
290
+
291
+ // Attach sub-components for convenience
292
+ Menu.Item = MenuItem
293
+ Menu.Separator = MenuSeparator
294
+ MenuTrigger.Button = MenuButton
295
+
296
+ // Re-export Key type for convenience
297
+ export type { Key }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Meter component for proyecto-viviana-ui
3
+ *
4
+ * Styled meter component built on top of the solidaria hook directly.
5
+ * Meters represent a quantity within a known range (unlike progress bars which show progress toward a goal).
6
+ */
7
+
8
+ import { type JSX, splitProps, Show, createMemo } from 'solid-js';
9
+ import { createMeter } from '@proyecto-viviana/solidaria';
10
+
11
+ // ============================================
12
+ // TYPES
13
+ // ============================================
14
+
15
+ export type MeterSize = 'sm' | 'md' | 'lg';
16
+ export type MeterVariant = 'primary' | 'accent' | 'success' | 'warning' | 'danger' | 'info';
17
+
18
+ export interface MeterProps {
19
+ /** The current value (controlled). @default 0 */
20
+ value?: number;
21
+ /** The smallest value allowed. @default 0 */
22
+ minValue?: number;
23
+ /** The largest value allowed. @default 100 */
24
+ maxValue?: number;
25
+ /** The content to display as the value's label (e.g. "75 GB"). */
26
+ valueLabel?: string;
27
+ /** The size of the meter. @default 'md' */
28
+ size?: MeterSize;
29
+ /** The visual style variant. @default 'primary' */
30
+ variant?: MeterVariant;
31
+ /** The label to display above the meter. */
32
+ label?: string;
33
+ /** Whether to show the value text. @default true */
34
+ showValueLabel?: boolean;
35
+ /** Additional CSS class name. */
36
+ class?: string;
37
+ /** An accessibility label. */
38
+ 'aria-label'?: string;
39
+ }
40
+
41
+ // ============================================
42
+ // STYLES
43
+ // ============================================
44
+
45
+ const sizeStyles = {
46
+ sm: {
47
+ track: 'h-1',
48
+ text: 'text-xs',
49
+ },
50
+ md: {
51
+ track: 'h-2',
52
+ text: 'text-sm',
53
+ },
54
+ lg: {
55
+ track: 'h-3',
56
+ text: 'text-base',
57
+ },
58
+ };
59
+
60
+ const variantStyles = {
61
+ primary: 'bg-primary-500',
62
+ accent: 'bg-accent',
63
+ success: 'bg-green-500',
64
+ warning: 'bg-yellow-500',
65
+ danger: 'bg-red-500',
66
+ info: 'bg-blue-500',
67
+ };
68
+
69
+ // ============================================
70
+ // UTILITIES
71
+ // ============================================
72
+
73
+ function clamp(value: number, min: number, max: number): number {
74
+ return Math.min(Math.max(value, min), max);
75
+ }
76
+
77
+ // ============================================
78
+ // METER COMPONENT
79
+ // ============================================
80
+
81
+ /**
82
+ * Meters represent a quantity within a known range, or a fractional value.
83
+ * Unlike progress bars, meters represent a current value rather than progress toward a goal.
84
+ *
85
+ * @example
86
+ * ```tsx
87
+ * // Storage usage meter
88
+ * <Meter value={75} label="Storage space" valueLabel="75 GB of 100 GB" />
89
+ *
90
+ * // Battery level
91
+ * <Meter value={25} variant="warning" label="Battery" />
92
+ *
93
+ * // CPU usage with dynamic color
94
+ * <Meter value={cpuUsage} variant={cpuUsage > 80 ? 'danger' : 'success'} label="CPU" />
95
+ * ```
96
+ */
97
+ export function Meter(props: MeterProps): JSX.Element {
98
+ const [local, ariaProps] = splitProps(props, [
99
+ 'size',
100
+ 'variant',
101
+ 'label',
102
+ 'showValueLabel',
103
+ 'class',
104
+ ]);
105
+
106
+ const size = () => local.size ?? 'md';
107
+ const variant = () => local.variant ?? 'primary';
108
+ const showValueLabel = () => local.showValueLabel ?? true;
109
+
110
+ // Create meter aria props
111
+ const meterAria = createMeter({
112
+ get value() { return ariaProps.value; },
113
+ get minValue() { return ariaProps.minValue; },
114
+ get maxValue() { return ariaProps.maxValue; },
115
+ get valueLabel() { return ariaProps.valueLabel; },
116
+ get label() { return local.label; },
117
+ get 'aria-label'() { return ariaProps['aria-label']; },
118
+ });
119
+
120
+ // Calculate percentage
121
+ const percentage = createMemo(() => {
122
+ const value = ariaProps.value ?? 0;
123
+ const minValue = ariaProps.minValue ?? 0;
124
+ const maxValue = ariaProps.maxValue ?? 100;
125
+ const clampedValue = clamp(value, minValue, maxValue);
126
+ return ((clampedValue - minValue) / (maxValue - minValue)) * 100;
127
+ });
128
+
129
+ // Get value text from aria props
130
+ const valueText = () => meterAria.meterProps['aria-valuetext'] as string | undefined;
131
+
132
+ const sizeConfig = () => sizeStyles[size()];
133
+
134
+ return (
135
+ <div
136
+ {...meterAria.meterProps}
137
+ class={`w-full ${local.class ?? ''}`}
138
+ >
139
+ {/* Label and value row */}
140
+ <Show when={local.label || showValueLabel()}>
141
+ <div class={`flex justify-between items-center mb-1 ${sizeConfig().text}`}>
142
+ <Show when={local.label}>
143
+ <span class="text-primary-200 font-medium">{local.label}</span>
144
+ </Show>
145
+ <Show when={showValueLabel()}>
146
+ <span class="text-primary-300">{valueText()}</span>
147
+ </Show>
148
+ </div>
149
+ </Show>
150
+
151
+ {/* Track */}
152
+ <div class={`w-full ${sizeConfig().track} bg-bg-300 rounded-full overflow-hidden`}>
153
+ {/* Fill */}
154
+ <div
155
+ class={`h-full rounded-full transition-all duration-300 ${variantStyles[variant()]}`}
156
+ style={{
157
+ width: `${percentage()}%`,
158
+ }}
159
+ />
160
+ </div>
161
+ </div>
162
+ );
163
+ }