@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.
- package/README.md +87 -272
- package/dist/DatePicker-D5nRFTUm.js +475 -0
- package/dist/DatePicker-D5nRFTUm.js.map +1 -0
- package/dist/Select-yZyKooXk.js +945 -0
- package/dist/Select-yZyKooXk.js.map +1 -0
- package/dist/Slider-0-oal5YR.js +644 -0
- package/dist/Slider-0-oal5YR.js.map +1 -0
- package/dist/TextField-hX15dY3U.js +509 -0
- package/dist/TextField-hX15dY3U.js.map +1 -0
- package/dist/components/advanced/Slider.d.ts +190 -0
- package/dist/components/advanced/Slider.d.ts.map +1 -0
- package/dist/components/advanced/Stepper.d.ts +161 -0
- package/dist/components/advanced/Stepper.d.ts.map +1 -0
- package/dist/components/advanced/index.d.ts +15 -0
- package/dist/components/advanced/index.d.ts.map +1 -0
- package/dist/components/advanced/index.js +6 -0
- package/dist/components/advanced/index.js.map +1 -0
- package/dist/components/date-picker/DatePicker.d.ts +126 -0
- package/dist/components/date-picker/DatePicker.d.ts.map +1 -0
- package/dist/components/date-picker/index.d.ts +14 -0
- package/dist/components/date-picker/index.d.ts.map +1 -0
- package/dist/components/date-picker/index.js +5 -0
- package/dist/components/date-picker/index.js.map +1 -0
- package/dist/components/form-container/index.d.ts +58 -0
- package/dist/components/form-container/index.d.ts.map +1 -0
- package/dist/components/selection/Checkbox.d.ts.map +1 -0
- package/dist/components/selection/Radio.d.ts.map +1 -0
- package/dist/components/selection/Select.d.ts.map +1 -0
- package/dist/components/selection/index.d.ts +68 -0
- package/dist/components/selection/index.d.ts.map +1 -0
- package/dist/components/selection/index.js +12 -0
- package/dist/components/selection/index.js.map +1 -0
- package/dist/components/text-input/TextField.d.ts.map +1 -0
- package/dist/components/text-input/index.d.ts +8 -0
- package/dist/components/text-input/index.d.ts.map +1 -0
- package/dist/components/text-input/index.js +18 -0
- package/dist/components/text-input/index.js.map +1 -0
- package/dist/{state/index.js → index-D3WfkqVv.js} +15 -8
- package/dist/index-D3WfkqVv.js.map +1 -0
- package/dist/index.d.ts +10 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +198 -376
- package/dist/index.js.map +1 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/index.d.ts +19 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/validation/component-validation.d.ts +11 -2
- package/dist/validation/component-validation.d.ts.map +1 -1
- package/dist/validation/index.d.ts.map +1 -1
- package/dist/validation/index.js +282 -191
- package/dist/validation/index.js.map +1 -0
- package/package.json +53 -39
- package/src/components/advanced/Slider.ts +722 -0
- package/src/components/advanced/Stepper.ts +715 -0
- package/src/components/advanced/index.ts +20 -0
- package/src/components/date-picker/DatePicker.ts +925 -0
- package/src/components/date-picker/index.ts +20 -0
- package/src/components/form-container/index.ts +266 -0
- package/src/components/selection/Checkbox.ts +478 -0
- package/src/components/selection/Radio.ts +470 -0
- package/src/components/selection/Select.ts +620 -0
- package/src/components/selection/index.ts +81 -0
- package/src/components/text-input/TextField.ts +728 -0
- package/src/components/text-input/index.ts +35 -0
- package/src/index.ts +48 -0
- package/src/state/index.ts +544 -0
- package/src/types/index.ts +579 -0
- package/src/utils/formatters.ts +184 -0
- package/src/utils/index.ts +57 -0
- package/src/validation/component-validation.ts +429 -0
- package/src/validation/index.ts +641 -0
- package/dist/TextField-CGBM3x7K.js +0 -1799
- package/dist/components/Form.d.ts +0 -76
- package/dist/components/Form.d.ts.map +0 -1
- package/dist/components/index.d.ts +0 -9
- package/dist/components/index.d.ts.map +0 -1
- package/dist/components/index.js +0 -28
- package/dist/components/input/Checkbox.d.ts.map +0 -1
- package/dist/components/input/Radio.d.ts.map +0 -1
- package/dist/components/input/Select.d.ts.map +0 -1
- package/dist/components/input/TextField.d.ts.map +0 -1
- package/dist/components/input/index.d.ts +0 -11
- package/dist/components/input/index.d.ts.map +0 -1
- package/dist/utils/validators.d.ts +0 -101
- package/dist/utils/validators.d.ts.map +0 -1
- /package/dist/components/{input → selection}/Checkbox.d.ts +0 -0
- /package/dist/components/{input → selection}/Radio.d.ts +0 -0
- /package/dist/components/{input → selection}/Select.d.ts +0 -0
- /package/dist/components/{input → text-input}/TextField.d.ts +0 -0
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced Slider Component (Phase 6.4.4)
|
|
3
|
+
*
|
|
4
|
+
* SwiftUI-inspired Slider component with range selection, formatting,
|
|
5
|
+
* and precise value control for numeric input.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ModifiableComponent, ModifierBuilder } from '@tachui/core'
|
|
9
|
+
import { createEffect, createSignal, isSignal } from '@tachui/core'
|
|
10
|
+
import type { Signal } from '@tachui/core'
|
|
11
|
+
import { h, text } from '@tachui/core'
|
|
12
|
+
import type { ComponentInstance, ComponentProps } from '@tachui/core'
|
|
13
|
+
import { withModifiers } from '@tachui/core'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Slider component properties
|
|
17
|
+
*/
|
|
18
|
+
export interface SliderProps extends ComponentProps {
|
|
19
|
+
// Value binding
|
|
20
|
+
value: number | Signal<number>
|
|
21
|
+
onValueChange?: (value: number) => void
|
|
22
|
+
|
|
23
|
+
// Range
|
|
24
|
+
min?: number
|
|
25
|
+
max?: number
|
|
26
|
+
step?: number
|
|
27
|
+
|
|
28
|
+
// Formatting
|
|
29
|
+
formatter?: (value: number) => string
|
|
30
|
+
minimumValueLabel?: string
|
|
31
|
+
maximumValueLabel?: string
|
|
32
|
+
|
|
33
|
+
// Appearance
|
|
34
|
+
variant?: 'default' | 'filled' | 'minimal'
|
|
35
|
+
trackColor?: string
|
|
36
|
+
thumbColor?: string
|
|
37
|
+
activeTrackColor?: string
|
|
38
|
+
size?: 'small' | 'medium' | 'large'
|
|
39
|
+
|
|
40
|
+
// Behavior
|
|
41
|
+
disabled?: boolean | Signal<boolean>
|
|
42
|
+
showValue?: boolean
|
|
43
|
+
showLabels?: boolean
|
|
44
|
+
|
|
45
|
+
// Advanced features
|
|
46
|
+
marks?: SliderMark[]
|
|
47
|
+
range?: boolean
|
|
48
|
+
vertical?: boolean
|
|
49
|
+
|
|
50
|
+
// Accessibility
|
|
51
|
+
accessibilityLabel?: string
|
|
52
|
+
accessibilityHint?: string
|
|
53
|
+
accessibilityValueDescription?: (value: number) => string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Slider mark configuration
|
|
58
|
+
*/
|
|
59
|
+
export interface SliderMark {
|
|
60
|
+
value: number
|
|
61
|
+
label?: string
|
|
62
|
+
color?: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Enhanced Slider component class
|
|
67
|
+
*/
|
|
68
|
+
export class EnhancedSlider implements ComponentInstance<SliderProps> {
|
|
69
|
+
public readonly type = 'component' as const
|
|
70
|
+
public readonly id: string
|
|
71
|
+
public mounted = false
|
|
72
|
+
public cleanup: (() => void)[] = []
|
|
73
|
+
private sliderElement: HTMLInputElement | null = null
|
|
74
|
+
private isDragging: () => boolean
|
|
75
|
+
private setIsDragging: (dragging: boolean) => boolean
|
|
76
|
+
|
|
77
|
+
constructor(public props: SliderProps) {
|
|
78
|
+
this.id = `slider-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
79
|
+
|
|
80
|
+
// Initialize internal state
|
|
81
|
+
const [dragging, setDragging] = createSignal(false)
|
|
82
|
+
this.isDragging = dragging
|
|
83
|
+
this.setIsDragging = setDragging
|
|
84
|
+
|
|
85
|
+
// Handle value changes reactively
|
|
86
|
+
createEffect(() => {
|
|
87
|
+
const currentValue = this.getValue()
|
|
88
|
+
if (this.sliderElement && !this.isDragging()) {
|
|
89
|
+
this.sliderElement.value = String(currentValue)
|
|
90
|
+
this.updateTrackFill()
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get current value
|
|
97
|
+
*/
|
|
98
|
+
private getValue(): number {
|
|
99
|
+
const { value } = this.props
|
|
100
|
+
if (isSignal(value)) {
|
|
101
|
+
return (value as () => number)()
|
|
102
|
+
}
|
|
103
|
+
return value as number
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if slider is disabled
|
|
108
|
+
*/
|
|
109
|
+
private isDisabled(): boolean {
|
|
110
|
+
const { disabled } = this.props
|
|
111
|
+
if (typeof disabled === 'boolean') return disabled
|
|
112
|
+
if (isSignal(disabled)) return (disabled as () => boolean)()
|
|
113
|
+
return false
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Format value for display
|
|
118
|
+
*/
|
|
119
|
+
private formatValue(value: number): string {
|
|
120
|
+
const { formatter } = this.props
|
|
121
|
+
if (formatter) {
|
|
122
|
+
return formatter(value)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Default formatting based on step
|
|
126
|
+
const step = this.props.step || 1
|
|
127
|
+
if (step < 1) {
|
|
128
|
+
const decimals = String(step).split('.')[1]?.length || 0
|
|
129
|
+
return value.toFixed(decimals)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return String(Math.round(value))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Handle value change
|
|
137
|
+
*/
|
|
138
|
+
private handleValueChange = (newValue: number) => {
|
|
139
|
+
const { min = 0, max = 100, step = 1 } = this.props
|
|
140
|
+
|
|
141
|
+
// Clamp value to range
|
|
142
|
+
let clampedValue = Math.max(min, Math.min(max, newValue))
|
|
143
|
+
|
|
144
|
+
// Snap to step
|
|
145
|
+
if (step > 0) {
|
|
146
|
+
clampedValue = Math.round((clampedValue - min) / step) * step + min
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Update value
|
|
150
|
+
if (this.props.onValueChange) {
|
|
151
|
+
this.props.onValueChange(clampedValue)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.updateTrackFill()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Update track fill visual
|
|
159
|
+
*/
|
|
160
|
+
private updateTrackFill() {
|
|
161
|
+
if (!this.sliderElement) return
|
|
162
|
+
|
|
163
|
+
const { min = 0, max = 100 } = this.props
|
|
164
|
+
const value = this.getValue()
|
|
165
|
+
const percentage = ((value - min) / (max - min)) * 100
|
|
166
|
+
|
|
167
|
+
// Update CSS custom property for track fill
|
|
168
|
+
this.sliderElement.style.setProperty('--slider-progress', `${percentage}%`)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Handle input events
|
|
173
|
+
*/
|
|
174
|
+
private handleInput = (event: Event) => {
|
|
175
|
+
const target = event.target as HTMLInputElement
|
|
176
|
+
const value = parseFloat(target.value)
|
|
177
|
+
|
|
178
|
+
if (!Number.isNaN(value)) {
|
|
179
|
+
this.handleValueChange(value)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Handle mouse/touch start
|
|
185
|
+
*/
|
|
186
|
+
private handleStart = () => {
|
|
187
|
+
if (!this.isDisabled()) {
|
|
188
|
+
this.setIsDragging(true)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Handle mouse/touch end
|
|
194
|
+
*/
|
|
195
|
+
private handleEnd = () => {
|
|
196
|
+
this.setIsDragging(false)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get slider size styles
|
|
201
|
+
*/
|
|
202
|
+
private getSizeStyles() {
|
|
203
|
+
const { size = 'medium' } = this.props
|
|
204
|
+
|
|
205
|
+
switch (size) {
|
|
206
|
+
case 'small':
|
|
207
|
+
return {
|
|
208
|
+
height: '4px',
|
|
209
|
+
thumbSize: '16px',
|
|
210
|
+
}
|
|
211
|
+
case 'large':
|
|
212
|
+
return {
|
|
213
|
+
height: '8px',
|
|
214
|
+
thumbSize: '24px',
|
|
215
|
+
}
|
|
216
|
+
default:
|
|
217
|
+
return {
|
|
218
|
+
height: '6px',
|
|
219
|
+
thumbSize: '20px',
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get slider variant styles
|
|
226
|
+
*/
|
|
227
|
+
private getVariantStyles() {
|
|
228
|
+
const {
|
|
229
|
+
variant = 'default',
|
|
230
|
+
trackColor = '#e2e8f0',
|
|
231
|
+
activeTrackColor = '#007AFF',
|
|
232
|
+
thumbColor = '#ffffff',
|
|
233
|
+
} = this.props
|
|
234
|
+
const sizeStyles = this.getSizeStyles()
|
|
235
|
+
|
|
236
|
+
const baseStyles = {
|
|
237
|
+
'--slider-track-color': trackColor,
|
|
238
|
+
'--slider-active-track-color': activeTrackColor,
|
|
239
|
+
'--slider-thumb-color': thumbColor,
|
|
240
|
+
'--slider-height': sizeStyles.height,
|
|
241
|
+
'--slider-thumb-size': sizeStyles.thumbSize,
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
switch (variant) {
|
|
245
|
+
case 'filled':
|
|
246
|
+
return {
|
|
247
|
+
...baseStyles,
|
|
248
|
+
'--slider-track-color': '#f1f5f9',
|
|
249
|
+
'--slider-active-track-color': activeTrackColor,
|
|
250
|
+
backgroundColor: '#f8fafc',
|
|
251
|
+
borderRadius: '12px',
|
|
252
|
+
padding: '8px',
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
case 'minimal':
|
|
256
|
+
return {
|
|
257
|
+
...baseStyles,
|
|
258
|
+
'--slider-track-color': 'transparent',
|
|
259
|
+
'--slider-active-track-color': activeTrackColor,
|
|
260
|
+
border: `1px solid ${trackColor}`,
|
|
261
|
+
}
|
|
262
|
+
default:
|
|
263
|
+
return baseStyles
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Render slider marks
|
|
269
|
+
*/
|
|
270
|
+
private renderMarks() {
|
|
271
|
+
const { marks, min = 0, max = 100 } = this.props
|
|
272
|
+
|
|
273
|
+
if (!marks || marks.length === 0) {
|
|
274
|
+
return []
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return marks.map(mark => {
|
|
278
|
+
const percentage = ((mark.value - min) / (max - min)) * 100
|
|
279
|
+
|
|
280
|
+
return h(
|
|
281
|
+
'div',
|
|
282
|
+
{
|
|
283
|
+
key: String(mark.value),
|
|
284
|
+
style: {
|
|
285
|
+
position: 'absolute',
|
|
286
|
+
left: `${percentage}%`,
|
|
287
|
+
top: '50%',
|
|
288
|
+
transform: 'translate(-50%, -50%)',
|
|
289
|
+
display: 'flex',
|
|
290
|
+
flexDirection: 'column',
|
|
291
|
+
alignItems: 'center',
|
|
292
|
+
pointerEvents: 'none',
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
// Mark indicator
|
|
296
|
+
h('div', {
|
|
297
|
+
style: {
|
|
298
|
+
width: '2px',
|
|
299
|
+
height: '12px',
|
|
300
|
+
backgroundColor: mark.color || '#666',
|
|
301
|
+
marginBottom: '4px',
|
|
302
|
+
},
|
|
303
|
+
}),
|
|
304
|
+
|
|
305
|
+
// Mark label
|
|
306
|
+
...(mark.label
|
|
307
|
+
? [
|
|
308
|
+
h(
|
|
309
|
+
'span',
|
|
310
|
+
{
|
|
311
|
+
style: {
|
|
312
|
+
fontSize: '12px',
|
|
313
|
+
color: '#666',
|
|
314
|
+
whiteSpace: 'nowrap',
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
text(mark.label)
|
|
318
|
+
),
|
|
319
|
+
]
|
|
320
|
+
: [])
|
|
321
|
+
)
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Render value labels
|
|
327
|
+
*/
|
|
328
|
+
private renderLabels() {
|
|
329
|
+
const {
|
|
330
|
+
showLabels,
|
|
331
|
+
min = 0,
|
|
332
|
+
max = 100,
|
|
333
|
+
minimumValueLabel,
|
|
334
|
+
maximumValueLabel,
|
|
335
|
+
} = this.props
|
|
336
|
+
|
|
337
|
+
if (!showLabels) return []
|
|
338
|
+
|
|
339
|
+
return [
|
|
340
|
+
h(
|
|
341
|
+
'div',
|
|
342
|
+
{
|
|
343
|
+
style: {
|
|
344
|
+
display: 'flex',
|
|
345
|
+
justifyContent: 'space-between',
|
|
346
|
+
marginTop: '8px',
|
|
347
|
+
fontSize: '12px',
|
|
348
|
+
color: '#666',
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
h('span', {}, text(minimumValueLabel || this.formatValue(min))),
|
|
352
|
+
h('span', {}, text(maximumValueLabel || this.formatValue(max)))
|
|
353
|
+
),
|
|
354
|
+
]
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Render value display
|
|
359
|
+
*/
|
|
360
|
+
private renderValueDisplay() {
|
|
361
|
+
const { showValue } = this.props
|
|
362
|
+
|
|
363
|
+
if (!showValue) return []
|
|
364
|
+
|
|
365
|
+
const currentValue = this.getValue()
|
|
366
|
+
|
|
367
|
+
return [
|
|
368
|
+
h(
|
|
369
|
+
'div',
|
|
370
|
+
{
|
|
371
|
+
style: {
|
|
372
|
+
display: 'flex',
|
|
373
|
+
justifyContent: 'center',
|
|
374
|
+
marginBottom: '8px',
|
|
375
|
+
fontSize: '14px',
|
|
376
|
+
fontWeight: '600',
|
|
377
|
+
color: '#1a1a1a',
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
text(this.formatValue(currentValue))
|
|
381
|
+
),
|
|
382
|
+
]
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
render() {
|
|
386
|
+
const {
|
|
387
|
+
min = 0,
|
|
388
|
+
max = 100,
|
|
389
|
+
step = 1,
|
|
390
|
+
vertical = false,
|
|
391
|
+
accessibilityLabel,
|
|
392
|
+
accessibilityHint,
|
|
393
|
+
accessibilityValueDescription,
|
|
394
|
+
} = this.props
|
|
395
|
+
|
|
396
|
+
const currentValue = this.getValue()
|
|
397
|
+
const variantStyles = this.getVariantStyles()
|
|
398
|
+
const isDisabled = this.isDisabled()
|
|
399
|
+
|
|
400
|
+
// Slider CSS styles
|
|
401
|
+
const sliderStyles = {
|
|
402
|
+
appearance: 'none' as const,
|
|
403
|
+
width: vertical ? '6px' : '100%',
|
|
404
|
+
height: vertical ? '200px' : 'var(--slider-height)',
|
|
405
|
+
background: `linear-gradient(to ${vertical ? 'top' : 'right'}, var(--slider-active-track-color) 0%, var(--slider-active-track-color) var(--slider-progress, 0%), var(--slider-track-color) var(--slider-progress, 0%), var(--slider-track-color) 100%)`,
|
|
406
|
+
borderRadius: 'calc(var(--slider-height) / 2)',
|
|
407
|
+
outline: 'none',
|
|
408
|
+
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
|
409
|
+
opacity: isDisabled ? 0.5 : 1,
|
|
410
|
+
transition: 'all 0.2s ease',
|
|
411
|
+
|
|
412
|
+
// Webkit styles
|
|
413
|
+
WebkitAppearance: 'none' as const,
|
|
414
|
+
|
|
415
|
+
// Thumb styles
|
|
416
|
+
'&::-webkit-slider-thumb': {
|
|
417
|
+
appearance: 'none',
|
|
418
|
+
width: 'var(--slider-thumb-size)',
|
|
419
|
+
height: 'var(--slider-thumb-size)',
|
|
420
|
+
borderRadius: '50%',
|
|
421
|
+
background: 'var(--slider-thumb-color)',
|
|
422
|
+
border: '2px solid var(--slider-active-track-color)',
|
|
423
|
+
cursor: isDisabled ? 'not-allowed' : 'grab',
|
|
424
|
+
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
|
425
|
+
transition: 'all 0.2s ease',
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
'&::-webkit-slider-thumb:hover': !isDisabled
|
|
429
|
+
? {
|
|
430
|
+
transform: 'scale(1.1)',
|
|
431
|
+
boxShadow: '0 4px 8px rgba(0,0,0,0.15)',
|
|
432
|
+
}
|
|
433
|
+
: {},
|
|
434
|
+
|
|
435
|
+
'&::-webkit-slider-thumb:active': !isDisabled
|
|
436
|
+
? {
|
|
437
|
+
cursor: 'grabbing',
|
|
438
|
+
transform: 'scale(1.05)',
|
|
439
|
+
}
|
|
440
|
+
: {},
|
|
441
|
+
|
|
442
|
+
// Firefox styles
|
|
443
|
+
'&::-moz-range-thumb': {
|
|
444
|
+
width: 'var(--slider-thumb-size)',
|
|
445
|
+
height: 'var(--slider-thumb-size)',
|
|
446
|
+
borderRadius: '50%',
|
|
447
|
+
background: 'var(--slider-thumb-color)',
|
|
448
|
+
border: '2px solid var(--slider-active-track-color)',
|
|
449
|
+
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
|
450
|
+
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
'&::-moz-range-track': {
|
|
454
|
+
height: 'var(--slider-height)',
|
|
455
|
+
borderRadius: 'calc(var(--slider-height) / 2)',
|
|
456
|
+
background: 'var(--slider-track-color)',
|
|
457
|
+
border: 'none',
|
|
458
|
+
},
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return h(
|
|
462
|
+
'div',
|
|
463
|
+
{
|
|
464
|
+
style: {
|
|
465
|
+
...variantStyles,
|
|
466
|
+
position: 'relative',
|
|
467
|
+
display: 'flex',
|
|
468
|
+
flexDirection: vertical ? 'row' : 'column',
|
|
469
|
+
alignItems: vertical ? 'center' : 'stretch',
|
|
470
|
+
gap: vertical ? '12px' : '0',
|
|
471
|
+
},
|
|
472
|
+
'aria-label': accessibilityLabel,
|
|
473
|
+
'aria-describedby': accessibilityHint ? `${this.id}-hint` : undefined,
|
|
474
|
+
},
|
|
475
|
+
// Value display
|
|
476
|
+
...this.renderValueDisplay(),
|
|
477
|
+
|
|
478
|
+
// Slider container
|
|
479
|
+
h(
|
|
480
|
+
'div',
|
|
481
|
+
{
|
|
482
|
+
style: {
|
|
483
|
+
position: 'relative',
|
|
484
|
+
width: vertical ? 'auto' : '100%',
|
|
485
|
+
height: vertical ? '200px' : 'auto',
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
// Slider input
|
|
489
|
+
h('input', {
|
|
490
|
+
ref: (el: HTMLInputElement) => {
|
|
491
|
+
this.sliderElement = el
|
|
492
|
+
|
|
493
|
+
if (el && !this.mounted) {
|
|
494
|
+
// Set up event listeners
|
|
495
|
+
el.addEventListener('input', this.handleInput)
|
|
496
|
+
el.addEventListener('mousedown', this.handleStart)
|
|
497
|
+
el.addEventListener('mouseup', this.handleEnd)
|
|
498
|
+
el.addEventListener('touchstart', this.handleStart)
|
|
499
|
+
el.addEventListener('touchend', this.handleEnd)
|
|
500
|
+
|
|
501
|
+
this.cleanup.push(() => {
|
|
502
|
+
el.removeEventListener('input', this.handleInput)
|
|
503
|
+
el.removeEventListener('mousedown', this.handleStart)
|
|
504
|
+
el.removeEventListener('mouseup', this.handleEnd)
|
|
505
|
+
el.removeEventListener('touchstart', this.handleStart)
|
|
506
|
+
el.removeEventListener('touchend', this.handleEnd)
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
// Initial setup
|
|
510
|
+
this.updateTrackFill()
|
|
511
|
+
this.mounted = true
|
|
512
|
+
}
|
|
513
|
+
},
|
|
514
|
+
type: 'range',
|
|
515
|
+
min: String(min),
|
|
516
|
+
max: String(max),
|
|
517
|
+
step: String(step),
|
|
518
|
+
value: String(currentValue),
|
|
519
|
+
disabled: isDisabled,
|
|
520
|
+
orient: vertical ? 'vertical' : undefined,
|
|
521
|
+
style: sliderStyles,
|
|
522
|
+
'aria-label': accessibilityLabel,
|
|
523
|
+
'aria-valuemin': min,
|
|
524
|
+
'aria-valuemax': max,
|
|
525
|
+
'aria-valuenow': currentValue,
|
|
526
|
+
'aria-valuetext': accessibilityValueDescription
|
|
527
|
+
? accessibilityValueDescription(currentValue)
|
|
528
|
+
: this.formatValue(currentValue),
|
|
529
|
+
}),
|
|
530
|
+
|
|
531
|
+
// Slider marks
|
|
532
|
+
...this.renderMarks()
|
|
533
|
+
),
|
|
534
|
+
|
|
535
|
+
// Labels
|
|
536
|
+
...this.renderLabels(),
|
|
537
|
+
|
|
538
|
+
// Accessibility hint
|
|
539
|
+
...(accessibilityHint
|
|
540
|
+
? [
|
|
541
|
+
h(
|
|
542
|
+
'div',
|
|
543
|
+
{
|
|
544
|
+
id: `${this.id}-hint`,
|
|
545
|
+
style: {
|
|
546
|
+
fontSize: '12px',
|
|
547
|
+
color: '#666',
|
|
548
|
+
marginTop: '4px',
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
text(accessibilityHint)
|
|
552
|
+
),
|
|
553
|
+
]
|
|
554
|
+
: [])
|
|
555
|
+
)
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Slider component function
|
|
561
|
+
*/
|
|
562
|
+
export function Slider(
|
|
563
|
+
value: number | Signal<number>,
|
|
564
|
+
props: Omit<SliderProps, 'value'> = {}
|
|
565
|
+
): ModifiableComponent<SliderProps> & {
|
|
566
|
+
modifier: ModifierBuilder<ModifiableComponent<SliderProps>>
|
|
567
|
+
} {
|
|
568
|
+
const sliderProps: SliderProps = { ...props, value }
|
|
569
|
+
const component = new EnhancedSlider(sliderProps)
|
|
570
|
+
return withModifiers(component)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Slider style variants
|
|
575
|
+
*/
|
|
576
|
+
export const SliderStyles = {
|
|
577
|
+
/**
|
|
578
|
+
* Default slider (default)
|
|
579
|
+
*/
|
|
580
|
+
Default(
|
|
581
|
+
value: number | Signal<number>,
|
|
582
|
+
props: Omit<SliderProps, 'value' | 'variant'> = {}
|
|
583
|
+
) {
|
|
584
|
+
return Slider(value, { ...props, variant: 'default' })
|
|
585
|
+
},
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Filled slider with background
|
|
589
|
+
*/
|
|
590
|
+
Filled(
|
|
591
|
+
value: number | Signal<number>,
|
|
592
|
+
props: Omit<SliderProps, 'value' | 'variant'> = {}
|
|
593
|
+
) {
|
|
594
|
+
return Slider(value, { ...props, variant: 'filled' })
|
|
595
|
+
},
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Minimal slider with minimal styling
|
|
599
|
+
*/
|
|
600
|
+
Minimal(
|
|
601
|
+
value: number | Signal<number>,
|
|
602
|
+
props: Omit<SliderProps, 'value' | 'variant'> = {}
|
|
603
|
+
) {
|
|
604
|
+
return Slider(value, { ...props, variant: 'minimal' })
|
|
605
|
+
},
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Vertical slider
|
|
609
|
+
*/
|
|
610
|
+
Vertical(
|
|
611
|
+
value: number | Signal<number>,
|
|
612
|
+
props: Omit<SliderProps, 'value' | 'vertical'> = {}
|
|
613
|
+
) {
|
|
614
|
+
return Slider(value, { ...props, vertical: true })
|
|
615
|
+
},
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Range slider with marks
|
|
619
|
+
*/
|
|
620
|
+
WithMarks(
|
|
621
|
+
value: number | Signal<number>,
|
|
622
|
+
marks: SliderMark[],
|
|
623
|
+
props: Omit<SliderProps, 'value' | 'marks'> = {}
|
|
624
|
+
) {
|
|
625
|
+
return Slider(value, { ...props, marks })
|
|
626
|
+
},
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Slider with value display
|
|
630
|
+
*/
|
|
631
|
+
WithValue(
|
|
632
|
+
value: number | Signal<number>,
|
|
633
|
+
props: Omit<SliderProps, 'value' | 'showValue'> = {}
|
|
634
|
+
) {
|
|
635
|
+
return Slider(value, { ...props, showValue: true })
|
|
636
|
+
},
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Slider with labels
|
|
640
|
+
*/
|
|
641
|
+
WithLabels(
|
|
642
|
+
value: number | Signal<number>,
|
|
643
|
+
minimumLabel: string,
|
|
644
|
+
maximumLabel: string,
|
|
645
|
+
props: Omit<
|
|
646
|
+
SliderProps,
|
|
647
|
+
'value' | 'showLabels' | 'minimumValueLabel' | 'maximumValueLabel'
|
|
648
|
+
> = {}
|
|
649
|
+
) {
|
|
650
|
+
return Slider(value, {
|
|
651
|
+
...props,
|
|
652
|
+
showLabels: true,
|
|
653
|
+
minimumValueLabel: minimumLabel,
|
|
654
|
+
maximumValueLabel: maximumLabel,
|
|
655
|
+
})
|
|
656
|
+
},
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Slider utilities
|
|
661
|
+
*/
|
|
662
|
+
export const SliderUtils = {
|
|
663
|
+
/**
|
|
664
|
+
* Create marks for common ranges
|
|
665
|
+
*/
|
|
666
|
+
createMarks(min: number, max: number, step: number): SliderMark[] {
|
|
667
|
+
const marks: SliderMark[] = []
|
|
668
|
+
|
|
669
|
+
for (let value = min; value <= max; value += step) {
|
|
670
|
+
marks.push({
|
|
671
|
+
value,
|
|
672
|
+
label: String(value),
|
|
673
|
+
})
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return marks
|
|
677
|
+
},
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Create percentage marks
|
|
681
|
+
*/
|
|
682
|
+
createPercentageMarks(): SliderMark[] {
|
|
683
|
+
return [
|
|
684
|
+
{ value: 0, label: '0%' },
|
|
685
|
+
{ value: 25, label: '25%' },
|
|
686
|
+
{ value: 50, label: '50%' },
|
|
687
|
+
{ value: 75, label: '75%' },
|
|
688
|
+
{ value: 100, label: '100%' },
|
|
689
|
+
]
|
|
690
|
+
},
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Create custom formatter
|
|
694
|
+
*/
|
|
695
|
+
createFormatter(
|
|
696
|
+
unit: string,
|
|
697
|
+
decimals: number = 0
|
|
698
|
+
): (value: number) => string {
|
|
699
|
+
return (value: number) => `${value.toFixed(decimals)}${unit}`
|
|
700
|
+
},
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Snap value to step
|
|
704
|
+
*/
|
|
705
|
+
snapToStep(value: number, min: number, step: number): number {
|
|
706
|
+
return Math.round((value - min) / step) * step + min
|
|
707
|
+
},
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Convert slider value to different scale
|
|
711
|
+
*/
|
|
712
|
+
mapToScale(
|
|
713
|
+
value: number,
|
|
714
|
+
fromMin: number,
|
|
715
|
+
fromMax: number,
|
|
716
|
+
toMin: number,
|
|
717
|
+
toMax: number
|
|
718
|
+
): number {
|
|
719
|
+
const percentage = (value - fromMin) / (fromMax - fromMin)
|
|
720
|
+
return toMin + percentage * (toMax - toMin)
|
|
721
|
+
},
|
|
722
|
+
}
|