@proyecto-viviana/ui 0.1.0 → 0.1.2
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/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
- package/src/alert/index.tsx +48 -0
- package/src/assets/favicon.png +0 -0
- package/src/assets/fire.gif +0 -0
- package/src/avatar/index.tsx +75 -0
- package/src/badge/index.tsx +43 -0
- package/src/button/Button.tsx +75 -0
- package/src/button/index.ts +2 -0
- package/src/button/types.ts +24 -0
- package/src/checkbox/index.tsx +258 -0
- package/src/components.css +975 -0
- package/src/custom/calendar-card/index.tsx +66 -0
- package/src/custom/chip/index.tsx +32 -0
- package/src/custom/conversation/index.tsx +105 -0
- package/src/custom/event-card/index.tsx +126 -0
- package/src/custom/lateral-nav/index.tsx +88 -0
- package/src/custom/logo/index.tsx +54 -0
- package/src/custom/nav-header/index.tsx +42 -0
- package/src/custom/page-layout/index.tsx +20 -0
- package/src/custom/profile-card/index.tsx +58 -0
- package/src/custom/project-card/index.tsx +59 -0
- package/src/custom/timeline-item/index.tsx +81 -0
- package/src/dialog/index.tsx +69 -0
- package/src/icon/icons/GitHubIcon.tsx +20 -0
- package/src/icon/index.tsx +48 -0
- package/src/index.ts +107 -0
- package/src/link/index.tsx +131 -0
- package/src/progress-bar/index.tsx +169 -0
- package/src/radio/index.tsx +219 -0
- package/src/separator/index.tsx +141 -0
- package/src/styles.css +450 -0
- package/src/switch/ToggleSwitch.tsx +113 -0
- package/src/switch/index.tsx +90 -0
- package/src/test-utils/index.ts +32 -0
- package/src/textfield/index.tsx +211 -0
- package/src/theme.css +101 -0
- package/src/tooltip/index.tsx +31 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RadioGroup and Radio components for proyecto-viviana-ui
|
|
3
|
+
*
|
|
4
|
+
* Styled radio components built on top of solidaria-components.
|
|
5
|
+
*
|
|
6
|
+
* IMPORTANT: Due to SolidJS's JSX evaluation order, we cannot wrap the headless
|
|
7
|
+
* components in a traditional way. Instead, we re-export them and provide
|
|
8
|
+
* styling through the class prop functions.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { type JSX, Show, createContext, useContext, splitProps } from 'solid-js'
|
|
12
|
+
import {
|
|
13
|
+
RadioGroup as HeadlessRadioGroup,
|
|
14
|
+
Radio as HeadlessRadio,
|
|
15
|
+
type RadioGroupProps as HeadlessRadioGroupProps,
|
|
16
|
+
type RadioProps as HeadlessRadioProps,
|
|
17
|
+
type RadioGroupRenderProps,
|
|
18
|
+
type RadioRenderProps,
|
|
19
|
+
} from '@proyecto-viviana/solidaria-components'
|
|
20
|
+
|
|
21
|
+
// ============================================
|
|
22
|
+
// SIZE CONTEXT
|
|
23
|
+
// ============================================
|
|
24
|
+
|
|
25
|
+
export type RadioGroupOrientation = 'horizontal' | 'vertical'
|
|
26
|
+
export type RadioGroupSize = 'sm' | 'md' | 'lg'
|
|
27
|
+
|
|
28
|
+
const RadioSizeContext = createContext<RadioGroupSize>('md')
|
|
29
|
+
|
|
30
|
+
// ============================================
|
|
31
|
+
// TYPES
|
|
32
|
+
// ============================================
|
|
33
|
+
|
|
34
|
+
export interface RadioGroupProps extends Omit<HeadlessRadioGroupProps, 'class' | 'style'> {
|
|
35
|
+
/** The size of the radio buttons. */
|
|
36
|
+
size?: RadioGroupSize
|
|
37
|
+
/** Additional CSS class name. */
|
|
38
|
+
class?: string
|
|
39
|
+
/** Label for the group. */
|
|
40
|
+
label?: string
|
|
41
|
+
/** Description for the group. */
|
|
42
|
+
description?: string
|
|
43
|
+
/** Error message when invalid. */
|
|
44
|
+
errorMessage?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RadioProps extends Omit<HeadlessRadioProps, 'class' | 'style'> {
|
|
48
|
+
/** Additional CSS class name. */
|
|
49
|
+
class?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ============================================
|
|
53
|
+
// STYLES
|
|
54
|
+
// ============================================
|
|
55
|
+
|
|
56
|
+
const sizeStyles = {
|
|
57
|
+
sm: {
|
|
58
|
+
circle: 'h-4 w-4',
|
|
59
|
+
dot: 'h-2 w-2',
|
|
60
|
+
label: 'text-sm',
|
|
61
|
+
},
|
|
62
|
+
md: {
|
|
63
|
+
circle: 'h-5 w-5',
|
|
64
|
+
dot: 'h-2.5 w-2.5',
|
|
65
|
+
label: 'text-base',
|
|
66
|
+
},
|
|
67
|
+
lg: {
|
|
68
|
+
circle: 'h-6 w-6',
|
|
69
|
+
dot: 'h-3 w-3',
|
|
70
|
+
label: 'text-lg',
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ============================================
|
|
75
|
+
// RADIO GROUP COMPONENT
|
|
76
|
+
// ============================================
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A radio group allows users to select a single option from a list of mutually exclusive options.
|
|
80
|
+
*
|
|
81
|
+
* Built on solidaria-components RadioGroup for full accessibility support.
|
|
82
|
+
*/
|
|
83
|
+
export function RadioGroup(props: RadioGroupProps): JSX.Element {
|
|
84
|
+
// Split out our custom styling props from the rest
|
|
85
|
+
const [local, headlessProps] = splitProps(props, [
|
|
86
|
+
'size',
|
|
87
|
+
'class',
|
|
88
|
+
'label',
|
|
89
|
+
'description',
|
|
90
|
+
'errorMessage',
|
|
91
|
+
'children',
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
const size = local.size ?? 'md'
|
|
95
|
+
const customClass = local.class ?? ''
|
|
96
|
+
|
|
97
|
+
// Generate class based on render props
|
|
98
|
+
const getClassName = (renderProps: RadioGroupRenderProps): string => {
|
|
99
|
+
const base = 'flex gap-2'
|
|
100
|
+
const orientationClass = renderProps.orientation === 'horizontal' ? 'flex-row flex-wrap' : 'flex-col'
|
|
101
|
+
const disabledClass = renderProps.isDisabled ? 'opacity-50' : ''
|
|
102
|
+
return [base, orientationClass, disabledClass, customClass].filter(Boolean).join(' ')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Create render children function
|
|
106
|
+
const renderChildren = (renderProps: RadioGroupRenderProps) => (
|
|
107
|
+
<>
|
|
108
|
+
<Show when={local.label}>
|
|
109
|
+
<span class="text-primary-200 font-medium mb-1">{local.label}</span>
|
|
110
|
+
</Show>
|
|
111
|
+
{typeof local.children === 'function' ? local.children(renderProps) : local.children}
|
|
112
|
+
<Show when={local.description && !renderProps.isInvalid}>
|
|
113
|
+
<span class="text-primary-400 text-sm">{local.description}</span>
|
|
114
|
+
</Show>
|
|
115
|
+
<Show when={local.errorMessage && renderProps.isInvalid}>
|
|
116
|
+
<span class="text-danger-400 text-sm">{local.errorMessage}</span>
|
|
117
|
+
</Show>
|
|
118
|
+
</>
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
// Pass remaining props through to headless component
|
|
122
|
+
// headlessProps maintains reactivity for controlled values like value/onChange
|
|
123
|
+
return (
|
|
124
|
+
<RadioSizeContext.Provider value={size}>
|
|
125
|
+
<HeadlessRadioGroup
|
|
126
|
+
{...headlessProps}
|
|
127
|
+
class={getClassName}
|
|
128
|
+
data-size={size}
|
|
129
|
+
children={renderChildren as any}
|
|
130
|
+
/>
|
|
131
|
+
</RadioSizeContext.Provider>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============================================
|
|
136
|
+
// RADIO COMPONENT
|
|
137
|
+
// ============================================
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* A radio button allows users to select a single option from a list.
|
|
141
|
+
* Must be used within a RadioGroup.
|
|
142
|
+
*
|
|
143
|
+
* Built on solidaria-components Radio for full accessibility support.
|
|
144
|
+
*/
|
|
145
|
+
export function Radio(props: RadioProps): JSX.Element {
|
|
146
|
+
const sizeFromContext = useContext(RadioSizeContext)
|
|
147
|
+
const size = () => sizeStyles[sizeFromContext]
|
|
148
|
+
const customClass = props.class ?? ''
|
|
149
|
+
|
|
150
|
+
// Generate class based on render props
|
|
151
|
+
const getClassName = (renderProps: RadioRenderProps): string => {
|
|
152
|
+
const base = 'inline-flex items-center gap-2 cursor-pointer'
|
|
153
|
+
const disabledClass = renderProps.isDisabled ? 'cursor-not-allowed opacity-50' : ''
|
|
154
|
+
return [base, disabledClass, customClass].filter(Boolean).join(' ')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Extract props to pass to headless
|
|
158
|
+
const { class: _, children, ...headlessProps } = props
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<HeadlessRadio
|
|
162
|
+
{...headlessProps}
|
|
163
|
+
class={getClassName}
|
|
164
|
+
>
|
|
165
|
+
{(renderProps: RadioRenderProps) => {
|
|
166
|
+
const circleClasses = () => {
|
|
167
|
+
const base = 'relative flex items-center justify-center rounded-full border-2 transition-all duration-200'
|
|
168
|
+
const sizeClass = size().circle
|
|
169
|
+
|
|
170
|
+
let colorClass: string
|
|
171
|
+
if (renderProps.isDisabled) {
|
|
172
|
+
colorClass = 'border-bg-300 bg-bg-200'
|
|
173
|
+
} else if (renderProps.isSelected) {
|
|
174
|
+
colorClass = 'border-accent bg-transparent'
|
|
175
|
+
} else {
|
|
176
|
+
colorClass = 'border-primary-600 bg-transparent hover:border-accent-300'
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const focusClass = renderProps.isFocusVisible
|
|
180
|
+
? 'ring-2 ring-accent-300 ring-offset-2 ring-offset-bg-400'
|
|
181
|
+
: ''
|
|
182
|
+
const cursorClass = renderProps.isDisabled ? 'cursor-not-allowed' : 'cursor-pointer'
|
|
183
|
+
|
|
184
|
+
return [base, sizeClass, colorClass, focusClass, cursorClass].filter(Boolean).join(' ')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const dotClasses = () => {
|
|
188
|
+
const base = 'rounded-full bg-accent transition-all duration-200'
|
|
189
|
+
const sizeClass = size().dot
|
|
190
|
+
const visibilityClass = renderProps.isSelected ? 'scale-100 opacity-100' : 'scale-0 opacity-0'
|
|
191
|
+
|
|
192
|
+
return [base, sizeClass, visibilityClass].filter(Boolean).join(' ')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const labelClasses = () => {
|
|
196
|
+
const base = 'text-primary-200'
|
|
197
|
+
const sizeClass = size().label
|
|
198
|
+
const disabledClass = renderProps.isDisabled ? 'opacity-50' : ''
|
|
199
|
+
|
|
200
|
+
return [base, sizeClass, disabledClass].filter(Boolean).join(' ')
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Resolve children - could be a function or JSX element
|
|
204
|
+
const resolvedChildren = typeof children === 'function' ? children(renderProps) : children
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<>
|
|
208
|
+
<span class={circleClasses()}>
|
|
209
|
+
<span class={dotClasses()} />
|
|
210
|
+
</span>
|
|
211
|
+
<Show when={resolvedChildren}>
|
|
212
|
+
<span class={labelClasses()}>{resolvedChildren}</span>
|
|
213
|
+
</Show>
|
|
214
|
+
</>
|
|
215
|
+
)
|
|
216
|
+
}}
|
|
217
|
+
</HeadlessRadio>
|
|
218
|
+
)
|
|
219
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Separator component for proyecto-viviana-ui
|
|
3
|
+
*
|
|
4
|
+
* Styled separator component built on top of solidaria hook directly.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { type JSX, splitProps, createMemo, Show } from 'solid-js';
|
|
8
|
+
import { createSeparator, type Orientation } from '@proyecto-viviana/solidaria';
|
|
9
|
+
|
|
10
|
+
// ============================================
|
|
11
|
+
// TYPES
|
|
12
|
+
// ============================================
|
|
13
|
+
|
|
14
|
+
export type SeparatorVariant = 'default' | 'subtle' | 'strong';
|
|
15
|
+
export type SeparatorSize = 'sm' | 'md' | 'lg';
|
|
16
|
+
|
|
17
|
+
export interface SeparatorProps {
|
|
18
|
+
/** The orientation of the separator. @default 'horizontal' */
|
|
19
|
+
orientation?: Orientation;
|
|
20
|
+
/** The visual style variant. @default 'default' */
|
|
21
|
+
variant?: SeparatorVariant;
|
|
22
|
+
/** The size/thickness of the separator. @default 'md' */
|
|
23
|
+
size?: SeparatorSize;
|
|
24
|
+
/** Additional CSS class name. */
|
|
25
|
+
class?: string;
|
|
26
|
+
/** An accessibility label for the separator. */
|
|
27
|
+
'aria-label'?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ============================================
|
|
31
|
+
// STYLES
|
|
32
|
+
// ============================================
|
|
33
|
+
|
|
34
|
+
const variantStyles = {
|
|
35
|
+
default: 'bg-bg-100',
|
|
36
|
+
subtle: 'bg-bg-200',
|
|
37
|
+
strong: 'bg-primary-600',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const horizontalSizeStyles = {
|
|
41
|
+
sm: 'h-px',
|
|
42
|
+
md: 'h-0.5',
|
|
43
|
+
lg: 'h-1',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const verticalSizeStyles = {
|
|
47
|
+
sm: 'w-px',
|
|
48
|
+
md: 'w-0.5',
|
|
49
|
+
lg: 'w-1',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// ============================================
|
|
53
|
+
// SEPARATOR COMPONENT
|
|
54
|
+
// ============================================
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A separator is a visual divider between two groups of content,
|
|
58
|
+
* e.g. groups of menu items or sections of a page.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```tsx
|
|
62
|
+
* <Separator />
|
|
63
|
+
*
|
|
64
|
+
* // Vertical separator
|
|
65
|
+
* <div class="flex items-center gap-4">
|
|
66
|
+
* <span>Item 1</span>
|
|
67
|
+
* <Separator orientation="vertical" />
|
|
68
|
+
* <span>Item 2</span>
|
|
69
|
+
* </div>
|
|
70
|
+
*
|
|
71
|
+
* // Different variants
|
|
72
|
+
* <Separator variant="strong" />
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export function Separator(props: SeparatorProps): JSX.Element {
|
|
76
|
+
const [local, ariaProps] = splitProps(props, [
|
|
77
|
+
'orientation',
|
|
78
|
+
'variant',
|
|
79
|
+
'size',
|
|
80
|
+
'class',
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
const orientation = () => local.orientation ?? 'horizontal';
|
|
84
|
+
const variant = () => local.variant ?? 'default';
|
|
85
|
+
const size = () => local.size ?? 'md';
|
|
86
|
+
|
|
87
|
+
// Determine the element type
|
|
88
|
+
const elementType = createMemo(() => {
|
|
89
|
+
// If vertical, use div since hr is inherently horizontal
|
|
90
|
+
if (orientation() === 'vertical') {
|
|
91
|
+
return 'div';
|
|
92
|
+
}
|
|
93
|
+
return 'hr';
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Create separator aria props
|
|
97
|
+
const separatorAria = createSeparator({
|
|
98
|
+
get orientation() { return orientation(); },
|
|
99
|
+
get elementType() { return elementType(); },
|
|
100
|
+
get 'aria-label'() { return ariaProps['aria-label']; },
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Build class string
|
|
104
|
+
const className = createMemo(() => {
|
|
105
|
+
const isVertical = orientation() === 'vertical';
|
|
106
|
+
const sizeStyles = isVertical ? verticalSizeStyles : horizontalSizeStyles;
|
|
107
|
+
|
|
108
|
+
const base = [
|
|
109
|
+
variantStyles[variant()],
|
|
110
|
+
sizeStyles[size()],
|
|
111
|
+
isVertical ? 'h-full self-stretch' : 'w-full',
|
|
112
|
+
'border-0', // Reset hr default border
|
|
113
|
+
local.class ?? '',
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
return base.filter(Boolean).join(' ');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Extract props without ref to avoid type issues with specific element types
|
|
120
|
+
const getAriaProps = () => {
|
|
121
|
+
const { ref: _, ...props } = separatorAria.separatorProps as Record<string, unknown>;
|
|
122
|
+
return props;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<Show
|
|
127
|
+
when={orientation() === 'vertical'}
|
|
128
|
+
fallback={
|
|
129
|
+
<hr
|
|
130
|
+
{...getAriaProps()}
|
|
131
|
+
class={className()}
|
|
132
|
+
/>
|
|
133
|
+
}
|
|
134
|
+
>
|
|
135
|
+
<div
|
|
136
|
+
{...getAriaProps()}
|
|
137
|
+
class={className()}
|
|
138
|
+
/>
|
|
139
|
+
</Show>
|
|
140
|
+
);
|
|
141
|
+
}
|