@proyecto-viviana/ui 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components.css +1077 -1077
- package/dist/index.js +236 -249
- package/dist/index.js.map +3 -3
- package/dist/index.ssr.js +78 -81
- package/dist/index.ssr.js.map +3 -3
- package/dist/radio/index.d.ts +12 -27
- package/dist/radio/index.d.ts.map +1 -1
- package/dist/test-utils/index.d.ts +2 -2
- package/dist/test-utils/index.d.ts.map +1 -1
- package/package.json +13 -12
- package/src/alert/index.tsx +48 -0
- package/src/assets/favicon.png +0 -0
- package/src/assets/fire.gif +0 -0
- package/src/autocomplete/index.tsx +313 -0
- package/src/avatar/index.tsx +75 -0
- package/src/badge/index.tsx +43 -0
- package/src/breadcrumbs/index.tsx +207 -0
- package/src/button/Button.tsx +74 -0
- package/src/button/index.ts +2 -0
- package/src/button/types.ts +24 -0
- package/src/calendar/DateField.tsx +200 -0
- package/src/calendar/DatePicker.tsx +298 -0
- package/src/calendar/RangeCalendar.tsx +236 -0
- package/src/calendar/TimeField.tsx +196 -0
- package/src/calendar/index.tsx +223 -0
- package/src/checkbox/index.tsx +257 -0
- package/src/color/index.tsx +687 -0
- package/src/combobox/index.tsx +383 -0
- package/src/components.css +1077 -0
- package/src/custom/calendar-card/index.tsx +66 -0
- package/src/custom/chip/index.tsx +46 -0
- package/src/custom/conversation/index.tsx +105 -0
- package/src/custom/event-card/index.tsx +132 -0
- package/src/custom/header/index.tsx +33 -0
- package/src/custom/lateral-nav/index.tsx +88 -0
- package/src/custom/logo/index.tsx +58 -0
- package/src/custom/nav-header/index.tsx +42 -0
- package/src/custom/page-layout/index.tsx +29 -0
- package/src/custom/profile-card/index.tsx +64 -0
- package/src/custom/project-card/index.tsx +59 -0
- package/src/custom/timeline-item/index.tsx +105 -0
- package/src/dialog/Dialog.tsx +260 -0
- package/src/dialog/index.tsx +3 -0
- package/src/disclosure/index.tsx +307 -0
- package/src/gridlist/index.tsx +403 -0
- package/src/icon/icons/GitHubIcon.tsx +20 -0
- package/src/icon/index.tsx +48 -0
- package/src/index.ts +322 -0
- package/src/landmark/index.tsx +231 -0
- package/src/link/index.tsx +130 -0
- package/src/listbox/index.tsx +231 -0
- package/src/menu/index.tsx +297 -0
- package/src/meter/index.tsx +163 -0
- package/src/numberfield/index.tsx +482 -0
- package/src/popover/index.tsx +260 -0
- package/src/progress-bar/index.tsx +169 -0
- package/src/radio/index.tsx +173 -0
- package/src/searchfield/index.tsx +453 -0
- package/src/select/index.tsx +349 -0
- package/src/separator/index.tsx +141 -0
- package/src/slider/index.tsx +382 -0
- package/src/styles.css +450 -0
- package/src/switch/ToggleSwitch.tsx +112 -0
- package/src/switch/index.tsx +90 -0
- package/src/table/index.tsx +531 -0
- package/src/tabs/index.tsx +273 -0
- package/src/tag-group/index.tsx +240 -0
- package/src/test-utils/index.ts +40 -0
- package/src/textfield/index.tsx +211 -0
- package/src/theme.css +101 -0
- package/src/toast/index.tsx +324 -0
- package/src/toolbar/index.tsx +108 -0
- package/src/tooltip/index.tsx +197 -0
- package/src/tree/index.tsx +494 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Popover component for proyecto-viviana-ui
|
|
3
|
+
*
|
|
4
|
+
* A popover displays content in an overlay positioned relative to a trigger.
|
|
5
|
+
* Built on top of solidaria-components for accessibility.
|
|
6
|
+
* Follows Spectrum 2 design patterns.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { type JSX, Show, splitProps } from 'solid-js'
|
|
10
|
+
import {
|
|
11
|
+
Popover as HeadlessPopover,
|
|
12
|
+
PopoverTrigger as HeadlessPopoverTrigger,
|
|
13
|
+
OverlayArrow as HeadlessOverlayArrow,
|
|
14
|
+
type PopoverProps as HeadlessPopoverProps,
|
|
15
|
+
type PopoverTriggerProps as HeadlessPopoverTriggerProps,
|
|
16
|
+
type PopoverRenderProps,
|
|
17
|
+
} from '@proyecto-viviana/solidaria-components'
|
|
18
|
+
import type { Placement, PlacementAxis } from '@proyecto-viviana/solidaria'
|
|
19
|
+
|
|
20
|
+
// ============================================
|
|
21
|
+
// TYPES
|
|
22
|
+
// ============================================
|
|
23
|
+
|
|
24
|
+
export type PopoverPlacement = Placement
|
|
25
|
+
export type PopoverSize = 'sm' | 'md' | 'lg'
|
|
26
|
+
|
|
27
|
+
export interface PopoverTriggerProps extends HeadlessPopoverTriggerProps {
|
|
28
|
+
/** The children of the popover trigger (trigger element and popover). */
|
|
29
|
+
children: JSX.Element
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PopoverProps extends Omit<HeadlessPopoverProps, 'class' | 'style' | 'children'> {
|
|
33
|
+
/** The content of the popover. */
|
|
34
|
+
children: JSX.Element
|
|
35
|
+
/** The position of the popover relative to the trigger. */
|
|
36
|
+
placement?: PopoverPlacement
|
|
37
|
+
/** Size variant of the popover. */
|
|
38
|
+
size?: PopoverSize
|
|
39
|
+
/** Additional CSS class name. */
|
|
40
|
+
class?: string
|
|
41
|
+
/** Whether to show an arrow pointing to the trigger. */
|
|
42
|
+
showArrow?: boolean
|
|
43
|
+
/** Custom padding inside the popover. */
|
|
44
|
+
padding?: 'none' | 'sm' | 'md' | 'lg'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ============================================
|
|
48
|
+
// STYLES
|
|
49
|
+
// ============================================
|
|
50
|
+
|
|
51
|
+
const baseStyles = [
|
|
52
|
+
'bg-bg-300',
|
|
53
|
+
'rounded-lg',
|
|
54
|
+
'shadow-xl',
|
|
55
|
+
'border border-primary-700',
|
|
56
|
+
'text-primary-200',
|
|
57
|
+
'outline-none',
|
|
58
|
+
// Animation
|
|
59
|
+
'animate-in fade-in-0 zoom-in-95',
|
|
60
|
+
'data-[placement=top]:slide-in-from-bottom-2',
|
|
61
|
+
'data-[placement=bottom]:slide-in-from-top-2',
|
|
62
|
+
'data-[placement=left]:slide-in-from-right-2',
|
|
63
|
+
'data-[placement=right]:slide-in-from-left-2',
|
|
64
|
+
'data-[exiting]:animate-out data-[exiting]:fade-out-0 data-[exiting]:zoom-out-95',
|
|
65
|
+
].join(' ')
|
|
66
|
+
|
|
67
|
+
const sizeStyles: Record<PopoverSize, string> = {
|
|
68
|
+
sm: 'max-w-xs',
|
|
69
|
+
md: 'max-w-sm',
|
|
70
|
+
lg: 'max-w-lg',
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const paddingStyles: Record<string, string> = {
|
|
74
|
+
none: '',
|
|
75
|
+
sm: 'p-2',
|
|
76
|
+
md: 'p-4',
|
|
77
|
+
lg: 'p-6',
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Arrow styles based on placement
|
|
81
|
+
const arrowBaseStyles = [
|
|
82
|
+
'fill-bg-300',
|
|
83
|
+
'stroke-primary-700',
|
|
84
|
+
'stroke-1',
|
|
85
|
+
].join(' ')
|
|
86
|
+
|
|
87
|
+
// Arrow positioning for each placement axis
|
|
88
|
+
const getArrowRotation = (placement: PlacementAxis | null): string => {
|
|
89
|
+
switch (placement) {
|
|
90
|
+
case 'top':
|
|
91
|
+
return 'rotate-180'
|
|
92
|
+
case 'bottom':
|
|
93
|
+
return ''
|
|
94
|
+
case 'left':
|
|
95
|
+
return 'rotate-90'
|
|
96
|
+
case 'right':
|
|
97
|
+
return '-rotate-90'
|
|
98
|
+
default:
|
|
99
|
+
return ''
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ============================================
|
|
104
|
+
// COMPONENTS
|
|
105
|
+
// ============================================
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* PopoverTrigger wraps around a trigger element and a Popover.
|
|
109
|
+
* It handles opening and closing the Popover when the user interacts
|
|
110
|
+
* with the trigger.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```tsx
|
|
114
|
+
* <PopoverTrigger>
|
|
115
|
+
* <Button>Open Popover</Button>
|
|
116
|
+
* <Popover>
|
|
117
|
+
* <p>Popover content here!</p>
|
|
118
|
+
* </Popover>
|
|
119
|
+
* </PopoverTrigger>
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
export function PopoverTrigger(props: PopoverTriggerProps): JSX.Element {
|
|
123
|
+
return <HeadlessPopoverTrigger {...props} />
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Styled popover component that displays content in an overlay.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```tsx
|
|
131
|
+
* <PopoverTrigger>
|
|
132
|
+
* <Button>Settings</Button>
|
|
133
|
+
* <Popover placement="bottom" size="md">
|
|
134
|
+
* <h3>Settings</h3>
|
|
135
|
+
* <p>Configure your preferences here.</p>
|
|
136
|
+
* </Popover>
|
|
137
|
+
* </PopoverTrigger>
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export function Popover(props: PopoverProps): JSX.Element {
|
|
141
|
+
const [local, rest] = splitProps(props, [
|
|
142
|
+
'placement',
|
|
143
|
+
'size',
|
|
144
|
+
'class',
|
|
145
|
+
'showArrow',
|
|
146
|
+
'padding',
|
|
147
|
+
])
|
|
148
|
+
|
|
149
|
+
const placement = () => local.placement ?? 'bottom'
|
|
150
|
+
const size = () => local.size ?? 'md'
|
|
151
|
+
const padding = () => local.padding ?? 'md'
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<HeadlessPopover
|
|
155
|
+
{...rest}
|
|
156
|
+
placement={placement()}
|
|
157
|
+
class={(_renderProps: PopoverRenderProps) => {
|
|
158
|
+
const classes = [
|
|
159
|
+
baseStyles,
|
|
160
|
+
sizeStyles[size()],
|
|
161
|
+
paddingStyles[padding()],
|
|
162
|
+
local.class ?? '',
|
|
163
|
+
].filter(Boolean).join(' ')
|
|
164
|
+
return classes
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
{(renderProps: PopoverRenderProps) => (
|
|
168
|
+
<>
|
|
169
|
+
<Show when={local.showArrow}>
|
|
170
|
+
<PopoverArrow placement={renderProps.placement} />
|
|
171
|
+
</Show>
|
|
172
|
+
{props.children}
|
|
173
|
+
</>
|
|
174
|
+
)}
|
|
175
|
+
</HeadlessPopover>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Arrow component for the popover.
|
|
181
|
+
* Automatically positions itself based on the popover placement.
|
|
182
|
+
*/
|
|
183
|
+
interface PopoverArrowProps {
|
|
184
|
+
/** The current placement axis. */
|
|
185
|
+
placement: PlacementAxis | null
|
|
186
|
+
/** Additional CSS class. */
|
|
187
|
+
class?: string
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function PopoverArrow(props: PopoverArrowProps): JSX.Element {
|
|
191
|
+
return (
|
|
192
|
+
<HeadlessOverlayArrow
|
|
193
|
+
class="absolute block"
|
|
194
|
+
style={{
|
|
195
|
+
// Position based on placement
|
|
196
|
+
...(props.placement === 'top' && { bottom: '100%', left: '50%', transform: 'translateX(-50%)' }),
|
|
197
|
+
...(props.placement === 'bottom' && { top: '-8px', left: '50%', transform: 'translateX(-50%)' }),
|
|
198
|
+
...(props.placement === 'left' && { right: '100%', top: '50%', transform: 'translateY(-50%)' }),
|
|
199
|
+
...(props.placement === 'right' && { left: '-8px', top: '50%', transform: 'translateY(-50%)' }),
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
<svg
|
|
203
|
+
width="12"
|
|
204
|
+
height="12"
|
|
205
|
+
viewBox="0 0 12 12"
|
|
206
|
+
class={`${arrowBaseStyles} ${getArrowRotation(props.placement)} ${props.class ?? ''}`}
|
|
207
|
+
>
|
|
208
|
+
<path d="M0 0 L6 6 L12 0" />
|
|
209
|
+
</svg>
|
|
210
|
+
</HeadlessOverlayArrow>
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ============================================
|
|
215
|
+
// POPOVER CONTENT SECTIONS
|
|
216
|
+
// ============================================
|
|
217
|
+
|
|
218
|
+
export interface PopoverHeaderProps {
|
|
219
|
+
/** The title of the popover. */
|
|
220
|
+
title: string
|
|
221
|
+
/** Optional description text. */
|
|
222
|
+
description?: string
|
|
223
|
+
/** Additional CSS class. */
|
|
224
|
+
class?: string
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Header section for popover with title and optional description.
|
|
229
|
+
*/
|
|
230
|
+
export function PopoverHeader(props: PopoverHeaderProps): JSX.Element {
|
|
231
|
+
return (
|
|
232
|
+
<div class={`mb-3 ${props.class ?? ''}`}>
|
|
233
|
+
<h3 class="text-lg font-semibold text-primary-100">{props.title}</h3>
|
|
234
|
+
<Show when={props.description}>
|
|
235
|
+
<p class="text-sm text-primary-400 mt-1">{props.description}</p>
|
|
236
|
+
</Show>
|
|
237
|
+
</div>
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface PopoverFooterProps {
|
|
242
|
+
/** Footer content, typically buttons. */
|
|
243
|
+
children: JSX.Element
|
|
244
|
+
/** Additional CSS class. */
|
|
245
|
+
class?: string
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Footer section for popover actions.
|
|
250
|
+
*/
|
|
251
|
+
export function PopoverFooter(props: PopoverFooterProps): JSX.Element {
|
|
252
|
+
return (
|
|
253
|
+
<div class={`flex gap-2 justify-end mt-4 pt-3 border-t border-primary-700 ${props.class ?? ''}`}>
|
|
254
|
+
{props.children}
|
|
255
|
+
</div>
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Re-export types
|
|
260
|
+
export type { PopoverRenderProps, Placement, PlacementAxis }
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProgressBar component for proyecto-viviana-ui
|
|
3
|
+
*
|
|
4
|
+
* Styled progress bar component built on top of the solidaria hook directly.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { type JSX, splitProps, Show, createMemo } from 'solid-js';
|
|
8
|
+
import { createProgressBar } from '@proyecto-viviana/solidaria';
|
|
9
|
+
|
|
10
|
+
// ============================================
|
|
11
|
+
// TYPES
|
|
12
|
+
// ============================================
|
|
13
|
+
|
|
14
|
+
export type ProgressBarSize = 'sm' | 'md' | 'lg';
|
|
15
|
+
export type ProgressBarVariant = 'primary' | 'accent' | 'success' | 'warning' | 'danger';
|
|
16
|
+
|
|
17
|
+
export interface ProgressBarProps {
|
|
18
|
+
/** The current value (controlled). @default 0 */
|
|
19
|
+
value?: number;
|
|
20
|
+
/** The smallest value allowed. @default 0 */
|
|
21
|
+
minValue?: number;
|
|
22
|
+
/** The largest value allowed. @default 100 */
|
|
23
|
+
maxValue?: number;
|
|
24
|
+
/** The content to display as the value's label (e.g. "1 of 4"). */
|
|
25
|
+
valueLabel?: string;
|
|
26
|
+
/** Whether presentation is indeterminate when progress isn't known. */
|
|
27
|
+
isIndeterminate?: boolean;
|
|
28
|
+
/** The size of the progress bar. @default 'md' */
|
|
29
|
+
size?: ProgressBarSize;
|
|
30
|
+
/** The visual style variant. @default 'primary' */
|
|
31
|
+
variant?: ProgressBarVariant;
|
|
32
|
+
/** The label to display above the progress bar. */
|
|
33
|
+
label?: string;
|
|
34
|
+
/** Whether to show the value text. @default true for determinate progress */
|
|
35
|
+
showValueLabel?: boolean;
|
|
36
|
+
/** Additional CSS class name. */
|
|
37
|
+
class?: string;
|
|
38
|
+
/** An accessibility label. */
|
|
39
|
+
'aria-label'?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ============================================
|
|
43
|
+
// STYLES
|
|
44
|
+
// ============================================
|
|
45
|
+
|
|
46
|
+
const sizeStyles = {
|
|
47
|
+
sm: {
|
|
48
|
+
track: 'h-1',
|
|
49
|
+
text: 'text-xs',
|
|
50
|
+
},
|
|
51
|
+
md: {
|
|
52
|
+
track: 'h-2',
|
|
53
|
+
text: 'text-sm',
|
|
54
|
+
},
|
|
55
|
+
lg: {
|
|
56
|
+
track: 'h-3',
|
|
57
|
+
text: 'text-base',
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const variantStyles = {
|
|
62
|
+
primary: 'bg-primary-500',
|
|
63
|
+
accent: 'bg-accent',
|
|
64
|
+
success: 'bg-green-500',
|
|
65
|
+
warning: 'bg-yellow-500',
|
|
66
|
+
danger: 'bg-red-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
|
+
// PROGRESSBAR COMPONENT
|
|
79
|
+
// ============================================
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Progress bars show either determinate or indeterminate progress of an operation
|
|
83
|
+
* over time.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```tsx
|
|
87
|
+
* <ProgressBar value={50} label="Loading..." />
|
|
88
|
+
*
|
|
89
|
+
* // Indeterminate
|
|
90
|
+
* <ProgressBar isIndeterminate label="Processing..." />
|
|
91
|
+
*
|
|
92
|
+
* // Different variants
|
|
93
|
+
* <ProgressBar value={75} variant="success" />
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
export function ProgressBar(props: ProgressBarProps): JSX.Element {
|
|
97
|
+
const [local, ariaProps] = splitProps(props, [
|
|
98
|
+
'size',
|
|
99
|
+
'variant',
|
|
100
|
+
'label',
|
|
101
|
+
'showValueLabel',
|
|
102
|
+
'class',
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
const size = () => local.size ?? 'md';
|
|
106
|
+
const variant = () => local.variant ?? 'primary';
|
|
107
|
+
const isIndeterminate = () => ariaProps.isIndeterminate ?? false;
|
|
108
|
+
const showValueLabel = () => local.showValueLabel ?? !isIndeterminate();
|
|
109
|
+
|
|
110
|
+
// Create progress bar aria props
|
|
111
|
+
const progressAria = createProgressBar({
|
|
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 isIndeterminate() { return ariaProps.isIndeterminate; },
|
|
117
|
+
get label() { return local.label; },
|
|
118
|
+
get 'aria-label'() { return ariaProps['aria-label']; },
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Calculate percentage
|
|
122
|
+
const percentage = createMemo(() => {
|
|
123
|
+
if (isIndeterminate()) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
const value = ariaProps.value ?? 0;
|
|
127
|
+
const minValue = ariaProps.minValue ?? 0;
|
|
128
|
+
const maxValue = ariaProps.maxValue ?? 100;
|
|
129
|
+
const clampedValue = clamp(value, minValue, maxValue);
|
|
130
|
+
return ((clampedValue - minValue) / (maxValue - minValue)) * 100;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Get value text from aria props
|
|
134
|
+
const valueText = () => progressAria.progressBarProps['aria-valuetext'] as string | undefined;
|
|
135
|
+
|
|
136
|
+
const sizeConfig = () => sizeStyles[size()];
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div
|
|
140
|
+
{...progressAria.progressBarProps}
|
|
141
|
+
class={`w-full ${local.class ?? ''}`}
|
|
142
|
+
>
|
|
143
|
+
{/* Label and value row */}
|
|
144
|
+
<Show when={local.label || showValueLabel()}>
|
|
145
|
+
<div class={`flex justify-between items-center mb-1 ${sizeConfig().text}`}>
|
|
146
|
+
<Show when={local.label}>
|
|
147
|
+
<span class="text-primary-200 font-medium">{local.label}</span>
|
|
148
|
+
</Show>
|
|
149
|
+
<Show when={showValueLabel() && !isIndeterminate()}>
|
|
150
|
+
<span class="text-primary-300">{valueText()}</span>
|
|
151
|
+
</Show>
|
|
152
|
+
</div>
|
|
153
|
+
</Show>
|
|
154
|
+
|
|
155
|
+
{/* Track */}
|
|
156
|
+
<div class={`w-full ${sizeConfig().track} bg-bg-300 rounded-full overflow-hidden`}>
|
|
157
|
+
{/* Fill */}
|
|
158
|
+
<div
|
|
159
|
+
class={`h-full rounded-full transition-all duration-300 ${variantStyles[variant()]} ${
|
|
160
|
+
isIndeterminate() ? 'animate-progress-indeterminate' : ''
|
|
161
|
+
}`}
|
|
162
|
+
style={{
|
|
163
|
+
width: isIndeterminate() ? '30%' : `${percentage()}%`,
|
|
164
|
+
}}
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RadioGroup and Radio components for proyecto-viviana-ui
|
|
3
|
+
*
|
|
4
|
+
* Styled radio components built on top of solidaria-components.
|
|
5
|
+
* SSR-compatible - renders children and UI elements directly without render props.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type JSX, Show, createContext, useContext, splitProps } from 'solid-js'
|
|
9
|
+
import {
|
|
10
|
+
RadioGroup as HeadlessRadioGroup,
|
|
11
|
+
Radio as HeadlessRadio,
|
|
12
|
+
type RadioGroupProps as HeadlessRadioGroupProps,
|
|
13
|
+
type RadioProps as HeadlessRadioProps,
|
|
14
|
+
type RadioGroupRenderProps,
|
|
15
|
+
type RadioRenderProps,
|
|
16
|
+
} from '@proyecto-viviana/solidaria-components'
|
|
17
|
+
|
|
18
|
+
// ============================================
|
|
19
|
+
// SIZE CONTEXT
|
|
20
|
+
// ============================================
|
|
21
|
+
|
|
22
|
+
export type RadioGroupOrientation = 'horizontal' | 'vertical'
|
|
23
|
+
export type RadioGroupSize = 'sm' | 'md' | 'lg'
|
|
24
|
+
|
|
25
|
+
const RadioSizeContext = createContext<RadioGroupSize>('md')
|
|
26
|
+
|
|
27
|
+
// ============================================
|
|
28
|
+
// TYPES
|
|
29
|
+
// ============================================
|
|
30
|
+
|
|
31
|
+
export interface RadioGroupProps extends Omit<HeadlessRadioGroupProps, 'class' | 'style'> {
|
|
32
|
+
/** The size of the radio buttons. */
|
|
33
|
+
size?: RadioGroupSize
|
|
34
|
+
/** Additional CSS class name. */
|
|
35
|
+
class?: string
|
|
36
|
+
/** Label for the group. */
|
|
37
|
+
label?: string
|
|
38
|
+
/** Description for the group. */
|
|
39
|
+
description?: string
|
|
40
|
+
/** Error message when invalid. */
|
|
41
|
+
errorMessage?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface RadioProps extends Omit<HeadlessRadioProps, 'class' | 'style'> {
|
|
45
|
+
/** Additional CSS class name. */
|
|
46
|
+
class?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================
|
|
50
|
+
// STYLES
|
|
51
|
+
// ============================================
|
|
52
|
+
|
|
53
|
+
const sizeStyles = {
|
|
54
|
+
sm: {
|
|
55
|
+
circle: 'h-4 w-4',
|
|
56
|
+
dot: 'h-2 w-2',
|
|
57
|
+
label: 'text-sm',
|
|
58
|
+
},
|
|
59
|
+
md: {
|
|
60
|
+
circle: 'h-5 w-5',
|
|
61
|
+
dot: 'h-2.5 w-2.5',
|
|
62
|
+
label: 'text-base',
|
|
63
|
+
},
|
|
64
|
+
lg: {
|
|
65
|
+
circle: 'h-6 w-6',
|
|
66
|
+
dot: 'h-3 w-3',
|
|
67
|
+
label: 'text-lg',
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================
|
|
72
|
+
// RADIO GROUP COMPONENT
|
|
73
|
+
// ============================================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A radio group allows users to select a single option from a list of mutually exclusive options.
|
|
77
|
+
*
|
|
78
|
+
* Built on solidaria-components RadioGroup for full accessibility support.
|
|
79
|
+
*/
|
|
80
|
+
export function RadioGroup(props: RadioGroupProps): JSX.Element {
|
|
81
|
+
// Split out our custom styling props from the rest
|
|
82
|
+
const [local, headlessProps] = splitProps(props, [
|
|
83
|
+
'size',
|
|
84
|
+
'class',
|
|
85
|
+
'label',
|
|
86
|
+
'description',
|
|
87
|
+
'errorMessage',
|
|
88
|
+
])
|
|
89
|
+
|
|
90
|
+
const size = local.size ?? 'md'
|
|
91
|
+
const customClass = local.class ?? ''
|
|
92
|
+
|
|
93
|
+
// Generate class based on render props
|
|
94
|
+
const getClassName = (renderProps: RadioGroupRenderProps): string => {
|
|
95
|
+
const base = 'flex gap-2'
|
|
96
|
+
const orientationClass = renderProps.orientation === 'horizontal' ? 'flex-row flex-wrap' : 'flex-col'
|
|
97
|
+
const disabledClass = renderProps.isDisabled ? 'opacity-50' : ''
|
|
98
|
+
return [base, orientationClass, disabledClass, customClass].filter(Boolean).join(' ')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Pass remaining props through to headless component
|
|
102
|
+
// headlessProps maintains reactivity for controlled values like value/onChange
|
|
103
|
+
return (
|
|
104
|
+
<RadioSizeContext.Provider value={size}>
|
|
105
|
+
<HeadlessRadioGroup
|
|
106
|
+
{...headlessProps}
|
|
107
|
+
class={getClassName}
|
|
108
|
+
data-size={size}
|
|
109
|
+
>
|
|
110
|
+
<Show when={local.label}>
|
|
111
|
+
<span class="text-primary-200 font-medium mb-1">{local.label}</span>
|
|
112
|
+
</Show>
|
|
113
|
+
{props.children as JSX.Element}
|
|
114
|
+
<Show when={local.description}>
|
|
115
|
+
<span class="text-primary-400 text-sm [&:has(~[data-invalid])]:hidden">{local.description}</span>
|
|
116
|
+
</Show>
|
|
117
|
+
<Show when={local.errorMessage}>
|
|
118
|
+
<span class="text-danger-400 text-sm hidden [[data-invalid]_&]:block">{local.errorMessage}</span>
|
|
119
|
+
</Show>
|
|
120
|
+
</HeadlessRadioGroup>
|
|
121
|
+
</RadioSizeContext.Provider>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ============================================
|
|
126
|
+
// RADIO COMPONENT
|
|
127
|
+
// ============================================
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* A radio button allows users to select a single option from a list.
|
|
131
|
+
* Must be used within a RadioGroup.
|
|
132
|
+
* SSR-compatible - renders static JSX without render prop children.
|
|
133
|
+
*
|
|
134
|
+
* Note: Unlike other styled components, Radio does not use render props for children.
|
|
135
|
+
* Instead, it relies on data attributes set by the headless Radio component for styling.
|
|
136
|
+
* However, since we need dynamic styling based on state, we accept that this component
|
|
137
|
+
* has some limitations compared to the render-props-based original implementation.
|
|
138
|
+
*
|
|
139
|
+
* Built on solidaria-components Radio for full accessibility support.
|
|
140
|
+
*/
|
|
141
|
+
export function Radio(props: RadioProps): JSX.Element {
|
|
142
|
+
const [local, headlessProps] = splitProps(props, ['class'])
|
|
143
|
+
const sizeFromContext = useContext(RadioSizeContext)
|
|
144
|
+
const sizeStyle = sizeStyles[sizeFromContext]
|
|
145
|
+
const customClass = local.class ?? ''
|
|
146
|
+
|
|
147
|
+
// Generate class based on render props
|
|
148
|
+
const getClassName = (renderProps: RadioRenderProps): string => {
|
|
149
|
+
const base = 'inline-flex items-center gap-2'
|
|
150
|
+
const cursorClass = renderProps.isDisabled ? 'cursor-not-allowed' : 'cursor-pointer'
|
|
151
|
+
const disabledClass = renderProps.isDisabled ? 'opacity-50' : ''
|
|
152
|
+
return [base, cursorClass, disabledClass, customClass].filter(Boolean).join(' ')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Static classes - will use a simplified visual style since we can't dynamically style based on state without render props
|
|
156
|
+
const circleClass = `relative flex items-center justify-center rounded-full border-2 transition-all duration-200 ${sizeStyle.circle} border-primary-600 bg-transparent hover:border-accent-300`
|
|
157
|
+
const dotClass = `rounded-full bg-accent transition-all duration-200 ${sizeStyle.dot}`
|
|
158
|
+
const labelClass = `text-primary-200 ${sizeStyle.label}`
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<HeadlessRadio
|
|
162
|
+
{...headlessProps}
|
|
163
|
+
class={getClassName}
|
|
164
|
+
>
|
|
165
|
+
<span class={circleClass}>
|
|
166
|
+
<span class={dotClass} />
|
|
167
|
+
</span>
|
|
168
|
+
<Show when={props.children}>
|
|
169
|
+
<span class={labelClass}>{props.children as JSX.Element}</span>
|
|
170
|
+
</Show>
|
|
171
|
+
</HeadlessRadio>
|
|
172
|
+
)
|
|
173
|
+
}
|