@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.
Files changed (39) hide show
  1. package/dist/index.js +2 -2
  2. package/dist/index.js.map +1 -1
  3. package/package.json +6 -5
  4. package/src/alert/index.tsx +48 -0
  5. package/src/assets/favicon.png +0 -0
  6. package/src/assets/fire.gif +0 -0
  7. package/src/avatar/index.tsx +75 -0
  8. package/src/badge/index.tsx +43 -0
  9. package/src/button/Button.tsx +75 -0
  10. package/src/button/index.ts +2 -0
  11. package/src/button/types.ts +24 -0
  12. package/src/checkbox/index.tsx +258 -0
  13. package/src/components.css +975 -0
  14. package/src/custom/calendar-card/index.tsx +66 -0
  15. package/src/custom/chip/index.tsx +32 -0
  16. package/src/custom/conversation/index.tsx +105 -0
  17. package/src/custom/event-card/index.tsx +126 -0
  18. package/src/custom/lateral-nav/index.tsx +88 -0
  19. package/src/custom/logo/index.tsx +54 -0
  20. package/src/custom/nav-header/index.tsx +42 -0
  21. package/src/custom/page-layout/index.tsx +20 -0
  22. package/src/custom/profile-card/index.tsx +58 -0
  23. package/src/custom/project-card/index.tsx +59 -0
  24. package/src/custom/timeline-item/index.tsx +81 -0
  25. package/src/dialog/index.tsx +69 -0
  26. package/src/icon/icons/GitHubIcon.tsx +20 -0
  27. package/src/icon/index.tsx +48 -0
  28. package/src/index.ts +107 -0
  29. package/src/link/index.tsx +131 -0
  30. package/src/progress-bar/index.tsx +169 -0
  31. package/src/radio/index.tsx +219 -0
  32. package/src/separator/index.tsx +141 -0
  33. package/src/styles.css +450 -0
  34. package/src/switch/ToggleSwitch.tsx +113 -0
  35. package/src/switch/index.tsx +90 -0
  36. package/src/test-utils/index.ts +32 -0
  37. package/src/textfield/index.tsx +211 -0
  38. package/src/theme.css +101 -0
  39. 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
+ }