@proyecto-viviana/ui 0.3.2 → 0.3.3

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 (76) hide show
  1. package/dist/components.css +1077 -1077
  2. package/dist/index.js +236 -249
  3. package/dist/index.js.map +3 -3
  4. package/dist/index.ssr.js +78 -81
  5. package/dist/index.ssr.js.map +3 -3
  6. package/dist/radio/index.d.ts +12 -27
  7. package/dist/radio/index.d.ts.map +1 -1
  8. package/dist/test-utils/index.d.ts +2 -2
  9. package/dist/test-utils/index.d.ts.map +1 -1
  10. package/package.json +13 -12
  11. package/src/alert/index.tsx +48 -0
  12. package/src/assets/favicon.png +0 -0
  13. package/src/assets/fire.gif +0 -0
  14. package/src/autocomplete/index.tsx +313 -0
  15. package/src/avatar/index.tsx +75 -0
  16. package/src/badge/index.tsx +43 -0
  17. package/src/breadcrumbs/index.tsx +207 -0
  18. package/src/button/Button.tsx +74 -0
  19. package/src/button/index.ts +2 -0
  20. package/src/button/types.ts +24 -0
  21. package/src/calendar/DateField.tsx +200 -0
  22. package/src/calendar/DatePicker.tsx +298 -0
  23. package/src/calendar/RangeCalendar.tsx +236 -0
  24. package/src/calendar/TimeField.tsx +196 -0
  25. package/src/calendar/index.tsx +223 -0
  26. package/src/checkbox/index.tsx +257 -0
  27. package/src/color/index.tsx +687 -0
  28. package/src/combobox/index.tsx +383 -0
  29. package/src/components.css +1077 -0
  30. package/src/custom/calendar-card/index.tsx +66 -0
  31. package/src/custom/chip/index.tsx +46 -0
  32. package/src/custom/conversation/index.tsx +105 -0
  33. package/src/custom/event-card/index.tsx +132 -0
  34. package/src/custom/header/index.tsx +33 -0
  35. package/src/custom/lateral-nav/index.tsx +88 -0
  36. package/src/custom/logo/index.tsx +58 -0
  37. package/src/custom/nav-header/index.tsx +42 -0
  38. package/src/custom/page-layout/index.tsx +29 -0
  39. package/src/custom/profile-card/index.tsx +64 -0
  40. package/src/custom/project-card/index.tsx +59 -0
  41. package/src/custom/timeline-item/index.tsx +105 -0
  42. package/src/dialog/Dialog.tsx +260 -0
  43. package/src/dialog/index.tsx +3 -0
  44. package/src/disclosure/index.tsx +307 -0
  45. package/src/gridlist/index.tsx +403 -0
  46. package/src/icon/icons/GitHubIcon.tsx +20 -0
  47. package/src/icon/index.tsx +48 -0
  48. package/src/index.ts +322 -0
  49. package/src/landmark/index.tsx +231 -0
  50. package/src/link/index.tsx +130 -0
  51. package/src/listbox/index.tsx +231 -0
  52. package/src/menu/index.tsx +297 -0
  53. package/src/meter/index.tsx +163 -0
  54. package/src/numberfield/index.tsx +482 -0
  55. package/src/popover/index.tsx +260 -0
  56. package/src/progress-bar/index.tsx +169 -0
  57. package/src/radio/index.tsx +173 -0
  58. package/src/searchfield/index.tsx +453 -0
  59. package/src/select/index.tsx +349 -0
  60. package/src/separator/index.tsx +141 -0
  61. package/src/slider/index.tsx +382 -0
  62. package/src/styles.css +450 -0
  63. package/src/switch/ToggleSwitch.tsx +112 -0
  64. package/src/switch/index.tsx +90 -0
  65. package/src/table/index.tsx +531 -0
  66. package/src/tabs/index.tsx +273 -0
  67. package/src/tag-group/index.tsx +240 -0
  68. package/src/test-utils/index.ts +40 -0
  69. package/src/textfield/index.tsx +211 -0
  70. package/src/theme.css +101 -0
  71. package/src/toast/index.tsx +324 -0
  72. package/src/toolbar/index.tsx +108 -0
  73. package/src/tooltip/index.tsx +197 -0
  74. package/src/tree/index.tsx +494 -0
  75. package/dist/index.jsx +0 -6658
  76. package/dist/index.jsx.map +0 -7
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Meter component for proyecto-viviana-ui
3
+ *
4
+ * Styled meter component built on top of the solidaria hook directly.
5
+ * Meters represent a quantity within a known range (unlike progress bars which show progress toward a goal).
6
+ */
7
+
8
+ import { type JSX, splitProps, Show, createMemo } from 'solid-js';
9
+ import { createMeter } from '@proyecto-viviana/solidaria';
10
+
11
+ // ============================================
12
+ // TYPES
13
+ // ============================================
14
+
15
+ export type MeterSize = 'sm' | 'md' | 'lg';
16
+ export type MeterVariant = 'primary' | 'accent' | 'success' | 'warning' | 'danger' | 'info';
17
+
18
+ export interface MeterProps {
19
+ /** The current value (controlled). @default 0 */
20
+ value?: number;
21
+ /** The smallest value allowed. @default 0 */
22
+ minValue?: number;
23
+ /** The largest value allowed. @default 100 */
24
+ maxValue?: number;
25
+ /** The content to display as the value's label (e.g. "75 GB"). */
26
+ valueLabel?: string;
27
+ /** The size of the meter. @default 'md' */
28
+ size?: MeterSize;
29
+ /** The visual style variant. @default 'primary' */
30
+ variant?: MeterVariant;
31
+ /** The label to display above the meter. */
32
+ label?: string;
33
+ /** Whether to show the value text. @default true */
34
+ showValueLabel?: boolean;
35
+ /** Additional CSS class name. */
36
+ class?: string;
37
+ /** An accessibility label. */
38
+ 'aria-label'?: string;
39
+ }
40
+
41
+ // ============================================
42
+ // STYLES
43
+ // ============================================
44
+
45
+ const sizeStyles = {
46
+ sm: {
47
+ track: 'h-1',
48
+ text: 'text-xs',
49
+ },
50
+ md: {
51
+ track: 'h-2',
52
+ text: 'text-sm',
53
+ },
54
+ lg: {
55
+ track: 'h-3',
56
+ text: 'text-base',
57
+ },
58
+ };
59
+
60
+ const variantStyles = {
61
+ primary: 'bg-primary-500',
62
+ accent: 'bg-accent',
63
+ success: 'bg-green-500',
64
+ warning: 'bg-yellow-500',
65
+ danger: 'bg-red-500',
66
+ info: 'bg-blue-500',
67
+ };
68
+
69
+ // ============================================
70
+ // UTILITIES
71
+ // ============================================
72
+
73
+ function clamp(value: number, min: number, max: number): number {
74
+ return Math.min(Math.max(value, min), max);
75
+ }
76
+
77
+ // ============================================
78
+ // METER COMPONENT
79
+ // ============================================
80
+
81
+ /**
82
+ * Meters represent a quantity within a known range, or a fractional value.
83
+ * Unlike progress bars, meters represent a current value rather than progress toward a goal.
84
+ *
85
+ * @example
86
+ * ```tsx
87
+ * // Storage usage meter
88
+ * <Meter value={75} label="Storage space" valueLabel="75 GB of 100 GB" />
89
+ *
90
+ * // Battery level
91
+ * <Meter value={25} variant="warning" label="Battery" />
92
+ *
93
+ * // CPU usage with dynamic color
94
+ * <Meter value={cpuUsage} variant={cpuUsage > 80 ? 'danger' : 'success'} label="CPU" />
95
+ * ```
96
+ */
97
+ export function Meter(props: MeterProps): JSX.Element {
98
+ const [local, ariaProps] = splitProps(props, [
99
+ 'size',
100
+ 'variant',
101
+ 'label',
102
+ 'showValueLabel',
103
+ 'class',
104
+ ]);
105
+
106
+ const size = () => local.size ?? 'md';
107
+ const variant = () => local.variant ?? 'primary';
108
+ const showValueLabel = () => local.showValueLabel ?? true;
109
+
110
+ // Create meter aria props
111
+ const meterAria = createMeter({
112
+ get value() { return ariaProps.value; },
113
+ get minValue() { return ariaProps.minValue; },
114
+ get maxValue() { return ariaProps.maxValue; },
115
+ get valueLabel() { return ariaProps.valueLabel; },
116
+ get label() { return local.label; },
117
+ get 'aria-label'() { return ariaProps['aria-label']; },
118
+ });
119
+
120
+ // Calculate percentage
121
+ const percentage = createMemo(() => {
122
+ const value = ariaProps.value ?? 0;
123
+ const minValue = ariaProps.minValue ?? 0;
124
+ const maxValue = ariaProps.maxValue ?? 100;
125
+ const clampedValue = clamp(value, minValue, maxValue);
126
+ return ((clampedValue - minValue) / (maxValue - minValue)) * 100;
127
+ });
128
+
129
+ // Get value text from aria props
130
+ const valueText = () => meterAria.meterProps['aria-valuetext'] as string | undefined;
131
+
132
+ const sizeConfig = () => sizeStyles[size()];
133
+
134
+ return (
135
+ <div
136
+ {...meterAria.meterProps}
137
+ class={`w-full ${local.class ?? ''}`}
138
+ >
139
+ {/* Label and value row */}
140
+ <Show when={local.label || showValueLabel()}>
141
+ <div class={`flex justify-between items-center mb-1 ${sizeConfig().text}`}>
142
+ <Show when={local.label}>
143
+ <span class="text-primary-200 font-medium">{local.label}</span>
144
+ </Show>
145
+ <Show when={showValueLabel()}>
146
+ <span class="text-primary-300">{valueText()}</span>
147
+ </Show>
148
+ </div>
149
+ </Show>
150
+
151
+ {/* Track */}
152
+ <div class={`w-full ${sizeConfig().track} bg-bg-300 rounded-full overflow-hidden`}>
153
+ {/* Fill */}
154
+ <div
155
+ class={`h-full rounded-full transition-all duration-300 ${variantStyles[variant()]}`}
156
+ style={{
157
+ width: `${percentage()}%`,
158
+ }}
159
+ />
160
+ </div>
161
+ </div>
162
+ );
163
+ }
@@ -0,0 +1,482 @@
1
+ /**
2
+ * NumberField component for proyecto-viviana-ui
3
+ *
4
+ * A styled number field component with increment/decrement buttons.
5
+ * Built directly on solidaria hooks for full accessibility support.
6
+ */
7
+
8
+ import { type JSX, splitProps, mergeProps as solidMergeProps, Show } from 'solid-js'
9
+ import {
10
+ createNumberField,
11
+ createFocusRing,
12
+ createPress,
13
+ createHover,
14
+ type AriaNumberFieldProps,
15
+ } from '@proyecto-viviana/solidaria'
16
+ import {
17
+ createNumberFieldState,
18
+ } from '@proyecto-viviana/solid-stately'
19
+
20
+ // ============================================
21
+ // TYPES
22
+ // ============================================
23
+
24
+ export type NumberFieldSize = 'sm' | 'md' | 'lg'
25
+ export type NumberFieldVariant = 'outline' | 'filled'
26
+
27
+ export interface NumberFieldProps extends Omit<AriaNumberFieldProps, 'label'> {
28
+ /** The size of the number field. */
29
+ size?: NumberFieldSize
30
+ /** The visual variant of the number field. */
31
+ variant?: NumberFieldVariant
32
+ /** Additional CSS class name. */
33
+ class?: string
34
+ /** Label text for the input. */
35
+ label?: string
36
+ /** Description text shown below the input. */
37
+ description?: string
38
+ /** Error message shown when invalid. */
39
+ errorMessage?: string
40
+ /** The current value (controlled). */
41
+ value?: number
42
+ /** The default value (uncontrolled). */
43
+ defaultValue?: number
44
+ /** Handler called when the value changes. */
45
+ onChange?: (value: number) => void
46
+ /** The minimum value. */
47
+ minValue?: number
48
+ /** The maximum value. */
49
+ maxValue?: number
50
+ /** The step value for increment/decrement. */
51
+ step?: number
52
+ /** The locale for number formatting. */
53
+ locale?: string
54
+ /** Number format options. */
55
+ formatOptions?: Intl.NumberFormatOptions
56
+ /** Whether to hide the stepper buttons. */
57
+ hideStepper?: boolean
58
+ }
59
+
60
+ // ============================================
61
+ // STYLES
62
+ // ============================================
63
+
64
+ const sizeStyles = {
65
+ sm: {
66
+ input: 'h-8 px-2 text-sm',
67
+ label: 'text-sm',
68
+ description: 'text-xs',
69
+ button: 'w-6 h-6 text-sm',
70
+ buttonGap: 'gap-0.5',
71
+ },
72
+ md: {
73
+ input: 'h-10 px-3 text-base',
74
+ label: 'text-sm',
75
+ description: 'text-sm',
76
+ button: 'w-8 h-8 text-base',
77
+ buttonGap: 'gap-1',
78
+ },
79
+ lg: {
80
+ input: 'h-12 px-4 text-lg',
81
+ label: 'text-base',
82
+ description: 'text-sm',
83
+ button: 'w-10 h-10 text-lg',
84
+ buttonGap: 'gap-1',
85
+ },
86
+ }
87
+
88
+ // ============================================
89
+ // ICONS
90
+ // ============================================
91
+
92
+ function PlusIcon(props: { class?: string }) {
93
+ return (
94
+ <svg
95
+ class={props.class}
96
+ viewBox="0 0 16 16"
97
+ fill="none"
98
+ stroke="currentColor"
99
+ stroke-width="2"
100
+ >
101
+ <path d="M8 3v10M3 8h10" />
102
+ </svg>
103
+ )
104
+ }
105
+
106
+ function MinusIcon(props: { class?: string }) {
107
+ return (
108
+ <svg
109
+ class={props.class}
110
+ viewBox="0 0 16 16"
111
+ fill="none"
112
+ stroke="currentColor"
113
+ stroke-width="2"
114
+ >
115
+ <path d="M3 8h10" />
116
+ </svg>
117
+ )
118
+ }
119
+
120
+ // ============================================
121
+ // COMPONENT
122
+ // ============================================
123
+
124
+ /**
125
+ * A number field allows users to enter a numeric value with increment/decrement controls.
126
+ *
127
+ * Built directly on solidaria hooks for full accessibility support.
128
+ */
129
+ export function NumberField(props: NumberFieldProps): JSX.Element {
130
+ const defaultProps: Partial<NumberFieldProps> = {
131
+ size: 'md',
132
+ variant: 'outline',
133
+ }
134
+
135
+ const merged = solidMergeProps(defaultProps, props)
136
+
137
+ const [local, stateProps, ariaProps] = splitProps(merged, [
138
+ 'size',
139
+ 'variant',
140
+ 'class',
141
+ 'label',
142
+ 'description',
143
+ 'errorMessage',
144
+ 'hideStepper',
145
+ ], [
146
+ 'value',
147
+ 'defaultValue',
148
+ 'onChange',
149
+ 'minValue',
150
+ 'maxValue',
151
+ 'step',
152
+ 'locale',
153
+ 'formatOptions',
154
+ ])
155
+
156
+ const size = () => sizeStyles[local.size!]
157
+
158
+ // Ref for input element
159
+ let inputRef: HTMLInputElement | undefined
160
+
161
+ // Create number field state
162
+ const state = createNumberFieldState({
163
+ get value() {
164
+ return stateProps.value
165
+ },
166
+ get defaultValue() {
167
+ return stateProps.defaultValue
168
+ },
169
+ get onChange() {
170
+ return stateProps.onChange
171
+ },
172
+ get minValue() {
173
+ return stateProps.minValue
174
+ },
175
+ get maxValue() {
176
+ return stateProps.maxValue
177
+ },
178
+ get step() {
179
+ return stateProps.step
180
+ },
181
+ get locale() {
182
+ return stateProps.locale
183
+ },
184
+ get formatOptions() {
185
+ return stateProps.formatOptions
186
+ },
187
+ get isDisabled() {
188
+ return ariaProps.isDisabled
189
+ },
190
+ get isReadOnly() {
191
+ return ariaProps.isReadOnly
192
+ },
193
+ })
194
+
195
+ // Create number field aria props
196
+ const numberFieldAria = createNumberField(
197
+ {
198
+ get label() {
199
+ return local.label
200
+ },
201
+ get 'aria-label'() {
202
+ return ariaProps['aria-label']
203
+ },
204
+ get 'aria-labelledby'() {
205
+ return ariaProps['aria-labelledby']
206
+ },
207
+ get 'aria-describedby'() {
208
+ return ariaProps['aria-describedby']
209
+ },
210
+ get isDisabled() {
211
+ return ariaProps.isDisabled
212
+ },
213
+ get isReadOnly() {
214
+ return ariaProps.isReadOnly
215
+ },
216
+ get isRequired() {
217
+ return ariaProps.isRequired
218
+ },
219
+ get isInvalid() {
220
+ return ariaProps.isInvalid
221
+ },
222
+ get description() {
223
+ return local.description
224
+ },
225
+ get errorMessage() {
226
+ return local.errorMessage
227
+ },
228
+ get id() {
229
+ return ariaProps.id
230
+ },
231
+ get autoFocus() {
232
+ return ariaProps.autoFocus
233
+ },
234
+ get name() {
235
+ return ariaProps.name
236
+ },
237
+ },
238
+ state,
239
+ () => inputRef ?? null
240
+ )
241
+
242
+ // Create focus ring for input
243
+ const { isFocused, isFocusVisible, focusProps } = createFocusRing()
244
+
245
+ // Increment button interactions
246
+ const { isPressed: incrementPressed, pressProps: incrementPressProps } = createPress({
247
+ get isDisabled() {
248
+ return ariaProps.isDisabled || !state.canIncrement()
249
+ },
250
+ onPress: () => {
251
+ state.increment()
252
+ inputRef?.focus()
253
+ },
254
+ })
255
+
256
+ const { isHovered: incrementHovered, hoverProps: incrementHoverProps } = createHover({
257
+ get isDisabled() {
258
+ return ariaProps.isDisabled || !state.canIncrement()
259
+ },
260
+ })
261
+
262
+ // Decrement button interactions
263
+ const { isPressed: decrementPressed, pressProps: decrementPressProps } = createPress({
264
+ get isDisabled() {
265
+ return ariaProps.isDisabled || !state.canDecrement()
266
+ },
267
+ onPress: () => {
268
+ state.decrement()
269
+ inputRef?.focus()
270
+ },
271
+ })
272
+
273
+ const { isHovered: decrementHovered, hoverProps: decrementHoverProps } = createHover({
274
+ get isDisabled() {
275
+ return ariaProps.isDisabled || !state.canDecrement()
276
+ },
277
+ })
278
+
279
+ // Compute classes
280
+ const containerClasses = () => {
281
+ const base = 'flex flex-col'
282
+ const disabledClass = ariaProps.isDisabled ? 'opacity-60' : ''
283
+ const custom = local.class || ''
284
+ return [base, disabledClass, custom].filter(Boolean).join(' ')
285
+ }
286
+
287
+ const groupClasses = () => {
288
+ const base = 'flex items-center'
289
+ const gapClass = size().buttonGap
290
+ return [base, gapClass].filter(Boolean).join(' ')
291
+ }
292
+
293
+ const inputClasses = () => {
294
+ const base = 'flex-1 rounded-md transition-all duration-200 outline-none text-center'
295
+ const sizeClass = size().input
296
+
297
+ let variantClass: string
298
+ if (local.variant === 'filled') {
299
+ variantClass = 'bg-bg-200 border border-transparent'
300
+ } else {
301
+ variantClass = 'bg-transparent border border-bg-400'
302
+ }
303
+
304
+ let stateClass: string
305
+ if (ariaProps.isDisabled) {
306
+ stateClass = 'bg-bg-200 text-primary-500 cursor-not-allowed'
307
+ } else if (ariaProps.isInvalid) {
308
+ stateClass = 'border-danger-500 focus:border-danger-400 focus:ring-2 focus:ring-danger-400/20'
309
+ } else {
310
+ stateClass = 'text-primary-100 placeholder:text-primary-500 focus:border-accent focus:ring-2 focus:ring-accent/20'
311
+ }
312
+
313
+ const hoverClass = ariaProps.isDisabled ? '' : 'hover:border-accent-300'
314
+
315
+ return [base, sizeClass, variantClass, stateClass, hoverClass].filter(Boolean).join(' ')
316
+ }
317
+
318
+ const buttonClasses = (isIncrement: boolean) => {
319
+ const base = 'flex items-center justify-center rounded-md transition-all duration-150 select-none'
320
+ const sizeClass = size().button
321
+
322
+ const isDisabled = ariaProps.isDisabled || (isIncrement ? !state.canIncrement() : !state.canDecrement())
323
+ const isPressed = isIncrement ? incrementPressed() : decrementPressed()
324
+ const isHovered = isIncrement ? incrementHovered() : decrementHovered()
325
+
326
+ let stateClass: string
327
+ if (isDisabled) {
328
+ stateClass = 'bg-bg-300 text-primary-600 cursor-not-allowed'
329
+ } else if (isPressed) {
330
+ stateClass = 'bg-accent-600 text-white scale-95'
331
+ } else if (isHovered) {
332
+ stateClass = 'bg-accent-500 text-white'
333
+ } else {
334
+ stateClass = 'bg-bg-300 text-primary-200 hover:bg-accent-500 hover:text-white'
335
+ }
336
+
337
+ return [base, sizeClass, stateClass].filter(Boolean).join(' ')
338
+ }
339
+
340
+ const labelClasses = () => {
341
+ const base = 'block font-medium text-primary-200 mb-1'
342
+ const sizeClass = size().label
343
+ return [base, sizeClass].filter(Boolean).join(' ')
344
+ }
345
+
346
+ const descriptionClasses = () => {
347
+ const base = 'mt-1 text-primary-400'
348
+ const sizeClass = size().description
349
+ return [base, sizeClass].filter(Boolean).join(' ')
350
+ }
351
+
352
+ const errorClasses = () => {
353
+ const base = 'mt-1 text-danger-500'
354
+ const sizeClass = size().description
355
+ return [base, sizeClass].filter(Boolean).join(' ')
356
+ }
357
+
358
+ // Clean props helpers
359
+ const cleanInputProps = () => {
360
+ const { ref: _ref, ...rest } = numberFieldAria.inputProps as Record<string, unknown>
361
+ return rest
362
+ }
363
+
364
+ const cleanFocusProps = () => {
365
+ const { ref: _ref, ...rest } = focusProps as Record<string, unknown>
366
+ return rest
367
+ }
368
+
369
+ const cleanGroupProps = () => {
370
+ const { ref: _ref, ...rest } = numberFieldAria.groupProps as Record<string, unknown>
371
+ return rest
372
+ }
373
+
374
+ const cleanDecrementProps = () => {
375
+ const { ref: _ref, ...rest } = numberFieldAria.decrementButtonProps as Record<string, unknown>
376
+ return rest
377
+ }
378
+
379
+ const cleanIncrementProps = () => {
380
+ const { ref: _ref, ...rest } = numberFieldAria.incrementButtonProps as Record<string, unknown>
381
+ return rest
382
+ }
383
+
384
+ const cleanDecrementPressProps = () => {
385
+ const { ref: _ref, ...rest } = decrementPressProps as Record<string, unknown>
386
+ return rest
387
+ }
388
+
389
+ const cleanDecrementHoverProps = () => {
390
+ const { ref: _ref, ...rest } = decrementHoverProps as Record<string, unknown>
391
+ return rest
392
+ }
393
+
394
+ const cleanIncrementPressProps = () => {
395
+ const { ref: _ref, ...rest } = incrementPressProps as Record<string, unknown>
396
+ return rest
397
+ }
398
+
399
+ const cleanIncrementHoverProps = () => {
400
+ const { ref: _ref, ...rest } = incrementHoverProps as Record<string, unknown>
401
+ return rest
402
+ }
403
+
404
+ return (
405
+ <div
406
+ {...cleanGroupProps()}
407
+ class={containerClasses()}
408
+ data-disabled={ariaProps.isDisabled || undefined}
409
+ data-invalid={ariaProps.isInvalid || undefined}
410
+ >
411
+ {/* Label */}
412
+ <Show when={local.label}>
413
+ <span {...numberFieldAria.labelProps} class={labelClasses()}>
414
+ {local.label}
415
+ <Show when={ariaProps.isRequired}>
416
+ <span class="text-danger-500 ml-1">*</span>
417
+ </Show>
418
+ </span>
419
+ </Show>
420
+
421
+ {/* Input Group */}
422
+ <div class={groupClasses()}>
423
+ {/* Decrement Button */}
424
+ <Show when={!local.hideStepper}>
425
+ <button
426
+ {...cleanDecrementProps()}
427
+ {...cleanDecrementPressProps()}
428
+ {...cleanDecrementHoverProps()}
429
+ class={buttonClasses(false)}
430
+ data-pressed={decrementPressed() || undefined}
431
+ data-hovered={decrementHovered() || undefined}
432
+ data-disabled={ariaProps.isDisabled || !state.canDecrement() || undefined}
433
+ >
434
+ <MinusIcon class="w-4 h-4" />
435
+ </button>
436
+ </Show>
437
+
438
+ {/* Input */}
439
+ <input
440
+ ref={inputRef}
441
+ {...cleanInputProps()}
442
+ {...cleanFocusProps()}
443
+ class={inputClasses()}
444
+ data-focused={isFocused() || undefined}
445
+ data-focus-visible={isFocusVisible() || undefined}
446
+ />
447
+
448
+ {/* Increment Button */}
449
+ <Show when={!local.hideStepper}>
450
+ <button
451
+ {...cleanIncrementProps()}
452
+ {...cleanIncrementPressProps()}
453
+ {...cleanIncrementHoverProps()}
454
+ class={buttonClasses(true)}
455
+ data-pressed={incrementPressed() || undefined}
456
+ data-hovered={incrementHovered() || undefined}
457
+ data-disabled={ariaProps.isDisabled || !state.canIncrement() || undefined}
458
+ >
459
+ <PlusIcon class="w-4 h-4" />
460
+ </button>
461
+ </Show>
462
+ </div>
463
+
464
+ {/* Description */}
465
+ <Show when={local.description && !ariaProps.isInvalid}>
466
+ <span {...numberFieldAria.descriptionProps} class={descriptionClasses()}>
467
+ {local.description}
468
+ </span>
469
+ </Show>
470
+
471
+ {/* Error Message */}
472
+ <Show when={ariaProps.isInvalid && local.errorMessage}>
473
+ <span {...numberFieldAria.errorMessageProps} class={errorClasses()}>
474
+ {local.errorMessage}
475
+ </span>
476
+ </Show>
477
+ </div>
478
+ )
479
+ }
480
+
481
+ // Re-export types
482
+ export type { NumberFieldState } from '@proyecto-viviana/solid-stately'