@tachui/forms 0.7.0-alpha1 → 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 (114) hide show
  1. package/README.md +136 -0
  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/{state → components/advanced}/index.js.map +1 -1
  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/{index.js.map → date-picker/index.js.map} +1 -1
  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/index-D3WfkqVv.js +249 -0
  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 +196 -376
  43. package/dist/index.js.map +1 -1
  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 +687 -17
  52. package/dist/validation/index.js.map +1 -1
  53. package/package.json +54 -41
  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/Form-ueYEcSg1.cjs +0 -2
  74. package/dist/Form-ueYEcSg1.cjs.map +0 -1
  75. package/dist/Form-ylAr3o_e.js +0 -376
  76. package/dist/Form-ylAr3o_e.js.map +0 -1
  77. package/dist/components/Form.d.ts +0 -76
  78. package/dist/components/Form.d.ts.map +0 -1
  79. package/dist/components/index.cjs +0 -2
  80. package/dist/components/index.cjs.map +0 -1
  81. package/dist/components/index.d.ts +0 -9
  82. package/dist/components/index.d.ts.map +0 -1
  83. package/dist/components/index.js +0 -31
  84. package/dist/components/input/Checkbox.d.ts.map +0 -1
  85. package/dist/components/input/Radio.d.ts.map +0 -1
  86. package/dist/components/input/Select.d.ts.map +0 -1
  87. package/dist/components/input/TextField.d.ts.map +0 -1
  88. package/dist/components/input/index.d.ts +0 -11
  89. package/dist/components/input/index.d.ts.map +0 -1
  90. package/dist/forms-complex-BiQsZZlT.js +0 -361
  91. package/dist/forms-complex-BiQsZZlT.js.map +0 -1
  92. package/dist/forms-complex-DLEnXXJ5.cjs +0 -2
  93. package/dist/forms-complex-DLEnXXJ5.cjs.map +0 -1
  94. package/dist/forms-core-B1bx1drO.js +0 -839
  95. package/dist/forms-core-B1bx1drO.js.map +0 -1
  96. package/dist/forms-core-W_JGVLAI.cjs +0 -9
  97. package/dist/forms-core-W_JGVLAI.cjs.map +0 -1
  98. package/dist/forms-inputs-6QdeMWFk.js +0 -1075
  99. package/dist/forms-inputs-6QdeMWFk.js.map +0 -1
  100. package/dist/forms-inputs-DQ5QI_SU.cjs +0 -2
  101. package/dist/forms-inputs-DQ5QI_SU.cjs.map +0 -1
  102. package/dist/index.cjs +0 -2
  103. package/dist/index.cjs.map +0 -1
  104. package/dist/state/index.cjs +0 -2
  105. package/dist/state/index.cjs.map +0 -1
  106. package/dist/state/index.js +0 -9
  107. package/dist/utils/validators.d.ts +0 -101
  108. package/dist/utils/validators.d.ts.map +0 -1
  109. package/dist/validation/index.cjs +0 -2
  110. package/dist/validation/index.cjs.map +0 -1
  111. /package/dist/components/{input → selection}/Checkbox.d.ts +0 -0
  112. /package/dist/components/{input → selection}/Radio.d.ts +0 -0
  113. /package/dist/components/{input → selection}/Select.d.ts +0 -0
  114. /package/dist/components/{input → text-input}/TextField.d.ts +0 -0
@@ -0,0 +1,470 @@
1
+ /**
2
+ * Radio Button Component
3
+ *
4
+ * SwiftUI-inspired radio button with group management,
5
+ * validation, and comprehensive accessibility support.
6
+ */
7
+
8
+ import type { Component, ComponentInstance } from '@tachui/core'
9
+ import { createEffect, createSignal, h, text } from '@tachui/core'
10
+ import { createField } from '../../state'
11
+ import type { RadioProps } from '../../types'
12
+
13
+ /**
14
+ * Individual Radio component
15
+ */
16
+ export const Radio: Component<RadioProps> = props => {
17
+ const {
18
+ name,
19
+ value,
20
+ label,
21
+ checked: controlledChecked,
22
+ groupName,
23
+ disabled = false,
24
+ required = false,
25
+ validation,
26
+ onChange,
27
+ onBlur,
28
+ onFocus,
29
+ error: externalError,
30
+ helperText,
31
+ ...restProps
32
+ } = props
33
+
34
+ // Get form context if available
35
+ const formContext = (props as any)._formContext
36
+
37
+ // For radio buttons, we use the group name or the name
38
+ const fieldName = groupName || name
39
+
40
+ // Create field state (shared across radio group)
41
+ const field = createField(
42
+ fieldName,
43
+ controlledChecked ? value : undefined,
44
+ validation
45
+ )
46
+
47
+ // Register field with form if form context exists
48
+ if (formContext) {
49
+ formContext.register(fieldName, validation)
50
+ }
51
+
52
+ const [focused, setFocused] = createSignal(false)
53
+
54
+ // Determine if this radio is checked
55
+ const isChecked = () => field.value() === value
56
+
57
+ // Handle radio change
58
+ const handleChange = (event: Event) => {
59
+ const target = event.target as HTMLInputElement
60
+
61
+ if (target.checked) {
62
+ field.setValue(value)
63
+
64
+ if (formContext) {
65
+ formContext.setValue(fieldName, value)
66
+ }
67
+
68
+ if (onChange) {
69
+ onChange(fieldName, value, field as any)
70
+ }
71
+ }
72
+ }
73
+
74
+ // Handle focus
75
+ const handleFocus = (_event: Event) => {
76
+ setFocused(true)
77
+ field.onFocus()
78
+
79
+ if (onFocus) {
80
+ onFocus(fieldName, field.value())
81
+ }
82
+ }
83
+
84
+ // Handle blur
85
+ const handleBlur = (_event: Event) => {
86
+ setFocused(false)
87
+ field.onBlur()
88
+
89
+ if (onBlur) {
90
+ onBlur(fieldName, field.value())
91
+ }
92
+ }
93
+
94
+ // Determine error message
95
+ const errorMessage =
96
+ externalError || field.error() || formContext?.getError(fieldName)
97
+
98
+ // Handle keyboard interaction
99
+ const handleKeyDown = (event: KeyboardEvent) => {
100
+ // Arrow keys for radio group navigation would be handled at the group level
101
+ if (event.key === ' ' || event.key === 'Enter') {
102
+ event.preventDefault()
103
+ handleChange(event)
104
+ }
105
+ }
106
+
107
+ const componentInstance: ComponentInstance = {
108
+ type: 'component',
109
+ id: restProps.id || `radio-${fieldName}-${value}`,
110
+ render: () =>
111
+ h(
112
+ 'div',
113
+ {
114
+ ...restProps,
115
+ class: `tachui-radio ${restProps.class || ''}`.trim(),
116
+ 'data-tachui-radio-container': true,
117
+ 'data-field-state': errorMessage
118
+ ? 'error'
119
+ : field.validating()
120
+ ? 'validating'
121
+ : 'valid',
122
+ 'data-checked': isChecked(),
123
+ 'data-disabled': disabled,
124
+ },
125
+ // Radio input and label wrapper
126
+ h(
127
+ 'label',
128
+ {
129
+ 'data-tachui-radio-label': true,
130
+ 'data-focused': focused(),
131
+ 'data-disabled': disabled,
132
+ },
133
+ // Hidden native radio for accessibility
134
+ h('input', {
135
+ type: 'radio',
136
+ id: restProps.id || `${fieldName}-${value}`,
137
+ name: fieldName,
138
+ value: value,
139
+ checked: isChecked(),
140
+ disabled,
141
+ required,
142
+ onchange: handleChange,
143
+ onfocus: handleFocus,
144
+ onblur: handleBlur,
145
+ onkeydown: handleKeyDown,
146
+ 'aria-invalid': !!errorMessage,
147
+ 'aria-describedby':
148
+ [
149
+ errorMessage ? `${fieldName}-error` : null,
150
+ helperText ? `${fieldName}-helper` : null,
151
+ ]
152
+ .filter(Boolean)
153
+ .join(' ') || undefined,
154
+ 'data-tachui-radio-input': true,
155
+ style: {
156
+ position: 'absolute',
157
+ opacity: '0',
158
+ width: '1px',
159
+ height: '1px',
160
+ margin: '-1px',
161
+ padding: '0',
162
+ border: '0',
163
+ clip: 'rect(0,0,0,0)',
164
+ },
165
+ }),
166
+
167
+ // Custom radio visual
168
+ h(
169
+ 'div',
170
+ {
171
+ 'data-tachui-radio-visual': true,
172
+ 'data-checked': isChecked(),
173
+ 'data-focused': focused(),
174
+ 'data-disabled': disabled,
175
+ 'data-error': !!errorMessage,
176
+ 'aria-hidden': 'true',
177
+ role: 'presentation',
178
+ },
179
+ // Radio dot indicator
180
+ ...(isChecked()
181
+ ? [
182
+ h('div', {
183
+ 'data-tachui-radio-dot': true,
184
+ }),
185
+ ]
186
+ : [])
187
+ ),
188
+
189
+ // Label text
190
+ ...(label
191
+ ? [
192
+ h(
193
+ 'span',
194
+ {
195
+ 'data-tachui-radio-text': true,
196
+ 'data-disabled': disabled,
197
+ },
198
+ text(label),
199
+ ...(required
200
+ ? [
201
+ h(
202
+ 'span',
203
+ {
204
+ 'aria-label': 'required',
205
+ 'data-required-indicator': true,
206
+ },
207
+ text(' *')
208
+ ),
209
+ ]
210
+ : [])
211
+ ),
212
+ ]
213
+ : [])
214
+ )
215
+ ),
216
+ props: props,
217
+ cleanup: [
218
+ () => {
219
+ // Only unregister if this is the last radio in the group
220
+ // This would need more sophisticated group management
221
+ // For now, we let the form handle cleanup
222
+ },
223
+ ],
224
+ }
225
+
226
+ return componentInstance
227
+ }
228
+
229
+ /**
230
+ * RadioGroup component for managing multiple radio buttons
231
+ */
232
+ export const RadioGroup: Component<{
233
+ name: string
234
+ label?: string
235
+ options: Array<{
236
+ value: any
237
+ label: string
238
+ disabled?: boolean
239
+ }>
240
+ value?: any
241
+ defaultValue?: any
242
+ onChange?: (name: string, value: any) => void
243
+ validation?: any
244
+ error?: string
245
+ helperText?: string
246
+ disabled?: boolean
247
+ required?: boolean
248
+ direction?: 'horizontal' | 'vertical'
249
+ id?: string
250
+ [key: string]: any
251
+ }> = props => {
252
+ const {
253
+ name,
254
+ label,
255
+ options,
256
+ value: controlledValue,
257
+ defaultValue,
258
+ onChange,
259
+ validation,
260
+ error,
261
+ helperText,
262
+ disabled = false,
263
+ required = false,
264
+ direction = 'vertical',
265
+ ...restProps
266
+ } = props
267
+
268
+ // Get form context if available
269
+ const formContext = (props as any)._formContext
270
+
271
+ // Create field state for the group
272
+ const field = createField(name, controlledValue ?? defaultValue, validation)
273
+
274
+ // Register field with form if form context exists
275
+ if (formContext) {
276
+ formContext.register(name, validation)
277
+ }
278
+
279
+ // Sync with controlled value
280
+ if (controlledValue !== undefined) {
281
+ createEffect(() => {
282
+ if (field.value() !== controlledValue) {
283
+ field.setValue(controlledValue)
284
+ }
285
+ })
286
+ }
287
+
288
+ // Handle radio selection change
289
+ const handleRadioChange = (optionValue: any) => {
290
+ field.setValue(optionValue)
291
+
292
+ if (formContext) {
293
+ formContext.setValue(name, optionValue)
294
+ }
295
+
296
+ if (onChange) {
297
+ onChange(name, optionValue)
298
+ }
299
+ }
300
+
301
+ // Keyboard navigation for radio group
302
+ const handleKeyDown = (event: KeyboardEvent) => {
303
+ const currentIndex = options.findIndex(opt => opt.value === field.value())
304
+ let nextIndex = currentIndex
305
+
306
+ switch (event.key) {
307
+ case 'ArrowDown':
308
+ case 'ArrowRight':
309
+ event.preventDefault()
310
+ nextIndex = (currentIndex + 1) % options.length
311
+ break
312
+ case 'ArrowUp':
313
+ case 'ArrowLeft':
314
+ event.preventDefault()
315
+ nextIndex = currentIndex === 0 ? options.length - 1 : currentIndex - 1
316
+ break
317
+ case 'Home':
318
+ event.preventDefault()
319
+ nextIndex = 0
320
+ break
321
+ case 'End':
322
+ event.preventDefault()
323
+ nextIndex = options.length - 1
324
+ break
325
+ default:
326
+ return
327
+ }
328
+
329
+ // Skip disabled options
330
+ while (options[nextIndex]?.disabled) {
331
+ if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
332
+ nextIndex = (nextIndex + 1) % options.length
333
+ } else {
334
+ nextIndex = nextIndex === 0 ? options.length - 1 : nextIndex - 1
335
+ }
336
+
337
+ // Prevent infinite loop
338
+ if (nextIndex === currentIndex) break
339
+ }
340
+
341
+ if (!options[nextIndex]?.disabled) {
342
+ handleRadioChange(options[nextIndex].value)
343
+
344
+ // Focus the newly selected radio
345
+ setTimeout(() => {
346
+ const radioInput = document.querySelector(
347
+ `input[name="${name}"][value="${options[nextIndex].value}"]`
348
+ ) as HTMLInputElement
349
+ if (radioInput) {
350
+ radioInput.focus()
351
+ }
352
+ }, 0)
353
+ }
354
+ }
355
+
356
+ const errorMessage = error || field.error()
357
+
358
+ const componentInstance: ComponentInstance = {
359
+ type: 'component',
360
+ id: restProps.id || `radio-group-${name}`,
361
+ render: () =>
362
+ h(
363
+ 'fieldset',
364
+ {
365
+ ...restProps,
366
+ 'data-tachui-radio-group': true,
367
+ 'data-direction': direction,
368
+ 'data-disabled': disabled,
369
+ role: 'radiogroup',
370
+ 'aria-invalid': !!errorMessage,
371
+ 'aria-describedby':
372
+ [
373
+ errorMessage ? `${name}-error` : null,
374
+ helperText ? `${name}-helper` : null,
375
+ ]
376
+ .filter(Boolean)
377
+ .join(' ') || undefined,
378
+ onkeydown: handleKeyDown,
379
+ },
380
+ // Group label
381
+ ...(label
382
+ ? [
383
+ h(
384
+ 'legend',
385
+ {
386
+ 'data-tachui-group-label': true,
387
+ },
388
+ text(label),
389
+ ...(required
390
+ ? [
391
+ h(
392
+ 'span',
393
+ {
394
+ 'aria-label': 'required',
395
+ 'data-required-indicator': true,
396
+ },
397
+ text(' *')
398
+ ),
399
+ ]
400
+ : [])
401
+ ),
402
+ ]
403
+ : []),
404
+
405
+ // Radio options
406
+ h(
407
+ 'div',
408
+ {
409
+ 'data-tachui-radio-options': true,
410
+ 'data-direction': direction,
411
+ },
412
+ ...options.flatMap((option, index) => {
413
+ const radio = Radio({
414
+ name: `${name}-${index}`,
415
+ groupName: name,
416
+ value: option.value,
417
+ label: option.label,
418
+ checked: field.value() === option.value,
419
+ disabled: disabled || option.disabled,
420
+ required,
421
+ onChange: () => handleRadioChange(option.value),
422
+ _formContext: formContext,
423
+ })
424
+ const result = radio.render()
425
+ return Array.isArray(result) ? result : [result]
426
+ })
427
+ ),
428
+
429
+ // Error message
430
+ ...(errorMessage
431
+ ? [
432
+ h(
433
+ 'div',
434
+ {
435
+ id: `${name}-error`,
436
+ role: 'alert',
437
+ 'aria-live': 'polite',
438
+ 'data-tachui-error': true,
439
+ },
440
+ text(errorMessage)
441
+ ),
442
+ ]
443
+ : []),
444
+
445
+ // Helper text
446
+ ...(helperText && !errorMessage
447
+ ? [
448
+ h(
449
+ 'div',
450
+ {
451
+ id: `${name}-helper`,
452
+ 'data-tachui-helper': true,
453
+ },
454
+ text(helperText)
455
+ ),
456
+ ]
457
+ : [])
458
+ ),
459
+ props: props,
460
+ cleanup: [
461
+ () => {
462
+ if (formContext) {
463
+ formContext.unregister(name)
464
+ }
465
+ },
466
+ ],
467
+ }
468
+
469
+ return componentInstance
470
+ }