@flikk/ui 1.0.0-beta.32 → 1.0.0-beta.33
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/CHANGELOG.md +202 -0
- package/dist/components/ai/AgentRequest/AgentRequest.theme.js +1 -1
- package/dist/components/core/Button/Button.theme.js +3 -3
- package/dist/components/core/Pill/Pill.js +32 -17
- package/dist/components/core/Pill/Pill.types.d.ts +14 -1
- package/dist/components/core/SlidingNumber/SlidingNumber.d.ts +8 -0
- package/dist/components/core/SlidingNumber/SlidingNumber.js +12 -3
- package/dist/components/forms/ColorPicker/ColorPicker.js +45 -3
- package/dist/components/forms/ColorPicker/ColorPickerBody.js +8 -4
- package/dist/components/forms/Combobox/Combobox.js +8 -4
- package/dist/components/forms/Combobox/Combobox.theme.js +9 -0
- package/dist/components/forms/Combobox/Combobox.types.d.ts +13 -0
- package/dist/components/forms/DatePicker/DatePicker.js +19 -12
- package/dist/components/forms/DatePicker/DatePicker.theme.js +5 -0
- package/dist/components/forms/DatePicker/DatePicker.types.d.ts +8 -0
- package/dist/components/forms/DatePicker/DatePickerContext.d.ts +0 -1
- package/dist/components/forms/DatePicker/DatePickerTrigger.js +5 -3
- package/dist/components/forms/DateRangePicker/DateRangePicker.theme.js +5 -0
- package/dist/components/forms/DateRangePicker/DateRangePicker.types.d.ts +8 -0
- package/dist/components/forms/DateRangePicker/DateRangePickerTrigger.js +5 -3
- package/dist/components/forms/Input/Input.js +17 -12
- package/dist/components/forms/Input/Input.theme.js +10 -0
- package/dist/components/forms/Input/Input.types.d.ts +14 -0
- package/dist/components/forms/InputAddress/InputAddress.js +5 -5
- package/dist/components/forms/InputAddress/InputAddress.types.d.ts +7 -0
- package/dist/components/forms/InputCounter/InputCounter.js +194 -17
- package/dist/components/forms/InputCounter/InputCounter.theme.d.ts +12 -0
- package/dist/components/forms/InputCounter/InputCounter.theme.js +92 -2
- package/dist/components/forms/InputCounter/InputCounter.types.d.ts +47 -4
- package/dist/components/forms/InputCreditCard/InputCreditCard.js +4 -4
- package/dist/components/forms/InputCreditCard/InputCreditCard.types.d.ts +7 -0
- package/dist/components/forms/Mention/Mention.js +13 -11
- package/dist/components/forms/RichTextEditor/RichTextEditor.js +12 -3
- package/dist/components/forms/Select/Select.js +15 -8
- package/dist/components/forms/Select/Select.theme.js +9 -1
- package/dist/components/forms/Select/Select.types.d.ts +16 -1
- package/dist/components/forms/Textarea/Textarea.js +14 -25
- package/dist/components/forms/TimePicker/TimePicker.theme.js +9 -0
- package/dist/components/forms/TimePicker/TimePicker.types.d.ts +11 -0
- package/dist/components/forms/TimePicker/TimePickerTrigger.js +7 -5
- package/dist/components/forms/forms.theme.d.ts +57 -0
- package/dist/components/forms/forms.theme.js +83 -8
- package/dist/components/generative/registry.js +1 -0
- package/dist/components/generative/schema.generated.js +21 -1
- package/dist/generative.schema.json +21 -1
- package/dist/registry.json +108 -17
- package/dist/styles.css +1 -1
- package/dist/tools.json +21 -1
- package/dist/utils/composeEventHandlers.d.ts +19 -0
- package/dist/utils/composeEventHandlers.js +26 -0
- package/dist/utils/composeEventHandlers.test.d.ts +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/styles/theme.css +33 -9
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
3
|
-
import React__default, { useId,
|
|
3
|
+
import React__default, { useId, useState, useRef, useCallback, useMemo, useEffect } from 'react';
|
|
4
4
|
import { useReducedMotion, motion } from 'motion/react';
|
|
5
5
|
import { inputCounterTheme } from './InputCounter.theme.js';
|
|
6
6
|
import { Button } from '../../core/Button/Button.js';
|
|
7
|
-
import { SlidingNumber } from '../../core/SlidingNumber/SlidingNumber.js';
|
|
7
|
+
import { SLIDING_NUMBER_CHAR_WIDTH_EM, SlidingNumber } from '../../core/SlidingNumber/SlidingNumber.js';
|
|
8
8
|
import { cn } from '../../../utils/cn.js';
|
|
9
9
|
import { Minus, Plus } from '@phosphor-icons/react';
|
|
10
10
|
import { FormLabel } from '../FormLabel/FormLabel.js';
|
|
11
11
|
|
|
12
|
+
/** Lands a typed value in place instead of rolling the digits to it. */
|
|
13
|
+
const INSTANT_TRANSITION = { duration: 0 };
|
|
12
14
|
/**
|
|
13
15
|
* InputCounter component with animated number display
|
|
14
16
|
*
|
|
@@ -35,12 +37,44 @@ import { FormLabel } from '../FormLabel/FormLabel.js';
|
|
|
35
37
|
* );
|
|
36
38
|
* ```
|
|
37
39
|
*/
|
|
38
|
-
const InputCounter = React__default.forwardRef(({ value, onChange, state = "default", label, helperText, errorMessage, min, max, step = 1, disabled = false, decimalPlaces,
|
|
40
|
+
const InputCounter = React__default.forwardRef(({ value, onChange, size = "md", editable = true, state = "default", label, helperText, errorMessage, min, max, step = 1, disabled = false, decimalPlaces, className, buttonProps, theme, transition = { type: "spring", bounce: 0, stiffness: 300, damping: 30 }, "aria-label": ariaLabel = "Counter", onAnimationStart, onAnimationEnd, onDrag, onDragStart, onDragEnd, ...props }, ref) => {
|
|
39
41
|
const shouldReduceMotion = useReducedMotion();
|
|
40
42
|
const shouldDisableAnimations = !!shouldReduceMotion;
|
|
41
43
|
const generatedId = useId();
|
|
42
44
|
const isDisabled = state === "disabled" || disabled;
|
|
43
45
|
const isInvalid = state === "invalid";
|
|
46
|
+
// --- Manual entry ---------------------------------------------------
|
|
47
|
+
// Clicking the number swaps SlidingNumber for a real field. Typing is
|
|
48
|
+
// free-form (no clamping, no onChange) so "5" on the way to "50" under
|
|
49
|
+
// min=10 isn't eaten; the value is parsed, clamped and emitted only on
|
|
50
|
+
// commit. `editSession` guards the commit path: Enter and Escape both
|
|
51
|
+
// unmount the field, which fires a blur that must not commit a second
|
|
52
|
+
// time (or re-commit a value Escape just discarded).
|
|
53
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
54
|
+
const [draft, setDraft] = useState("");
|
|
55
|
+
/**
|
|
56
|
+
* True when the value on screen got there by being TYPED. The odometer roll
|
|
57
|
+
* is feedback for stepping — it shows the value travelling by one step. A
|
|
58
|
+
* number entered outright never travelled, so rolling 4 → 9 through 5, 6, 7,
|
|
59
|
+
* 8 reads as the control second-guessing the entry; it should simply appear.
|
|
60
|
+
*
|
|
61
|
+
* Carried as state rather than a remount: remounting SlidingNumber makes it
|
|
62
|
+
* animate from zero instead (a motion element without `initial` animates ON
|
|
63
|
+
* MOUNT), which is the same roll in a worse costume. Every emit declares
|
|
64
|
+
* which kind it is via `emitValue`, so an async parent that applies the value
|
|
65
|
+
* a tick later still lands it instantly, and the next step still animates.
|
|
66
|
+
*/
|
|
67
|
+
const [landsInstantly, setLandsInstantly] = useState(false);
|
|
68
|
+
const fieldRef = useRef(null);
|
|
69
|
+
const containerRef = useRef(null);
|
|
70
|
+
const editSessionRef = useRef("idle");
|
|
71
|
+
const setContainerRef = useCallback((node) => {
|
|
72
|
+
containerRef.current = node;
|
|
73
|
+
if (typeof ref === "function")
|
|
74
|
+
ref(node);
|
|
75
|
+
else if (ref)
|
|
76
|
+
ref.current = node;
|
|
77
|
+
}, [ref]);
|
|
44
78
|
// Auto-detect decimal places from step if not explicitly provided
|
|
45
79
|
const autoDecimalPlaces = useMemo(() => {
|
|
46
80
|
if (decimalPlaces !== undefined)
|
|
@@ -58,6 +92,16 @@ const InputCounter = React__default.forwardRef(({ value, onChange, state = "defa
|
|
|
58
92
|
...inputCounterTheme,
|
|
59
93
|
...theme,
|
|
60
94
|
}), [theme]);
|
|
95
|
+
/**
|
|
96
|
+
* The single emit path. Every value change declares whether it was typed
|
|
97
|
+
* (lands instantly) or stepped (rolls), so the flag can never drift out of
|
|
98
|
+
* sync with what is on screen.
|
|
99
|
+
*/
|
|
100
|
+
const emitValue = useCallback((next, options) => {
|
|
101
|
+
var _a;
|
|
102
|
+
setLandsInstantly((_a = options === null || options === void 0 ? void 0 : options.typed) !== null && _a !== void 0 ? _a : false);
|
|
103
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(next);
|
|
104
|
+
}, [onChange]);
|
|
61
105
|
// Auto-clamp value to min/max bounds when they change
|
|
62
106
|
useEffect(() => {
|
|
63
107
|
let clampedValue = value;
|
|
@@ -73,9 +117,9 @@ const InputCounter = React__default.forwardRef(({ value, onChange, state = "defa
|
|
|
73
117
|
needsUpdate = true;
|
|
74
118
|
}
|
|
75
119
|
if (needsUpdate) {
|
|
76
|
-
|
|
120
|
+
emitValue(clampedValue);
|
|
77
121
|
}
|
|
78
|
-
}, [min, max, value,
|
|
122
|
+
}, [min, max, value, emitValue]);
|
|
79
123
|
// Handle increment
|
|
80
124
|
const handleIncrement = useCallback(() => {
|
|
81
125
|
if (isDisabled)
|
|
@@ -91,8 +135,8 @@ const InputCounter = React__default.forwardRef(({ value, onChange, state = "defa
|
|
|
91
135
|
}
|
|
92
136
|
return;
|
|
93
137
|
}
|
|
94
|
-
|
|
95
|
-
}, [value, step, max, isDisabled,
|
|
138
|
+
emitValue(roundedValue);
|
|
139
|
+
}, [value, step, max, isDisabled, emitValue]);
|
|
96
140
|
// Handle decrement
|
|
97
141
|
const handleDecrement = useCallback(() => {
|
|
98
142
|
if (isDisabled)
|
|
@@ -108,8 +152,113 @@ const InputCounter = React__default.forwardRef(({ value, onChange, state = "defa
|
|
|
108
152
|
}
|
|
109
153
|
return;
|
|
110
154
|
}
|
|
111
|
-
|
|
112
|
-
}, [value, step, min, isDisabled,
|
|
155
|
+
emitValue(roundedValue);
|
|
156
|
+
}, [value, step, min, isDisabled, emitValue]);
|
|
157
|
+
const canEdit = editable && !isDisabled;
|
|
158
|
+
// --- Value slot width -----------------------------------------------
|
|
159
|
+
// SlidingNumber gives every character (digit, `-`, `.`) the same fixed em
|
|
160
|
+
// box, so an exact width can be reserved from a character COUNT without
|
|
161
|
+
// measuring. Both states are laid into that one box, which is what keeps
|
|
162
|
+
// clicking the number from resizing the control: the edit field used to
|
|
163
|
+
// size itself via the `size` attribute — the browser's average character
|
|
164
|
+
// width — and landed 1–3px wider than the number it replaced, a gap the
|
|
165
|
+
// root's `layout` animation then made visible as a wobble.
|
|
166
|
+
const formatChars = useCallback((n) => (autoDecimalPlaces > 0
|
|
167
|
+
? Number(n).toFixed(autoDecimalPlaces)
|
|
168
|
+
: String(Math.round(n))).length, [autoDecimalPlaces]);
|
|
169
|
+
// Bounded counters reserve room for their widest legal value up front, so
|
|
170
|
+
// stepping 9 → 10 doesn't resize the box either.
|
|
171
|
+
//
|
|
172
|
+
// 🔴 The floor is TWO characters, not one, and it is load-bearing at `sm`:
|
|
173
|
+
// the steppers there are undersized for WCAG 2.2 SC 2.5.8 and clear it via
|
|
174
|
+
// the spacing exception, which is measured to this slot's centre (it is a
|
|
175
|
+
// click-to-edit target). A 1-character slot pulls those centres to 21.6px
|
|
176
|
+
// and fails; two characters keeps them at 25.2px. See InputCounter.theme.ts.
|
|
177
|
+
const slotChars = useMemo(() => {
|
|
178
|
+
let chars = Math.max(formatChars(value), 2);
|
|
179
|
+
if (min !== undefined)
|
|
180
|
+
chars = Math.max(chars, formatChars(min));
|
|
181
|
+
if (max !== undefined)
|
|
182
|
+
chars = Math.max(chars, formatChars(max));
|
|
183
|
+
if (isEditing)
|
|
184
|
+
chars = Math.max(chars, draft.length);
|
|
185
|
+
return chars;
|
|
186
|
+
}, [formatChars, value, min, max, isEditing, draft]);
|
|
187
|
+
/**
|
|
188
|
+
* A draft that parses to a number outside min/max. Typing stays free-form —
|
|
189
|
+
* clamping mid-edit would eat the `5` of `50` under `min={10}` — so the
|
|
190
|
+
* counter instead shows the shared invalid edge the moment the value is out
|
|
191
|
+
* of range, and clamps on commit. Unparseable input isn't "out of range";
|
|
192
|
+
* it simply reverts.
|
|
193
|
+
*/
|
|
194
|
+
const draftOutOfRange = useMemo(() => {
|
|
195
|
+
if (!isEditing)
|
|
196
|
+
return false;
|
|
197
|
+
const trimmed = draft.trim();
|
|
198
|
+
if (trimmed === "")
|
|
199
|
+
return false;
|
|
200
|
+
const parsed = Number(trimmed);
|
|
201
|
+
if (!Number.isFinite(parsed))
|
|
202
|
+
return false;
|
|
203
|
+
return ((min !== undefined && parsed < min) || (max !== undefined && parsed > max));
|
|
204
|
+
}, [isEditing, draft, min, max]);
|
|
205
|
+
const beginEdit = useCallback(() => {
|
|
206
|
+
if (!canEdit)
|
|
207
|
+
return;
|
|
208
|
+
editSessionRef.current = "active";
|
|
209
|
+
setDraft(autoDecimalPlaces > 0 ? value.toFixed(autoDecimalPlaces) : String(value));
|
|
210
|
+
setIsEditing(true);
|
|
211
|
+
}, [canEdit, value, autoDecimalPlaces]);
|
|
212
|
+
/** Focus and select the field once it exists, so typing replaces. */
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
if (!isEditing)
|
|
215
|
+
return;
|
|
216
|
+
const field = fieldRef.current;
|
|
217
|
+
if (!field)
|
|
218
|
+
return;
|
|
219
|
+
field.focus();
|
|
220
|
+
field.select();
|
|
221
|
+
}, [isEditing]);
|
|
222
|
+
const endEdit = useCallback((commit, refocusContainer) => {
|
|
223
|
+
var _a;
|
|
224
|
+
if (editSessionRef.current !== "active")
|
|
225
|
+
return;
|
|
226
|
+
editSessionRef.current = "idle";
|
|
227
|
+
// Every exit from an edit session lands instantly, not just a changed
|
|
228
|
+
// value: the display is MOUNTED FRESH when the field unmounts, and a
|
|
229
|
+
// motion element without `initial` animates on mount — so without this,
|
|
230
|
+
// even Escape would roll the untouched number up from zero.
|
|
231
|
+
setLandsInstantly(true);
|
|
232
|
+
setIsEditing(false);
|
|
233
|
+
if (refocusContainer)
|
|
234
|
+
(_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
235
|
+
if (!commit)
|
|
236
|
+
return;
|
|
237
|
+
const trimmed = draft.trim();
|
|
238
|
+
if (trimmed === "")
|
|
239
|
+
return; // empty is a revert, not a zero
|
|
240
|
+
const parsed = Number(trimmed);
|
|
241
|
+
if (!Number.isFinite(parsed))
|
|
242
|
+
return; // unparseable reverts
|
|
243
|
+
let next = Number(parsed.toFixed(autoDecimalPlaces));
|
|
244
|
+
if (min !== undefined && next < min)
|
|
245
|
+
next = min;
|
|
246
|
+
if (max !== undefined && next > max)
|
|
247
|
+
next = max;
|
|
248
|
+
// §9: never emit a change the user didn't produce.
|
|
249
|
+
if (next !== value)
|
|
250
|
+
emitValue(next, { typed: true });
|
|
251
|
+
}, [draft, autoDecimalPlaces, min, max, value, emitValue]);
|
|
252
|
+
const handleFieldKeyDown = useCallback((event) => {
|
|
253
|
+
if (event.key === "Enter") {
|
|
254
|
+
event.preventDefault();
|
|
255
|
+
endEdit(true, true);
|
|
256
|
+
}
|
|
257
|
+
else if (event.key === "Escape") {
|
|
258
|
+
event.preventDefault();
|
|
259
|
+
endEdit(false, true);
|
|
260
|
+
}
|
|
261
|
+
}, [endEdit]);
|
|
113
262
|
// Keyboard support — scoped to THIS counter instance via onKeyDown on the
|
|
114
263
|
// focusable root. Because the root is focusable (tabIndex 0) and its child
|
|
115
264
|
// buttons bubble keydown to it, this fires only when focus is within THIS
|
|
@@ -117,7 +266,20 @@ const InputCounter = React__default.forwardRef(({ value, onChange, state = "defa
|
|
|
117
266
|
const handleKeyDown = useCallback((event) => {
|
|
118
267
|
if (isDisabled)
|
|
119
268
|
return;
|
|
269
|
+
// While the edit field is open its keys belong to it — arrows move the
|
|
270
|
+
// caret, they must not step the counter.
|
|
271
|
+
if (isEditing)
|
|
272
|
+
return;
|
|
120
273
|
switch (event.key) {
|
|
274
|
+
// Enter opens the edit field, so manual entry is reachable without a
|
|
275
|
+
// mouse. Field-level keys are handled by the field itself; this
|
|
276
|
+
// handler bails while editing (see the isEditing guard above).
|
|
277
|
+
case "Enter":
|
|
278
|
+
if (canEdit) {
|
|
279
|
+
event.preventDefault();
|
|
280
|
+
beginEdit();
|
|
281
|
+
}
|
|
282
|
+
break;
|
|
121
283
|
case "ArrowUp":
|
|
122
284
|
event.preventDefault();
|
|
123
285
|
handleIncrement();
|
|
@@ -130,10 +292,10 @@ const InputCounter = React__default.forwardRef(({ value, onChange, state = "defa
|
|
|
130
292
|
event.preventDefault();
|
|
131
293
|
const pageUpValue = value + step * 10;
|
|
132
294
|
if (max === undefined || pageUpValue <= max) {
|
|
133
|
-
|
|
295
|
+
emitValue(pageUpValue);
|
|
134
296
|
}
|
|
135
297
|
else if (max !== undefined) {
|
|
136
|
-
|
|
298
|
+
emitValue(max);
|
|
137
299
|
}
|
|
138
300
|
break;
|
|
139
301
|
}
|
|
@@ -141,21 +303,24 @@ const InputCounter = React__default.forwardRef(({ value, onChange, state = "defa
|
|
|
141
303
|
event.preventDefault();
|
|
142
304
|
const pageDownValue = value - step * 10;
|
|
143
305
|
if (min === undefined || pageDownValue >= min) {
|
|
144
|
-
|
|
306
|
+
emitValue(pageDownValue);
|
|
145
307
|
}
|
|
146
308
|
else if (min !== undefined) {
|
|
147
|
-
|
|
309
|
+
emitValue(min);
|
|
148
310
|
}
|
|
149
311
|
break;
|
|
150
312
|
}
|
|
151
313
|
}
|
|
152
314
|
}, [
|
|
315
|
+
isEditing,
|
|
316
|
+
canEdit,
|
|
317
|
+
beginEdit,
|
|
153
318
|
value,
|
|
154
319
|
step,
|
|
155
320
|
min,
|
|
156
321
|
max,
|
|
157
322
|
isDisabled,
|
|
158
|
-
|
|
323
|
+
emitValue,
|
|
159
324
|
handleIncrement,
|
|
160
325
|
handleDecrement,
|
|
161
326
|
]);
|
|
@@ -166,7 +331,7 @@ const InputCounter = React__default.forwardRef(({ value, onChange, state = "defa
|
|
|
166
331
|
// errorMessage takes precedence over helperText while invalid; helperText
|
|
167
332
|
// behavior is unchanged otherwise.
|
|
168
333
|
const displayedMessage = isInvalid && errorMessage ? errorMessage : helperText;
|
|
169
|
-
return (jsxs("div", { className: "flex flex-col", children: [label && (typeof label === "string" ? (jsx(FormLabel, { htmlFor: `${generatedId}-counter`, state: isDisabled ? "disabled" : isInvalid ? "invalid" : "default", className: "mb-1.5", children: label })) : (jsx("div", { className: "mb-1.5", children: label }))), jsxs(motion.div, { ref:
|
|
334
|
+
return (jsxs("div", { className: "flex flex-col", children: [label && (typeof label === "string" ? (jsx(FormLabel, { htmlFor: `${generatedId}-counter`, state: isDisabled ? "disabled" : isInvalid ? "invalid" : "default", className: "mb-1.5", children: label })) : (jsx("div", { className: "mb-1.5", children: label }))), jsxs(motion.div, { ref: setContainerRef, id: `${generatedId}-counter`, "data-counter": "true", "data-size": size, layout: true, transition: shouldDisableAnimations ? undefined : transition, className: cn(mergedTheme.baseStyle, mergedTheme.sizes[size], isDisabled && mergedTheme.disabledStyle, (isInvalid || draftOutOfRange) && mergedTheme.invalidStyle, className), role: "spinbutton", "aria-label": ariaLabel, "aria-valuenow": value, "aria-valuemin": min, "aria-valuemax": max, "aria-disabled": isDisabled, "aria-invalid": isInvalid || undefined, "aria-describedby": displayedMessage ? helperId : undefined, tabIndex: isDisabled ? -1 : 0, onKeyDown: handleKeyDown, ...props, children: [jsx(motion.div, { className: mergedTheme.buttonWrapperStyle,
|
|
170
335
|
// Framer Motion's press gesture (triggered by `whileTap`) adds
|
|
171
336
|
// `tabIndex=0` to any non-natively-focusable element that
|
|
172
337
|
// doesn't already declare a `tabindex`, to make tap gestures
|
|
@@ -175,7 +340,16 @@ const InputCounter = React__default.forwardRef(({ value, onChange, state = "defa
|
|
|
175
340
|
// focusable <Button>, so an explicit tabIndex={-1} keeps it out
|
|
176
341
|
// of the tab order (motion.js only adds its own when the
|
|
177
342
|
// attribute is absent).
|
|
178
|
-
tabIndex: -1, whileHover: !isDisabled && !shouldDisableAnimations ? { scale: 1.05 } : undefined, whileTap: !isDisabled && !shouldDisableAnimations ? { scale: 0.95 } : undefined, children: jsx(Button, { size:
|
|
343
|
+
tabIndex: -1, whileHover: !isDisabled && !shouldDisableAnimations ? { scale: 1.05 } : undefined, whileTap: !isDisabled && !shouldDisableAnimations ? { scale: 0.95 } : undefined, children: jsx(Button, { size: mergedTheme.buttonSizes[size], variant: "ghost", iconOnly: true, ...buttonProps,
|
|
344
|
+
// The square comes from the COUNTER's height, not Button's size
|
|
345
|
+
// scale; a consumer className still wins (shadcn, §4).
|
|
346
|
+
className: cn(mergedTheme.stepperSizes[size], buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.className), onClick: handleDecrement, disabled: !canDecrement, "aria-label": "Decrement", children: jsx(Minus, { className: cn(mergedTheme.stepperIconSizes[size], "text-[var(--color-text-primary)]"), weight: "bold" }) }) }), jsx("span", { "data-testid": "input-counter-slot", className: cn(mergedTheme.valueSlotStyle, mergedTheme.textSizes[size]),
|
|
347
|
+
// The one box both states occupy, sized in the same `em` metric
|
|
348
|
+
// SlidingNumber lays its characters out on — so the field that
|
|
349
|
+
// replaces the number is exactly as wide as the number was.
|
|
350
|
+
style: {
|
|
351
|
+
width: `calc(${slotChars} * ${SLIDING_NUMBER_CHAR_WIDTH_EM}em)`,
|
|
352
|
+
}, children: isEditing ? (jsx("input", { ref: fieldRef, type: "text", inputMode: "decimal", "aria-label": ariaLabel, "aria-invalid": draftOutOfRange || isInvalid || undefined, value: draft, className: cn(mergedTheme.editFieldStyle, mergedTheme.textSizes[size]), onChange: (event) => setDraft(event.target.value), onKeyDown: handleFieldKeyDown, onBlur: () => endEdit(true, false) })) : (jsx("span", { "data-testid": "input-counter-value", onClick: beginEdit, className: cn("flex w-full items-center justify-center", canEdit && "cursor-text"), children: jsx(SlidingNumber, { value: value, decimalPlaces: autoDecimalPlaces, className: cn(mergedTheme.numberStyle, mergedTheme.textSizes[size], isDisabled && mergedTheme.numberDisabledStyle), transition: landsInstantly ? INSTANT_TRANSITION : transition }) })) }), jsx(motion.div, { className: mergedTheme.buttonWrapperStyle,
|
|
179
353
|
// Framer Motion's press gesture (triggered by `whileTap`) adds
|
|
180
354
|
// `tabIndex=0` to any non-natively-focusable element that
|
|
181
355
|
// doesn't already declare a `tabindex`, to make tap gestures
|
|
@@ -184,7 +358,10 @@ const InputCounter = React__default.forwardRef(({ value, onChange, state = "defa
|
|
|
184
358
|
// focusable <Button>, so an explicit tabIndex={-1} keeps it out
|
|
185
359
|
// of the tab order (motion.js only adds its own when the
|
|
186
360
|
// attribute is absent).
|
|
187
|
-
tabIndex: -1, whileHover: !isDisabled && !shouldDisableAnimations ? { scale: 1.05 } : undefined, whileTap: !isDisabled && !shouldDisableAnimations ? { scale: 0.95 } : undefined, children: jsx(Button, { size:
|
|
361
|
+
tabIndex: -1, whileHover: !isDisabled && !shouldDisableAnimations ? { scale: 1.05 } : undefined, whileTap: !isDisabled && !shouldDisableAnimations ? { scale: 0.95 } : undefined, children: jsx(Button, { size: mergedTheme.buttonSizes[size], variant: "ghost", iconOnly: true, ...buttonProps,
|
|
362
|
+
// The square comes from the COUNTER's height, not Button's size
|
|
363
|
+
// scale; a consumer className still wins (shadcn, §4).
|
|
364
|
+
className: cn(mergedTheme.stepperSizes[size], buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.className), onClick: handleIncrement, disabled: !canIncrement, "aria-label": "Increment", children: jsx(Plus, { className: cn(mergedTheme.stepperIconSizes[size], "text-[var(--color-text-primary)]"), weight: "bold" }) }) })] }), displayedMessage && (jsx("div", { id: helperId, role: isInvalid ? "alert" : undefined, className: cn("text-sm mt-1", isInvalid
|
|
188
365
|
? "text-[var(--color-danger)]"
|
|
189
366
|
: "text-[var(--color-text-muted)]"), children: displayedMessage }))] }));
|
|
190
367
|
});
|
|
@@ -2,5 +2,17 @@ import { InputCounterTheme } from "./InputCounter.types";
|
|
|
2
2
|
/**
|
|
3
3
|
* InputCounter theme configuration
|
|
4
4
|
* Follows Flikkui design system with CSS variables
|
|
5
|
+
*
|
|
6
|
+
* 🔴 Sizing comes from the SHARED `--form-*` scale (`formsBaseTheme.sizes`), not
|
|
7
|
+
* from literals — a counter sits in the same column as an Input/Select and has
|
|
8
|
+
* to land on the same baseline. Before this, height was implicit (`p-1` plus a
|
|
9
|
+
* hardcoded `size="sm"` Button) and the number was locked to `text-sm`, so a
|
|
10
|
+
* counter could not be made to match the fields around it.
|
|
11
|
+
*
|
|
12
|
+
* 🔴 The edge is the shared inset box-shadow (`formsBaseTheme.inputGroupBorderStyle`),
|
|
13
|
+
* NOT a real `border` — CLAUDE.md §9. A real border was doing three kinds of damage:
|
|
14
|
+
* it read differently from the Select beside it, it consumed 2px of the fixed
|
|
15
|
+
* `--form-min-h-*` height (which is what made the `sm` steppers overflow the box),
|
|
16
|
+
* and it left the counter as the only tabbable form control with no focus ring.
|
|
5
17
|
*/
|
|
6
18
|
export declare const inputCounterTheme: InputCounterTheme;
|
|
@@ -1,11 +1,101 @@
|
|
|
1
|
+
import { formsBaseTheme } from '../forms.theme.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* InputCounter theme configuration
|
|
3
5
|
* Follows Flikkui design system with CSS variables
|
|
6
|
+
*
|
|
7
|
+
* 🔴 Sizing comes from the SHARED `--form-*` scale (`formsBaseTheme.sizes`), not
|
|
8
|
+
* from literals — a counter sits in the same column as an Input/Select and has
|
|
9
|
+
* to land on the same baseline. Before this, height was implicit (`p-1` plus a
|
|
10
|
+
* hardcoded `size="sm"` Button) and the number was locked to `text-sm`, so a
|
|
11
|
+
* counter could not be made to match the fields around it.
|
|
12
|
+
*
|
|
13
|
+
* 🔴 The edge is the shared inset box-shadow (`formsBaseTheme.inputGroupBorderStyle`),
|
|
14
|
+
* NOT a real `border` — CLAUDE.md §9. A real border was doing three kinds of damage:
|
|
15
|
+
* it read differently from the Select beside it, it consumed 2px of the fixed
|
|
16
|
+
* `--form-min-h-*` height (which is what made the `sm` steppers overflow the box),
|
|
17
|
+
* and it left the counter as the only tabbable form control with no focus ring.
|
|
4
18
|
*/
|
|
5
19
|
const inputCounterTheme = {
|
|
6
|
-
baseStyle: "flex items-center gap-
|
|
20
|
+
baseStyle: "flex items-center gap-1 w-fit bg-[var(--field-fill)] " +
|
|
21
|
+
// 🔴 Explicitly listed properties, never `transition-all`: this root is a
|
|
22
|
+
// motion element carrying `layout`, and a CSS transition covering
|
|
23
|
+
// `transform` re-interpolates motion's per-frame writes (§12).
|
|
24
|
+
"transition-[box-shadow,background-color] duration-[var(--motion-duration-fast)] " +
|
|
25
|
+
formsBaseTheme.inputGroupBorderStyle,
|
|
26
|
+
invalidStyle: formsBaseTheme.states.invalid,
|
|
27
|
+
// The same disabled treatment every other field gets (disabled fill, resting
|
|
28
|
+
// border held, no hover edge, not-allowed cursor on the steppers). The number
|
|
29
|
+
// is a <span>, not an <input>, so `states.disabled`'s `[&_input]` text rule
|
|
30
|
+
// can't reach it — `numberDisabledStyle` greys it explicitly.
|
|
31
|
+
disabledStyle: formsBaseTheme.states.disabled,
|
|
32
|
+
numberDisabledStyle: "text-[var(--color-text-disabled)]",
|
|
7
33
|
buttonWrapperStyle: "flex-shrink-0",
|
|
8
|
-
numberStyle: "min-w-fit text-center text-
|
|
34
|
+
numberStyle: "min-w-fit text-center text-[var(--color-text-primary)] tabular-nums select-none",
|
|
35
|
+
// The value slot is a fixed box both states share — the animated number at rest
|
|
36
|
+
// and the edit field while typing. Its width is set inline from the character
|
|
37
|
+
// count (see InputCounter.tsx), so clicking the number cannot resize the
|
|
38
|
+
// control: the `size` attribute the field used to carry sized it by the
|
|
39
|
+
// browser's average character width, which is NOT SlidingNumber's fixed
|
|
40
|
+
// per-character em box, so the box grew ~1–3px on every click.
|
|
41
|
+
valueSlotStyle: "relative flex items-center justify-center flex-shrink-0",
|
|
42
|
+
// Inset all round, so the stepper squares come out 20 / 26 / 30 against the
|
|
43
|
+
// 28 / 36 / 40 heights. md/lg take 5px — the button lands at roughly 0.7× the
|
|
44
|
+
// control height, and at 4px the square read as filling its box.
|
|
45
|
+
//
|
|
46
|
+
// 🔴 `sm` is the one rung where WCAG 2.2 SC 2.5.8 (Target Size, AA) binds. A
|
|
47
|
+
// 28px box cannot hold a 24px square at any sensible inset, so the steppers
|
|
48
|
+
// are undersized and have to clear the spacing exception instead: a 24px
|
|
49
|
+
// circle centred on each target must not intersect the next one's. The
|
|
50
|
+
// neighbour is the click-to-edit value, so `sm` widens the gap to 8px, which
|
|
51
|
+
// puts the centres 25.2px apart (`gap-1` gave 21.2px and failed). The value
|
|
52
|
+
// slot's 2-character floor (see InputCounter.tsx) is what keeps that true for
|
|
53
|
+
// a single-digit counter — a 1-char slot would pull the centres back to 21.6px.
|
|
54
|
+
sizes: {
|
|
55
|
+
sm: `${formsBaseTheme.sizes.sm.height} p-1 gap-2`,
|
|
56
|
+
md: `${formsBaseTheme.sizes.md.height} p-[5px]`,
|
|
57
|
+
lg: `${formsBaseTheme.sizes.lg.height} p-[5px]`,
|
|
58
|
+
},
|
|
59
|
+
textSizes: {
|
|
60
|
+
sm: formsBaseTheme.sizes.sm.text,
|
|
61
|
+
md: formsBaseTheme.sizes.md.text,
|
|
62
|
+
lg: formsBaseTheme.sizes.lg.text,
|
|
63
|
+
},
|
|
64
|
+
// The steppers are squares derived from the COUNTER's height — `--form-min-h-*`
|
|
65
|
+
// minus the container's inset on each side (20 / 26 / 30 at the default
|
|
66
|
+
// theme) — rather than from Button's own size scale. The old `buttonSizes` map
|
|
67
|
+
// landed on whichever Button size was closest, which gave `sm` and `md` the
|
|
68
|
+
// SAME 32px stepper and, at `sm`, a square TALLER than the counter's content
|
|
69
|
+
// box: the ghost hover fill punched straight through the border.
|
|
70
|
+
// Radius is concentric with the container's (`--form-radius` − the 5px inset),
|
|
71
|
+
// the same construction `--form-dropdown-inner-radius` uses for dropdown rows.
|
|
72
|
+
// 🔴 Written out per size, never assembled from a template — Tailwind scans
|
|
73
|
+
// source text, so an interpolated class name emits no CSS at all (§3). The
|
|
74
|
+
// `_-_` is the codebase's arbitrary-value spelling for the whitespace CSS
|
|
75
|
+
// requires around a calc `-`.
|
|
76
|
+
stepperSizes: {
|
|
77
|
+
sm: "size-[calc(var(--form-min-h-sm)_-_8px)] p-0 rounded-[calc(var(--form-radius)_-_4px)]",
|
|
78
|
+
md: "size-[calc(var(--form-min-h-md)_-_10px)] p-0 rounded-[calc(var(--form-radius)_-_5px)]",
|
|
79
|
+
lg: "size-[calc(var(--form-min-h-lg)_-_10px)] p-0 rounded-[calc(var(--form-radius)_-_5px)]",
|
|
80
|
+
},
|
|
81
|
+
// Icons ramp with the square they sit in (12 / 14 / 16px inside 22 / 26 / 30),
|
|
82
|
+
// holding roughly the same optical breathing room at every size. They were a
|
|
83
|
+
// flat size-4 at all three, which is what made the `sm` stepper look packed.
|
|
84
|
+
stepperIconSizes: {
|
|
85
|
+
sm: "size-3",
|
|
86
|
+
md: "size-3.5",
|
|
87
|
+
lg: "size-4",
|
|
88
|
+
},
|
|
89
|
+
buttonSizes: {
|
|
90
|
+
sm: "sm",
|
|
91
|
+
md: "sm",
|
|
92
|
+
lg: "md",
|
|
93
|
+
},
|
|
94
|
+
// The counter's own container paints the border, background and radius — this
|
|
95
|
+
// field is the text layer inside it, so it stays transparent and chrome-free
|
|
96
|
+
// (the same split Input uses between its group wrapper and inner <input>).
|
|
97
|
+
// `w-full` fills the shared value slot; it must NOT size itself.
|
|
98
|
+
editFieldStyle: "w-full min-w-0 bg-transparent border-0 outline-none text-center text-[var(--color-text-primary)] tabular-nums p-0 focus:outline-none",
|
|
9
99
|
};
|
|
10
100
|
|
|
11
101
|
export { inputCounterTheme };
|
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import type { Transition } from 'motion/react';
|
|
3
3
|
import { ButtonProps } from '../../core/Button/Button.types';
|
|
4
4
|
export type InputCounterState = 'default' | 'disabled' | 'invalid';
|
|
5
|
+
export type InputCounterSize = 'sm' | 'md' | 'lg';
|
|
5
6
|
/**
|
|
6
7
|
* InputCounter component props
|
|
7
8
|
* @property {number} value - Current counter value (controlled)
|
|
@@ -10,7 +11,6 @@ export type InputCounterState = 'default' | 'disabled' | 'invalid';
|
|
|
10
11
|
* @property {number} max - Maximum allowed value (optional)
|
|
11
12
|
* @property {number} step - Increment/decrement step size (default: 1)
|
|
12
13
|
* @property {boolean} disabled - Whether the counter is disabled
|
|
13
|
-
* @property {function} formatNumber - Custom number formatter
|
|
14
14
|
* @property {object} buttonProps - Props to pass to increment/decrement buttons
|
|
15
15
|
* @property {object} theme - Theme overrides for styling
|
|
16
16
|
* @property {object} transition - Custom Framer Motion transition
|
|
@@ -20,6 +20,18 @@ export interface InputCounterProps extends Omit<React.HTMLAttributes<HTMLDivElem
|
|
|
20
20
|
value: number;
|
|
21
21
|
/** Callback when value changes */
|
|
22
22
|
onChange?: (value: number) => void;
|
|
23
|
+
/**
|
|
24
|
+
* Control size, drawn from the shared `--form-*` scale so a counter lines up
|
|
25
|
+
* with the Input/Select/Combobox it sits beside.
|
|
26
|
+
* @default 'md'
|
|
27
|
+
*/
|
|
28
|
+
size?: InputCounterSize;
|
|
29
|
+
/**
|
|
30
|
+
* Whether clicking the number swaps it for a text field so the value can be
|
|
31
|
+
* typed directly. Committed on Enter or blur; Escape reverts.
|
|
32
|
+
* @default true
|
|
33
|
+
*/
|
|
34
|
+
editable?: boolean;
|
|
23
35
|
/** Validation/interaction state */
|
|
24
36
|
state?: InputCounterState;
|
|
25
37
|
/** Field label rendered above the counter */
|
|
@@ -42,8 +54,6 @@ export interface InputCounterProps extends Omit<React.HTMLAttributes<HTMLDivElem
|
|
|
42
54
|
disabled?: boolean;
|
|
43
55
|
/** Number of decimal places to display (auto-detected from step if not specified) */
|
|
44
56
|
decimalPlaces?: number;
|
|
45
|
-
/** Custom number formatter (e.g., for decimals or localization) */
|
|
46
|
-
formatNumber?: (value: number) => string;
|
|
47
57
|
/** Additional class names */
|
|
48
58
|
className?: string;
|
|
49
59
|
/** Props to pass to increment/decrement buttons (omitting onClick) */
|
|
@@ -59,10 +69,43 @@ export interface InputCounterProps extends Omit<React.HTMLAttributes<HTMLDivElem
|
|
|
59
69
|
* Theme configuration for InputCounter component
|
|
60
70
|
*/
|
|
61
71
|
export interface InputCounterTheme {
|
|
62
|
-
/** Base container style */
|
|
72
|
+
/** Base container style — includes the shared inset-shadow field edge */
|
|
63
73
|
baseStyle: string;
|
|
74
|
+
/** Invalid edge, from the shared `formsBaseTheme.states.invalid` */
|
|
75
|
+
invalidStyle: string;
|
|
76
|
+
/** Disabled treatment, from the shared `formsBaseTheme.states.disabled` */
|
|
77
|
+
disabledStyle: string;
|
|
78
|
+
/** Number colour while disabled (the span `states.disabled` can't reach) */
|
|
79
|
+
numberDisabledStyle: string;
|
|
64
80
|
/** Wrapper style for button animations */
|
|
65
81
|
buttonWrapperStyle: string;
|
|
66
82
|
/** Number display style */
|
|
67
83
|
numberStyle: string;
|
|
84
|
+
/**
|
|
85
|
+
* The fixed box the animated number and the edit field share, so swapping one
|
|
86
|
+
* for the other cannot change the control's width. Its WIDTH is set inline
|
|
87
|
+
* from the character count, not by this class.
|
|
88
|
+
*/
|
|
89
|
+
valueSlotStyle: string;
|
|
90
|
+
/** Per-size container height/padding, from the shared `--form-*` scale */
|
|
91
|
+
sizes: Record<InputCounterSize, string>;
|
|
92
|
+
/** Per-size text sizing, shared by the number display and the edit field */
|
|
93
|
+
textSizes: Record<InputCounterSize, string>;
|
|
94
|
+
/**
|
|
95
|
+
* Per-size stepper geometry — a square derived from the counter's own height
|
|
96
|
+
* (`--form-min-h-*` minus the container inset) with a concentric radius, applied
|
|
97
|
+
* over whatever `buttonSizes` picks. This is what makes the steppers scale with
|
|
98
|
+
* the counter rather than with Button's size scale.
|
|
99
|
+
*/
|
|
100
|
+
stepperSizes: Record<InputCounterSize, string>;
|
|
101
|
+
/** Per-size stepper icon sizing, tracking the type ramp */
|
|
102
|
+
stepperIconSizes: Record<InputCounterSize, string>;
|
|
103
|
+
/**
|
|
104
|
+
* Which Button size the steppers take at each counter size. Mapped rather
|
|
105
|
+
* than derived: Button has only three sizes and both must nest inside the
|
|
106
|
+
* counter's own height. `stepperSizes` then sets the exact square.
|
|
107
|
+
*/
|
|
108
|
+
buttonSizes: Record<InputCounterSize, ButtonProps['size']>;
|
|
109
|
+
/** The transparent, centered edit field swapped in on click */
|
|
110
|
+
editFieldStyle: string;
|
|
68
111
|
}
|
|
@@ -21,7 +21,7 @@ import { formsBaseTheme } from '../forms.theme.js';
|
|
|
21
21
|
* />
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
|
-
const InputCreditCard = React__default.forwardRef(({ state = 'default', errorMessage, error, onCardNumberChange, onExpiryDateChange, onCvcChange, expiryDateLabel = "MM / YY", cvcLabel = "CVC", legendText = "Payment Information", cardNumberInputProps = {}, expiryDateInputProps = {}, cvcInputProps = {}, className = "", theme = {}, }, ref) => {
|
|
24
|
+
const InputCreditCard = React__default.forwardRef(({ state = 'default', size = 'md', errorMessage, error, onCardNumberChange, onExpiryDateChange, onCvcChange, expiryDateLabel = "MM / YY", cvcLabel = "CVC", legendText = "Payment Information", cardNumberInputProps = {}, expiryDateInputProps = {}, cvcInputProps = {}, className = "", theme = {}, }, ref) => {
|
|
25
25
|
var _a;
|
|
26
26
|
const isDisabled = state === 'disabled';
|
|
27
27
|
// Clear the error visually as soon as the user starts re-entering after an error.
|
|
@@ -90,9 +90,9 @@ const InputCreditCard = React__default.forwardRef(({ state = 'default', errorMes
|
|
|
90
90
|
const fieldState = isDisabled ? 'disabled' : 'default';
|
|
91
91
|
// Merge top-level state into each field's props; strip per-field helperText
|
|
92
92
|
// so only the consolidated error message is shown at the bottom.
|
|
93
|
-
const { helperText: _cn, ...cardProps } = { state: fieldState, ...cardNumberInputProps };
|
|
94
|
-
const { helperText: _ex, ...expiryProps } = { state: fieldState, ...expiryDateInputProps };
|
|
95
|
-
const { helperText: _cv, ...cvcProps } = { state: fieldState, ...cvcInputProps };
|
|
93
|
+
const { helperText: _cn, ...cardProps } = { state: fieldState, size, ...cardNumberInputProps };
|
|
94
|
+
const { helperText: _ex, ...expiryProps } = { state: fieldState, size, ...expiryDateInputProps };
|
|
95
|
+
const { helperText: _cv, ...cvcProps } = { state: fieldState, size, ...cvcInputProps };
|
|
96
96
|
return (jsxs("fieldset", { ref: ref, className: `w-full border-0 m-0 p-0 ${isInvalid ? 'state-invalid' : ''} ${className}`, "data-testid": "input-credit-card-fieldset", "aria-invalid": isInvalid || undefined, "aria-errormessage": isInvalid && resolvedError ? errorId : undefined, children: [jsx("legend", { className: `block text-sm/6 font-medium mb-2 text-[var(--color-text-primary)] ${(_a = mergedTheme.legendStyles) !== null && _a !== void 0 ? _a : ""}`, children: legendText }), jsxs("div", { className: `relative grid gap-0 ${mergedTheme.contentStyles}`, "data-testid": "input-credit-card-content", children: [jsx("div", { className: "w-full relative focus-within:z-10 hover:z-10", "data-testid": "input-card-number", children: jsx(Input, { id: cardNumberId, type: "creditCard", value: cardNumber, onChange: handleCardNumberChange, placeholder: "Card Number", "aria-label": "card number", onFocus: () => handleFocus("cardNumber"), onBlur: handleBlur, inputGroupClassName: "rounded-b-none", ...cardProps }) }), jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 w-full -mt-px relative focus-within:z-10 hover:z-10", children: [jsx("div", { className: `${mergedTheme.expiryDateWrapperStyles || ""} w-full sm:pr-0 relative focus-within:z-10 hover:z-10`, "data-testid": "input-expiry-date", children: jsx(Input, { id: expiryDateId, type: "expiryDate", value: expiryDate, onChange: handleExpiryDateChange, placeholder: expiryDateLabel, "aria-label": expiryDateLabel, onFocus: () => handleFocus("expiryDate"), onBlur: handleBlur, inputGroupClassName: "sm:rounded-bl-[var(--form-radius)] rounded-t-none sm:rounded-tr-none sm:rounded-br-none", ...expiryProps }) }), jsx("div", { className: `${mergedTheme.cvcWrapperStyles || ""} w-full -mt-px sm:mt-0 sm:-ml-px sm:w-[calc(100%+1px)] relative focus-within:z-10 hover:z-10`, "data-testid": "input-cvc", children: jsx(Input, { id: cvcId, type: "tel", maxLength: 3, pattern: "[0-9]*", inputMode: "numeric", value: cvc, onChange: handleCvcChange, placeholder: cvcLabel, "aria-label": cvcLabel, onFocus: () => handleFocus("cvc"), onBlur: handleBlur, inputGroupClassName: " sm:rounded-br-[var(--form-radius)] rounded-t-none rounded-bl-[var(--form-radius)] sm:rounded-bl-none", ...cvcProps }) })] }), isInvalid && jsx("div", { "aria-hidden": "true", className: formsBaseTheme.groupInvalidOverlayStyle })] }), isInvalid && resolvedError && (jsx("p", { id: errorId, role: "alert", className: formsBaseTheme.helperText, children: resolvedError }))] }));
|
|
97
97
|
});
|
|
98
98
|
InputCreditCard.displayName = "InputCreditCard";
|
|
@@ -13,6 +13,13 @@ import { InputProps, InputTheme } from '../Input/Input.types';
|
|
|
13
13
|
export interface InputCreditCardProps {
|
|
14
14
|
/** Visual/validation state of the entire card group */
|
|
15
15
|
state?: 'default' | 'disabled' | 'invalid';
|
|
16
|
+
/**
|
|
17
|
+
* Size of every field in the group, from the shared `--form-*` scale, so the
|
|
18
|
+
* card block lines up with the Inputs and Selects around it. A per-field
|
|
19
|
+
* `*InputProps.size` still wins.
|
|
20
|
+
* @default 'md'
|
|
21
|
+
*/
|
|
22
|
+
size?: InputProps['size'];
|
|
16
23
|
/** Consolidated error message shown below all fields when `state="invalid"` */
|
|
17
24
|
errorMessage?: React.ReactNode;
|
|
18
25
|
/**
|