@tachui/forms 0.7.1-alpha → 0.8.0-alpha

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 (90) hide show
  1. package/README.md +87 -272
  2. package/dist/DatePicker-D5nRFTUm.js +475 -0
  3. package/dist/DatePicker-D5nRFTUm.js.map +1 -0
  4. package/dist/Select-yZyKooXk.js +945 -0
  5. package/dist/Select-yZyKooXk.js.map +1 -0
  6. package/dist/Slider-0-oal5YR.js +644 -0
  7. package/dist/Slider-0-oal5YR.js.map +1 -0
  8. package/dist/TextField-hX15dY3U.js +509 -0
  9. package/dist/TextField-hX15dY3U.js.map +1 -0
  10. package/dist/components/advanced/Slider.d.ts +190 -0
  11. package/dist/components/advanced/Slider.d.ts.map +1 -0
  12. package/dist/components/advanced/Stepper.d.ts +161 -0
  13. package/dist/components/advanced/Stepper.d.ts.map +1 -0
  14. package/dist/components/advanced/index.d.ts +15 -0
  15. package/dist/components/advanced/index.d.ts.map +1 -0
  16. package/dist/components/advanced/index.js +6 -0
  17. package/dist/components/advanced/index.js.map +1 -0
  18. package/dist/components/date-picker/DatePicker.d.ts +126 -0
  19. package/dist/components/date-picker/DatePicker.d.ts.map +1 -0
  20. package/dist/components/date-picker/index.d.ts +14 -0
  21. package/dist/components/date-picker/index.d.ts.map +1 -0
  22. package/dist/components/date-picker/index.js +5 -0
  23. package/dist/components/date-picker/index.js.map +1 -0
  24. package/dist/components/form-container/index.d.ts +58 -0
  25. package/dist/components/form-container/index.d.ts.map +1 -0
  26. package/dist/components/selection/Checkbox.d.ts.map +1 -0
  27. package/dist/components/selection/Radio.d.ts.map +1 -0
  28. package/dist/components/selection/Select.d.ts.map +1 -0
  29. package/dist/components/selection/index.d.ts +68 -0
  30. package/dist/components/selection/index.d.ts.map +1 -0
  31. package/dist/components/selection/index.js +12 -0
  32. package/dist/components/selection/index.js.map +1 -0
  33. package/dist/components/text-input/TextField.d.ts.map +1 -0
  34. package/dist/components/text-input/index.d.ts +8 -0
  35. package/dist/components/text-input/index.d.ts.map +1 -0
  36. package/dist/components/text-input/index.js +18 -0
  37. package/dist/components/text-input/index.js.map +1 -0
  38. package/dist/{state/index.js → index-D3WfkqVv.js} +15 -8
  39. package/dist/index-D3WfkqVv.js.map +1 -0
  40. package/dist/index.d.ts +10 -15
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +198 -376
  43. package/dist/index.js.map +1 -0
  44. package/dist/state/index.d.ts.map +1 -1
  45. package/dist/types/index.d.ts.map +1 -1
  46. package/dist/utils/index.d.ts +19 -0
  47. package/dist/utils/index.d.ts.map +1 -0
  48. package/dist/validation/component-validation.d.ts +11 -2
  49. package/dist/validation/component-validation.d.ts.map +1 -1
  50. package/dist/validation/index.d.ts.map +1 -1
  51. package/dist/validation/index.js +282 -191
  52. package/dist/validation/index.js.map +1 -0
  53. package/package.json +53 -39
  54. package/src/components/advanced/Slider.ts +722 -0
  55. package/src/components/advanced/Stepper.ts +715 -0
  56. package/src/components/advanced/index.ts +20 -0
  57. package/src/components/date-picker/DatePicker.ts +925 -0
  58. package/src/components/date-picker/index.ts +20 -0
  59. package/src/components/form-container/index.ts +266 -0
  60. package/src/components/selection/Checkbox.ts +478 -0
  61. package/src/components/selection/Radio.ts +470 -0
  62. package/src/components/selection/Select.ts +620 -0
  63. package/src/components/selection/index.ts +81 -0
  64. package/src/components/text-input/TextField.ts +728 -0
  65. package/src/components/text-input/index.ts +35 -0
  66. package/src/index.ts +48 -0
  67. package/src/state/index.ts +544 -0
  68. package/src/types/index.ts +579 -0
  69. package/src/utils/formatters.ts +184 -0
  70. package/src/utils/index.ts +57 -0
  71. package/src/validation/component-validation.ts +429 -0
  72. package/src/validation/index.ts +641 -0
  73. package/dist/TextField-CGBM3x7K.js +0 -1799
  74. package/dist/components/Form.d.ts +0 -76
  75. package/dist/components/Form.d.ts.map +0 -1
  76. package/dist/components/index.d.ts +0 -9
  77. package/dist/components/index.d.ts.map +0 -1
  78. package/dist/components/index.js +0 -28
  79. package/dist/components/input/Checkbox.d.ts.map +0 -1
  80. package/dist/components/input/Radio.d.ts.map +0 -1
  81. package/dist/components/input/Select.d.ts.map +0 -1
  82. package/dist/components/input/TextField.d.ts.map +0 -1
  83. package/dist/components/input/index.d.ts +0 -11
  84. package/dist/components/input/index.d.ts.map +0 -1
  85. package/dist/utils/validators.d.ts +0 -101
  86. package/dist/utils/validators.d.ts.map +0 -1
  87. /package/dist/components/{input → selection}/Checkbox.d.ts +0 -0
  88. /package/dist/components/{input → selection}/Radio.d.ts +0 -0
  89. /package/dist/components/{input → selection}/Select.d.ts +0 -0
  90. /package/dist/components/{input → text-input}/TextField.d.ts +0 -0
@@ -0,0 +1,925 @@
1
+ /**
2
+ * DatePicker Component (TachUI)
3
+ *
4
+ * SwiftUI-inspired date and time selection component with multiple styles
5
+ * and display modes. Supports reactive bindings and comprehensive customization.
6
+ */
7
+
8
+ import type { ModifiableComponent, ModifierBuilder } from '@tachui/core'
9
+ import { createEffect, isSignal } from '@tachui/core'
10
+ import type { Signal } from '@tachui/core'
11
+ import { h } from '@tachui/core'
12
+ import type { ComponentInstance, ComponentProps, DOMNode } from '@tachui/core'
13
+ import { withModifiers } from '@tachui/core'
14
+
15
+ /**
16
+ * Date picker display components
17
+ */
18
+ export type DatePickerDisplayComponents = 'date' | 'time' | 'dateAndTime'
19
+
20
+ /**
21
+ * Date picker style options
22
+ */
23
+ export type DatePickerStyle = 'compact' | 'wheel' | 'graphical'
24
+
25
+ /**
26
+ * DatePicker component properties
27
+ */
28
+ export interface DatePickerProps extends ComponentProps {
29
+ // Core properties
30
+ title?: string
31
+ selection: Signal<Date> | Date
32
+
33
+ // Display options
34
+ displayedComponents?:
35
+ | DatePickerDisplayComponents
36
+ | Signal<DatePickerDisplayComponents>
37
+ style?: DatePickerStyle | Signal<DatePickerStyle>
38
+
39
+ // Constraints
40
+ minimumDate?: Date | Signal<Date>
41
+ maximumDate?: Date | Signal<Date>
42
+
43
+ // Localization
44
+ locale?: string | Signal<string>
45
+ dateFormat?: string | Signal<string>
46
+ timeFormat?: string | Signal<string>
47
+
48
+ // Behavior
49
+ onChange?: (date: Date) => void
50
+ disabled?: boolean | Signal<boolean>
51
+
52
+ // Accessibility
53
+ accessibilityLabel?: string
54
+ accessibilityHint?: string
55
+ }
56
+
57
+ /**
58
+ * DatePicker theme configuration
59
+ */
60
+ export interface DatePickerTheme {
61
+ colors: {
62
+ background: string
63
+ border: string
64
+ text: string
65
+ selectedBackground: string
66
+ selectedText: string
67
+ disabledText: string
68
+ accent: string
69
+ }
70
+ spacing: {
71
+ padding: number
72
+ gap: number
73
+ itemHeight: number
74
+ }
75
+ borderRadius: number
76
+ fontSize: number
77
+ fontFamily: string
78
+ }
79
+
80
+ /**
81
+ * Default DatePicker theme
82
+ */
83
+ const defaultDatePickerTheme: DatePickerTheme = {
84
+ colors: {
85
+ background: '#FFFFFF',
86
+ border: '#D1D1D6',
87
+ text: '#000000',
88
+ selectedBackground: '#007AFF',
89
+ selectedText: '#FFFFFF',
90
+ disabledText: '#8E8E93',
91
+ accent: '#007AFF',
92
+ },
93
+ spacing: {
94
+ padding: 12,
95
+ gap: 8,
96
+ itemHeight: 44,
97
+ },
98
+ borderRadius: 8,
99
+ fontSize: 16,
100
+ fontFamily:
101
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
102
+ }
103
+
104
+ /**
105
+ * DatePicker component implementation
106
+ */
107
+ export class DatePickerComponent implements ComponentInstance<DatePickerProps> {
108
+ public readonly type = 'component' as const
109
+ public readonly id: string
110
+ public readonly props: DatePickerProps
111
+ private theme: DatePickerTheme = defaultDatePickerTheme
112
+ private containerElement: HTMLElement | null = null
113
+
114
+ constructor(props: DatePickerProps) {
115
+ this.props = props
116
+ this.id = `datepicker-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
117
+ }
118
+
119
+ private resolveValue<T>(value: T | Signal<T>): T {
120
+ return isSignal(value) ? value() : value
121
+ }
122
+
123
+ private getSelectedDate(): Date {
124
+ return this.resolveValue(this.props.selection)
125
+ }
126
+
127
+ private setSelectedDate(date: Date): void {
128
+ if (isSignal(this.props.selection)) {
129
+ // biome-ignore lint/suspicious/noExplicitAny: Signal setter requires dynamic typing
130
+ ;(this.props.selection as any)(date)
131
+ }
132
+
133
+ if (this.props.onChange) {
134
+ this.props.onChange(date)
135
+ }
136
+ }
137
+
138
+ private getDisplayedComponents(): DatePickerDisplayComponents {
139
+ return this.resolveValue(this.props.displayedComponents || 'date')
140
+ }
141
+
142
+ private getStyle(): DatePickerStyle {
143
+ return this.resolveValue(this.props.style || 'compact')
144
+ }
145
+
146
+ private isDisabled(): boolean {
147
+ return this.resolveValue(this.props.disabled || false)
148
+ }
149
+
150
+ private getMinimumDate(): Date | null {
151
+ return this.props.minimumDate
152
+ ? this.resolveValue(this.props.minimumDate)
153
+ : null
154
+ }
155
+
156
+ private getMaximumDate(): Date | null {
157
+ return this.props.maximumDate
158
+ ? this.resolveValue(this.props.maximumDate)
159
+ : null
160
+ }
161
+
162
+ private isDateInRange(date: Date): boolean {
163
+ const min = this.getMinimumDate()
164
+ const max = this.getMaximumDate()
165
+
166
+ if (min && date < min) return false
167
+ if (max && date > max) return false
168
+
169
+ return true
170
+ }
171
+
172
+ private createCompactPicker(): DOMNode {
173
+ const selectedDate = this.getSelectedDate()
174
+ const components = this.getDisplayedComponents()
175
+
176
+ const container = h('div', {
177
+ style: {
178
+ position: 'relative',
179
+ display: 'inline-block',
180
+ },
181
+ })
182
+
183
+ const input = h('input', {
184
+ type:
185
+ components === 'time'
186
+ ? 'time'
187
+ : components === 'date'
188
+ ? 'date'
189
+ : 'datetime-local',
190
+ value: this.getInputValue(selectedDate, components),
191
+ disabled: this.isDisabled(),
192
+ min: this.getMinimumDate()?.toISOString().split('T')[0],
193
+ max: this.getMaximumDate()?.toISOString().split('T')[0],
194
+ style: {
195
+ padding: `${this.theme.spacing.padding}px`,
196
+ border: `1px solid ${this.theme.colors.border}`,
197
+ borderRadius: `${this.theme.borderRadius}px`,
198
+ backgroundColor: this.theme.colors.background,
199
+ color: this.isDisabled()
200
+ ? this.theme.colors.disabledText
201
+ : this.theme.colors.text,
202
+ fontSize: `${this.theme.fontSize}px`,
203
+ fontFamily: this.theme.fontFamily,
204
+ cursor: this.isDisabled() ? 'not-allowed' : 'pointer',
205
+ outline: 'none',
206
+ },
207
+ onchange: (e: Event) => {
208
+ const target = e.target as HTMLInputElement
209
+ if (target.value) {
210
+ const newDate = new Date(target.value)
211
+ if (this.isDateInRange(newDate)) {
212
+ this.setSelectedDate(newDate)
213
+ }
214
+ }
215
+ },
216
+ onfocus: (e: Event) => {
217
+ const target = e.target as HTMLInputElement
218
+ target.style.borderColor = this.theme.colors.accent
219
+ },
220
+ onblur: (e: Event) => {
221
+ const target = e.target as HTMLInputElement
222
+ target.style.borderColor = this.theme.colors.border
223
+ },
224
+ })
225
+
226
+ const containerDOM = container.element as HTMLElement
227
+ const inputDOM = input.element as HTMLElement
228
+
229
+ if (containerDOM && inputDOM) {
230
+ containerDOM.appendChild(inputDOM)
231
+ }
232
+
233
+ return container
234
+ }
235
+
236
+ private getInputValue(
237
+ date: Date,
238
+ components: DatePickerDisplayComponents
239
+ ): string {
240
+ // Handle invalid dates gracefully
241
+ if (Number.isNaN(date.getTime())) {
242
+ switch (components) {
243
+ case 'date':
244
+ return ''
245
+ case 'time':
246
+ return '00:00'
247
+ case 'dateAndTime':
248
+ return ''
249
+ default:
250
+ return ''
251
+ }
252
+ }
253
+
254
+ try {
255
+ switch (components) {
256
+ case 'date':
257
+ return date.toISOString().split('T')[0]
258
+ case 'time':
259
+ return date.toTimeString().split(' ')[0].substring(0, 5)
260
+ case 'dateAndTime':
261
+ return date.toISOString().slice(0, 16)
262
+ default:
263
+ return date.toISOString().split('T')[0]
264
+ }
265
+ } catch {
266
+ // Fallback for any date formatting errors
267
+ return ''
268
+ }
269
+ }
270
+
271
+ private createWheelPicker(): DOMNode {
272
+ // Wheel picker with scrollable date/time components
273
+ const container = h('div', {
274
+ style: {
275
+ display: 'flex',
276
+ justifyContent: 'center',
277
+ gap: `${this.theme.spacing.gap}px`,
278
+ padding: `${this.theme.spacing.padding}px`,
279
+ border: `1px solid ${this.theme.colors.border}`,
280
+ borderRadius: `${this.theme.borderRadius}px`,
281
+ backgroundColor: this.theme.colors.background,
282
+ },
283
+ })
284
+
285
+ const components = this.getDisplayedComponents()
286
+ const selectedDate = this.getSelectedDate()
287
+
288
+ // Create wheel components based on display mode
289
+ if (components === 'date' || components === 'dateAndTime') {
290
+ this.createDateWheels(container, selectedDate)
291
+ }
292
+
293
+ if (components === 'time' || components === 'dateAndTime') {
294
+ this.createTimeWheels(container, selectedDate)
295
+ }
296
+
297
+ return container
298
+ }
299
+
300
+ private createDateWheels(container: DOMNode, selectedDate: Date): void {
301
+ // Month wheel
302
+ const monthWheel = this.createWheel(
303
+ Array.from({ length: 12 }, (_, i) => ({
304
+ value: i,
305
+ label: new Date(2024, i, 1).toLocaleDateString(
306
+ this.resolveValue(this.props.locale || 'en-US'),
307
+ { month: 'short' }
308
+ ),
309
+ })),
310
+ selectedDate.getMonth(),
311
+ value => {
312
+ const newDate = new Date(selectedDate)
313
+ newDate.setMonth(value)
314
+ if (this.isDateInRange(newDate)) {
315
+ this.setSelectedDate(newDate)
316
+ }
317
+ }
318
+ )
319
+
320
+ // Day wheel
321
+ const daysInMonth = new Date(
322
+ selectedDate.getFullYear(),
323
+ selectedDate.getMonth() + 1,
324
+ 0
325
+ ).getDate()
326
+ const dayWheel = this.createWheel(
327
+ Array.from({ length: daysInMonth }, (_, i) => ({
328
+ value: i + 1,
329
+ label: (i + 1).toString(),
330
+ })),
331
+ selectedDate.getDate(),
332
+ value => {
333
+ const newDate = new Date(selectedDate)
334
+ newDate.setDate(value)
335
+ if (this.isDateInRange(newDate)) {
336
+ this.setSelectedDate(newDate)
337
+ }
338
+ }
339
+ )
340
+
341
+ // Year wheel
342
+ const currentYear = selectedDate.getFullYear()
343
+ const yearRange = 50
344
+ const yearWheel = this.createWheel(
345
+ Array.from({ length: yearRange * 2 }, (_, i) => {
346
+ const year = currentYear - yearRange + i
347
+ return { value: year, label: year.toString() }
348
+ }),
349
+ currentYear,
350
+ value => {
351
+ const newDate = new Date(selectedDate)
352
+ newDate.setFullYear(value)
353
+ if (this.isDateInRange(newDate)) {
354
+ this.setSelectedDate(newDate)
355
+ }
356
+ }
357
+ )
358
+
359
+ const containerDOM = container.element as HTMLElement
360
+ if (containerDOM) {
361
+ const monthWheelDOM = monthWheel.element as HTMLElement
362
+ const dayWheelDOM = dayWheel.element as HTMLElement
363
+ const yearWheelDOM = yearWheel.element as HTMLElement
364
+
365
+ if (monthWheelDOM) containerDOM.appendChild(monthWheelDOM)
366
+ if (dayWheelDOM) containerDOM.appendChild(dayWheelDOM)
367
+ if (yearWheelDOM) containerDOM.appendChild(yearWheelDOM)
368
+ }
369
+ }
370
+
371
+ private createTimeWheels(container: DOMNode, selectedDate: Date): void {
372
+ // Hour wheel
373
+ const hourWheel = this.createWheel(
374
+ Array.from({ length: 24 }, (_, i) => ({
375
+ value: i,
376
+ label: i.toString().padStart(2, '0'),
377
+ })),
378
+ selectedDate.getHours(),
379
+ value => {
380
+ const newDate = new Date(selectedDate)
381
+ newDate.setHours(value)
382
+ if (this.isDateInRange(newDate)) {
383
+ this.setSelectedDate(newDate)
384
+ }
385
+ }
386
+ )
387
+
388
+ // Minute wheel
389
+ const minuteWheel = this.createWheel(
390
+ Array.from({ length: 60 }, (_, i) => ({
391
+ value: i,
392
+ label: i.toString().padStart(2, '0'),
393
+ })),
394
+ selectedDate.getMinutes(),
395
+ value => {
396
+ const newDate = new Date(selectedDate)
397
+ newDate.setMinutes(value)
398
+ if (this.isDateInRange(newDate)) {
399
+ this.setSelectedDate(newDate)
400
+ }
401
+ }
402
+ )
403
+
404
+ const containerDOM = container.element as HTMLElement
405
+ if (containerDOM) {
406
+ const hourWheelDOM = hourWheel.element as HTMLElement
407
+ const minuteWheelDOM = minuteWheel.element as HTMLElement
408
+
409
+ if (hourWheelDOM) containerDOM.appendChild(hourWheelDOM)
410
+ if (minuteWheelDOM) containerDOM.appendChild(minuteWheelDOM)
411
+ }
412
+ }
413
+
414
+ private createWheel(
415
+ items: { value: number; label: string }[],
416
+ selectedValue: number,
417
+ onSelect: (value: number) => void
418
+ ): DOMNode {
419
+ const wheel = h('div', {
420
+ style: {
421
+ width: '80px',
422
+ height: '200px',
423
+ overflowY: 'scroll',
424
+ border: `1px solid ${this.theme.colors.border}`,
425
+ borderRadius: `${this.theme.borderRadius}px`,
426
+ scrollSnapType: 'y mandatory',
427
+ },
428
+ })
429
+
430
+ items.forEach(item => {
431
+ const option = h('div', {
432
+ style: {
433
+ height: `${this.theme.spacing.itemHeight}px`,
434
+ display: 'flex',
435
+ alignItems: 'center',
436
+ justifyContent: 'center',
437
+ backgroundColor:
438
+ item.value === selectedValue
439
+ ? this.theme.colors.selectedBackground
440
+ : 'transparent',
441
+ color:
442
+ item.value === selectedValue
443
+ ? this.theme.colors.selectedText
444
+ : this.theme.colors.text,
445
+ cursor: 'pointer',
446
+ scrollSnapAlign: 'center',
447
+ fontSize: `${this.theme.fontSize}px`,
448
+ fontFamily: this.theme.fontFamily,
449
+ },
450
+ onclick: () => onSelect(item.value),
451
+ })
452
+
453
+ const optionDOM = option.element as HTMLElement
454
+ if (optionDOM) {
455
+ optionDOM.textContent = item.label
456
+ }
457
+
458
+ const wheelDOM = wheel.element as HTMLElement
459
+ if (wheelDOM && optionDOM) {
460
+ wheelDOM.appendChild(optionDOM)
461
+ }
462
+ })
463
+
464
+ return wheel
465
+ }
466
+
467
+ private createGraphicalPicker(): DOMNode {
468
+ // Calendar grid for date selection
469
+ const selectedDate = this.getSelectedDate()
470
+ const currentMonth = selectedDate.getMonth()
471
+ const currentYear = selectedDate.getFullYear()
472
+
473
+ const container = h('div', {
474
+ style: {
475
+ border: `1px solid ${this.theme.colors.border}`,
476
+ borderRadius: `${this.theme.borderRadius}px`,
477
+ backgroundColor: this.theme.colors.background,
478
+ padding: `${this.theme.spacing.padding}px`,
479
+ fontFamily: this.theme.fontFamily,
480
+ },
481
+ })
482
+
483
+ // Month/Year header
484
+ const header = h('div', {
485
+ style: {
486
+ display: 'flex',
487
+ justifyContent: 'space-between',
488
+ alignItems: 'center',
489
+ marginBottom: `${this.theme.spacing.gap}px`,
490
+ padding: `${this.theme.spacing.gap}px`,
491
+ },
492
+ })
493
+
494
+ const monthYear = h('div', {
495
+ style: {
496
+ fontSize: `${this.theme.fontSize + 2}px`,
497
+ fontWeight: '600',
498
+ color: this.theme.colors.text,
499
+ },
500
+ })
501
+
502
+ const monthYearDOM = monthYear.element as HTMLElement
503
+ if (monthYearDOM) {
504
+ monthYearDOM.textContent = new Date(
505
+ currentYear,
506
+ currentMonth,
507
+ 1
508
+ ).toLocaleDateString(this.resolveValue(this.props.locale || 'en-US'), {
509
+ month: 'long',
510
+ year: 'numeric',
511
+ })
512
+ }
513
+
514
+ // Navigation buttons
515
+ const prevButton = h('button', {
516
+ style: {
517
+ backgroundColor: 'transparent',
518
+ border: 'none',
519
+ color: this.theme.colors.accent,
520
+ cursor: 'pointer',
521
+ fontSize: `${this.theme.fontSize + 4}px`,
522
+ padding: '4px 8px',
523
+ },
524
+ onclick: () => {
525
+ const newDate = new Date(selectedDate)
526
+ newDate.setMonth(currentMonth - 1)
527
+ if (this.isDateInRange(newDate)) {
528
+ this.setSelectedDate(newDate)
529
+ }
530
+ },
531
+ })
532
+
533
+ const prevButtonDOM = prevButton.element as HTMLElement
534
+ if (prevButtonDOM) {
535
+ prevButtonDOM.textContent = '‹'
536
+ }
537
+
538
+ const nextButton = h('button', {
539
+ style: {
540
+ backgroundColor: 'transparent',
541
+ border: 'none',
542
+ color: this.theme.colors.accent,
543
+ cursor: 'pointer',
544
+ fontSize: `${this.theme.fontSize + 4}px`,
545
+ padding: '4px 8px',
546
+ },
547
+ onclick: () => {
548
+ const newDate = new Date(selectedDate)
549
+ newDate.setMonth(currentMonth + 1)
550
+ if (this.isDateInRange(newDate)) {
551
+ this.setSelectedDate(newDate)
552
+ }
553
+ },
554
+ })
555
+
556
+ const nextButtonDOM = nextButton.element as HTMLElement
557
+ if (nextButtonDOM) {
558
+ nextButtonDOM.textContent = '›'
559
+ }
560
+
561
+ // Calendar grid
562
+ const grid = this.createCalendarGrid(selectedDate)
563
+
564
+ // Assemble components
565
+ const headerDOM = header.element as HTMLElement
566
+ const containerDOM = container.element as HTMLElement
567
+
568
+ if (headerDOM) {
569
+ const prevButtonDOM = prevButton.element as HTMLElement
570
+ const monthYearDOM = monthYear.element as HTMLElement
571
+ const nextButtonDOM = nextButton.element as HTMLElement
572
+
573
+ if (prevButtonDOM) headerDOM.appendChild(prevButtonDOM)
574
+ if (monthYearDOM) headerDOM.appendChild(monthYearDOM)
575
+ if (nextButtonDOM) headerDOM.appendChild(nextButtonDOM)
576
+ }
577
+
578
+ if (containerDOM) {
579
+ if (headerDOM) containerDOM.appendChild(headerDOM)
580
+ const gridDOM = grid.element as HTMLElement
581
+ if (gridDOM) containerDOM.appendChild(gridDOM)
582
+ }
583
+
584
+ return container
585
+ }
586
+
587
+ private createCalendarGrid(selectedDate: Date): DOMNode {
588
+ const currentMonth = selectedDate.getMonth()
589
+ const currentYear = selectedDate.getFullYear()
590
+
591
+ const grid = h('div', {
592
+ style: {
593
+ display: 'grid',
594
+ gridTemplateColumns: 'repeat(7, 1fr)',
595
+ gap: '2px',
596
+ fontSize: `${this.theme.fontSize}px`,
597
+ },
598
+ })
599
+
600
+ // Day headers
601
+ const dayHeaders = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
602
+ dayHeaders.forEach(day => {
603
+ const dayHeader = h('div', {
604
+ style: {
605
+ padding: '8px 4px',
606
+ textAlign: 'center',
607
+ fontWeight: '600',
608
+ color: this.theme.colors.disabledText,
609
+ fontSize: `${this.theme.fontSize - 2}px`,
610
+ },
611
+ })
612
+
613
+ const dayHeaderDOM = dayHeader.element as HTMLElement
614
+ if (dayHeaderDOM) {
615
+ dayHeaderDOM.textContent = day
616
+ }
617
+
618
+ const gridDOM = grid.element as HTMLElement
619
+ if (gridDOM && dayHeaderDOM) {
620
+ gridDOM.appendChild(dayHeaderDOM)
621
+ }
622
+ })
623
+
624
+ // Calendar days
625
+ const firstDay = new Date(currentYear, currentMonth, 1).getDay()
626
+ const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate()
627
+ const daysInPrevMonth = new Date(currentYear, currentMonth, 0).getDate()
628
+
629
+ // Previous month's trailing days
630
+ for (let i = firstDay - 1; i >= 0; i--) {
631
+ const day = daysInPrevMonth - i
632
+ const dayElement = this.createDayElement(
633
+ new Date(currentYear, currentMonth - 1, day),
634
+ selectedDate,
635
+ true // isOtherMonth
636
+ )
637
+ const gridDOM = grid.element as HTMLElement
638
+ const dayElementDOM = dayElement.element as HTMLElement
639
+ if (gridDOM && dayElementDOM) {
640
+ gridDOM.appendChild(dayElementDOM)
641
+ }
642
+ }
643
+
644
+ // Current month days
645
+ for (let day = 1; day <= daysInMonth; day++) {
646
+ const date = new Date(currentYear, currentMonth, day)
647
+ const dayElement = this.createDayElement(date, selectedDate, false)
648
+ const gridDOM = grid.element as HTMLElement
649
+ const dayElementDOM = dayElement.element as HTMLElement
650
+ if (gridDOM && dayElementDOM) {
651
+ gridDOM.appendChild(dayElementDOM)
652
+ }
653
+ }
654
+
655
+ // Next month's leading days
656
+ const totalCells = Math.ceil((firstDay + daysInMonth) / 7) * 7
657
+ const remainingCells = totalCells - (firstDay + daysInMonth)
658
+ for (let day = 1; day <= remainingCells; day++) {
659
+ const dayElement = this.createDayElement(
660
+ new Date(currentYear, currentMonth + 1, day),
661
+ selectedDate,
662
+ true // isOtherMonth
663
+ )
664
+ const gridDOM = grid.element as HTMLElement
665
+ const dayElementDOM = dayElement.element as HTMLElement
666
+ if (gridDOM && dayElementDOM) {
667
+ gridDOM.appendChild(dayElementDOM)
668
+ }
669
+ }
670
+
671
+ return grid
672
+ }
673
+
674
+ private createDayElement(
675
+ date: Date,
676
+ selectedDate: Date,
677
+ isOtherMonth: boolean
678
+ ): DOMNode {
679
+ const isSelected = date.toDateString() === selectedDate.toDateString()
680
+ const isToday = date.toDateString() === new Date().toDateString()
681
+ const isInRange = this.isDateInRange(date)
682
+ const isDisabled = this.isDisabled() || !isInRange
683
+
684
+ const dayElement = h('button', {
685
+ disabled: isDisabled,
686
+ style: {
687
+ padding: '8px',
688
+ border: 'none',
689
+ backgroundColor: isSelected
690
+ ? this.theme.colors.selectedBackground
691
+ : isToday
692
+ ? `${this.theme.colors.accent}20`
693
+ : 'transparent',
694
+ color: isSelected
695
+ ? this.theme.colors.selectedText
696
+ : isOtherMonth || isDisabled
697
+ ? this.theme.colors.disabledText
698
+ : this.theme.colors.text,
699
+ borderRadius: '4px',
700
+ cursor: isDisabled ? 'not-allowed' : 'pointer',
701
+ fontSize: `${this.theme.fontSize}px`,
702
+ fontWeight: isSelected || isToday ? '600' : '400',
703
+ textAlign: 'center',
704
+ minHeight: '36px',
705
+ display: 'flex',
706
+ alignItems: 'center',
707
+ justifyContent: 'center',
708
+ },
709
+ onclick: () => {
710
+ if (!isDisabled) {
711
+ this.setSelectedDate(date)
712
+ }
713
+ },
714
+ onmouseenter: (e: Event) => {
715
+ if (!isDisabled && !isSelected) {
716
+ const target = e.target as HTMLElement
717
+ target.style.backgroundColor = `${this.theme.colors.accent}10`
718
+ }
719
+ },
720
+ onmouseleave: (e: Event) => {
721
+ if (!isDisabled && !isSelected && !isToday) {
722
+ const target = e.target as HTMLElement
723
+ target.style.backgroundColor = 'transparent'
724
+ }
725
+ },
726
+ })
727
+
728
+ const dayElementDOM = dayElement.element as HTMLElement
729
+ if (dayElementDOM) {
730
+ dayElementDOM.textContent = date.getDate().toString()
731
+ }
732
+
733
+ return dayElement
734
+ }
735
+
736
+ render(): DOMNode {
737
+ const container = h('div', {
738
+ id: this.id,
739
+ 'data-component': 'datepicker',
740
+ style: {
741
+ display: 'inline-block',
742
+ },
743
+ })
744
+
745
+ // Add title if provided
746
+ if (this.props.title) {
747
+ const label = h('label', {
748
+ for: `${this.id}-input`,
749
+ style: {
750
+ display: 'block',
751
+ marginBottom: `${this.theme.spacing.gap}px`,
752
+ fontSize: `${this.theme.fontSize}px`,
753
+ fontWeight: '500',
754
+ color: this.theme.colors.text,
755
+ fontFamily: this.theme.fontFamily,
756
+ },
757
+ })
758
+
759
+ const labelDOM = label.element as HTMLElement
760
+ if (labelDOM) {
761
+ labelDOM.textContent = this.props.title
762
+ }
763
+
764
+ const containerDOM = container.element as HTMLElement
765
+ if (containerDOM && labelDOM) {
766
+ containerDOM.appendChild(labelDOM)
767
+ }
768
+ }
769
+
770
+ // Create picker based on style
771
+ const style = this.getStyle()
772
+ let picker: DOMNode
773
+
774
+ switch (style) {
775
+ case 'wheel':
776
+ picker = this.createWheelPicker()
777
+ break
778
+ case 'graphical':
779
+ picker = this.createGraphicalPicker()
780
+ break
781
+ default:
782
+ picker = this.createCompactPicker()
783
+ break
784
+ }
785
+
786
+ const containerDOM = container.element as HTMLElement
787
+ const pickerDOM = picker.element as HTMLElement
788
+ if (containerDOM && pickerDOM) {
789
+ containerDOM.appendChild(pickerDOM)
790
+ }
791
+
792
+ // Set up reactive effects
793
+ createEffect(() => {
794
+ // Update picker when reactive props change
795
+ if (
796
+ isSignal(this.props.selection) ||
797
+ isSignal(this.props.style) ||
798
+ isSignal(this.props.displayedComponents)
799
+ ) {
800
+ // Re-render picker with new values
801
+ this.updatePicker()
802
+ }
803
+ })
804
+
805
+ return container
806
+ }
807
+
808
+ private updatePicker(): void {
809
+ // Re-render the picker when reactive properties change
810
+ if (this.containerElement) {
811
+ const style = this.getStyle()
812
+ let newPicker: DOMNode
813
+
814
+ switch (style) {
815
+ case 'wheel':
816
+ newPicker = this.createWheelPicker()
817
+ break
818
+ case 'graphical':
819
+ newPicker = this.createGraphicalPicker()
820
+ break
821
+ default:
822
+ newPicker = this.createCompactPicker()
823
+ break
824
+ }
825
+
826
+ const newPickerDOM = newPicker.element as HTMLElement
827
+ if (newPickerDOM) {
828
+ // Replace the old picker with the new one
829
+ const oldPicker = this.containerElement.querySelector(
830
+ '[data-component="datepicker"] > *:last-child'
831
+ )
832
+ if (oldPicker && this.containerElement) {
833
+ this.containerElement.replaceChild(newPickerDOM, oldPicker)
834
+ }
835
+ }
836
+ }
837
+ }
838
+ }
839
+
840
+ /**
841
+ * Create a DatePicker component
842
+ */
843
+ export function DatePicker(
844
+ props: DatePickerProps
845
+ ): ModifiableComponent<DatePickerProps> & {
846
+ modifier: ModifierBuilder<ModifiableComponent<DatePickerProps>>
847
+ } {
848
+ return withModifiers(new DatePickerComponent(props))
849
+ }
850
+
851
+ /**
852
+ * DatePicker utility functions and presets
853
+ */
854
+ export const DatePickerUtils = {
855
+ /**
856
+ * Create a birthday picker (past dates only)
857
+ */
858
+ birthday(selection: Signal<Date>): DatePickerProps {
859
+ return {
860
+ title: 'Birthday',
861
+ selection,
862
+ displayedComponents: 'date',
863
+ style: 'compact',
864
+ maximumDate: new Date(),
865
+ }
866
+ },
867
+
868
+ /**
869
+ * Create a meeting time picker (future dates only)
870
+ */
871
+ meetingTime(selection: Signal<Date>): DatePickerProps {
872
+ return {
873
+ title: 'Meeting Time',
874
+ selection,
875
+ displayedComponents: 'dateAndTime',
876
+ style: 'compact',
877
+ minimumDate: new Date(),
878
+ }
879
+ },
880
+
881
+ /**
882
+ * Create a deadline picker with date range
883
+ */
884
+ deadline(
885
+ selection: Signal<Date>,
886
+ minimumDate?: Date,
887
+ maximumDate?: Date
888
+ ): DatePickerProps {
889
+ return {
890
+ title: 'Deadline',
891
+ selection,
892
+ displayedComponents: 'date',
893
+ style: 'graphical',
894
+ minimumDate: minimumDate || new Date(),
895
+ maximumDate:
896
+ maximumDate || new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days from now
897
+ }
898
+ },
899
+
900
+ /**
901
+ * Create a time-only picker
902
+ */
903
+ timeOnly(selection: Signal<Date>): DatePickerProps {
904
+ return {
905
+ title: 'Time',
906
+ selection,
907
+ displayedComponents: 'time',
908
+ style: 'wheel',
909
+ }
910
+ },
911
+ }
912
+
913
+ /**
914
+ * DatePicker styles and theming
915
+ */
916
+ export const DatePickerStyles = {
917
+ theme: defaultDatePickerTheme,
918
+
919
+ /**
920
+ * Create a custom theme
921
+ */
922
+ createTheme(overrides: Partial<DatePickerTheme>): DatePickerTheme {
923
+ return { ...defaultDatePickerTheme, ...overrides }
924
+ },
925
+ }