@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
@@ -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
- 'children',
32
- 'variant',
33
- 'buttonStyle',
34
- 'size',
35
- 'fullWidth',
36
- 'staticColor',
37
- 'class',
38
- ]);
39
-
40
- // Generate class based on render props
41
- const getClassName = (renderProps: ButtonRenderProps): string => {
42
- const classList = [
43
- 'vui-button',
44
- `vui-button--${local.buttonStyle}`,
45
- `vui-button--${local.variant}`,
46
- `vui-button--${local.size}`,
47
- ];
48
-
49
- if (renderProps.isPressed) {
50
- classList.push('is-pressed');
51
- }
52
-
53
- if (local.fullWidth) {
54
- classList.push('vui-button--full-width');
55
- }
56
-
57
- if (local.class) {
58
- classList.push(local.class);
59
- }
60
-
61
- return classList.join(' ');
62
- };
63
-
64
- return (
65
- <HeadlessButton
66
- {...headlessProps}
67
- class={getClassName}
68
- data-variant={local.variant}
69
- data-style={local.buttonStyle}
70
- data-static-color={local.staticColor || undefined}
71
- >
72
- {local.children}
73
- </HeadlessButton>
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 };