@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,715 @@
1
+ /**
2
+ * Stepper Component (TachUI)
3
+ *
4
+ * SwiftUI-inspired stepper component for numeric input with increment/decrement controls.
5
+ * Provides bounded value adjustment with customizable step intervals and formatting.
6
+ */
7
+
8
+ import type { ModifiableComponent, ModifierBuilder } from '@tachui/core'
9
+ import { createEffect, getSignalImpl, 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
+ * Stepper value types - supports integers and floating point numbers
17
+ */
18
+ export type StepperValue = number
19
+
20
+ /**
21
+ * Stepper component properties
22
+ */
23
+ export interface StepperProps extends ComponentProps {
24
+ // Core properties
25
+ title?: string
26
+ value: Signal<StepperValue> | StepperValue
27
+
28
+ // Range and stepping
29
+ minimumValue?: StepperValue
30
+ maximumValue?: StepperValue
31
+ step?: StepperValue
32
+
33
+ // Custom actions (alternative to value binding)
34
+ onIncrement?: () => void
35
+ onDecrement?: () => void
36
+
37
+ // Event handling
38
+ onChange?: (value: StepperValue) => void
39
+ onEditingChanged?: (editing: boolean) => void
40
+
41
+ // Behavior
42
+ disabled?: boolean | Signal<boolean>
43
+ allowsEmptyValue?: boolean
44
+
45
+ // Formatting
46
+ valueFormatter?: (value: StepperValue) => string
47
+ displayValueInLabel?: boolean
48
+
49
+ // Accessibility
50
+ accessibilityLabel?: string
51
+ accessibilityHint?: string
52
+ incrementAccessibilityLabel?: string
53
+ decrementAccessibilityLabel?: string
54
+ }
55
+
56
+ /**
57
+ * Stepper theme configuration
58
+ */
59
+ export interface StepperTheme {
60
+ colors: {
61
+ background: string
62
+ border: string
63
+ buttonBackground: string
64
+ buttonHover: string
65
+ buttonPress: string
66
+ buttonDisabled: string
67
+ text: string
68
+ buttonText: string
69
+ disabledText: string
70
+ focusRing: string
71
+ }
72
+ spacing: {
73
+ padding: number
74
+ gap: number
75
+ borderRadius: number
76
+ buttonSize: number
77
+ }
78
+ typography: {
79
+ labelSize: number
80
+ buttonSize: number
81
+ labelWeight: string
82
+ buttonWeight: string
83
+ fontFamily: string
84
+ }
85
+ transitions: {
86
+ duration: number
87
+ easing: string
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Default Stepper theme
93
+ */
94
+ const defaultStepperTheme: StepperTheme = {
95
+ colors: {
96
+ background: '#FFFFFF',
97
+ border: '#D1D1D6',
98
+ buttonBackground: '#F2F2F7',
99
+ buttonHover: '#E5E5EA',
100
+ buttonPress: '#D1D1D6',
101
+ buttonDisabled: '#F2F2F7',
102
+ text: '#000000',
103
+ buttonText: '#007AFF',
104
+ disabledText: '#8E8E93',
105
+ focusRing: '#007AFF',
106
+ },
107
+ spacing: {
108
+ padding: 12,
109
+ gap: 8,
110
+ borderRadius: 8,
111
+ buttonSize: 32,
112
+ },
113
+ typography: {
114
+ labelSize: 16,
115
+ buttonSize: 18,
116
+ labelWeight: '400',
117
+ buttonWeight: '600',
118
+ fontFamily:
119
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
120
+ },
121
+ transitions: {
122
+ duration: 150,
123
+ easing: 'cubic-bezier(0.2, 0.8, 0.2, 1)',
124
+ },
125
+ }
126
+
127
+ /**
128
+ * Stepper component implementation
129
+ */
130
+ export class StepperComponent implements ComponentInstance<StepperProps> {
131
+ public readonly type = 'component' as const
132
+ public readonly id: string
133
+ public readonly props: StepperProps
134
+ private theme: StepperTheme = defaultStepperTheme
135
+ private incrementButton: HTMLElement | null = null
136
+ private decrementButton: HTMLElement | null = null
137
+ private isEditing = false
138
+ private longPressTimer: ReturnType<typeof setTimeout> | null = null
139
+ private longPressInterval: ReturnType<typeof setInterval> | null = null
140
+
141
+ constructor(props: StepperProps) {
142
+ this.props = props
143
+ this.id = `stepper-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
144
+ }
145
+
146
+ private resolveValue<T>(value: T | Signal<T>): T {
147
+ return isSignal(value) ? value() : value
148
+ }
149
+
150
+ private getValue(): StepperValue {
151
+ return this.resolveValue(this.props.value)
152
+ }
153
+
154
+ private setValue(newValue: StepperValue): void {
155
+ const constrainedValue = this.constrainValue(newValue)
156
+
157
+ if (isSignal(this.props.value)) {
158
+ const signalImpl = getSignalImpl(this.props.value as any)
159
+ if (signalImpl) {
160
+ signalImpl.set(constrainedValue)
161
+ }
162
+ }
163
+
164
+ if (this.props.onChange) {
165
+ this.props.onChange(constrainedValue)
166
+ }
167
+ }
168
+
169
+ private getMinimumValue(): StepperValue {
170
+ return this.props.minimumValue ?? -Infinity
171
+ }
172
+
173
+ private getMaximumValue(): StepperValue {
174
+ return this.props.maximumValue ?? Infinity
175
+ }
176
+
177
+ private getStep(): StepperValue {
178
+ return this.props.step ?? 1
179
+ }
180
+
181
+ private isDisabled(): boolean {
182
+ return this.resolveValue(this.props.disabled ?? false)
183
+ }
184
+
185
+ private constrainValue(value: StepperValue): StepperValue {
186
+ const min = this.getMinimumValue()
187
+ const max = this.getMaximumValue()
188
+ return Math.min(Math.max(value, min), max)
189
+ }
190
+
191
+ private canIncrement(): boolean {
192
+ if (this.isDisabled()) return false
193
+ const currentValue = this.getValue()
194
+ const maxValue = this.getMaximumValue()
195
+ return currentValue < maxValue
196
+ }
197
+
198
+ private canDecrement(): boolean {
199
+ if (this.isDisabled()) return false
200
+ const currentValue = this.getValue()
201
+ const minValue = this.getMinimumValue()
202
+ return currentValue > minValue
203
+ }
204
+
205
+ private increment(): void {
206
+ if (!this.canIncrement()) return
207
+
208
+ if (this.props.onIncrement) {
209
+ this.props.onIncrement()
210
+ } else {
211
+ const currentValue = this.getValue()
212
+ const step = this.getStep()
213
+ this.setValue(currentValue + step)
214
+ }
215
+ }
216
+
217
+ private decrement(): void {
218
+ if (!this.canDecrement()) return
219
+
220
+ if (this.props.onDecrement) {
221
+ this.props.onDecrement()
222
+ } else {
223
+ const currentValue = this.getValue()
224
+ const step = this.getStep()
225
+ this.setValue(currentValue - step)
226
+ }
227
+ }
228
+
229
+ private formatValue(value: StepperValue): string {
230
+ if (this.props.valueFormatter) {
231
+ return this.props.valueFormatter(value)
232
+ }
233
+
234
+ // Default formatting with appropriate decimal places
235
+ if (Number.isInteger(value)) {
236
+ return value.toString()
237
+ } else {
238
+ // For decimal values, show up to 2 decimal places, removing trailing zeros
239
+ return parseFloat(value.toFixed(2)).toString()
240
+ }
241
+ }
242
+
243
+ private startLongPress(action: () => void): void {
244
+ this.stopLongPress()
245
+ this.setEditing(true)
246
+
247
+ // Initial delay before starting continuous increment
248
+ this.longPressTimer = setTimeout(() => {
249
+ // Start continuous increment/decrement
250
+ this.longPressInterval = setInterval(() => {
251
+ action()
252
+ }, 100) // 100ms interval for smooth continuous changes
253
+ }, 500) // 500ms initial delay
254
+ }
255
+
256
+ private stopLongPress(): void {
257
+ if (this.longPressTimer) {
258
+ clearTimeout(this.longPressTimer)
259
+ this.longPressTimer = null
260
+ }
261
+
262
+ if (this.longPressInterval) {
263
+ clearInterval(this.longPressInterval)
264
+ this.longPressInterval = null
265
+ }
266
+
267
+ this.setEditing(false)
268
+ }
269
+
270
+ private setEditing(editing: boolean): void {
271
+ if (this.isEditing !== editing) {
272
+ this.isEditing = editing
273
+ if (this.props.onEditingChanged) {
274
+ this.props.onEditingChanged(editing)
275
+ }
276
+ }
277
+ }
278
+
279
+ private createButton(
280
+ type: 'increment' | 'decrement',
281
+ symbol: string,
282
+ action: () => void,
283
+ canPerformAction: boolean
284
+ ): DOMNode {
285
+ const button = h('button', {
286
+ type: 'button',
287
+ disabled: !canPerformAction,
288
+ 'aria-label':
289
+ type === 'increment'
290
+ ? this.props.incrementAccessibilityLabel || 'Increment'
291
+ : this.props.decrementAccessibilityLabel || 'Decrement',
292
+ style: {
293
+ width: `${this.theme.spacing.buttonSize}px`,
294
+ height: `${this.theme.spacing.buttonSize}px`,
295
+ border: `1px solid ${this.theme.colors.border}`,
296
+ borderRadius: `${this.theme.spacing.borderRadius}px`,
297
+ backgroundColor: canPerformAction
298
+ ? this.theme.colors.buttonBackground
299
+ : this.theme.colors.buttonDisabled,
300
+ color: canPerformAction
301
+ ? this.theme.colors.buttonText
302
+ : this.theme.colors.disabledText,
303
+ fontSize: `${this.theme.typography.buttonSize}px`,
304
+ fontWeight: this.theme.typography.buttonWeight,
305
+ fontFamily: this.theme.typography.fontFamily,
306
+ cursor: canPerformAction ? 'pointer' : 'not-allowed',
307
+ display: 'flex',
308
+ alignItems: 'center',
309
+ justifyContent: 'center',
310
+ userSelect: 'none',
311
+ outline: 'none',
312
+ transition: `all ${this.theme.transitions.duration}ms ${this.theme.transitions.easing}`,
313
+ touchAction: 'manipulation', // Prevent double-tap zoom on mobile
314
+ },
315
+ onclick: (e: Event) => {
316
+ e.preventDefault()
317
+ if (canPerformAction) {
318
+ action()
319
+ }
320
+ },
321
+ onmousedown: (e: Event) => {
322
+ e.preventDefault()
323
+ if (canPerformAction) {
324
+ this.startLongPress(action)
325
+ }
326
+ },
327
+ onmouseup: () => {
328
+ this.stopLongPress()
329
+ },
330
+ ontouchstart: (e: Event) => {
331
+ e.preventDefault()
332
+ if (canPerformAction) {
333
+ this.startLongPress(action)
334
+ }
335
+ },
336
+ ontouchend: (e: Event) => {
337
+ e.preventDefault()
338
+ this.stopLongPress()
339
+ },
340
+ onmouseenter: (e: Event) => {
341
+ if (canPerformAction) {
342
+ const target = e.target as HTMLElement
343
+ target.style.backgroundColor = this.theme.colors.buttonHover
344
+ }
345
+ },
346
+ onmouseleave: (e: Event) => {
347
+ const target = e.target as HTMLElement
348
+ target.style.backgroundColor = canPerformAction
349
+ ? this.theme.colors.buttonBackground
350
+ : this.theme.colors.buttonDisabled
351
+ this.stopLongPress()
352
+ },
353
+ onkeydown: (e: KeyboardEvent) => {
354
+ if (e.key === 'Enter' || e.key === ' ') {
355
+ e.preventDefault()
356
+ if (canPerformAction) {
357
+ action()
358
+ }
359
+ }
360
+ },
361
+ onfocus: (e: Event) => {
362
+ const target = e.target as HTMLElement
363
+ target.style.boxShadow = `0 0 0 2px ${this.theme.colors.focusRing}40`
364
+ },
365
+ onblur: (e: Event) => {
366
+ const target = e.target as HTMLElement
367
+ target.style.boxShadow = 'none'
368
+ this.stopLongPress()
369
+ },
370
+ })
371
+
372
+ const buttonDOM = button.element as HTMLElement
373
+ if (buttonDOM) {
374
+ buttonDOM.textContent = symbol
375
+ buttonDOM.setAttribute('tabindex', '0')
376
+
377
+ if (type === 'increment') {
378
+ this.incrementButton = buttonDOM
379
+ } else {
380
+ this.decrementButton = buttonDOM
381
+ }
382
+ }
383
+
384
+ return button
385
+ }
386
+
387
+ private createLabel(): DOMNode {
388
+ const currentValue = this.getValue()
389
+ const formattedValue = this.formatValue(currentValue)
390
+
391
+ let labelText = this.props.title || ''
392
+ if (this.props.displayValueInLabel !== false && this.props.title) {
393
+ labelText = `${this.props.title}: ${formattedValue}`
394
+ } else if (!this.props.title && this.props.displayValueInLabel !== false) {
395
+ labelText = formattedValue
396
+ }
397
+
398
+ const label = h('span', {
399
+ style: {
400
+ fontSize: `${this.theme.typography.labelSize}px`,
401
+ fontWeight: this.theme.typography.labelWeight,
402
+ fontFamily: this.theme.typography.fontFamily,
403
+ color: this.isDisabled()
404
+ ? this.theme.colors.disabledText
405
+ : this.theme.colors.text,
406
+ userSelect: 'none',
407
+ display: 'flex',
408
+ alignItems: 'center',
409
+ minHeight: `${this.theme.spacing.buttonSize}px`,
410
+ },
411
+ })
412
+
413
+ const labelDOM = label.element as HTMLElement
414
+ if (labelDOM) {
415
+ labelDOM.textContent = labelText
416
+ }
417
+
418
+ return label
419
+ }
420
+
421
+ render(): DOMNode {
422
+ const container = h('div', {
423
+ id: this.id,
424
+ 'data-component': 'stepper',
425
+ role: 'group',
426
+ 'aria-label':
427
+ this.props.accessibilityLabel || this.props.title || 'Numeric stepper',
428
+ 'aria-describedby': this.props.accessibilityHint
429
+ ? `${this.id}-hint`
430
+ : undefined,
431
+ style: {
432
+ display: 'inline-flex',
433
+ alignItems: 'center',
434
+ gap: `${this.theme.spacing.gap}px`,
435
+ padding: `${this.theme.spacing.padding}px`,
436
+ backgroundColor: this.theme.colors.background,
437
+ border: `1px solid ${this.theme.colors.border}`,
438
+ borderRadius: `${this.theme.spacing.borderRadius}px`,
439
+ fontFamily: this.theme.typography.fontFamily,
440
+ },
441
+ })
442
+
443
+ // Create components
444
+ const decrementButton = this.createButton(
445
+ 'decrement',
446
+ '−', // Using minus sign (U+2212) instead of hyphen for better visual appearance
447
+ () => this.decrement(),
448
+ this.canDecrement()
449
+ )
450
+
451
+ const label = this.createLabel()
452
+
453
+ const incrementButton = this.createButton(
454
+ 'increment',
455
+ '+',
456
+ () => this.increment(),
457
+ this.canIncrement()
458
+ )
459
+
460
+ // Assemble container
461
+ const containerDOM = container.element as HTMLElement
462
+ if (containerDOM) {
463
+ const decrementDOM = decrementButton.element as HTMLElement
464
+ const labelDOM = label.element as HTMLElement
465
+ const incrementDOM = incrementButton.element as HTMLElement
466
+
467
+ if (decrementDOM) containerDOM.appendChild(decrementDOM)
468
+ if (labelDOM) containerDOM.appendChild(labelDOM)
469
+ if (incrementDOM) containerDOM.appendChild(incrementDOM)
470
+
471
+ // Add accessibility hint if provided
472
+ if (this.props.accessibilityHint) {
473
+ const hint = h('span', {
474
+ id: `${this.id}-hint`,
475
+ style: {
476
+ position: 'absolute',
477
+ width: '1px',
478
+ height: '1px',
479
+ padding: '0',
480
+ margin: '-1px',
481
+ overflow: 'hidden',
482
+ clip: 'rect(0, 0, 0, 0)',
483
+ whiteSpace: 'nowrap',
484
+ border: '0',
485
+ },
486
+ })
487
+
488
+ const hintDOM = hint.element as HTMLElement
489
+ if (hintDOM) {
490
+ hintDOM.textContent = this.props.accessibilityHint
491
+ containerDOM.appendChild(hintDOM)
492
+ }
493
+ }
494
+ }
495
+
496
+ // Set up reactive effects for value changes
497
+ createEffect(() => {
498
+ // Update button states when value changes
499
+ if (this.incrementButton) {
500
+ const canInc: boolean = this.canIncrement()
501
+ ;(this.incrementButton as any).disabled = !canInc
502
+ this.incrementButton.style.backgroundColor = canInc
503
+ ? this.theme.colors.buttonBackground
504
+ : this.theme.colors.buttonDisabled
505
+ this.incrementButton.style.color = canInc
506
+ ? this.theme.colors.buttonText
507
+ : this.theme.colors.disabledText
508
+ this.incrementButton.style.cursor = canInc ? 'pointer' : 'not-allowed'
509
+ }
510
+
511
+ if (this.decrementButton) {
512
+ const canDec: boolean = this.canDecrement()
513
+ ;(this.decrementButton as any).disabled = !canDec
514
+ this.decrementButton.style.backgroundColor = canDec
515
+ ? this.theme.colors.buttonBackground
516
+ : this.theme.colors.buttonDisabled
517
+ this.decrementButton.style.color = canDec
518
+ ? this.theme.colors.buttonText
519
+ : this.theme.colors.disabledText
520
+ this.decrementButton.style.cursor = canDec ? 'pointer' : 'not-allowed'
521
+ }
522
+
523
+ // Update label text
524
+ const labelElement = containerDOM?.querySelector(
525
+ 'span:not([id$="-hint"])'
526
+ ) as HTMLElement
527
+ if (labelElement) {
528
+ const currentValue = this.getValue()
529
+ const formattedValue = this.formatValue(currentValue)
530
+
531
+ let labelText = this.props.title || ''
532
+ if (this.props.displayValueInLabel !== false && this.props.title) {
533
+ labelText = `${this.props.title}: ${formattedValue}`
534
+ } else if (
535
+ !this.props.title &&
536
+ this.props.displayValueInLabel !== false
537
+ ) {
538
+ labelText = formattedValue
539
+ }
540
+
541
+ labelElement.textContent = labelText
542
+ labelElement.style.color = this.isDisabled()
543
+ ? this.theme.colors.disabledText
544
+ : this.theme.colors.text
545
+ }
546
+ })
547
+
548
+ return container
549
+ }
550
+ }
551
+
552
+ /**
553
+ * Create a Stepper component
554
+ */
555
+ export function Stepper(
556
+ props: StepperProps
557
+ ): ModifiableComponent<StepperProps> & {
558
+ modifier: ModifierBuilder<ModifiableComponent<StepperProps>>
559
+ } {
560
+ return withModifiers(new StepperComponent(props))
561
+ }
562
+
563
+ /**
564
+ * Stepper utility functions and presets
565
+ */
566
+ export const StepperUtils = {
567
+ /**
568
+ * Create a quantity stepper (1-99, integer values)
569
+ */
570
+ quantity(
571
+ value: Signal<number>,
572
+ onChange?: (value: number) => void
573
+ ): Omit<StepperProps, 'value'> & { value: Signal<number> } {
574
+ return {
575
+ title: 'Quantity',
576
+ value,
577
+ minimumValue: 1,
578
+ maximumValue: 99,
579
+ step: 1,
580
+ onChange,
581
+ displayValueInLabel: true,
582
+ accessibilityLabel: 'Product quantity',
583
+ incrementAccessibilityLabel: 'Increase quantity',
584
+ decrementAccessibilityLabel: 'Decrease quantity',
585
+ }
586
+ },
587
+
588
+ /**
589
+ * Create an age stepper (0-120, integer values)
590
+ */
591
+ age(
592
+ value: Signal<number>,
593
+ onChange?: (value: number) => void
594
+ ): Omit<StepperProps, 'value'> & { value: Signal<number> } {
595
+ return {
596
+ title: 'Age',
597
+ value,
598
+ minimumValue: 0,
599
+ maximumValue: 120,
600
+ step: 1,
601
+ onChange,
602
+ displayValueInLabel: true,
603
+ accessibilityLabel: 'Age in years',
604
+ incrementAccessibilityLabel: 'Increase age',
605
+ decrementAccessibilityLabel: 'Decrease age',
606
+ }
607
+ },
608
+
609
+ /**
610
+ * Create a percentage stepper (0-100%, integer values)
611
+ */
612
+ percentage(
613
+ value: Signal<number>,
614
+ onChange?: (value: number) => void
615
+ ): Omit<StepperProps, 'value'> & { value: Signal<number> } {
616
+ return {
617
+ title: 'Percentage',
618
+ value,
619
+ minimumValue: 0,
620
+ maximumValue: 100,
621
+ step: 1,
622
+ onChange,
623
+ displayValueInLabel: true,
624
+ valueFormatter: (val: number) => `${val}%`,
625
+ accessibilityLabel: 'Percentage value',
626
+ incrementAccessibilityLabel: 'Increase percentage',
627
+ decrementAccessibilityLabel: 'Decrease percentage',
628
+ }
629
+ },
630
+
631
+ /**
632
+ * Create a rating stepper (1-5 or 1-10, decimal values allowed)
633
+ */
634
+ rating(
635
+ value: Signal<number>,
636
+ maxRating: number = 5,
637
+ step: number = 0.5,
638
+ onChange?: (value: number) => void
639
+ ): Omit<StepperProps, 'value'> & { value: Signal<number> } {
640
+ return {
641
+ title: 'Rating',
642
+ value,
643
+ minimumValue: 0,
644
+ maximumValue: maxRating,
645
+ step,
646
+ onChange,
647
+ displayValueInLabel: true,
648
+ valueFormatter: (val: number) => `${val}/${maxRating}`,
649
+ accessibilityLabel: `Rating out of ${maxRating}`,
650
+ incrementAccessibilityLabel: 'Increase rating',
651
+ decrementAccessibilityLabel: 'Decrease rating',
652
+ }
653
+ },
654
+
655
+ /**
656
+ * Create a price stepper (0+, decimal values with currency formatting)
657
+ */
658
+ price(
659
+ value: Signal<number>,
660
+ currency: string = '$',
661
+ step: number = 0.01,
662
+ maxValue?: number,
663
+ onChange?: (value: number) => void
664
+ ): Omit<StepperProps, 'value'> & { value: Signal<number> } {
665
+ return {
666
+ title: 'Price',
667
+ value,
668
+ minimumValue: 0,
669
+ maximumValue: maxValue,
670
+ step,
671
+ onChange,
672
+ displayValueInLabel: true,
673
+ valueFormatter: (val: number) => `${currency}${val.toFixed(2)}`,
674
+ accessibilityLabel: 'Price amount',
675
+ incrementAccessibilityLabel: 'Increase price',
676
+ decrementAccessibilityLabel: 'Decrease price',
677
+ }
678
+ },
679
+
680
+ /**
681
+ * Create a font size stepper (8-72pt, integer values)
682
+ */
683
+ fontSize(
684
+ value: Signal<number>,
685
+ onChange?: (value: number) => void
686
+ ): Omit<StepperProps, 'value'> & { value: Signal<number> } {
687
+ return {
688
+ title: 'Font Size',
689
+ value,
690
+ minimumValue: 8,
691
+ maximumValue: 72,
692
+ step: 1,
693
+ onChange,
694
+ displayValueInLabel: true,
695
+ valueFormatter: (val: number) => `${val}pt`,
696
+ accessibilityLabel: 'Font size in points',
697
+ incrementAccessibilityLabel: 'Increase font size',
698
+ decrementAccessibilityLabel: 'Decrease font size',
699
+ }
700
+ },
701
+ }
702
+
703
+ /**
704
+ * Stepper styles and theming
705
+ */
706
+ export const StepperStyles = {
707
+ theme: defaultStepperTheme,
708
+
709
+ /**
710
+ * Create a custom theme
711
+ */
712
+ createTheme(overrides: Partial<StepperTheme>): StepperTheme {
713
+ return { ...defaultStepperTheme, ...overrides }
714
+ },
715
+ }