@acusti/css-value-input 2.1.2 → 2.2.1

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 CHANGED
@@ -5,29 +5,75 @@
5
5
  [![downloads per month](https://img.shields.io/npm/dm/@acusti/css-value-input?style=for-the-badge)](https://www.npmjs.com/package/@acusti/css-value-input)
6
6
  [![bundle size](https://deno.bundlejs.com/badge?q=@acusti/css-value-input)](https://bundlejs.com/?q=%40acusti%2Fcss-value-input)
7
7
 
8
- `CSSValueInput` is a React component that renders a text input that can
9
- take and update a CSS value of a particular type with a default unit. The
10
- input’s behavior is similar to those of design applications such as Adobe
11
- Illustrator.
8
+ `CSSValueInput` is a React component that renders a specialized text input
9
+ for CSS values with intelligent unit handling, increment/decrement
10
+ controls, validation, and normalization. Designed with the user experience
11
+ of professional design tools like Adobe Illustrator, it automatically
12
+ manages units, enforces constraints, and provides intuitive keyboard
13
+ interactions.
12
14
 
13
- See the [storybook docs and demo][] to get a feel for what it can do.
15
+ ## Key Features
14
16
 
15
- [storybook docs and demo]:
16
- https://uikit.acusti.ca/?path=/docs/uikit-controls-CSSValueInput--docs
17
+ - **Smart Unit Management** - Automatically applies appropriate units based
18
+ on CSS value type
19
+ - **Arrow Key Increment/Decrement** - Use ↑/↓ keys to adjust values (Shift
20
+ for 10x multiplier)
21
+ - **Automatic Validation** - Enforces min/max bounds and CSS value type
22
+ constraints
23
+ - **Value Normalization** - Converts inputs to valid CSS values with proper
24
+ units
25
+ - **Escape to Revert** - Press Escape to restore the last valid value
26
+ - **Custom Validators** - Support for regex or function-based validation of
27
+ non-numeric values
28
+ - **Flexible Input Types** - Supports length, angle, time, percentage, and
29
+ integer CSS values
30
+ - **Design Tool UX** - Text selection on focus, enter to confirm, intuitive
31
+ interactions
17
32
 
18
- ## Usage
33
+ ## Installation
19
34
 
20
- ```
35
+ ```bash
21
36
  npm install @acusti/css-value-input
22
37
  # or
23
38
  yarn add @acusti/css-value-input
24
39
  ```
25
40
 
26
- ### Props
41
+ ## Quick Start
42
+
43
+ ```tsx
44
+ import CSSValueInput from '@acusti/css-value-input';
45
+ import { useState } from 'react';
46
+
47
+ function StyleEditor() {
48
+ const [width, setWidth] = useState('100px');
49
+ const [rotation, setRotation] = useState('0deg');
50
+
51
+ return (
52
+ <div>
53
+ <CSSValueInput
54
+ label="Width"
55
+ cssValueType="length"
56
+ value={width}
57
+ onSubmitValue={setWidth}
58
+ min={0}
59
+ max={1000}
60
+ />
27
61
 
28
- This is the type signature for the props you can pass to `CSSValueInput`.
29
- The unique features provided by the component are called out and explained
30
- above the corresponding prop via JSDoc comments:
62
+ <CSSValueInput
63
+ label="Rotation"
64
+ cssValueType="angle"
65
+ value={rotation}
66
+ onSubmitValue={setRotation}
67
+ step={15}
68
+ />
69
+ </div>
70
+ );
71
+ }
72
+ ```
73
+
74
+ ## API Reference
75
+
76
+ ### Props
31
77
 
32
78
  ```ts
33
79
  type Props = {
@@ -36,37 +82,890 @@ type Props = {
36
82
  * the value). Defaults to true.
37
83
  */
38
84
  allowEmpty?: boolean;
85
+
86
+ /** Additional CSS class name for styling */
39
87
  className?: string;
88
+
89
+ /** Type of CSS value: 'length', 'angle', 'time', 'percent', or 'integer' */
40
90
  cssValueType?: CSSValueType;
91
+
92
+ /** Disable the input */
41
93
  disabled?: boolean;
94
+
42
95
  /**
43
96
  * Function that receives a value and converts it to its numerical equivalent
44
97
  * (i.e. '12px' → 12). Defaults to parseFloat().
45
98
  */
46
99
  getValueAsNumber?: (value: string | number) => number;
100
+
101
+ /** Icon element to display before the input */
47
102
  icon?: React.ReactNode;
103
+
104
+ /** Label text displayed above the input */
48
105
  label?: string;
106
+
107
+ /** Maximum allowed numeric value */
49
108
  max?: number;
109
+
110
+ /** Minimum allowed numeric value */
50
111
  min?: number;
112
+
113
+ /** HTML name attribute for forms */
51
114
  name?: string;
115
+
116
+ /** Called when input loses focus */
52
117
  onBlur?: (event: React.FocusEvent<HTMLInputElement>) => unknown;
118
+
119
+ /** Called on each keystroke (before validation) */
53
120
  onChange?: (event: React.ChangeEvent<HTMLInputElement>) => unknown;
121
+
122
+ /** Called when input gains focus */
54
123
  onFocus?: (event: React.FocusEvent<HTMLInputElement>) => unknown;
124
+
125
+ /** Called on key press (before built-in key handling) */
55
126
  onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => unknown;
127
+
128
+ /** Called on key release */
56
129
  onKeyUp?: (event: React.KeyboardEvent<HTMLInputElement>) => unknown;
130
+
57
131
  /**
58
- * Custom event handler triggered when the user presses enter/return
59
- * or blurs the input after making a change. Hitting esc will restore
60
- * the previous submitted value or original value.
132
+ * Called when the user submits a value (Enter key or blur after change).
133
+ * This is your main callback for getting the validated, normalized CSS value.
61
134
  */
62
135
  onSubmitValue: (value: string) => unknown;
136
+
137
+ /** Placeholder text when input is empty */
63
138
  placeholder?: string;
139
+
140
+ /** Step size for arrow key increments (default: 1) */
64
141
  step?: number;
142
+
143
+ /** HTML tabindex for focus order */
65
144
  tabIndex?: number;
145
+
146
+ /** Tooltip text */
66
147
  title?: string;
148
+
149
+ /** Default unit to apply (auto-detected from cssValueType if not provided) */
67
150
  unit?: string;
68
- /** Regex or validator function to validate non-numeric values */
151
+
152
+ /** Custom validator for non-numeric values (RegExp or function) */
69
153
  validator?: RegExp | ((value: string) => boolean);
154
+
155
+ /** Current value of the input */
70
156
  value?: string;
71
157
  };
72
158
  ```
159
+
160
+ ### CSS Value Types
161
+
162
+ The component supports all CSS value types from `@acusti/css-values`:
163
+
164
+ - **`length`** - px, em, rem, %, vh, vw, etc. (default: px)
165
+ - **`angle`** - deg, rad, grad, turn (default: deg)
166
+ - **`time`** - s, ms (default: s)
167
+ - **`percent`** - % (default: %)
168
+ - **`integer`** - whole numbers only (no unit)
169
+
170
+ ## Usage Examples
171
+
172
+ ### Design Tool Property Panel
173
+
174
+ ```tsx
175
+ import CSSValueInput from '@acusti/css-value-input';
176
+ import { useState } from 'react';
177
+
178
+ function PropertyPanel({ selectedElement }) {
179
+ const [styles, setStyles] = useState({
180
+ width: '100px',
181
+ height: '100px',
182
+ borderRadius: '0px',
183
+ rotation: '0deg',
184
+ opacity: '100%',
185
+ animationDuration: '0.3s',
186
+ });
187
+
188
+ const updateStyle = (property: string) => (value: string) => {
189
+ setStyles((prev) => ({ ...prev, [property]: value }));
190
+ // Apply to selected element
191
+ if (selectedElement) {
192
+ selectedElement.style[property] = value;
193
+ }
194
+ };
195
+
196
+ return (
197
+ <div className="property-panel">
198
+ <h3>Transform</h3>
199
+ <div className="input-group">
200
+ <CSSValueInput
201
+ label="Width"
202
+ cssValueType="length"
203
+ value={styles.width}
204
+ onSubmitValue={updateStyle('width')}
205
+ min={0}
206
+ icon="📏"
207
+ />
208
+
209
+ <CSSValueInput
210
+ label="Height"
211
+ cssValueType="length"
212
+ value={styles.height}
213
+ onSubmitValue={updateStyle('height')}
214
+ min={0}
215
+ icon="📐"
216
+ />
217
+ </div>
218
+
219
+ <CSSValueInput
220
+ label="Border Radius"
221
+ cssValueType="length"
222
+ value={styles.borderRadius}
223
+ onSubmitValue={updateStyle('borderRadius')}
224
+ min={0}
225
+ step={5}
226
+ icon="⭕"
227
+ />
228
+
229
+ <CSSValueInput
230
+ label="Rotation"
231
+ cssValueType="angle"
232
+ value={styles.rotation}
233
+ onSubmitValue={updateStyle('rotation')}
234
+ step={15}
235
+ icon="🔄"
236
+ />
237
+
238
+ <h3>Appearance</h3>
239
+ <CSSValueInput
240
+ label="Opacity"
241
+ cssValueType="percent"
242
+ value={styles.opacity}
243
+ onSubmitValue={updateStyle('opacity')}
244
+ min={0}
245
+ max={100}
246
+ step={5}
247
+ icon="👁️"
248
+ />
249
+
250
+ <CSSValueInput
251
+ label="Animation Duration"
252
+ cssValueType="time"
253
+ value={styles.animationDuration}
254
+ onSubmitValue={updateStyle('animationDuration')}
255
+ min={0}
256
+ step={0.1}
257
+ icon="⏱️"
258
+ />
259
+ </div>
260
+ );
261
+ }
262
+ ```
263
+
264
+ ### Responsive Design Controls
265
+
266
+ ```tsx
267
+ import CSSValueInput from '@acusti/css-value-input';
268
+ import { useState } from 'react';
269
+
270
+ function ResponsiveControls() {
271
+ const [breakpoints, setBreakpoints] = useState({
272
+ mobile: '480px',
273
+ tablet: '768px',
274
+ desktop: '1024px',
275
+ wide: '1440px',
276
+ });
277
+
278
+ const [spacing, setSpacing] = useState({
279
+ xs: '4px',
280
+ sm: '8px',
281
+ md: '16px',
282
+ lg: '24px',
283
+ xl: '32px',
284
+ });
285
+
286
+ const updateBreakpoint = (key: string) => (value: string) => {
287
+ setBreakpoints((prev) => ({ ...prev, [key]: value }));
288
+ };
289
+
290
+ const updateSpacing = (key: string) => (value: string) => {
291
+ setSpacing((prev) => ({ ...prev, [key]: value }));
292
+ };
293
+
294
+ return (
295
+ <div className="responsive-controls">
296
+ <section>
297
+ <h3>Breakpoints</h3>
298
+ {Object.entries(breakpoints).map(([key, value]) => (
299
+ <CSSValueInput
300
+ key={key}
301
+ label={key.charAt(0).toUpperCase() + key.slice(1)}
302
+ cssValueType="length"
303
+ value={value}
304
+ onSubmitValue={updateBreakpoint(key)}
305
+ min={200}
306
+ max={2560}
307
+ step={10}
308
+ unit="px"
309
+ />
310
+ ))}
311
+ </section>
312
+
313
+ <section>
314
+ <h3>Spacing Scale</h3>
315
+ {Object.entries(spacing).map(([key, value]) => (
316
+ <CSSValueInput
317
+ key={key}
318
+ label={key.toUpperCase()}
319
+ cssValueType="length"
320
+ value={value}
321
+ onSubmitValue={updateSpacing(key)}
322
+ min={0}
323
+ max={100}
324
+ step={2}
325
+ />
326
+ ))}
327
+ </section>
328
+ </div>
329
+ );
330
+ }
331
+ ```
332
+
333
+ ### Animation Keyframe Editor
334
+
335
+ ```tsx
336
+ import CSSValueInput from '@acusti/css-value-input';
337
+ import { useState } from 'react';
338
+
339
+ function KeyframeEditor() {
340
+ const [keyframes, setKeyframes] = useState([
341
+ {
342
+ offset: '0%',
343
+ transform: 'translateX(0px) rotate(0deg)',
344
+ opacity: '100%',
345
+ },
346
+ {
347
+ offset: '50%',
348
+ transform: 'translateX(100px) rotate(180deg)',
349
+ opacity: '50%',
350
+ },
351
+ {
352
+ offset: '100%',
353
+ transform: 'translateX(0px) rotate(360deg)',
354
+ opacity: '100%',
355
+ },
356
+ ]);
357
+
358
+ const [animationSettings, setAnimationSettings] = useState({
359
+ duration: '2s',
360
+ delay: '0s',
361
+ timingFunction: 'ease-in-out',
362
+ iterations: '1',
363
+ });
364
+
365
+ const updateKeyframe = (
366
+ index: number,
367
+ property: string,
368
+ value: string,
369
+ ) => {
370
+ setKeyframes((prev) =>
371
+ prev.map((kf, i) =>
372
+ i === index ? { ...kf, [property]: value } : kf,
373
+ ),
374
+ );
375
+ };
376
+
377
+ return (
378
+ <div className="keyframe-editor">
379
+ <h3>Animation Settings</h3>
380
+ <div className="animation-controls">
381
+ <CSSValueInput
382
+ label="Duration"
383
+ cssValueType="time"
384
+ value={animationSettings.duration}
385
+ onSubmitValue={(value) =>
386
+ setAnimationSettings((prev) => ({
387
+ ...prev,
388
+ duration: value,
389
+ }))
390
+ }
391
+ min={0}
392
+ step={0.1}
393
+ />
394
+
395
+ <CSSValueInput
396
+ label="Delay"
397
+ cssValueType="time"
398
+ value={animationSettings.delay}
399
+ onSubmitValue={(value) =>
400
+ setAnimationSettings((prev) => ({
401
+ ...prev,
402
+ delay: value,
403
+ }))
404
+ }
405
+ min={0}
406
+ step={0.1}
407
+ />
408
+
409
+ <CSSValueInput
410
+ label="Iterations"
411
+ cssValueType="integer"
412
+ value={animationSettings.iterations}
413
+ onSubmitValue={(value) =>
414
+ setAnimationSettings((prev) => ({
415
+ ...prev,
416
+ iterations: value,
417
+ }))
418
+ }
419
+ min={1}
420
+ validator={(value) =>
421
+ value === 'infinite' || !isNaN(Number(value))
422
+ }
423
+ />
424
+ </div>
425
+
426
+ <h3>Keyframes</h3>
427
+ {keyframes.map((keyframe, index) => (
428
+ <div key={index} className="keyframe">
429
+ <h4>Keyframe {index + 1}</h4>
430
+ <div className="keyframe-controls">
431
+ <CSSValueInput
432
+ label="Offset"
433
+ cssValueType="percent"
434
+ value={keyframe.offset}
435
+ onSubmitValue={(value) =>
436
+ updateKeyframe(index, 'offset', value)
437
+ }
438
+ min={0}
439
+ max={100}
440
+ step={5}
441
+ />
442
+
443
+ <CSSValueInput
444
+ label="Opacity"
445
+ cssValueType="percent"
446
+ value={keyframe.opacity}
447
+ onSubmitValue={(value) =>
448
+ updateKeyframe(index, 'opacity', value)
449
+ }
450
+ min={0}
451
+ max={100}
452
+ step={10}
453
+ />
454
+ </div>
455
+ </div>
456
+ ))}
457
+ </div>
458
+ );
459
+ }
460
+ ```
461
+
462
+ ### CSS Grid Layout Builder
463
+
464
+ ```tsx
465
+ import CSSValueInput from '@acusti/css-value-input';
466
+ import { useState } from 'react';
467
+
468
+ function GridLayoutBuilder() {
469
+ const [gridSettings, setGridSettings] = useState({
470
+ columns: '1fr 1fr 1fr',
471
+ rows: 'auto auto',
472
+ columnGap: '16px',
473
+ rowGap: '16px',
474
+ padding: '20px',
475
+ });
476
+
477
+ const [itemSettings, setItemSettings] = useState({
478
+ columnStart: '1',
479
+ columnEnd: '2',
480
+ rowStart: '1',
481
+ rowEnd: '2',
482
+ });
483
+
484
+ return (
485
+ <div className="grid-builder">
486
+ <h3>Grid Container</h3>
487
+ <div className="grid-controls">
488
+ <CSSValueInput
489
+ label="Column Gap"
490
+ cssValueType="length"
491
+ value={gridSettings.columnGap}
492
+ onSubmitValue={(value) =>
493
+ setGridSettings((prev) => ({
494
+ ...prev,
495
+ columnGap: value,
496
+ }))
497
+ }
498
+ min={0}
499
+ step={4}
500
+ />
501
+
502
+ <CSSValueInput
503
+ label="Row Gap"
504
+ cssValueType="length"
505
+ value={gridSettings.rowGap}
506
+ onSubmitValue={(value) =>
507
+ setGridSettings((prev) => ({
508
+ ...prev,
509
+ rowGap: value,
510
+ }))
511
+ }
512
+ min={0}
513
+ step={4}
514
+ />
515
+
516
+ <CSSValueInput
517
+ label="Padding"
518
+ cssValueType="length"
519
+ value={gridSettings.padding}
520
+ onSubmitValue={(value) =>
521
+ setGridSettings((prev) => ({
522
+ ...prev,
523
+ padding: value,
524
+ }))
525
+ }
526
+ min={0}
527
+ step={4}
528
+ />
529
+ </div>
530
+
531
+ <h3>Grid Item Position</h3>
532
+ <div className="item-controls">
533
+ <CSSValueInput
534
+ label="Column Start"
535
+ cssValueType="integer"
536
+ value={itemSettings.columnStart}
537
+ onSubmitValue={(value) =>
538
+ setItemSettings((prev) => ({
539
+ ...prev,
540
+ columnStart: value,
541
+ }))
542
+ }
543
+ min={1}
544
+ />
545
+
546
+ <CSSValueInput
547
+ label="Column End"
548
+ cssValueType="integer"
549
+ value={itemSettings.columnEnd}
550
+ onSubmitValue={(value) =>
551
+ setItemSettings((prev) => ({
552
+ ...prev,
553
+ columnEnd: value,
554
+ }))
555
+ }
556
+ min={1}
557
+ />
558
+
559
+ <CSSValueInput
560
+ label="Row Start"
561
+ cssValueType="integer"
562
+ value={itemSettings.rowStart}
563
+ onSubmitValue={(value) =>
564
+ setItemSettings((prev) => ({
565
+ ...prev,
566
+ rowStart: value,
567
+ }))
568
+ }
569
+ min={1}
570
+ />
571
+
572
+ <CSSValueInput
573
+ label="Row End"
574
+ cssValueType="integer"
575
+ value={itemSettings.rowEnd}
576
+ onSubmitValue={(value) =>
577
+ setItemSettings((prev) => ({
578
+ ...prev,
579
+ rowEnd: value,
580
+ }))
581
+ }
582
+ min={1}
583
+ />
584
+ </div>
585
+
586
+ <div className="preview">
587
+ <div
588
+ style={{
589
+ display: 'grid',
590
+ gridTemplateColumns: gridSettings.columns,
591
+ gridTemplateRows: gridSettings.rows,
592
+ columnGap: gridSettings.columnGap,
593
+ rowGap: gridSettings.rowGap,
594
+ padding: gridSettings.padding,
595
+ border: '1px dashed #ccc',
596
+ minHeight: '200px',
597
+ }}
598
+ >
599
+ <div
600
+ style={{
601
+ gridColumnStart: itemSettings.columnStart,
602
+ gridColumnEnd: itemSettings.columnEnd,
603
+ gridRowStart: itemSettings.rowStart,
604
+ gridRowEnd: itemSettings.rowEnd,
605
+ backgroundColor: '#e3f2fd',
606
+ padding: '8px',
607
+ border: '1px solid #2196f3',
608
+ }}
609
+ >
610
+ Grid Item
611
+ </div>
612
+ </div>
613
+ </div>
614
+ </div>
615
+ );
616
+ }
617
+ ```
618
+
619
+ ### Typography Controls
620
+
621
+ ```tsx
622
+ import CSSValueInput from '@acusti/css-value-input';
623
+ import { useState } from 'react';
624
+
625
+ function TypographyControls() {
626
+ const [typography, setTypography] = useState({
627
+ fontSize: '16px',
628
+ lineHeight: '1.5',
629
+ letterSpacing: '0px',
630
+ wordSpacing: '0px',
631
+ textIndent: '0px',
632
+ });
633
+
634
+ const updateTypography = (property: string) => (value: string) => {
635
+ setTypography((prev) => ({ ...prev, [property]: value }));
636
+ };
637
+
638
+ return (
639
+ <div className="typography-controls">
640
+ <h3>Typography</h3>
641
+
642
+ <CSSValueInput
643
+ label="Font Size"
644
+ cssValueType="length"
645
+ value={typography.fontSize}
646
+ onSubmitValue={updateTypography('fontSize')}
647
+ min={8}
648
+ max={72}
649
+ step={1}
650
+ icon="🔤"
651
+ />
652
+
653
+ <CSSValueInput
654
+ label="Line Height"
655
+ cssValueType="length"
656
+ value={typography.lineHeight}
657
+ onSubmitValue={updateTypography('lineHeight')}
658
+ min={0.5}
659
+ max={3}
660
+ step={0.1}
661
+ unit="" // Line height can be unitless
662
+ validator={(value) => {
663
+ // Allow unitless numbers or length values
664
+ return /^(\d*\.?\d+)(px|em|rem|%)?$/.test(value);
665
+ }}
666
+ icon="📏"
667
+ />
668
+
669
+ <CSSValueInput
670
+ label="Letter Spacing"
671
+ cssValueType="length"
672
+ value={typography.letterSpacing}
673
+ onSubmitValue={updateTypography('letterSpacing')}
674
+ min={-5}
675
+ max={10}
676
+ step={0.5}
677
+ icon="🔤"
678
+ />
679
+
680
+ <CSSValueInput
681
+ label="Word Spacing"
682
+ cssValueType="length"
683
+ value={typography.wordSpacing}
684
+ onSubmitValue={updateTypography('wordSpacing')}
685
+ min={-10}
686
+ max={20}
687
+ step={1}
688
+ icon="📝"
689
+ />
690
+
691
+ <CSSValueInput
692
+ label="Text Indent"
693
+ cssValueType="length"
694
+ value={typography.textIndent}
695
+ onSubmitValue={updateTypography('textIndent')}
696
+ min={0}
697
+ max={100}
698
+ step={5}
699
+ icon="⬅️"
700
+ />
701
+
702
+ <div className="preview-text" style={typography}>
703
+ <p>
704
+ Lorem ipsum dolor sit amet, consectetur adipiscing
705
+ elit. Sed do eiusmod tempor incididunt ut labore et
706
+ dolore magna aliqua. Ut enim ad minim veniam, quis
707
+ nostrud exercitation.
708
+ </p>
709
+ </div>
710
+ </div>
711
+ );
712
+ }
713
+ ```
714
+
715
+ ### Custom Validator Examples
716
+
717
+ ```tsx
718
+ import CSSValueInput from '@acusti/css-value-input';
719
+
720
+ function CustomValidators() {
721
+ // CSS function validator (e.g., calc(), var(), etc.)
722
+ const cssFunctionValidator = (value: string) => {
723
+ return (
724
+ /^(calc|var|min|max|clamp)\(.*\)$/.test(value) ||
725
+ !isNaN(parseFloat(value))
726
+ );
727
+ };
728
+
729
+ // Color hex validator
730
+ const hexColorValidator = /^#([0-9A-Fa-f]{3}){1,2}$/;
731
+
732
+ // CSS keyword validator for display property
733
+ const displayKeywordValidator = (value: string) => {
734
+ const validKeywords = [
735
+ 'block',
736
+ 'inline',
737
+ 'flex',
738
+ 'grid',
739
+ 'none',
740
+ 'inline-block',
741
+ ];
742
+ return validKeywords.includes(value) || !isNaN(parseFloat(value));
743
+ };
744
+
745
+ return (
746
+ <div>
747
+ <CSSValueInput
748
+ label="Width (supports calc)"
749
+ cssValueType="length"
750
+ onSubmitValue={(value) => console.log('Width:', value)}
751
+ validator={cssFunctionValidator}
752
+ placeholder="100px or calc(50% - 10px)"
753
+ />
754
+
755
+ <CSSValueInput
756
+ label="Border Color"
757
+ cssValueType="length" // We'll override the unit behavior
758
+ onSubmitValue={(value) => console.log('Color:', value)}
759
+ validator={hexColorValidator}
760
+ unit="" // No default unit
761
+ placeholder="#ff0000"
762
+ />
763
+
764
+ <CSSValueInput
765
+ label="Z-Index"
766
+ cssValueType="integer"
767
+ onSubmitValue={(value) => console.log('Z-Index:', value)}
768
+ min={-999}
769
+ max={999}
770
+ step={1}
771
+ validator={(value) =>
772
+ value === 'auto' || !isNaN(parseInt(value))
773
+ }
774
+ />
775
+ </div>
776
+ );
777
+ }
778
+ ```
779
+
780
+ ## Keyboard Interactions
781
+
782
+ ### Arrow Keys
783
+
784
+ - **↑/↓** - Increment/decrement by step amount
785
+ - **Shift + ↑/↓** - Increment/decrement by step × 10
786
+ - Works with all numeric CSS value types
787
+
788
+ ### Special Keys
789
+
790
+ - **Enter** - Submit value and blur input
791
+ - **Escape** - Revert to last submitted value and blur
792
+ - **Tab** - Submit value and move to next input
793
+
794
+ ### Value Handling
795
+
796
+ - **Auto-complete units** - Typing "100" becomes "100px" for length inputs
797
+ - **Unit preservation** - Keeps the unit from the previous value when
798
+ possible
799
+ - **Range enforcement** - Automatically clamps values to min/max bounds
800
+ - **Type coercion** - Converts integers when cssValueType="integer"
801
+
802
+ ## Styling
803
+
804
+ The component uses CSS classes with the prefix `cssvalueinput`:
805
+
806
+ ```css
807
+ .cssvalueinput {
808
+ /* Main container styles */
809
+ }
810
+
811
+ .cssvalueinput-icon {
812
+ /* Icon container styles */
813
+ }
814
+
815
+ .cssvalueinput-label {
816
+ /* Label container styles */
817
+ }
818
+
819
+ .cssvalueinput-label-text {
820
+ /* Label text styles */
821
+ }
822
+
823
+ .cssvalueinput-value {
824
+ /* Input wrapper styles */
825
+ }
826
+
827
+ .cssvalueinput.disabled {
828
+ /* Disabled state styles */
829
+ }
830
+ ```
831
+
832
+ ### Example Styling
833
+
834
+ ```css
835
+ .cssvalueinput {
836
+ display: flex;
837
+ flex-direction: column;
838
+ gap: 4px;
839
+ margin-bottom: 12px;
840
+ }
841
+
842
+ .cssvalueinput-label-text {
843
+ font-size: 12px;
844
+ font-weight: 600;
845
+ color: #333;
846
+ margin: 0;
847
+ }
848
+
849
+ .cssvalueinput-icon {
850
+ font-size: 16px;
851
+ margin-right: 8px;
852
+ }
853
+
854
+ .cssvalueinput input {
855
+ padding: 6px 8px;
856
+ border: 1px solid #ccc;
857
+ border-radius: 4px;
858
+ font-family: monospace;
859
+ text-align: center;
860
+ }
861
+
862
+ .cssvalueinput input:focus {
863
+ outline: 2px solid #007bff;
864
+ border-color: transparent;
865
+ }
866
+
867
+ .cssvalueinput.disabled {
868
+ opacity: 0.6;
869
+ pointer-events: none;
870
+ }
871
+ ```
872
+
873
+ ## Integration with CSS-in-JS
874
+
875
+ ```tsx
876
+ import CSSValueInput from '@acusti/css-value-input';
877
+ import styled from 'styled-components';
878
+
879
+ const StyledBox = styled.div<{
880
+ width: string;
881
+ height: string;
882
+ rotation: string;
883
+ }>`
884
+ width: ${(props) => props.width};
885
+ height: ${(props) => props.height};
886
+ transform: rotate(${(props) => props.rotation});
887
+ background: linear-gradient(45deg, #007bff, #28a745);
888
+ transition: all 0.3s ease;
889
+ `;
890
+
891
+ function StyledComponentEditor() {
892
+ const [boxStyles, setBoxStyles] = useState({
893
+ width: '200px',
894
+ height: '200px',
895
+ rotation: '0deg',
896
+ });
897
+
898
+ return (
899
+ <div>
900
+ <div className="controls">
901
+ <CSSValueInput
902
+ label="Width"
903
+ cssValueType="length"
904
+ value={boxStyles.width}
905
+ onSubmitValue={(value) =>
906
+ setBoxStyles((prev) => ({ ...prev, width: value }))
907
+ }
908
+ />
909
+
910
+ <CSSValueInput
911
+ label="Height"
912
+ cssValueType="length"
913
+ value={boxStyles.height}
914
+ onSubmitValue={(value) =>
915
+ setBoxStyles((prev) => ({
916
+ ...prev,
917
+ height: value,
918
+ }))
919
+ }
920
+ />
921
+
922
+ <CSSValueInput
923
+ label="Rotation"
924
+ cssValueType="angle"
925
+ value={boxStyles.rotation}
926
+ onSubmitValue={(value) =>
927
+ setBoxStyles((prev) => ({
928
+ ...prev,
929
+ rotation: value,
930
+ }))
931
+ }
932
+ step={15}
933
+ />
934
+ </div>
935
+
936
+ <StyledBox {...boxStyles}>Styled Component</StyledBox>
937
+ </div>
938
+ );
939
+ }
940
+ ```
941
+
942
+ ## Accessibility
943
+
944
+ - **Label Association** - Proper label/input relationships for screen
945
+ readers
946
+ - **Keyboard Navigation** - Full keyboard control without mouse dependency
947
+ - **Focus Management** - Clear focus indicators and logical tab order
948
+ - **Value Announcements** - Screen readers announce value changes
949
+ - **Error Handling** - Invalid values are reverted with visual feedback
950
+
951
+ ## Browser Compatibility
952
+
953
+ - **Modern Browsers** - Chrome, Firefox, Safari, Edge (latest)
954
+ - **Mobile Support** - Touch-friendly with virtual keyboard support
955
+ - **SSR Compatible** - Works with Next.js, React Router, etc.
956
+
957
+ ## Common Use Cases
958
+
959
+ - **Design Tools** - Property panels, style editors, layout builders
960
+ - **CSS Generators** - Live CSS property editors
961
+ - **Animation Tools** - Keyframe editors, timing controls
962
+ - **Theme Builders** - Design system value editors
963
+ - **Form Builders** - CSS-aware form inputs
964
+ - **Component Libraries** - Styleable component property editors
965
+
966
+ ## Demo
967
+
968
+ See the
969
+ [Storybook documentation and examples](https://uikit.acusti.ca/?path=/docs/uikit-controls-CSSValueInput--docs)
970
+ for interactive demonstrations of all CSS value input features and
971
+ configurations.
@@ -1,5 +1,5 @@
1
1
  import { CSSValueType } from '../../css-values/src';
2
- import { ChangeEvent, FocusEvent, KeyboardEvent, ReactNode } from 'react';
2
+ import { ChangeEvent, FocusEvent, KeyboardEvent, ReactNode, Ref } from 'react';
3
3
  export type Props = {
4
4
  /**
5
5
  * Boolean indicating if the user can submit an empty value (i.e. clear
@@ -31,6 +31,7 @@ export type Props = {
31
31
  */
32
32
  onSubmitValue: (value: string) => unknown;
33
33
  placeholder?: string;
34
+ ref?: Ref<HTMLInputElement>;
34
35
  step?: number;
35
36
  tabIndex?: number;
36
37
  title?: string;
@@ -39,5 +40,4 @@ export type Props = {
39
40
  validator?: ((value: string) => boolean) | RegExp;
40
41
  value?: string;
41
42
  };
42
- declare const _default: import('react').ForwardRefExoticComponent<Props & import('react').RefAttributes<HTMLInputElement>>;
43
- export default _default;
43
+ export default function CSSValueInput({ allowEmpty, className, cssValueType, disabled, getValueAsNumber, icon, label, max, min, name, onBlur, onChange, onFocus, onKeyDown, onKeyUp, onSubmitValue, placeholder, ref, step, tabIndex, title, unit, validator, value: valueFromProps, }: Props): import("react/jsx-runtime").JSX.Element;
@@ -3,9 +3,9 @@ import { c } from "react/compiler-runtime";
3
3
  import { DEFAULT_UNIT_BY_CSS_VALUE_TYPE, DEFAULT_CSS_VALUE_TYPE, getCSSValueAsNumber, getUnitFromCSSValue, getCSSValueWithUnit, roundToPrecision } from "@acusti/css-values";
4
4
  import InputText from "@acusti/input-text";
5
5
  import clsx from "clsx";
6
- import { forwardRef, useRef, useImperativeHandle, useEffect } from "react";
6
+ import { useRef, useImperativeHandle, useEffect } from "react";
7
7
  const ROOT_CLASS_NAME = "cssvalueinput";
8
- const CSSValueInput = forwardRef(function CSSValueInput2(t0, ref) {
8
+ function CSSValueInput(t0) {
9
9
  const $ = c(56);
10
10
  const {
11
11
  allowEmpty: t1,
@@ -25,6 +25,7 @@ const CSSValueInput = forwardRef(function CSSValueInput2(t0, ref) {
25
25
  onKeyUp,
26
26
  onSubmitValue,
27
27
  placeholder,
28
+ ref,
28
29
  step: t4,
29
30
  tabIndex,
30
31
  title,
@@ -46,7 +47,7 @@ const CSSValueInput = forwardRef(function CSSValueInput2(t0, ref) {
46
47
  t6 = $[0];
47
48
  }
48
49
  useImperativeHandle(ref, t6);
49
- const value = typeof valueFromProps === "number" && !Number.isNaN(valueFromProps) ? `${valueFromProps}` : valueFromProps;
50
+ const value = typeof valueFromProps === "number" && Number.isFinite(valueFromProps) ? `${valueFromProps}` : valueFromProps;
50
51
  const submittedValueRef = useRef(value ?? "");
51
52
  let t7;
52
53
  let t8;
@@ -66,9 +67,8 @@ const CSSValueInput = forwardRef(function CSSValueInput2(t0, ref) {
66
67
  let t9;
67
68
  if ($[4] !== onSubmitValue) {
68
69
  t9 = (event) => {
69
- const currentValue = event.currentTarget.value;
70
- submittedValueRef.current = currentValue;
71
- onSubmitValue(currentValue);
70
+ submittedValueRef.current = event.currentTarget.value;
71
+ onSubmitValue(event.currentTarget.value);
72
72
  };
73
73
  $[4] = onSubmitValue;
74
74
  $[5] = t9;
@@ -84,12 +84,12 @@ const CSSValueInput = forwardRef(function CSSValueInput2(t0, ref) {
84
84
  if (onBlur) {
85
85
  onBlur(event_0);
86
86
  }
87
- const currentValue_0 = input.value.trim();
88
- if (allowEmpty && !currentValue_0) {
87
+ const currentValue = input.value.trim();
88
+ if (allowEmpty && !currentValue) {
89
89
  handleSubmitValue(event_0);
90
90
  return;
91
91
  }
92
- const currentValueAsNumber = getValueAsNumber(currentValue_0);
92
+ const currentValueAsNumber = getValueAsNumber(currentValue);
93
93
  const isCurrentValueFinite = Number.isFinite(currentValueAsNumber);
94
94
  const defaultUnit = unit ? getUnitFromCSSValue({
95
95
  cssValueType,
@@ -99,10 +99,10 @@ const CSSValueInput = forwardRef(function CSSValueInput2(t0, ref) {
99
99
  if (!isCurrentValueFinite) {
100
100
  let isValid = false;
101
101
  if (validator instanceof RegExp) {
102
- isValid = validator.test(currentValue_0);
102
+ isValid = validator.test(currentValue);
103
103
  } else {
104
104
  if (validator) {
105
- isValid = validator(currentValue_0);
105
+ isValid = validator(currentValue);
106
106
  }
107
107
  }
108
108
  if (isValid) {
@@ -130,14 +130,14 @@ const CSSValueInput = forwardRef(function CSSValueInput2(t0, ref) {
130
130
  const currentUnit = getUnitFromCSSValue({
131
131
  cssValueType,
132
132
  defaultUnit,
133
- value: currentValue_0
133
+ value: currentValue
134
134
  });
135
135
  input.value = normalizedValueAsNumber + currentUnit;
136
136
  } else {
137
137
  input.value = getCSSValueWithUnit({
138
138
  cssValueType,
139
139
  defaultUnit,
140
- value: currentValue_0
140
+ value: currentValue
141
141
  });
142
142
  }
143
143
  handleSubmitValue(event_0);
@@ -160,16 +160,16 @@ const CSSValueInput = forwardRef(function CSSValueInput2(t0, ref) {
160
160
  if ($[16] !== cssValueType || $[17] !== getValueAsNumber || $[18] !== max || $[19] !== min || $[20] !== step || $[21] !== unit) {
161
161
  t11 = (t122) => {
162
162
  const {
163
- currentValue: currentValue_1,
163
+ currentValue: currentValue_0,
164
164
  multiplier: t132,
165
165
  signum: t142
166
166
  } = t122;
167
167
  const multiplier = t132 === void 0 ? 1 : t132;
168
168
  const signum = t142 === void 0 ? 1 : t142;
169
169
  const modifier = multiplier * step * signum;
170
- const currentValueAsNumber_0 = getValueAsNumber(currentValue_1);
171
- if (typeof currentValue_1 === "string" && Number.isNaN(currentValueAsNumber_0)) {
172
- return currentValue_1;
170
+ const currentValueAsNumber_0 = getValueAsNumber(currentValue_0);
171
+ if (typeof currentValue_0 === "string" && Number.isNaN(currentValueAsNumber_0)) {
172
+ return currentValue_0;
173
173
  }
174
174
  let nextValue = currentValueAsNumber_0 + modifier;
175
175
  if (cssValueType === "integer") {
@@ -186,7 +186,7 @@ const CSSValueInput = forwardRef(function CSSValueInput2(t0, ref) {
186
186
  const nextUnit = getUnitFromCSSValue({
187
187
  cssValueType,
188
188
  defaultUnit: unit,
189
- value: currentValue_1
189
+ value: currentValue_0
190
190
  });
191
191
  return `${nextValue}${nextUnit}`;
192
192
  };
@@ -209,33 +209,21 @@ const CSSValueInput = forwardRef(function CSSValueInput2(t0, ref) {
209
209
  if (onKeyDown) {
210
210
  onKeyDown(event_1);
211
211
  }
212
- const currentValue_2 = input_0.value ?? placeholder ?? `0${unit}`;
213
- let nextValue_0;
214
- switch (event_1.key) {
215
- case "ArrowDown":
216
- case "ArrowUp": {
217
- nextValue_0 = getNextValue({
218
- currentValue: currentValue_2,
219
- multiplier: event_1.shiftKey ? 10 : 1,
220
- signum: event_1.key === "ArrowUp" ? 1 : -1
221
- });
222
- if (nextValue_0 === currentValue_2) {
223
- return;
224
- }
225
- event_1.stopPropagation();
226
- event_1.preventDefault();
227
- input_0.value = nextValue_0;
228
- return;
229
- }
230
- case "Enter":
231
- case "Escape": {
232
- if (event_1.key === "Escape") {
233
- input_0.value = submittedValueRef.current;
234
- }
235
- input_0.blur();
236
- return;
237
- }
212
+ if (event_1.key !== "ArrowDown" && event_1.key !== "ArrowUp") {
213
+ return;
214
+ }
215
+ const currentValue_1 = input_0.value ?? placeholder ?? `0${unit}`;
216
+ const nextValue_0 = getNextValue({
217
+ currentValue: currentValue_1,
218
+ multiplier: event_1.shiftKey ? 10 : 1,
219
+ signum: event_1.key === "ArrowUp" ? 1 : -1
220
+ });
221
+ if (nextValue_0 === currentValue_1) {
222
+ return;
238
223
  }
224
+ event_1.stopPropagation();
225
+ event_1.preventDefault();
226
+ input_0.value = nextValue_0;
239
227
  };
240
228
  $[23] = getNextValue;
241
229
  $[24] = onKeyDown;
@@ -293,7 +281,7 @@ const CSSValueInput = forwardRef(function CSSValueInput2(t0, ref) {
293
281
  }
294
282
  let t18;
295
283
  if ($[38] !== disabled || $[39] !== handleBlur || $[40] !== handleKeyDown || $[41] !== handleKeyUp || $[42] !== name || $[43] !== onChange || $[44] !== onFocus || $[45] !== placeholder || $[46] !== tabIndex || $[47] !== value) {
296
- t18 = /* @__PURE__ */ jsx("div", { className: `${ROOT_CLASS_NAME}-value`, children: /* @__PURE__ */ jsx(InputText, { disabled, initialValue: value, name, onBlur: handleBlur, onChange, onFocus, onKeyDown: handleKeyDown, onKeyUp: handleKeyUp, placeholder, ref: inputRef, selectTextOnFocus: true, tabIndex }) });
284
+ t18 = /* @__PURE__ */ jsx("div", { className: `${ROOT_CLASS_NAME}-value`, children: /* @__PURE__ */ jsx(InputText, { disabled, discardOnEscape: true, initialValue: value, name, onBlur: handleBlur, onChange, onFocus, onKeyDown: handleKeyDown, onKeyUp: handleKeyUp, placeholder, ref: inputRef, selectTextOnFocus: true, tabIndex }) });
297
285
  $[38] = disabled;
298
286
  $[39] = handleBlur;
299
287
  $[40] = handleKeyDown;
@@ -326,7 +314,7 @@ const CSSValueInput = forwardRef(function CSSValueInput2(t0, ref) {
326
314
  t19 = $[55];
327
315
  }
328
316
  return t19;
329
- });
317
+ }
330
318
  export {
331
319
  CSSValueInput as default
332
320
  };
@@ -1 +1 @@
1
- {"version":3,"file":"CSSValueInput.js","sources":["../src/CSSValueInput.tsx"],"sourcesContent":["import {\n type CSSValueType,\n DEFAULT_CSS_VALUE_TYPE,\n DEFAULT_UNIT_BY_CSS_VALUE_TYPE,\n getCSSValueAsNumber,\n getCSSValueWithUnit,\n getUnitFromCSSValue,\n roundToPrecision,\n} from '@acusti/css-values';\nimport InputText from '@acusti/input-text';\nimport clsx from 'clsx';\nimport {\n type ChangeEvent,\n type FocusEvent,\n forwardRef,\n type KeyboardEvent,\n type ReactNode,\n type SyntheticEvent,\n useEffect,\n useImperativeHandle,\n useRef,\n} from 'react';\n\nexport type Props = {\n /**\n * Boolean indicating if the user can submit an empty value (i.e. clear\n * the value). Defaults to true.\n */\n allowEmpty?: boolean;\n className?: string;\n cssValueType?: CSSValueType;\n disabled?: boolean;\n /**\n * Function that receives a value and converts it to its numerical equivalent\n * (i.e. '12px' → 12). Defaults to parseFloat().\n */\n getValueAsNumber?: (value: number | string) => number;\n icon?: ReactNode;\n label?: string;\n max?: number;\n min?: number;\n name?: string;\n onBlur?: (event: FocusEvent<HTMLInputElement>) => unknown;\n onChange?: (event: ChangeEvent<HTMLInputElement>) => unknown;\n onFocus?: (event: FocusEvent<HTMLInputElement>) => unknown;\n onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => unknown;\n onKeyUp?: (event: KeyboardEvent<HTMLInputElement>) => unknown;\n /**\n * Custom event handler triggered when the user presses enter/return\n * or blurs the input after making a change. Hitting esc will restore\n * the previous submitted value or original value.\n */\n onSubmitValue: (value: string) => unknown;\n placeholder?: string;\n step?: number;\n tabIndex?: number;\n title?: string;\n unit?: string;\n /** Regex or validator function to validate non-numeric values */\n validator?: ((value: string) => boolean) | RegExp;\n value?: string;\n};\n\ntype InputRef = HTMLInputElement | null;\n\nconst ROOT_CLASS_NAME = 'cssvalueinput';\n\nexport default forwardRef<HTMLInputElement, Props>(function CSSValueInput(\n {\n allowEmpty = true,\n className,\n cssValueType = DEFAULT_CSS_VALUE_TYPE,\n disabled,\n getValueAsNumber = getCSSValueAsNumber,\n icon,\n label,\n max,\n min,\n name,\n onBlur,\n onChange,\n onFocus,\n onKeyDown,\n onKeyUp,\n onSubmitValue,\n placeholder,\n step = 1,\n tabIndex,\n title,\n unit = DEFAULT_UNIT_BY_CSS_VALUE_TYPE[cssValueType],\n validator,\n value: valueFromProps,\n }: Props,\n ref,\n) {\n const inputRef = useRef<InputRef>(null);\n useImperativeHandle<InputRef, InputRef>(ref, () => inputRef.current);\n // props.value should be a string; if it’s a number, convert it here\n const value =\n typeof valueFromProps === 'number' && !Number.isNaN(valueFromProps)\n ? `${valueFromProps}`\n : valueFromProps;\n const submittedValueRef = useRef<string>(value ?? '');\n\n useEffect(() => {\n submittedValueRef.current = value ?? '';\n }, [value]);\n\n const handleSubmitValue = (event: SyntheticEvent<HTMLInputElement>) => {\n const currentValue = event.currentTarget.value;\n // Store last submittedValue (used to reset value on invalid input)\n submittedValueRef.current = currentValue;\n onSubmitValue(currentValue);\n };\n\n const handleBlur = (event: FocusEvent<HTMLInputElement>) => {\n const input = event.currentTarget;\n inputRef.current = input;\n if (onBlur) onBlur(event);\n\n const currentValue = input.value.trim();\n\n // If allowEmpty and value is empty, skip all validation + normalization\n if (allowEmpty && !currentValue) {\n handleSubmitValue(event);\n return;\n }\n\n const currentValueAsNumber = getValueAsNumber(currentValue);\n const isCurrentValueFinite = Number.isFinite(currentValueAsNumber);\n // Inherit unit from last submitted value unless default is unitless;\n // ensures that submitting a new value with no unit doesn’t add a unit\n const defaultUnit = unit\n ? getUnitFromCSSValue({\n cssValueType,\n defaultUnit: unit,\n value: submittedValueRef.current,\n })\n : '';\n\n if (!isCurrentValueFinite) {\n let isValid = false;\n if (validator instanceof RegExp) {\n isValid = validator.test(currentValue);\n } else if (validator) {\n isValid = validator(currentValue);\n }\n\n if (isValid) {\n handleSubmitValue(event);\n } else {\n // If current value isn’t valid, revert to last submitted value\n input.value = submittedValueRef.current;\n }\n\n return;\n }\n\n // Normalize value by applying min/max and integer constraints\n let normalizedValueAsNumber = currentValueAsNumber;\n\n if (isCurrentValueFinite) {\n if (min != null && currentValueAsNumber < min) {\n normalizedValueAsNumber = min;\n } else if (max != null && currentValueAsNumber > max) {\n normalizedValueAsNumber = max;\n } else if (cssValueType === 'integer') {\n normalizedValueAsNumber = Math.floor(currentValueAsNumber);\n }\n }\n\n if (normalizedValueAsNumber !== currentValueAsNumber) {\n const currentUnit = getUnitFromCSSValue({\n cssValueType,\n defaultUnit,\n value: currentValue,\n });\n input.value = normalizedValueAsNumber + currentUnit;\n } else {\n input.value = getCSSValueWithUnit({\n cssValueType,\n defaultUnit,\n value: currentValue,\n });\n }\n\n handleSubmitValue(event);\n };\n\n const getNextValue = ({\n currentValue,\n multiplier = 1,\n signum = 1,\n }: {\n currentValue: number | string;\n multiplier?: number;\n signum?: number;\n }) => {\n const modifier = multiplier * step * signum;\n const currentValueAsNumber = getValueAsNumber(currentValue);\n // If currentValue isn’t numeric, don’t try to increment/decrement it\n if (typeof currentValue === 'string' && Number.isNaN(currentValueAsNumber)) {\n return currentValue;\n }\n\n let nextValue = currentValueAsNumber + modifier;\n if (cssValueType === 'integer') {\n nextValue = Math.floor(nextValue);\n } else {\n nextValue = roundToPrecision(nextValue, 5);\n }\n\n if (typeof max === 'number' && Number.isFinite(max)) {\n nextValue = Math.min(max, nextValue);\n }\n\n if (typeof min === 'number' && Number.isFinite(min)) {\n nextValue = Math.max(min, nextValue);\n }\n\n const nextUnit = getUnitFromCSSValue({\n cssValueType,\n defaultUnit: unit,\n value: currentValue,\n });\n return `${nextValue}${nextUnit}`;\n };\n\n const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n const input = event.currentTarget;\n inputRef.current = input;\n if (onKeyDown) onKeyDown(event);\n\n const currentValue = input.value ?? placeholder ?? `0${unit}`;\n let nextValue = '';\n\n switch (event.key) {\n case 'ArrowDown':\n case 'ArrowUp':\n nextValue = getNextValue({\n currentValue,\n multiplier: event.shiftKey ? 10 : 1,\n signum: event.key === 'ArrowUp' ? 1 : -1,\n });\n\n if (nextValue === currentValue) return;\n\n event.stopPropagation();\n event.preventDefault();\n\n input.value = nextValue;\n return;\n case 'Enter':\n case 'Escape':\n if (event.key === 'Escape') {\n input.value = submittedValueRef.current;\n }\n input.blur();\n return;\n default:\n // No default key handling\n }\n };\n\n const handleKeyUp = (event: KeyboardEvent<HTMLInputElement>) => {\n if (onKeyUp) onKeyUp(event);\n // If this is the key up from ↑ or ↓ keys, time to handleSubmitValue\n if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {\n handleSubmitValue(event);\n }\n };\n\n return (\n <label\n aria-label={label ? undefined : title}\n className={clsx(ROOT_CLASS_NAME, className, { disabled })}\n title={title}\n >\n {icon == null ? null : (\n <div className={`${ROOT_CLASS_NAME}-icon`}>{icon}</div>\n )}\n {label ? (\n <div className={`${ROOT_CLASS_NAME}-label`}>\n <p className={`${ROOT_CLASS_NAME}-label-text`}>{label}</p>\n </div>\n ) : null}\n <div className={`${ROOT_CLASS_NAME}-value`}>\n <InputText\n disabled={disabled}\n initialValue={value}\n name={name}\n onBlur={handleBlur}\n onChange={onChange}\n onFocus={onFocus}\n onKeyDown={handleKeyDown}\n onKeyUp={handleKeyUp}\n placeholder={placeholder}\n ref={inputRef}\n selectTextOnFocus\n tabIndex={tabIndex}\n />\n </div>\n </label>\n );\n});\n"],"names":["ROOT_CLASS_NAME","forwardRef","CSSValueInput","t0","ref","$","_c","allowEmpty","t1","className","cssValueType","t2","disabled","getValueAsNumber","t3","icon","label","max","min","name","onBlur","onChange","onFocus","onKeyDown","onKeyUp","onSubmitValue","placeholder","step","t4","tabIndex","title","unit","t5","validator","value","valueFromProps","undefined","DEFAULT_CSS_VALUE_TYPE","getCSSValueAsNumber","DEFAULT_UNIT_BY_CSS_VALUE_TYPE","inputRef","useRef","t6","Symbol","for","current","useImperativeHandle","Number","isNaN","submittedValueRef","t7","t8","useEffect","t9","event","currentValue","currentTarget","handleSubmitValue","t10","event_0","input","currentValue_0","trim","currentValueAsNumber","isCurrentValueFinite","isFinite","defaultUnit","getUnitFromCSSValue","isValid","RegExp","test","normalizedValueAsNumber","currentUnit","getCSSValueWithUnit","handleBlur","t11","t12","currentValue_1","multiplier","t13","signum","t14","modifier","currentValueAsNumber_0","nextValue","nextUnit","getNextValue","event_1","input_0","currentValue_2","nextValue_0","key","shiftKey","stopPropagation","preventDefault","blur","handleKeyDown","event_2","handleKeyUp","t15","clsx","t16","t17","t18","t19"],"mappings":"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"}
1
+ {"version":3,"file":"CSSValueInput.js","sources":["../src/CSSValueInput.tsx"],"sourcesContent":["import {\n type CSSValueType,\n DEFAULT_CSS_VALUE_TYPE,\n DEFAULT_UNIT_BY_CSS_VALUE_TYPE,\n getCSSValueAsNumber,\n getCSSValueWithUnit,\n getUnitFromCSSValue,\n roundToPrecision,\n} from '@acusti/css-values';\nimport InputText from '@acusti/input-text';\nimport clsx from 'clsx';\nimport {\n type ChangeEvent,\n type FocusEvent,\n type KeyboardEvent,\n type ReactNode,\n type Ref,\n type SyntheticEvent,\n useEffect,\n useImperativeHandle,\n useRef,\n} from 'react';\n\nexport type Props = {\n /**\n * Boolean indicating if the user can submit an empty value (i.e. clear\n * the value). Defaults to true.\n */\n allowEmpty?: boolean;\n className?: string;\n cssValueType?: CSSValueType;\n disabled?: boolean;\n /**\n * Function that receives a value and converts it to its numerical equivalent\n * (i.e. '12px' → 12). Defaults to parseFloat().\n */\n getValueAsNumber?: (value: number | string) => number;\n icon?: ReactNode;\n label?: string;\n max?: number;\n min?: number;\n name?: string;\n onBlur?: (event: FocusEvent<HTMLInputElement>) => unknown;\n onChange?: (event: ChangeEvent<HTMLInputElement>) => unknown;\n onFocus?: (event: FocusEvent<HTMLInputElement>) => unknown;\n onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => unknown;\n onKeyUp?: (event: KeyboardEvent<HTMLInputElement>) => unknown;\n /**\n * Custom event handler triggered when the user presses enter/return\n * or blurs the input after making a change. Hitting esc will restore\n * the previous submitted value or original value.\n */\n onSubmitValue: (value: string) => unknown;\n placeholder?: string;\n ref?: Ref<HTMLInputElement>;\n step?: number;\n tabIndex?: number;\n title?: string;\n unit?: string;\n /** Regex or validator function to validate non-numeric values */\n validator?: ((value: string) => boolean) | RegExp;\n value?: string;\n};\n\ntype InputRef = HTMLInputElement | null;\n\nconst ROOT_CLASS_NAME = 'cssvalueinput';\n\nexport default function CSSValueInput({\n allowEmpty = true,\n className,\n cssValueType = DEFAULT_CSS_VALUE_TYPE,\n disabled,\n getValueAsNumber = getCSSValueAsNumber,\n icon,\n label,\n max,\n min,\n name,\n onBlur,\n onChange,\n onFocus,\n onKeyDown,\n onKeyUp,\n onSubmitValue,\n placeholder,\n ref,\n step = 1,\n tabIndex,\n title,\n unit = DEFAULT_UNIT_BY_CSS_VALUE_TYPE[cssValueType],\n validator,\n value: valueFromProps,\n}: Props) {\n const inputRef = useRef<InputRef>(null);\n\n useImperativeHandle<InputRef, InputRef>(ref, () => inputRef.current);\n\n // props.value should be a string; if it’s a number, convert it here\n const value =\n typeof valueFromProps === 'number' && Number.isFinite(valueFromProps)\n ? `${valueFromProps}`\n : valueFromProps;\n const submittedValueRef = useRef(value ?? '');\n\n useEffect(() => {\n submittedValueRef.current = value ?? '';\n }, [value]);\n\n const handleSubmitValue = (event: SyntheticEvent<HTMLInputElement>) => {\n // Store last submittedValue (used to reset value on invalid input)\n submittedValueRef.current = event.currentTarget.value;\n onSubmitValue(event.currentTarget.value);\n };\n\n const handleBlur = (event: FocusEvent<HTMLInputElement>) => {\n const input = event.currentTarget;\n inputRef.current = input;\n if (onBlur) onBlur(event);\n\n const currentValue = input.value.trim();\n\n // If allowEmpty and value is empty, skip all validation + normalization\n if (allowEmpty && !currentValue) {\n handleSubmitValue(event);\n return;\n }\n\n const currentValueAsNumber = getValueAsNumber(currentValue);\n const isCurrentValueFinite = Number.isFinite(currentValueAsNumber);\n // Inherit unit from last submitted value unless default is unitless;\n // ensures that submitting a new value with no unit doesn’t add a unit\n const defaultUnit = unit\n ? getUnitFromCSSValue({\n cssValueType,\n defaultUnit: unit,\n value: submittedValueRef.current,\n })\n : '';\n\n if (!isCurrentValueFinite) {\n let isValid = false;\n if (validator instanceof RegExp) {\n isValid = validator.test(currentValue);\n } else if (validator) {\n isValid = validator(currentValue);\n }\n\n if (isValid) {\n handleSubmitValue(event);\n } else {\n // If current value isn’t valid, revert to last submitted value\n input.value = submittedValueRef.current;\n }\n\n return;\n }\n\n // Normalize value by applying min/max and integer constraints\n let normalizedValueAsNumber = currentValueAsNumber;\n\n if (isCurrentValueFinite) {\n if (min != null && currentValueAsNumber < min) {\n normalizedValueAsNumber = min;\n } else if (max != null && currentValueAsNumber > max) {\n normalizedValueAsNumber = max;\n } else if (cssValueType === 'integer') {\n normalizedValueAsNumber = Math.floor(currentValueAsNumber);\n }\n }\n\n if (normalizedValueAsNumber !== currentValueAsNumber) {\n const currentUnit = getUnitFromCSSValue({\n cssValueType,\n defaultUnit,\n value: currentValue,\n });\n input.value = normalizedValueAsNumber + currentUnit;\n } else {\n input.value = getCSSValueWithUnit({\n cssValueType,\n defaultUnit,\n value: currentValue,\n });\n }\n\n handleSubmitValue(event);\n };\n\n const getNextValue = ({\n currentValue,\n multiplier = 1,\n signum = 1,\n }: {\n currentValue: number | string;\n multiplier?: number;\n signum?: number;\n }) => {\n const modifier = multiplier * step * signum;\n const currentValueAsNumber = getValueAsNumber(currentValue);\n // If currentValue isn’t numeric, don’t try to increment/decrement it\n if (typeof currentValue === 'string' && Number.isNaN(currentValueAsNumber)) {\n return currentValue;\n }\n\n let nextValue = currentValueAsNumber + modifier;\n if (cssValueType === 'integer') {\n nextValue = Math.floor(nextValue);\n } else {\n nextValue = roundToPrecision(nextValue, 5);\n }\n\n if (typeof max === 'number' && Number.isFinite(max)) {\n nextValue = Math.min(max, nextValue);\n }\n\n if (typeof min === 'number' && Number.isFinite(min)) {\n nextValue = Math.max(min, nextValue);\n }\n\n const nextUnit = getUnitFromCSSValue({\n cssValueType,\n defaultUnit: unit,\n value: currentValue,\n });\n return `${nextValue}${nextUnit}`;\n };\n\n const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n const input = event.currentTarget;\n inputRef.current = input;\n if (onKeyDown) onKeyDown(event);\n if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;\n\n const currentValue = input.value ?? placeholder ?? `0${unit}`;\n const nextValue = getNextValue({\n currentValue,\n multiplier: event.shiftKey ? 10 : 1,\n signum: event.key === 'ArrowUp' ? 1 : -1,\n });\n\n if (nextValue === currentValue) return;\n\n event.stopPropagation();\n event.preventDefault();\n\n input.value = nextValue;\n };\n\n const handleKeyUp = (event: KeyboardEvent<HTMLInputElement>) => {\n if (onKeyUp) onKeyUp(event);\n // If this is the key up from ↑ or ↓ keys, time to handleSubmitValue\n if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {\n handleSubmitValue(event);\n }\n };\n\n return (\n <label\n aria-label={label ? undefined : title}\n className={clsx(ROOT_CLASS_NAME, className, { disabled })}\n title={title}\n >\n {icon == null ? null : (\n <div className={`${ROOT_CLASS_NAME}-icon`}>{icon}</div>\n )}\n {label ? (\n <div className={`${ROOT_CLASS_NAME}-label`}>\n <p className={`${ROOT_CLASS_NAME}-label-text`}>{label}</p>\n </div>\n ) : null}\n <div className={`${ROOT_CLASS_NAME}-value`}>\n <InputText\n disabled={disabled}\n discardOnEscape\n initialValue={value}\n name={name}\n onBlur={handleBlur}\n onChange={onChange}\n onFocus={onFocus}\n onKeyDown={handleKeyDown}\n onKeyUp={handleKeyUp}\n placeholder={placeholder}\n ref={inputRef}\n selectTextOnFocus\n tabIndex={tabIndex}\n />\n </div>\n </label>\n );\n}\n"],"names":["ROOT_CLASS_NAME","CSSValueInput","t0","$","_c","allowEmpty","t1","className","cssValueType","t2","disabled","getValueAsNumber","t3","icon","label","max","min","name","onBlur","onChange","onFocus","onKeyDown","onKeyUp","onSubmitValue","placeholder","ref","step","t4","tabIndex","title","unit","t5","validator","value","valueFromProps","undefined","DEFAULT_CSS_VALUE_TYPE","getCSSValueAsNumber","DEFAULT_UNIT_BY_CSS_VALUE_TYPE","inputRef","useRef","t6","Symbol","for","current","useImperativeHandle","Number","isFinite","submittedValueRef","t7","t8","useEffect","t9","event","currentTarget","handleSubmitValue","t10","event_0","input","currentValue","trim","currentValueAsNumber","isCurrentValueFinite","defaultUnit","getUnitFromCSSValue","isValid","RegExp","test","normalizedValueAsNumber","Math","floor","currentUnit","getCSSValueWithUnit","handleBlur","t11","t12","currentValue_0","multiplier","t13","signum","t14","modifier","currentValueAsNumber_0","isNaN","nextValue","roundToPrecision","nextUnit","getNextValue","event_1","input_0","key","currentValue_1","nextValue_0","shiftKey","stopPropagation","preventDefault","handleKeyDown","event_2","handleKeyUp","t15","clsx","t16","t17","t18","t19"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acusti/css-value-input",
3
- "version": "2.1.2",
3
+ "version": "2.2.1",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "exports": "./dist/CSSValueInput.js",
@@ -38,23 +38,23 @@
38
38
  },
39
39
  "homepage": "https://github.com/acusti/uikit/tree/main/packages/css-value-input#readme",
40
40
  "devDependencies": {
41
- "@testing-library/dom": "^10.4.0",
41
+ "@testing-library/dom": "^10.4.1",
42
42
  "@testing-library/react": "^16.3.0",
43
43
  "@testing-library/user-event": "^14.6.1",
44
- "@types/react": "^19.1.8",
45
- "@vitejs/plugin-react": "^4.6.0",
46
- "babel-plugin-react-compiler": "rc",
47
- "happy-dom": "^18.0.1",
48
- "react": "^19.0.0",
49
- "react-dom": "^19.0.0",
50
- "typescript": "5.8.3",
51
- "unplugin-dts": "^1.0.0-beta.2",
52
- "vite": "^7.0.0",
44
+ "@types/react": "^19.2.2",
45
+ "@vitejs/plugin-react": "^5.0.4",
46
+ "babel-plugin-react-compiler": "^1",
47
+ "happy-dom": "^20.0.10",
48
+ "react": "^19.2.0",
49
+ "react-dom": "^19.2.0",
50
+ "typescript": "5.9.2",
51
+ "unplugin-dts": "^1.0.0-beta.6",
52
+ "vite": "^7.1.10",
53
53
  "vitest": "^3.2.4"
54
54
  },
55
55
  "dependencies": {
56
56
  "@acusti/css-values": "^1.2.0",
57
- "@acusti/input-text": "^2.1.1",
57
+ "@acusti/input-text": "^2.2.1",
58
58
  "clsx": "^2"
59
59
  },
60
60
  "peerDependencies": {