@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.
- package/README.md +192 -0
- package/dist/autocomplete/index.d.ts +89 -0
- package/dist/autocomplete/index.d.ts.map +1 -0
- package/dist/breadcrumbs/index.d.ts +38 -0
- package/dist/breadcrumbs/index.d.ts.map +1 -0
- package/dist/button/Button.d.ts.map +1 -1
- package/dist/calendar/DateField.d.ts +47 -0
- package/dist/calendar/DateField.d.ts.map +1 -0
- package/dist/calendar/DatePicker.d.ts +48 -0
- package/dist/calendar/DatePicker.d.ts.map +1 -0
- package/dist/calendar/RangeCalendar.d.ts +42 -0
- package/dist/calendar/RangeCalendar.d.ts.map +1 -0
- package/dist/calendar/TimeField.d.ts +44 -0
- package/dist/calendar/TimeField.d.ts.map +1 -0
- package/dist/calendar/index.d.ts +50 -0
- package/dist/calendar/index.d.ts.map +1 -0
- package/dist/checkbox/index.d.ts.map +1 -1
- package/dist/color/index.d.ts +228 -0
- package/dist/color/index.d.ts.map +1 -0
- package/dist/combobox/index.d.ts +81 -0
- package/dist/combobox/index.d.ts.map +1 -0
- package/dist/components.css +116 -14
- package/dist/custom/chip/index.d.ts +7 -2
- package/dist/custom/chip/index.d.ts.map +1 -1
- package/dist/custom/event-card/index.d.ts +5 -1
- package/dist/custom/event-card/index.d.ts.map +1 -1
- package/dist/custom/header/index.d.ts +16 -0
- package/dist/custom/header/index.d.ts.map +1 -0
- package/dist/custom/logo/index.d.ts +2 -0
- package/dist/custom/logo/index.d.ts.map +1 -1
- package/dist/custom/page-layout/index.d.ts +2 -0
- package/dist/custom/page-layout/index.d.ts.map +1 -1
- package/dist/custom/profile-card/index.d.ts +5 -1
- package/dist/custom/profile-card/index.d.ts.map +1 -1
- package/dist/custom/timeline-item/index.d.ts +12 -2
- package/dist/custom/timeline-item/index.d.ts.map +1 -1
- package/dist/dialog/Dialog.d.ts +67 -0
- package/dist/dialog/Dialog.d.ts.map +1 -0
- package/dist/dialog/index.d.ts +2 -17
- package/dist/dialog/index.d.ts.map +1 -1
- package/dist/disclosure/index.d.ts +84 -0
- package/dist/disclosure/index.d.ts.map +1 -0
- package/dist/gridlist/index.d.ts +92 -0
- package/dist/gridlist/index.d.ts.map +1 -0
- package/dist/index.d.ts +58 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6984 -783
- package/dist/index.js.map +1 -1
- package/dist/index.ssr.js +5905 -571
- package/dist/index.ssr.js.map +1 -1
- package/dist/landmark/index.d.ts +83 -0
- package/dist/landmark/index.d.ts.map +1 -0
- package/dist/link/index.d.ts.map +1 -1
- package/dist/listbox/index.d.ts +47 -0
- package/dist/listbox/index.d.ts.map +1 -0
- package/dist/menu/index.d.ts +74 -0
- package/dist/menu/index.d.ts.map +1 -0
- package/dist/meter/index.d.ts +49 -0
- package/dist/meter/index.d.ts.map +1 -0
- package/dist/numberfield/index.d.ts +50 -0
- package/dist/numberfield/index.d.ts.map +1 -0
- package/dist/popover/index.d.ts +85 -0
- package/dist/popover/index.d.ts.map +1 -0
- package/dist/radio/index.d.ts +7 -4
- package/dist/radio/index.d.ts.map +1 -1
- package/dist/searchfield/index.d.ts +44 -0
- package/dist/searchfield/index.d.ts.map +1 -0
- package/dist/select/index.d.ts +72 -0
- package/dist/select/index.d.ts.map +1 -0
- package/dist/slider/index.d.ts +53 -0
- package/dist/slider/index.d.ts.map +1 -0
- package/dist/switch/ToggleSwitch.d.ts.map +1 -1
- package/dist/table/index.d.ts +140 -0
- package/dist/table/index.d.ts.map +1 -0
- package/dist/tabs/index.d.ts +56 -0
- package/dist/tabs/index.d.ts.map +1 -0
- package/dist/tag-group/index.d.ts +80 -0
- package/dist/tag-group/index.d.ts.map +1 -0
- package/dist/toast/index.d.ts +101 -0
- package/dist/toast/index.d.ts.map +1 -0
- package/dist/toolbar/index.d.ts +42 -0
- package/dist/toolbar/index.d.ts.map +1 -0
- package/dist/tooltip/index.d.ts +66 -5
- package/dist/tooltip/index.d.ts.map +1 -1
- package/dist/tree/index.d.ts +99 -0
- package/dist/tree/index.d.ts.map +1 -0
- package/package.json +66 -58
- package/src/autocomplete/index.tsx +313 -0
- package/src/breadcrumbs/index.tsx +207 -0
- package/src/button/Button.tsx +74 -75
- 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 +3 -4
- package/src/color/index.tsx +687 -0
- package/src/combobox/index.tsx +383 -0
- package/src/components.css +116 -14
- package/src/custom/chip/index.tsx +17 -3
- package/src/custom/event-card/index.tsx +8 -2
- package/src/custom/header/index.tsx +33 -0
- package/src/custom/logo/index.tsx +7 -3
- package/src/custom/page-layout/index.tsx +12 -3
- package/src/custom/profile-card/index.tsx +8 -2
- package/src/custom/timeline-item/index.tsx +28 -4
- package/src/dialog/Dialog.tsx +260 -0
- package/src/dialog/index.tsx +3 -69
- package/src/disclosure/index.tsx +307 -0
- package/src/gridlist/index.tsx +403 -0
- package/src/index.ts +219 -4
- package/src/landmark/index.tsx +231 -0
- package/src/link/index.tsx +1 -2
- 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/radio/index.tsx +36 -82
- package/src/searchfield/index.tsx +453 -0
- package/src/select/index.tsx +349 -0
- package/src/slider/index.tsx +382 -0
- package/src/switch/ToggleSwitch.tsx +1 -2
- package/src/table/index.tsx +531 -0
- package/src/tabs/index.tsx +273 -0
- package/src/tag-group/index.tsx +240 -0
- package/src/toast/index.tsx +324 -0
- package/src/toolbar/index.tsx +108 -0
- package/src/tooltip/index.tsx +171 -5
- package/src/tree/index.tsx +494 -0
package/src/button/Button.tsx
CHANGED
|
@@ -1,75 +1,74 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Button component for proyecto-viviana-ui
|
|
3
|
-
*
|
|
4
|
-
* A styled button component built on top of solidaria-components.
|
|
5
|
-
* This component only handles styling - all behavior and accessibility
|
|
6
|
-
* is provided by the headless Button from solidaria-components.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { type JSX, splitProps, mergeProps as solidMergeProps } from 'solid-js';
|
|
10
|
-
import { Button as HeadlessButton, type ButtonRenderProps } from '@proyecto-viviana/solidaria-components';
|
|
11
|
-
import type { ButtonProps } from './types';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Buttons allow users to perform an action or to navigate to another page.
|
|
15
|
-
* They have multiple styles for various needs, and are ideal for calling attention to
|
|
16
|
-
* where a user needs to do something in order to move forward in a flow.
|
|
17
|
-
*
|
|
18
|
-
* Built on solidaria-components Button for full accessibility support.
|
|
19
|
-
* Styles are defined in components.css using the vui-button class system.
|
|
20
|
-
*/
|
|
21
|
-
export function Button(props: ButtonProps): JSX.Element {
|
|
22
|
-
const defaultProps: Partial<ButtonProps> = {
|
|
23
|
-
variant: 'primary',
|
|
24
|
-
buttonStyle: 'fill',
|
|
25
|
-
size: 'md',
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const merged = solidMergeProps(defaultProps, props);
|
|
29
|
-
|
|
30
|
-
const [local, headlessProps] = splitProps(merged, [
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
`vui-button--${local.
|
|
45
|
-
`vui-button--${local.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
{
|
|
67
|
-
|
|
68
|
-
data-
|
|
69
|
-
data-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Button component for proyecto-viviana-ui
|
|
3
|
+
*
|
|
4
|
+
* A styled button component built on top of solidaria-components.
|
|
5
|
+
* This component only handles styling - all behavior and accessibility
|
|
6
|
+
* is provided by the headless Button from solidaria-components.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { type JSX, splitProps, mergeProps as solidMergeProps } from 'solid-js';
|
|
10
|
+
import { Button as HeadlessButton, type ButtonRenderProps } from '@proyecto-viviana/solidaria-components';
|
|
11
|
+
import type { ButtonProps } from './types';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Buttons allow users to perform an action or to navigate to another page.
|
|
15
|
+
* They have multiple styles for various needs, and are ideal for calling attention to
|
|
16
|
+
* where a user needs to do something in order to move forward in a flow.
|
|
17
|
+
*
|
|
18
|
+
* Built on solidaria-components Button for full accessibility support.
|
|
19
|
+
* Styles are defined in components.css using the vui-button class system.
|
|
20
|
+
*/
|
|
21
|
+
export function Button(props: ButtonProps): JSX.Element {
|
|
22
|
+
const defaultProps: Partial<ButtonProps> = {
|
|
23
|
+
variant: 'primary',
|
|
24
|
+
buttonStyle: 'fill',
|
|
25
|
+
size: 'md',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const merged = solidMergeProps(defaultProps, props);
|
|
29
|
+
|
|
30
|
+
const [local, headlessProps] = splitProps(merged, [
|
|
31
|
+
'variant',
|
|
32
|
+
'buttonStyle',
|
|
33
|
+
'size',
|
|
34
|
+
'fullWidth',
|
|
35
|
+
'staticColor',
|
|
36
|
+
'class',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
// Generate class based on render props
|
|
40
|
+
const getClassName = (renderProps: ButtonRenderProps): string => {
|
|
41
|
+
const classList = [
|
|
42
|
+
'vui-button',
|
|
43
|
+
`vui-button--${local.buttonStyle}`,
|
|
44
|
+
`vui-button--${local.variant}`,
|
|
45
|
+
`vui-button--${local.size}`,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
if (renderProps.isPressed) {
|
|
49
|
+
classList.push('is-pressed');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (local.fullWidth) {
|
|
53
|
+
classList.push('vui-button--full-width');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (local.class) {
|
|
57
|
+
classList.push(local.class);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return classList.join(' ');
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<HeadlessButton
|
|
65
|
+
{...headlessProps}
|
|
66
|
+
class={getClassName}
|
|
67
|
+
data-variant={local.variant}
|
|
68
|
+
data-style={local.buttonStyle}
|
|
69
|
+
data-static-color={local.staticColor || undefined}
|
|
70
|
+
>
|
|
71
|
+
{props.children}
|
|
72
|
+
</HeadlessButton>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DateField component for proyecto-viviana-ui
|
|
3
|
+
*
|
|
4
|
+
* Styled date field component with segment-based editing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { type JSX, splitProps } from 'solid-js';
|
|
8
|
+
import {
|
|
9
|
+
DateField as HeadlessDateField,
|
|
10
|
+
DateInput,
|
|
11
|
+
DateSegment,
|
|
12
|
+
type DateFieldProps as HeadlessDateFieldProps,
|
|
13
|
+
type CalendarDate,
|
|
14
|
+
type DateValue,
|
|
15
|
+
} from '@proyecto-viviana/solidaria-components';
|
|
16
|
+
|
|
17
|
+
// ============================================
|
|
18
|
+
// TYPES
|
|
19
|
+
// ============================================
|
|
20
|
+
|
|
21
|
+
export type DateFieldSize = 'sm' | 'md' | 'lg';
|
|
22
|
+
|
|
23
|
+
export interface DateFieldProps<T extends DateValue = DateValue>
|
|
24
|
+
extends Omit<HeadlessDateFieldProps<T>, 'class' | 'style' | 'children'> {
|
|
25
|
+
/** The size of the field. @default 'md' */
|
|
26
|
+
size?: DateFieldSize;
|
|
27
|
+
/** Additional CSS class name. */
|
|
28
|
+
class?: string;
|
|
29
|
+
/** Label for the field. */
|
|
30
|
+
label?: string;
|
|
31
|
+
/** Description text. */
|
|
32
|
+
description?: string;
|
|
33
|
+
/** Error message. */
|
|
34
|
+
errorMessage?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ============================================
|
|
38
|
+
// STYLES
|
|
39
|
+
// ============================================
|
|
40
|
+
|
|
41
|
+
const sizeStyles = {
|
|
42
|
+
sm: {
|
|
43
|
+
container: 'text-sm',
|
|
44
|
+
input: 'px-2 py-1 gap-0.5',
|
|
45
|
+
segment: 'px-0.5',
|
|
46
|
+
label: 'text-xs',
|
|
47
|
+
},
|
|
48
|
+
md: {
|
|
49
|
+
container: 'text-base',
|
|
50
|
+
input: 'px-3 py-2 gap-1',
|
|
51
|
+
segment: 'px-1',
|
|
52
|
+
label: 'text-sm',
|
|
53
|
+
},
|
|
54
|
+
lg: {
|
|
55
|
+
container: 'text-lg',
|
|
56
|
+
input: 'px-4 py-3 gap-1.5',
|
|
57
|
+
segment: 'px-1.5',
|
|
58
|
+
label: 'text-base',
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// ============================================
|
|
63
|
+
// DATE FIELD COMPONENT
|
|
64
|
+
// ============================================
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A date field allows users to enter and edit date values using a keyboard.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```tsx
|
|
71
|
+
* // Basic usage
|
|
72
|
+
* <DateField label="Birth date" />
|
|
73
|
+
*
|
|
74
|
+
* // Controlled
|
|
75
|
+
* const [date, setDate] = createSignal<CalendarDate | null>(null);
|
|
76
|
+
* <DateField
|
|
77
|
+
* label="Event date"
|
|
78
|
+
* value={date()}
|
|
79
|
+
* onChange={setDate}
|
|
80
|
+
* />
|
|
81
|
+
*
|
|
82
|
+
* // With validation
|
|
83
|
+
* <DateField
|
|
84
|
+
* label="Future date"
|
|
85
|
+
* minValue={today(getLocalTimeZone())}
|
|
86
|
+
* errorMessage="Date must be in the future"
|
|
87
|
+
* />
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export function DateField<T extends DateValue = CalendarDate>(
|
|
91
|
+
props: DateFieldProps<T>
|
|
92
|
+
): JSX.Element {
|
|
93
|
+
const [local, rest] = splitProps(props, [
|
|
94
|
+
'size',
|
|
95
|
+
'class',
|
|
96
|
+
'label',
|
|
97
|
+
'description',
|
|
98
|
+
'errorMessage',
|
|
99
|
+
'isInvalid',
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
const size = () => local.size ?? 'md';
|
|
103
|
+
const sizeConfig = () => sizeStyles[size()];
|
|
104
|
+
const isInvalid = () => local.isInvalid || !!local.errorMessage;
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<HeadlessDateField
|
|
108
|
+
{...rest}
|
|
109
|
+
isInvalid={isInvalid()}
|
|
110
|
+
class={`
|
|
111
|
+
flex flex-col gap-1
|
|
112
|
+
${sizeConfig().container}
|
|
113
|
+
${local.class ?? ''}
|
|
114
|
+
`}
|
|
115
|
+
>
|
|
116
|
+
{/* Label */}
|
|
117
|
+
{local.label && (
|
|
118
|
+
<label class={`font-medium text-primary-200 ${sizeConfig().label}`}>
|
|
119
|
+
{local.label}
|
|
120
|
+
{rest.isRequired && <span class="text-red-500 ml-0.5">*</span>}
|
|
121
|
+
</label>
|
|
122
|
+
)}
|
|
123
|
+
|
|
124
|
+
{/* Input container */}
|
|
125
|
+
<DateInput
|
|
126
|
+
class={({ isFocused, isDisabled }) => {
|
|
127
|
+
const base = `
|
|
128
|
+
inline-flex items-center
|
|
129
|
+
${sizeConfig().input}
|
|
130
|
+
bg-bg-400 rounded-md border
|
|
131
|
+
transition-colors duration-150
|
|
132
|
+
`;
|
|
133
|
+
|
|
134
|
+
let borderClass = 'border-primary-600';
|
|
135
|
+
if (isInvalid()) {
|
|
136
|
+
borderClass = 'border-red-500';
|
|
137
|
+
} else if (isFocused) {
|
|
138
|
+
borderClass = 'border-accent';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const disabledClass = isDisabled
|
|
142
|
+
? 'opacity-50 cursor-not-allowed'
|
|
143
|
+
: '';
|
|
144
|
+
|
|
145
|
+
const focusClass = isFocused
|
|
146
|
+
? 'ring-2 ring-accent/30'
|
|
147
|
+
: '';
|
|
148
|
+
|
|
149
|
+
return `${base} ${borderClass} ${disabledClass} ${focusClass}`.trim();
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
152
|
+
{(segment) => (
|
|
153
|
+
<DateSegment
|
|
154
|
+
segment={segment}
|
|
155
|
+
class={({ isFocused, isPlaceholder, isEditable }) => {
|
|
156
|
+
const base = `
|
|
157
|
+
${sizeConfig().segment}
|
|
158
|
+
rounded
|
|
159
|
+
outline-none
|
|
160
|
+
tabular-nums
|
|
161
|
+
`;
|
|
162
|
+
|
|
163
|
+
let stateClass = '';
|
|
164
|
+
if (segment.type === 'literal') {
|
|
165
|
+
stateClass = 'text-primary-400';
|
|
166
|
+
} else if (isPlaceholder) {
|
|
167
|
+
stateClass = 'text-primary-500 italic';
|
|
168
|
+
} else {
|
|
169
|
+
stateClass = 'text-primary-100';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const focusClass = isFocused && isEditable
|
|
173
|
+
? 'bg-accent text-white'
|
|
174
|
+
: '';
|
|
175
|
+
|
|
176
|
+
return `${base} ${stateClass} ${focusClass}`.trim();
|
|
177
|
+
}}
|
|
178
|
+
/>
|
|
179
|
+
)}
|
|
180
|
+
</DateInput>
|
|
181
|
+
|
|
182
|
+
{/* Description */}
|
|
183
|
+
{local.description && !isInvalid() && (
|
|
184
|
+
<p class={`text-primary-400 ${sizeConfig().label}`}>
|
|
185
|
+
{local.description}
|
|
186
|
+
</p>
|
|
187
|
+
)}
|
|
188
|
+
|
|
189
|
+
{/* Error message */}
|
|
190
|
+
{isInvalid() && local.errorMessage && (
|
|
191
|
+
<p class={`text-red-500 ${sizeConfig().label}`}>
|
|
192
|
+
{local.errorMessage}
|
|
193
|
+
</p>
|
|
194
|
+
)}
|
|
195
|
+
</HeadlessDateField>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Re-export types
|
|
200
|
+
export type { CalendarDate, DateValue };
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DatePicker component for proyecto-viviana-ui
|
|
3
|
+
*
|
|
4
|
+
* Styled date picker component that combines a date field with a calendar popup.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { type JSX, splitProps, Show } from 'solid-js';
|
|
8
|
+
import {
|
|
9
|
+
DatePicker as HeadlessDatePicker,
|
|
10
|
+
DatePickerButton,
|
|
11
|
+
DateInput,
|
|
12
|
+
DateSegment,
|
|
13
|
+
useDatePickerContext,
|
|
14
|
+
type DatePickerProps as HeadlessDatePickerProps,
|
|
15
|
+
type CalendarDate,
|
|
16
|
+
type DateValue,
|
|
17
|
+
} from '@proyecto-viviana/solidaria-components';
|
|
18
|
+
import { Calendar } from './index';
|
|
19
|
+
|
|
20
|
+
// Calendar icon component - use function to ensure consistent hydration
|
|
21
|
+
function CalendarIcon(): JSX.Element {
|
|
22
|
+
return (
|
|
23
|
+
<svg
|
|
24
|
+
viewBox="0 0 24 24"
|
|
25
|
+
fill="none"
|
|
26
|
+
stroke="currentColor"
|
|
27
|
+
stroke-width="2"
|
|
28
|
+
stroke-linecap="round"
|
|
29
|
+
stroke-linejoin="round"
|
|
30
|
+
class="w-5 h-5"
|
|
31
|
+
aria-hidden="true"
|
|
32
|
+
>
|
|
33
|
+
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
|
|
34
|
+
<line x1="16" y1="2" x2="16" y2="6" />
|
|
35
|
+
<line x1="8" y1="2" x2="8" y2="6" />
|
|
36
|
+
<line x1="3" y1="10" x2="21" y2="10" />
|
|
37
|
+
</svg>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================
|
|
42
|
+
// TYPES
|
|
43
|
+
// ============================================
|
|
44
|
+
|
|
45
|
+
export type DatePickerSize = 'sm' | 'md' | 'lg';
|
|
46
|
+
|
|
47
|
+
export interface DatePickerProps<T extends DateValue = DateValue>
|
|
48
|
+
extends Omit<HeadlessDatePickerProps<T>, 'class' | 'style' | 'children'> {
|
|
49
|
+
/** The size of the picker. @default 'md' */
|
|
50
|
+
size?: DatePickerSize;
|
|
51
|
+
/** Additional CSS class name. */
|
|
52
|
+
class?: string;
|
|
53
|
+
/** Label for the field. */
|
|
54
|
+
label?: string;
|
|
55
|
+
/** Description text. */
|
|
56
|
+
description?: string;
|
|
57
|
+
/** Error message. */
|
|
58
|
+
errorMessage?: string;
|
|
59
|
+
/** Placeholder text. */
|
|
60
|
+
placeholder?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============================================
|
|
64
|
+
// STYLES
|
|
65
|
+
// ============================================
|
|
66
|
+
|
|
67
|
+
const sizeStyles = {
|
|
68
|
+
sm: {
|
|
69
|
+
container: 'text-sm',
|
|
70
|
+
input: 'px-2 py-1 gap-0.5',
|
|
71
|
+
segment: 'px-0.5',
|
|
72
|
+
label: 'text-xs',
|
|
73
|
+
button: 'w-7 h-7',
|
|
74
|
+
},
|
|
75
|
+
md: {
|
|
76
|
+
container: 'text-base',
|
|
77
|
+
input: 'px-3 py-2 gap-1',
|
|
78
|
+
segment: 'px-1',
|
|
79
|
+
label: 'text-sm',
|
|
80
|
+
button: 'w-9 h-9',
|
|
81
|
+
},
|
|
82
|
+
lg: {
|
|
83
|
+
container: 'text-lg',
|
|
84
|
+
input: 'px-4 py-3 gap-1.5',
|
|
85
|
+
segment: 'px-1.5',
|
|
86
|
+
label: 'text-base',
|
|
87
|
+
button: 'w-11 h-11',
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// ============================================
|
|
92
|
+
// DATE PICKER COMPONENT
|
|
93
|
+
// ============================================
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A date picker combines a date field and a calendar popup.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```tsx
|
|
100
|
+
* // Basic usage
|
|
101
|
+
* <DatePicker label="Event date" />
|
|
102
|
+
*
|
|
103
|
+
* // Controlled
|
|
104
|
+
* const [date, setDate] = createSignal<CalendarDate | null>(null);
|
|
105
|
+
* <DatePicker
|
|
106
|
+
* label="Appointment"
|
|
107
|
+
* value={date()}
|
|
108
|
+
* onChange={setDate}
|
|
109
|
+
* />
|
|
110
|
+
*
|
|
111
|
+
* // With constraints
|
|
112
|
+
* <DatePicker
|
|
113
|
+
* label="Future booking"
|
|
114
|
+
* minValue={today(getLocalTimeZone())}
|
|
115
|
+
* />
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
export function DatePicker<T extends DateValue = CalendarDate>(
|
|
119
|
+
props: DatePickerProps<T>
|
|
120
|
+
): JSX.Element {
|
|
121
|
+
const [local, rest] = splitProps(props, [
|
|
122
|
+
'size',
|
|
123
|
+
'class',
|
|
124
|
+
'label',
|
|
125
|
+
'description',
|
|
126
|
+
'errorMessage',
|
|
127
|
+
'isInvalid',
|
|
128
|
+
'placeholder',
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
const size = () => local.size ?? 'md';
|
|
132
|
+
const sizeConfig = () => sizeStyles[size()];
|
|
133
|
+
const isInvalid = () => local.isInvalid || !!local.errorMessage;
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<HeadlessDatePicker
|
|
137
|
+
{...rest}
|
|
138
|
+
isInvalid={isInvalid()}
|
|
139
|
+
class={`
|
|
140
|
+
flex flex-col gap-1 relative
|
|
141
|
+
${sizeConfig().container}
|
|
142
|
+
${local.class ?? ''}
|
|
143
|
+
`}
|
|
144
|
+
>
|
|
145
|
+
{/* Label */}
|
|
146
|
+
{local.label && (
|
|
147
|
+
<label class={`font-medium text-primary-200 ${sizeConfig().label}`}>
|
|
148
|
+
{local.label}
|
|
149
|
+
{rest.isRequired && <span class="text-red-500 ml-0.5">*</span>}
|
|
150
|
+
</label>
|
|
151
|
+
)}
|
|
152
|
+
|
|
153
|
+
{/* Input group */}
|
|
154
|
+
<div class="relative flex items-center">
|
|
155
|
+
{/* Date input */}
|
|
156
|
+
<DateInput
|
|
157
|
+
class={({ isFocused, isDisabled }) => {
|
|
158
|
+
const base = `
|
|
159
|
+
inline-flex items-center flex-1
|
|
160
|
+
${sizeConfig().input}
|
|
161
|
+
bg-bg-400 rounded-l-md border-y border-l
|
|
162
|
+
transition-colors duration-150
|
|
163
|
+
`;
|
|
164
|
+
|
|
165
|
+
let borderClass = 'border-primary-600';
|
|
166
|
+
if (isInvalid()) {
|
|
167
|
+
borderClass = 'border-red-500';
|
|
168
|
+
} else if (isFocused) {
|
|
169
|
+
borderClass = 'border-accent';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const disabledClass = isDisabled
|
|
173
|
+
? 'opacity-50 cursor-not-allowed'
|
|
174
|
+
: '';
|
|
175
|
+
|
|
176
|
+
const focusClass = isFocused
|
|
177
|
+
? 'ring-2 ring-accent/30'
|
|
178
|
+
: '';
|
|
179
|
+
|
|
180
|
+
return `${base} ${borderClass} ${disabledClass} ${focusClass}`.trim();
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
{(segment) => (
|
|
184
|
+
<DateSegment
|
|
185
|
+
segment={segment}
|
|
186
|
+
class={({ isFocused, isPlaceholder, isEditable }) => {
|
|
187
|
+
const base = `
|
|
188
|
+
${sizeConfig().segment}
|
|
189
|
+
rounded
|
|
190
|
+
outline-none
|
|
191
|
+
tabular-nums
|
|
192
|
+
`;
|
|
193
|
+
|
|
194
|
+
let stateClass = '';
|
|
195
|
+
if (segment.type === 'literal') {
|
|
196
|
+
stateClass = 'text-primary-400';
|
|
197
|
+
} else if (isPlaceholder) {
|
|
198
|
+
stateClass = 'text-primary-500 italic';
|
|
199
|
+
} else {
|
|
200
|
+
stateClass = 'text-primary-100';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const focusClass = isFocused && isEditable
|
|
204
|
+
? 'bg-accent text-white'
|
|
205
|
+
: '';
|
|
206
|
+
|
|
207
|
+
return `${base} ${stateClass} ${focusClass}`.trim();
|
|
208
|
+
}}
|
|
209
|
+
/>
|
|
210
|
+
)}
|
|
211
|
+
</DateInput>
|
|
212
|
+
|
|
213
|
+
{/* Calendar button */}
|
|
214
|
+
<DatePickerButton
|
|
215
|
+
class={({ isDisabled, isOpen }) => {
|
|
216
|
+
const base = `
|
|
217
|
+
${sizeConfig().button}
|
|
218
|
+
flex items-center justify-center
|
|
219
|
+
bg-bg-400 border-y border-r rounded-r-md
|
|
220
|
+
text-primary-200
|
|
221
|
+
transition-colors duration-150
|
|
222
|
+
focus:outline-none focus:ring-2 focus:ring-accent/50
|
|
223
|
+
`;
|
|
224
|
+
|
|
225
|
+
let borderClass = 'border-primary-600';
|
|
226
|
+
if (isInvalid()) {
|
|
227
|
+
borderClass = 'border-red-500';
|
|
228
|
+
} else if (isOpen) {
|
|
229
|
+
borderClass = 'border-accent bg-bg-300';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const disabledClass = isDisabled
|
|
233
|
+
? 'opacity-50 cursor-not-allowed'
|
|
234
|
+
: 'hover:bg-bg-300 cursor-pointer';
|
|
235
|
+
|
|
236
|
+
return `${base} ${borderClass} ${disabledClass}`.trim();
|
|
237
|
+
}}
|
|
238
|
+
>
|
|
239
|
+
<CalendarIcon />
|
|
240
|
+
</DatePickerButton>
|
|
241
|
+
|
|
242
|
+
{/* Calendar popup */}
|
|
243
|
+
<DatePickerPopup size={size()} />
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
{/* Description */}
|
|
247
|
+
{local.description && !isInvalid() && (
|
|
248
|
+
<p class={`text-primary-400 ${sizeConfig().label}`}>
|
|
249
|
+
{local.description}
|
|
250
|
+
</p>
|
|
251
|
+
)}
|
|
252
|
+
|
|
253
|
+
{/* Error message */}
|
|
254
|
+
{isInvalid() && local.errorMessage && (
|
|
255
|
+
<p class={`text-red-500 ${sizeConfig().label}`}>
|
|
256
|
+
{local.errorMessage}
|
|
257
|
+
</p>
|
|
258
|
+
)}
|
|
259
|
+
</HeadlessDatePicker>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ============================================
|
|
264
|
+
// POPUP COMPONENT (uses context)
|
|
265
|
+
// ============================================
|
|
266
|
+
|
|
267
|
+
function DatePickerPopup(props: { size: DatePickerSize }): JSX.Element {
|
|
268
|
+
const context = useDatePickerContext();
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<Show when={context.overlayState.isOpen}>
|
|
272
|
+
<div
|
|
273
|
+
class={`
|
|
274
|
+
absolute top-full left-0 z-50 mt-1
|
|
275
|
+
shadow-lg rounded-lg
|
|
276
|
+
`}
|
|
277
|
+
>
|
|
278
|
+
<Calendar
|
|
279
|
+
value={context.calendarState.value()}
|
|
280
|
+
onChange={(date) => {
|
|
281
|
+
context.fieldState.setValue(date as any);
|
|
282
|
+
context.overlayState.close();
|
|
283
|
+
}}
|
|
284
|
+
minValue={context.calendarState.visibleRange().start}
|
|
285
|
+
size={props.size}
|
|
286
|
+
/>
|
|
287
|
+
</div>
|
|
288
|
+
{/* Backdrop */}
|
|
289
|
+
<div
|
|
290
|
+
class="fixed inset-0 z-40"
|
|
291
|
+
onClick={() => context.overlayState.close()}
|
|
292
|
+
/>
|
|
293
|
+
</Show>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Re-export types
|
|
298
|
+
export type { CalendarDate, DateValue };
|