@xsolla/xui-counter 0.200.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.
@@ -0,0 +1,646 @@
1
+ // src/Counter.tsx
2
+ import React2, { forwardRef, useRef, useState } from "react";
3
+
4
+ // ../../foundation/primitives-native/src/Box.tsx
5
+ import {
6
+ View,
7
+ Pressable,
8
+ Image
9
+ } from "react-native";
10
+ import { jsx } from "react/jsx-runtime";
11
+ var roleToAccessibilityRole = {
12
+ button: "button",
13
+ link: "link",
14
+ img: "image",
15
+ image: "image",
16
+ summary: "summary",
17
+ switch: "switch",
18
+ checkbox: "checkbox",
19
+ radio: "radio",
20
+ heading: "header",
21
+ text: "text",
22
+ search: "search",
23
+ alert: "alert",
24
+ none: "none",
25
+ presentation: "none",
26
+ progressbar: "progressbar",
27
+ scrollbar: "scrollbar",
28
+ adjustable: "adjustable",
29
+ tab: "tab",
30
+ menu: "menu",
31
+ menuitem: "menuitem",
32
+ list: "list",
33
+ timer: "timer"
34
+ };
35
+ var toGapValue = (gap) => {
36
+ if (gap === void 0) return void 0;
37
+ if (typeof gap === "number") return gap;
38
+ const pxMatch = /^(\d+(?:\.\d+)?)px$/.exec(gap);
39
+ if (pxMatch) return Number(pxMatch[1]);
40
+ const asNumber = Number(gap);
41
+ return Number.isFinite(asNumber) ? asNumber : void 0;
42
+ };
43
+ var Box = ({
44
+ children,
45
+ onPress,
46
+ onLayout,
47
+ onMoveShouldSetResponder,
48
+ onResponderGrant,
49
+ onResponderMove,
50
+ onResponderRelease,
51
+ onResponderTerminate,
52
+ backgroundColor,
53
+ borderColor,
54
+ borderWidth,
55
+ borderBottomWidth,
56
+ borderBottomColor,
57
+ borderTopWidth,
58
+ borderTopColor,
59
+ borderLeftWidth,
60
+ borderLeftColor,
61
+ borderRightWidth,
62
+ borderRightColor,
63
+ borderRadius,
64
+ borderStyle,
65
+ height,
66
+ padding,
67
+ paddingHorizontal,
68
+ paddingVertical,
69
+ margin,
70
+ marginTop,
71
+ marginBottom,
72
+ marginLeft,
73
+ marginRight,
74
+ flexDirection,
75
+ flexWrap,
76
+ alignItems,
77
+ justifyContent,
78
+ alignSelf,
79
+ position,
80
+ top,
81
+ bottom,
82
+ left,
83
+ right,
84
+ width,
85
+ minWidth,
86
+ minHeight,
87
+ maxWidth,
88
+ maxHeight,
89
+ flex,
90
+ flexShrink,
91
+ gap,
92
+ overflow,
93
+ zIndex,
94
+ hoverStyle,
95
+ pressStyle,
96
+ style,
97
+ disabled,
98
+ "data-testid": dataTestId,
99
+ testID,
100
+ as,
101
+ src,
102
+ alt,
103
+ ...rest
104
+ }) => {
105
+ const getContainerStyle = (pressed) => ({
106
+ backgroundColor: pressed && pressStyle?.backgroundColor ? pressStyle.backgroundColor : backgroundColor,
107
+ borderColor,
108
+ borderWidth,
109
+ borderBottomWidth,
110
+ borderBottomColor,
111
+ borderTopWidth,
112
+ borderTopColor,
113
+ borderLeftWidth,
114
+ borderLeftColor,
115
+ borderRightWidth,
116
+ borderRightColor,
117
+ borderRadius,
118
+ borderStyle,
119
+ overflow,
120
+ zIndex,
121
+ height,
122
+ width,
123
+ minWidth,
124
+ minHeight,
125
+ maxWidth,
126
+ maxHeight,
127
+ padding,
128
+ paddingHorizontal,
129
+ paddingVertical,
130
+ margin,
131
+ marginTop,
132
+ marginBottom,
133
+ marginLeft,
134
+ marginRight,
135
+ flexDirection,
136
+ flexWrap,
137
+ alignItems,
138
+ justifyContent,
139
+ alignSelf,
140
+ position,
141
+ top,
142
+ bottom,
143
+ left,
144
+ right,
145
+ flex,
146
+ flexShrink,
147
+ gap: toGapValue(gap),
148
+ ...style
149
+ });
150
+ const finalTestID = dataTestId || testID;
151
+ const {
152
+ role,
153
+ tabIndex,
154
+ onKeyDown,
155
+ onKeyUp,
156
+ "aria-label": ariaLabel,
157
+ "aria-labelledby": _ariaLabelledBy,
158
+ "aria-current": _ariaCurrent,
159
+ "aria-disabled": ariaDisabled,
160
+ "aria-live": ariaLive,
161
+ "aria-busy": ariaBusy,
162
+ className,
163
+ "data-testid": _dataTestId,
164
+ cursor: _cursor,
165
+ type: _type,
166
+ ...nativeRest
167
+ } = rest;
168
+ const accessibilityRole = typeof role === "string" && roleToAccessibilityRole[role] || (as === "button" ? "button" : void 0);
169
+ const isDisabled = Boolean(disabled || ariaDisabled);
170
+ const accessibilityState = {
171
+ disabled: isDisabled || void 0,
172
+ busy: Boolean(ariaBusy) || void 0
173
+ };
174
+ const accessibilityLiveRegion = ariaLive === "polite" || ariaLive === "assertive" ? ariaLive : void 0;
175
+ if (as === "img" && src) {
176
+ const imageStyle = {
177
+ width,
178
+ height,
179
+ borderRadius,
180
+ position,
181
+ top,
182
+ bottom,
183
+ left,
184
+ right,
185
+ ...style
186
+ };
187
+ return /* @__PURE__ */ jsx(
188
+ Image,
189
+ {
190
+ source: { uri: src },
191
+ style: imageStyle,
192
+ testID: finalTestID,
193
+ accessibilityLabel: typeof ariaLabel === "string" ? ariaLabel : alt,
194
+ resizeMode: "cover",
195
+ ...nativeRest
196
+ }
197
+ );
198
+ }
199
+ if (onPress || as === "button") {
200
+ return /* @__PURE__ */ jsx(
201
+ Pressable,
202
+ {
203
+ onPress: !isDisabled && onPress ? onPress : void 0,
204
+ disabled: isDisabled,
205
+ onLayout,
206
+ onMoveShouldSetResponder,
207
+ onResponderGrant,
208
+ onResponderMove,
209
+ onResponderRelease,
210
+ onResponderTerminate,
211
+ style: ({ pressed }) => getContainerStyle(pressed),
212
+ testID: finalTestID,
213
+ accessibilityRole,
214
+ accessibilityLabel: typeof ariaLabel === "string" ? ariaLabel : void 0,
215
+ accessibilityState,
216
+ accessibilityLiveRegion,
217
+ ...nativeRest,
218
+ children
219
+ }
220
+ );
221
+ }
222
+ return /* @__PURE__ */ jsx(
223
+ View,
224
+ {
225
+ style: getContainerStyle(),
226
+ testID: finalTestID,
227
+ onLayout,
228
+ onMoveShouldSetResponder,
229
+ onResponderGrant,
230
+ onResponderMove,
231
+ onResponderRelease,
232
+ onResponderTerminate,
233
+ accessibilityRole,
234
+ accessibilityLabel: typeof ariaLabel === "string" ? ariaLabel : void 0,
235
+ accessibilityState,
236
+ accessibilityLiveRegion,
237
+ ...nativeRest,
238
+ children
239
+ }
240
+ );
241
+ };
242
+
243
+ // ../../foundation/primitives-native/src/Text.tsx
244
+ import {
245
+ Text as RNText,
246
+ StyleSheet
247
+ } from "react-native";
248
+ import { jsx as jsx2 } from "react/jsx-runtime";
249
+ var roleMap = {
250
+ alert: "alert",
251
+ heading: "header",
252
+ button: "button",
253
+ link: "link",
254
+ text: "text"
255
+ };
256
+ var parseNumericValue = (value) => {
257
+ if (value === void 0) return void 0;
258
+ if (typeof value === "number") return value;
259
+ const parsed = parseFloat(value);
260
+ return isNaN(parsed) ? void 0 : parsed;
261
+ };
262
+ var Text = ({
263
+ children,
264
+ color,
265
+ fontSize,
266
+ fontWeight,
267
+ fontFamily,
268
+ textAlign,
269
+ lineHeight,
270
+ numberOfLines,
271
+ id,
272
+ role,
273
+ testID,
274
+ "data-testid": dataTestId,
275
+ style: styleProp,
276
+ ...props
277
+ }) => {
278
+ let resolvedFontFamily = fontFamily ? fontFamily.split(",")[0].replace(/['"]/g, "").trim() : void 0;
279
+ if (resolvedFontFamily === "Pilat Wide" || resolvedFontFamily === "Pilat Wide Bold" || resolvedFontFamily === "Aktiv Grotesk") {
280
+ resolvedFontFamily = void 0;
281
+ }
282
+ const incomingStyle = StyleSheet.flatten(styleProp);
283
+ const baseStyle = {
284
+ color: color ?? incomingStyle?.color,
285
+ fontSize: typeof fontSize === "number" ? fontSize : void 0,
286
+ fontWeight,
287
+ fontFamily: resolvedFontFamily,
288
+ textDecorationLine: props.textDecoration,
289
+ textAlign: textAlign ?? incomingStyle?.textAlign,
290
+ lineHeight: parseNumericValue(lineHeight ?? incomingStyle?.lineHeight),
291
+ marginTop: parseNumericValue(
292
+ incomingStyle?.marginTop
293
+ ),
294
+ marginBottom: parseNumericValue(
295
+ incomingStyle?.marginBottom
296
+ )
297
+ };
298
+ const accessibilityRole = role ? roleMap[role] : void 0;
299
+ return /* @__PURE__ */ jsx2(
300
+ RNText,
301
+ {
302
+ style: baseStyle,
303
+ numberOfLines,
304
+ testID: dataTestId || testID || id,
305
+ accessibilityRole,
306
+ children
307
+ }
308
+ );
309
+ };
310
+
311
+ // ../../foundation/primitives-native/src/Icon.tsx
312
+ import React from "react";
313
+ import { View as View2 } from "react-native";
314
+ import { jsx as jsx3 } from "react/jsx-runtime";
315
+ var Icon = ({
316
+ children,
317
+ color,
318
+ size,
319
+ testID,
320
+ "data-testid": dataTestId
321
+ }) => {
322
+ const style = {
323
+ width: typeof size === "number" ? size : void 0,
324
+ height: typeof size === "number" ? size : void 0,
325
+ alignItems: "center",
326
+ justifyContent: "center"
327
+ };
328
+ const childrenWithProps = React.Children.map(children, (child) => {
329
+ if (React.isValidElement(child)) {
330
+ return React.cloneElement(child, {
331
+ color: child.props.color || color,
332
+ // Also pass size if child seems to be an icon that needs it
333
+ size: child.props.size || size
334
+ });
335
+ }
336
+ return child;
337
+ });
338
+ return /* @__PURE__ */ jsx3(View2, { style, testID: dataTestId || testID, children: childrenWithProps });
339
+ };
340
+
341
+ // ../../foundation/primitives-native/src/index.tsx
342
+ var isWeb = false;
343
+
344
+ // src/Counter.tsx
345
+ import { useResolvedTheme, useId } from "@xsolla/xui-core";
346
+ import { Input } from "@xsolla/xui-input";
347
+ import { MinusSq, PlusSq } from "@xsolla/xui-icons-base";
348
+
349
+ // src/utils.ts
350
+ var COUNTER_SIZE_CONFIG = {
351
+ xl: { control: 28, gap: 10, inputWidth: 149, readOnlyWidth: 100 },
352
+ lg: { control: 24, gap: 10, inputWidth: 142, readOnlyWidth: 90 },
353
+ md: { control: 24, gap: 8, inputWidth: 132, readOnlyWidth: 83 },
354
+ sm: { control: 18, gap: 8, inputWidth: 92, readOnlyWidth: 69 },
355
+ xs: { control: 16, gap: 6, inputWidth: 82, readOnlyWidth: 59 }
356
+ };
357
+ var clampToRange = (value, min, max) => {
358
+ let next = value;
359
+ if (typeof min === "number" && next < min) next = min;
360
+ if (typeof max === "number" && next > max) next = max;
361
+ return next;
362
+ };
363
+ var sanitizeCounterInput = (raw, allowNegative) => {
364
+ const digitsAndSigns = raw.replace(/[^\d-]/g, "");
365
+ const digits = digitsAndSigns.replace(/-/g, "");
366
+ if (allowNegative && digitsAndSigns.startsWith("-")) {
367
+ return `-${digits}`;
368
+ }
369
+ return digits;
370
+ };
371
+ var parseCounterValue = (raw) => {
372
+ const trimmed = raw.trim();
373
+ if (trimmed === "" || trimmed === "-") return null;
374
+ const parsed = Number.parseInt(trimmed, 10);
375
+ return Number.isNaN(parsed) ? null : parsed;
376
+ };
377
+ var allowsNegative = (min) => typeof min !== "number" || min < 0;
378
+
379
+ // src/Counter.tsx
380
+ import { jsx as jsx4, jsxs } from "react/jsx-runtime";
381
+ var CounterButton = ({
382
+ icon,
383
+ label,
384
+ onPress,
385
+ available,
386
+ boxSize,
387
+ borderRadius,
388
+ color,
389
+ hoverColor,
390
+ testID
391
+ }) => /* @__PURE__ */ jsx4(
392
+ Box,
393
+ {
394
+ as: "button",
395
+ type: "button",
396
+ width: boxSize,
397
+ height: boxSize,
398
+ flexShrink: 0,
399
+ alignItems: "center",
400
+ justifyContent: "center",
401
+ padding: 0,
402
+ borderWidth: 0,
403
+ borderRadius,
404
+ backgroundColor: "transparent",
405
+ cursor: available ? "pointer" : "not-allowed",
406
+ disabled: !available,
407
+ "aria-disabled": !available || void 0,
408
+ "aria-label": label,
409
+ onPress,
410
+ hoverStyle: available ? { backgroundColor: hoverColor } : void 0,
411
+ ...isWeb && {
412
+ onMouseDown: (e) => e.preventDefault()
413
+ },
414
+ "data-testid": testID,
415
+ testID,
416
+ children: /* @__PURE__ */ jsx4(Icon, { size: boxSize, color, children: icon })
417
+ }
418
+ );
419
+ var Counter = forwardRef(
420
+ ({
421
+ value,
422
+ defaultValue = null,
423
+ onChange,
424
+ min,
425
+ max,
426
+ step = 1,
427
+ size = "md",
428
+ type = "input",
429
+ disabled = false,
430
+ label,
431
+ "aria-label": ariaLabel,
432
+ decrementLabel = "Decrease quantity",
433
+ incrementLabel = "Increase quantity",
434
+ name,
435
+ id: providedId,
436
+ testID,
437
+ themeMode,
438
+ themeProductContext
439
+ }, ref) => {
440
+ const { theme } = useResolvedTheme({ themeMode, themeProductContext });
441
+ const rawId = useId();
442
+ const safeId = rawId.replace(/:/g, "");
443
+ const counterId = providedId || `counter-${safeId}`;
444
+ const isControlled = value !== void 0;
445
+ const [uncontrolledValue, setUncontrolledValue] = useState(
446
+ defaultValue
447
+ );
448
+ const committedValue = isControlled ? value ?? null : uncontrolledValue;
449
+ const [draft, setDraft] = useState(null);
450
+ const inputRef = useRef(null);
451
+ React2.useImperativeHandle(
452
+ ref,
453
+ () => inputRef.current,
454
+ []
455
+ );
456
+ const sizeConfig = COUNTER_SIZE_CONFIG[size];
457
+ const inputSizing = theme.sizing.input(size);
458
+ const isReadOnly = type === "readOnly";
459
+ const pendingValue = draft !== null ? parseCounterValue(draft) : committedValue;
460
+ const stepBase = pendingValue ?? min ?? 0;
461
+ const canDecrement = !disabled && (typeof min !== "number" || stepBase > min);
462
+ const canIncrement = !disabled && (typeof max !== "number" || stepBase < max);
463
+ const commit = (next) => {
464
+ setDraft(null);
465
+ const resolved = next ?? committedValue;
466
+ if (resolved === null) return;
467
+ const clamped = clampToRange(resolved, min, max);
468
+ if (!isControlled) setUncontrolledValue(clamped);
469
+ if (clamped !== committedValue) onChange?.(clamped);
470
+ };
471
+ const stepBy = (direction) => {
472
+ commit(clampToRange(stepBase + direction * step, min, max));
473
+ };
474
+ const handleInputChange = (e) => {
475
+ setDraft(sanitizeCounterInput(e.target.value, allowsNegative(min)));
476
+ };
477
+ const handleBlur = () => {
478
+ commit(draft !== null ? parseCounterValue(draft) : committedValue);
479
+ };
480
+ const handleKeyDown = (e) => {
481
+ if (e.key === "ArrowUp") {
482
+ e.preventDefault();
483
+ if (canIncrement) stepBy(1);
484
+ return;
485
+ }
486
+ if (e.key === "ArrowDown") {
487
+ e.preventDefault();
488
+ if (canDecrement) stepBy(-1);
489
+ return;
490
+ }
491
+ if (e.key === "Home" && typeof min === "number") {
492
+ e.preventDefault();
493
+ commit(min);
494
+ return;
495
+ }
496
+ if (e.key === "End" && typeof max === "number") {
497
+ e.preventDefault();
498
+ commit(max);
499
+ return;
500
+ }
501
+ if (e.key === "Enter" || e.key === "\n") {
502
+ e.preventDefault();
503
+ commit(draft !== null ? parseCounterValue(draft) : committedValue);
504
+ }
505
+ };
506
+ const enabledIconColor = theme.colors.content.secondary;
507
+ const disabledIconColor = theme.colors.control.text.disable;
508
+ const hoverColor = theme.colors.overlay.mono;
509
+ const buttonRadius = theme.shape.input[size].borderRadius;
510
+ const decrementButton = /* @__PURE__ */ jsx4(
511
+ CounterButton,
512
+ {
513
+ icon: /* @__PURE__ */ jsx4(MinusSq, {}),
514
+ label: decrementLabel,
515
+ onPress: () => {
516
+ if (canDecrement) stepBy(-1);
517
+ },
518
+ available: canDecrement,
519
+ boxSize: sizeConfig.control,
520
+ borderRadius: buttonRadius,
521
+ color: canDecrement ? enabledIconColor : disabledIconColor,
522
+ hoverColor,
523
+ testID: testID ? `${testID}__decrement` : void 0
524
+ }
525
+ );
526
+ const incrementButton = /* @__PURE__ */ jsx4(
527
+ CounterButton,
528
+ {
529
+ icon: /* @__PURE__ */ jsx4(PlusSq, {}),
530
+ label: incrementLabel,
531
+ onPress: () => {
532
+ if (canIncrement) stepBy(1);
533
+ },
534
+ available: canIncrement,
535
+ boxSize: sizeConfig.control,
536
+ borderRadius: buttonRadius,
537
+ color: canIncrement ? enabledIconColor : disabledIconColor,
538
+ hoverColor,
539
+ testID: testID ? `${testID}__increment` : void 0
540
+ }
541
+ );
542
+ const labelId = `${counterId}-label`;
543
+ const showLabel = Boolean(label) && !isReadOnly;
544
+ const labelNode = showLabel ? /* @__PURE__ */ jsx4(Box, { as: "label", id: labelId, children: /* @__PURE__ */ jsx4(
545
+ Text,
546
+ {
547
+ color: theme.colors.content.secondary,
548
+ fontSize: inputSizing.fontSize - 2,
549
+ fontWeight: "500",
550
+ children: label
551
+ }
552
+ ) }) : null;
553
+ if (isReadOnly) {
554
+ const readOnlyValue = String(committedValue ?? min ?? 0);
555
+ return /* @__PURE__ */ jsxs(
556
+ Box,
557
+ {
558
+ flexDirection: "row",
559
+ alignItems: "center",
560
+ gap: sizeConfig.gap,
561
+ width: sizeConfig.readOnlyWidth,
562
+ role: "group",
563
+ "aria-label": ariaLabel || label,
564
+ "aria-disabled": disabled || void 0,
565
+ "data-testid": testID,
566
+ children: [
567
+ decrementButton,
568
+ /* @__PURE__ */ jsx4(Box, { flex: 1, minWidth: 0, "aria-live": "polite", alignItems: "center", children: /* @__PURE__ */ jsx4(
569
+ Text,
570
+ {
571
+ color: disabled ? theme.colors.control.input.textDisable : theme.colors.control.input.text,
572
+ fontSize: inputSizing.fontSize,
573
+ lineHeight: inputSizing.fontSize + 2,
574
+ textAlign: "center",
575
+ "data-testid": testID ? `${testID}__value` : void 0,
576
+ children: readOnlyValue
577
+ }
578
+ ) }),
579
+ incrementButton
580
+ ]
581
+ }
582
+ );
583
+ }
584
+ const displayValue = draft !== null ? draft : committedValue === null ? "" : String(committedValue);
585
+ return /* @__PURE__ */ jsxs(
586
+ Box,
587
+ {
588
+ flexDirection: "column",
589
+ gap: inputSizing.fieldGap,
590
+ alignItems: "flex-start",
591
+ role: "group",
592
+ "aria-label": showLabel ? void 0 : ariaLabel,
593
+ "aria-labelledby": showLabel ? labelId : void 0,
594
+ "aria-disabled": disabled || void 0,
595
+ "data-testid": testID,
596
+ children: [
597
+ labelNode,
598
+ /* @__PURE__ */ jsxs(
599
+ Box,
600
+ {
601
+ flexDirection: "row",
602
+ alignItems: "center",
603
+ gap: sizeConfig.gap,
604
+ width: sizeConfig.inputWidth,
605
+ children: [
606
+ decrementButton,
607
+ /* @__PURE__ */ jsx4(Box, { flex: 1, minWidth: 0, children: /* @__PURE__ */ jsx4(
608
+ Input,
609
+ {
610
+ ref: inputRef,
611
+ id: counterId,
612
+ name,
613
+ size,
614
+ value: displayValue,
615
+ disabled,
616
+ onChange: handleInputChange,
617
+ onBlur: handleBlur,
618
+ onKeyDown: handleKeyDown,
619
+ type: "text",
620
+ inputMode: "numeric",
621
+ role: "spinbutton",
622
+ "aria-valuenow": committedValue ?? void 0,
623
+ "aria-valuemin": min,
624
+ "aria-valuemax": max,
625
+ "aria-label": showLabel ? void 0 : ariaLabel,
626
+ ...showLabel ? { "aria-labelledby": labelId } : {},
627
+ style: { textAlign: "center" },
628
+ themeMode,
629
+ themeProductContext,
630
+ testID: testID ? `${testID}__input` : void 0
631
+ }
632
+ ) }),
633
+ incrementButton
634
+ ]
635
+ }
636
+ )
637
+ ]
638
+ }
639
+ );
640
+ }
641
+ );
642
+ Counter.displayName = "Counter";
643
+ export {
644
+ Counter
645
+ };
646
+ //# sourceMappingURL=index.mjs.map