@acusti/css-value-input 2.1.1 → 2.2.0
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 +916 -17
- package/dist/CSSValueInput.d.ts +3 -3
- package/dist/CSSValueInput.js +86 -96
- package/dist/CSSValueInput.js.map +1 -1
- package/package.json +12 -12
package/README.md
CHANGED
|
@@ -5,29 +5,75 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@acusti/css-value-input)
|
|
6
6
|
[](https://bundlejs.com/?q=%40acusti%2Fcss-value-input)
|
|
7
7
|
|
|
8
|
-
`CSSValueInput` is a React component that renders a text input
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
15
|
+
## Key Features
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
*
|
|
59
|
-
*
|
|
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
|
-
|
|
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.
|