@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.
package/web/index.mjs ADDED
@@ -0,0 +1,659 @@
1
+ // src/Counter.tsx
2
+ import React3, { forwardRef, useRef, useState } from "react";
3
+
4
+ // ../../foundation/primitives-web/src/Box.tsx
5
+ import React2 from "react";
6
+ import styled from "styled-components";
7
+
8
+ // ../../foundation/primitives-web/src/filterDOMProps.ts
9
+ import React from "react";
10
+
11
+ // ../../../node_modules/@emotion/memoize/dist/memoize.esm.js
12
+ function memoize(fn) {
13
+ var cache = {};
14
+ return function(arg) {
15
+ if (cache[arg] === void 0) cache[arg] = fn(arg);
16
+ return cache[arg];
17
+ };
18
+ }
19
+ var memoize_esm_default = memoize;
20
+
21
+ // ../../../node_modules/@emotion/is-prop-valid/dist/is-prop-valid.esm.js
22
+ var reactPropsRegex = /^((children|dangerouslySetInnerHTML|key|ref|autoFocus|defaultValue|defaultChecked|innerHTML|suppressContentEditableWarning|suppressHydrationWarning|valueLink|accept|acceptCharset|accessKey|action|allow|allowUserMedia|allowPaymentRequest|allowFullScreen|allowTransparency|alt|async|autoComplete|autoPlay|capture|cellPadding|cellSpacing|challenge|charSet|checked|cite|classID|className|cols|colSpan|content|contentEditable|contextMenu|controls|controlsList|coords|crossOrigin|data|dateTime|decoding|default|defer|dir|disabled|disablePictureInPicture|download|draggable|encType|form|formAction|formEncType|formMethod|formNoValidate|formTarget|frameBorder|headers|height|hidden|high|href|hrefLang|htmlFor|httpEquiv|id|inputMode|integrity|is|keyParams|keyType|kind|label|lang|list|loading|loop|low|marginHeight|marginWidth|max|maxLength|media|mediaGroup|method|min|minLength|multiple|muted|name|nonce|noValidate|open|optimum|pattern|placeholder|playsInline|poster|preload|profile|radioGroup|readOnly|referrerPolicy|rel|required|reversed|role|rows|rowSpan|sandbox|scope|scoped|scrolling|seamless|selected|shape|size|sizes|slot|span|spellCheck|src|srcDoc|srcLang|srcSet|start|step|style|summary|tabIndex|target|title|type|useMap|value|width|wmode|wrap|about|datatype|inlist|prefix|property|resource|typeof|vocab|autoCapitalize|autoCorrect|autoSave|color|inert|itemProp|itemScope|itemType|itemID|itemRef|on|results|security|unselectable|accentHeight|accumulate|additive|alignmentBaseline|allowReorder|alphabetic|amplitude|arabicForm|ascent|attributeName|attributeType|autoReverse|azimuth|baseFrequency|baselineShift|baseProfile|bbox|begin|bias|by|calcMode|capHeight|clip|clipPathUnits|clipPath|clipRule|colorInterpolation|colorInterpolationFilters|colorProfile|colorRendering|contentScriptType|contentStyleType|cursor|cx|cy|d|decelerate|descent|diffuseConstant|direction|display|divisor|dominantBaseline|dur|dx|dy|edgeMode|elevation|enableBackground|end|exponent|externalResourcesRequired|fill|fillOpacity|fillRule|filter|filterRes|filterUnits|floodColor|floodOpacity|focusable|fontFamily|fontSize|fontSizeAdjust|fontStretch|fontStyle|fontVariant|fontWeight|format|from|fr|fx|fy|g1|g2|glyphName|glyphOrientationHorizontal|glyphOrientationVertical|glyphRef|gradientTransform|gradientUnits|hanging|horizAdvX|horizOriginX|ideographic|imageRendering|in|in2|intercept|k|k1|k2|k3|k4|kernelMatrix|kernelUnitLength|kerning|keyPoints|keySplines|keyTimes|lengthAdjust|letterSpacing|lightingColor|limitingConeAngle|local|markerEnd|markerMid|markerStart|markerHeight|markerUnits|markerWidth|mask|maskContentUnits|maskUnits|mathematical|mode|numOctaves|offset|opacity|operator|order|orient|orientation|origin|overflow|overlinePosition|overlineThickness|panose1|paintOrder|pathLength|patternContentUnits|patternTransform|patternUnits|pointerEvents|points|pointsAtX|pointsAtY|pointsAtZ|preserveAlpha|preserveAspectRatio|primitiveUnits|r|radius|refX|refY|renderingIntent|repeatCount|repeatDur|requiredExtensions|requiredFeatures|restart|result|rotate|rx|ry|scale|seed|shapeRendering|slope|spacing|specularConstant|specularExponent|speed|spreadMethod|startOffset|stdDeviation|stemh|stemv|stitchTiles|stopColor|stopOpacity|strikethroughPosition|strikethroughThickness|string|stroke|strokeDasharray|strokeDashoffset|strokeLinecap|strokeLinejoin|strokeMiterlimit|strokeOpacity|strokeWidth|surfaceScale|systemLanguage|tableValues|targetX|targetY|textAnchor|textDecoration|textRendering|textLength|to|transform|u1|u2|underlinePosition|underlineThickness|unicode|unicodeBidi|unicodeRange|unitsPerEm|vAlphabetic|vHanging|vIdeographic|vMathematical|values|vectorEffect|version|vertAdvY|vertOriginX|vertOriginY|viewBox|viewTarget|visibility|widths|wordSpacing|writingMode|x|xHeight|x1|x2|xChannelSelector|xlinkActuate|xlinkArcrole|xlinkHref|xlinkRole|xlinkShow|xlinkTitle|xlinkType|xmlBase|xmlns|xmlnsXlink|xmlLang|xmlSpace|y|y1|y2|yChannelSelector|z|zoomAndPan|for|class|autofocus)|(([Dd][Aa][Tt][Aa]|[Aa][Rr][Ii][Aa]|x)-.*))$/;
23
+ var index = memoize_esm_default(
24
+ function(prop) {
25
+ return reactPropsRegex.test(prop) || prop.charCodeAt(0) === 111 && prop.charCodeAt(1) === 110 && prop.charCodeAt(2) < 91;
26
+ }
27
+ /* Z+1 */
28
+ );
29
+ var is_prop_valid_esm_default = index;
30
+
31
+ // ../../foundation/primitives-web/src/filterDOMProps.ts
32
+ var ADDITIONAL_BLOCKED_PROPS = /* @__PURE__ */ new Set([
33
+ // RN-only event handlers (pass isPropValid's on* pattern)
34
+ "onPress",
35
+ "onChangeText",
36
+ "onLayout",
37
+ "onMoveShouldSetResponder",
38
+ "onResponderGrant",
39
+ "onResponderMove",
40
+ "onResponderRelease",
41
+ "onResponderTerminate",
42
+ // SVG attributes that pass isPropValid
43
+ "strokeWidth",
44
+ // CSS properties that pass isPropValid but are used as component props
45
+ "overflow",
46
+ "cursor",
47
+ "fontSize",
48
+ "fontWeight",
49
+ "fontFamily",
50
+ "textDecoration",
51
+ // Cross-platform layout prop that Text consumes as CSS, never as an
52
+ // attribute. Pinned explicitly because it is now spread into the styled
53
+ // component rather than destructured away before it can reach the DOM.
54
+ "numberOfLines"
55
+ ]);
56
+ function shouldForwardProp(key) {
57
+ if (ADDITIONAL_BLOCKED_PROPS.has(key)) return false;
58
+ return is_prop_valid_esm_default(key);
59
+ }
60
+ function createFilteredElement(defaultTag) {
61
+ const Component = React.forwardRef(
62
+ ({ children, elementType, ...props }, ref) => {
63
+ const Tag = elementType || defaultTag;
64
+ const htmlProps = {};
65
+ for (const key of Object.keys(props)) {
66
+ if (shouldForwardProp(key)) {
67
+ htmlProps[key] = props[key];
68
+ }
69
+ }
70
+ return React.createElement(
71
+ Tag,
72
+ { ref, ...htmlProps },
73
+ children
74
+ );
75
+ }
76
+ );
77
+ Component.displayName = `Filtered(${defaultTag})`;
78
+ return Component;
79
+ }
80
+
81
+ // ../../foundation/primitives-web/src/Box.tsx
82
+ import { jsx } from "react/jsx-runtime";
83
+ var FilteredDiv = createFilteredElement("div");
84
+ var StyledBox = styled(FilteredDiv)`
85
+ display: flex;
86
+ box-sizing: border-box;
87
+ background-color: ${(props) => props.backgroundColor || "transparent"};
88
+ border-color: ${(props) => props.borderColor || "transparent"};
89
+ border-width: ${(props) => typeof props.borderWidth === "number" ? `${props.borderWidth}px` : props.borderWidth || 0};
90
+
91
+ ${(props) => props.borderBottomWidth !== void 0 && `
92
+ border-bottom-width: ${typeof props.borderBottomWidth === "number" ? `${props.borderBottomWidth}px` : props.borderBottomWidth};
93
+ border-bottom-color: ${props.borderBottomColor || props.borderColor || "transparent"};
94
+ border-bottom-style: solid;
95
+ `}
96
+ ${(props) => props.borderTopWidth !== void 0 && `
97
+ border-top-width: ${typeof props.borderTopWidth === "number" ? `${props.borderTopWidth}px` : props.borderTopWidth};
98
+ border-top-color: ${props.borderTopColor || props.borderColor || "transparent"};
99
+ border-top-style: solid;
100
+ `}
101
+ ${(props) => props.borderLeftWidth !== void 0 && `
102
+ border-left-width: ${typeof props.borderLeftWidth === "number" ? `${props.borderLeftWidth}px` : props.borderLeftWidth};
103
+ border-left-color: ${props.borderLeftColor || props.borderColor || "transparent"};
104
+ border-left-style: solid;
105
+ `}
106
+ ${(props) => props.borderRightWidth !== void 0 && `
107
+ border-right-width: ${typeof props.borderRightWidth === "number" ? `${props.borderRightWidth}px` : props.borderRightWidth};
108
+ border-right-color: ${props.borderRightColor || props.borderColor || "transparent"};
109
+ border-right-style: solid;
110
+ `}
111
+
112
+ border-style: ${(props) => props.borderStyle || (props.borderWidth || props.borderBottomWidth || props.borderTopWidth || props.borderLeftWidth || props.borderRightWidth ? "solid" : "none")};
113
+ border-radius: ${(props) => typeof props.borderRadius === "number" ? `${props.borderRadius}px` : props.borderRadius || 0};
114
+ height: ${(props) => typeof props.height === "number" ? `${props.height}px` : props.height || "auto"};
115
+ width: ${(props) => typeof props.width === "number" ? `${props.width}px` : props.width || "auto"};
116
+ min-width: ${(props) => typeof props.minWidth === "number" ? `${props.minWidth}px` : props.minWidth || "auto"};
117
+ min-height: ${(props) => typeof props.minHeight === "number" ? `${props.minHeight}px` : props.minHeight || "auto"};
118
+ max-width: ${(props) => typeof props.maxWidth === "number" ? `${props.maxWidth}px` : props.maxWidth || "none"};
119
+ max-height: ${(props) => typeof props.maxHeight === "number" ? `${props.maxHeight}px` : props.maxHeight || "none"};
120
+
121
+ padding: ${(props) => typeof props.padding === "number" ? `${props.padding}px` : props.padding || 0};
122
+ ${(props) => props.paddingHorizontal && `
123
+ padding-left: ${typeof props.paddingHorizontal === "number" ? `${props.paddingHorizontal}px` : props.paddingHorizontal};
124
+ padding-right: ${typeof props.paddingHorizontal === "number" ? `${props.paddingHorizontal}px` : props.paddingHorizontal};
125
+ `}
126
+ ${(props) => props.paddingVertical && `
127
+ padding-top: ${typeof props.paddingVertical === "number" ? `${props.paddingVertical}px` : props.paddingVertical};
128
+ padding-bottom: ${typeof props.paddingVertical === "number" ? `${props.paddingVertical}px` : props.paddingVertical};
129
+ `}
130
+ ${(props) => props.paddingTop !== void 0 && `padding-top: ${typeof props.paddingTop === "number" ? `${props.paddingTop}px` : props.paddingTop};`}
131
+ ${(props) => props.paddingBottom !== void 0 && `padding-bottom: ${typeof props.paddingBottom === "number" ? `${props.paddingBottom}px` : props.paddingBottom};`}
132
+ ${(props) => props.paddingLeft !== void 0 && `padding-left: ${typeof props.paddingLeft === "number" ? `${props.paddingLeft}px` : props.paddingLeft};`}
133
+ ${(props) => props.paddingRight !== void 0 && `padding-right: ${typeof props.paddingRight === "number" ? `${props.paddingRight}px` : props.paddingRight};`}
134
+
135
+ margin: ${(props) => typeof props.margin === "number" ? `${props.margin}px` : props.margin || 0};
136
+ ${(props) => props.marginTop !== void 0 && `margin-top: ${typeof props.marginTop === "number" ? `${props.marginTop}px` : props.marginTop};`}
137
+ ${(props) => props.marginBottom !== void 0 && `margin-bottom: ${typeof props.marginBottom === "number" ? `${props.marginBottom}px` : props.marginBottom};`}
138
+ ${(props) => props.marginLeft !== void 0 && `margin-left: ${typeof props.marginLeft === "number" ? `${props.marginLeft}px` : props.marginLeft};`}
139
+ ${(props) => props.marginRight !== void 0 && `margin-right: ${typeof props.marginRight === "number" ? `${props.marginRight}px` : props.marginRight};`}
140
+
141
+ flex-direction: ${(props) => props.flexDirection || "column"};
142
+ flex-wrap: ${(props) => props.flexWrap || "nowrap"};
143
+ align-items: ${(props) => props.alignItems || "stretch"};
144
+ justify-content: ${(props) => props.justifyContent || "flex-start"};
145
+ cursor: ${(props) => props.cursor ? props.cursor : props.onClick || props.onPress ? "pointer" : "inherit"};
146
+ position: ${(props) => props.position || "static"};
147
+ top: ${(props) => typeof props.top === "number" ? `${props.top}px` : props.top};
148
+ bottom: ${(props) => typeof props.bottom === "number" ? `${props.bottom}px` : props.bottom};
149
+ left: ${(props) => typeof props.left === "number" ? `${props.left}px` : props.left};
150
+ right: ${(props) => typeof props.right === "number" ? `${props.right}px` : props.right};
151
+ flex: ${(props) => props.flex};
152
+ flex-shrink: ${(props) => props.flexShrink ?? 1};
153
+ gap: ${(props) => typeof props.gap === "number" ? `${props.gap}px` : props.gap || 0};
154
+ align-self: ${(props) => props.alignSelf || "auto"};
155
+ overflow: ${(props) => props.overflow || "visible"};
156
+ overflow-x: ${(props) => props.overflowX || "visible"};
157
+ overflow-y: ${(props) => props.overflowY || "visible"};
158
+ z-index: ${(props) => props.zIndex};
159
+ opacity: ${(props) => props.disabled ? 0.5 : 1};
160
+ pointer-events: ${(props) => props.disabled ? "none" : "auto"};
161
+
162
+ &:hover {
163
+ ${(props) => props.hoverStyle?.backgroundColor && `background-color: ${props.hoverStyle.backgroundColor};`}
164
+ ${(props) => props.hoverStyle?.borderColor && `border-color: ${props.hoverStyle.borderColor};`}
165
+ }
166
+
167
+ &:active {
168
+ ${(props) => props.pressStyle?.backgroundColor && `background-color: ${props.pressStyle.backgroundColor};`}
169
+ }
170
+ `;
171
+ var Box = React2.forwardRef(
172
+ ({
173
+ children,
174
+ onPress,
175
+ onKeyDown,
176
+ onKeyUp,
177
+ role,
178
+ "aria-label": ariaLabel,
179
+ "aria-labelledby": ariaLabelledBy,
180
+ "aria-current": ariaCurrent,
181
+ "aria-disabled": ariaDisabled,
182
+ "aria-live": ariaLive,
183
+ "aria-busy": ariaBusy,
184
+ "aria-describedby": ariaDescribedBy,
185
+ "aria-expanded": ariaExpanded,
186
+ "aria-haspopup": ariaHasPopup,
187
+ "aria-pressed": ariaPressed,
188
+ "aria-controls": ariaControls,
189
+ tabIndex,
190
+ as,
191
+ src,
192
+ alt,
193
+ onError,
194
+ onLoad,
195
+ type,
196
+ disabled,
197
+ id,
198
+ testID,
199
+ "data-testid": dataTestId,
200
+ ...props
201
+ }, ref) => {
202
+ if (as === "img" && src) {
203
+ return /* @__PURE__ */ jsx(
204
+ "img",
205
+ {
206
+ src,
207
+ alt: alt || "",
208
+ onError,
209
+ onLoad,
210
+ style: {
211
+ display: "block",
212
+ objectFit: "cover",
213
+ width: typeof props.width === "number" ? `${props.width}px` : props.width,
214
+ height: typeof props.height === "number" ? `${props.height}px` : props.height,
215
+ borderRadius: typeof props.borderRadius === "number" ? `${props.borderRadius}px` : props.borderRadius,
216
+ position: props.position,
217
+ top: typeof props.top === "number" ? `${props.top}px` : props.top,
218
+ left: typeof props.left === "number" ? `${props.left}px` : props.left,
219
+ right: typeof props.right === "number" ? `${props.right}px` : props.right,
220
+ bottom: typeof props.bottom === "number" ? `${props.bottom}px` : props.bottom,
221
+ ...props.style
222
+ }
223
+ }
224
+ );
225
+ }
226
+ return /* @__PURE__ */ jsx(
227
+ StyledBox,
228
+ {
229
+ ref,
230
+ elementType: as,
231
+ id,
232
+ type: as === "button" ? type || "button" : void 0,
233
+ disabled: as === "button" ? disabled : void 0,
234
+ onClick: onPress,
235
+ onKeyDown,
236
+ onKeyUp,
237
+ role,
238
+ "aria-label": ariaLabel,
239
+ "aria-labelledby": ariaLabelledBy,
240
+ "aria-current": ariaCurrent,
241
+ "aria-disabled": ariaDisabled,
242
+ "aria-busy": ariaBusy,
243
+ "aria-describedby": ariaDescribedBy,
244
+ "aria-expanded": ariaExpanded,
245
+ "aria-haspopup": ariaHasPopup,
246
+ "aria-pressed": ariaPressed,
247
+ "aria-controls": ariaControls,
248
+ "aria-live": ariaLive,
249
+ tabIndex: tabIndex !== void 0 ? tabIndex : void 0,
250
+ "data-testid": dataTestId || testID,
251
+ ...props,
252
+ children
253
+ }
254
+ );
255
+ }
256
+ );
257
+ Box.displayName = "Box";
258
+
259
+ // ../../foundation/primitives-web/src/Text.tsx
260
+ import styled2 from "styled-components";
261
+
262
+ // ../../foundation/primitives-web/src/textTruncation.ts
263
+ var truncationStyles = (numberOfLines) => {
264
+ if (!numberOfLines || numberOfLines < 1) return "";
265
+ if (numberOfLines === 1) {
266
+ return `
267
+ display: block;
268
+ max-width: 100%;
269
+ min-width: 0;
270
+ overflow: hidden;
271
+ text-overflow: ellipsis;
272
+ white-space: nowrap;
273
+ `;
274
+ }
275
+ return `
276
+ display: -webkit-box;
277
+ -webkit-box-orient: vertical;
278
+ -webkit-line-clamp: ${numberOfLines};
279
+ line-clamp: ${numberOfLines};
280
+ max-width: 100%;
281
+ min-width: 0;
282
+ overflow: hidden;
283
+ `;
284
+ };
285
+
286
+ // ../../foundation/primitives-web/src/Text.tsx
287
+ import { jsx as jsx2 } from "react/jsx-runtime";
288
+ var FilteredSpan = createFilteredElement("span");
289
+ var StyledText = styled2(FilteredSpan)`
290
+ color: ${(props) => props.color || "inherit"};
291
+ font-size: ${(props) => typeof props.fontSize === "number" ? `${props.fontSize}px` : props.fontSize || "inherit"};
292
+ font-weight: ${(props) => props.fontWeight || "normal"};
293
+ font-family: ${(props) => props.fontFamily || '"Aktiv Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'};
294
+ line-height: ${(props) => typeof props.lineHeight === "number" ? `${props.lineHeight}px` : props.lineHeight || "inherit"};
295
+ white-space: ${(props) => props.whiteSpace || "normal"};
296
+ text-align: ${(props) => props.textAlign || "inherit"};
297
+ text-decoration: ${(props) => props.textDecoration || "none"};
298
+
299
+ /* Keep last: single-line truncation has to win over white-space above. */
300
+ ${(props) => truncationStyles(props.numberOfLines)}
301
+ `;
302
+ var Text = ({
303
+ style,
304
+ className,
305
+ id,
306
+ role,
307
+ testID,
308
+ "data-testid": dataTestId,
309
+ ...props
310
+ }) => {
311
+ return /* @__PURE__ */ jsx2(
312
+ StyledText,
313
+ {
314
+ ...props,
315
+ style,
316
+ className,
317
+ id,
318
+ role,
319
+ "data-testid": dataTestId || testID
320
+ }
321
+ );
322
+ };
323
+
324
+ // ../../foundation/primitives-web/src/Icon.tsx
325
+ import styled3 from "styled-components";
326
+ import { jsx as jsx3 } from "react/jsx-runtime";
327
+ var FilteredDiv2 = createFilteredElement("div");
328
+ var StyledIcon = styled3(FilteredDiv2)`
329
+ display: flex;
330
+ align-items: center;
331
+ justify-content: center;
332
+ width: ${(props) => typeof props.size === "number" ? `${props.size}px` : props.size || "24px"};
333
+ height: ${(props) => typeof props.size === "number" ? `${props.size}px` : props.size || "24px"};
334
+ color: ${(props) => props.color || "currentColor"};
335
+
336
+ /* Icons paint themselves — each icon's own SVG sets its fill/stroke
337
+ (icons-base via inline "fill: currentColor", Lucide via "fill"/"stroke"
338
+ attributes). Do not add fill/stroke here: it re-strokes fill-based icons
339
+ and thickens the glyph. See FEP-877. */
340
+ svg {
341
+ width: 100%;
342
+ height: 100%;
343
+ }
344
+ `;
345
+ var Icon = ({
346
+ children,
347
+ testID,
348
+ "data-testid": dataTestId,
349
+ ...props
350
+ }) => {
351
+ return /* @__PURE__ */ jsx3(StyledIcon, { "data-testid": dataTestId || testID, ...props, children });
352
+ };
353
+
354
+ // ../../foundation/primitives-web/src/index.tsx
355
+ var isWeb = true;
356
+
357
+ // src/Counter.tsx
358
+ import { useResolvedTheme, useId } from "@xsolla/xui-core";
359
+ import { Input } from "@xsolla/xui-input";
360
+ import { MinusSq, PlusSq } from "@xsolla/xui-icons-base";
361
+
362
+ // src/utils.ts
363
+ var COUNTER_SIZE_CONFIG = {
364
+ xl: { control: 28, gap: 10, inputWidth: 149, readOnlyWidth: 100 },
365
+ lg: { control: 24, gap: 10, inputWidth: 142, readOnlyWidth: 90 },
366
+ md: { control: 24, gap: 8, inputWidth: 132, readOnlyWidth: 83 },
367
+ sm: { control: 18, gap: 8, inputWidth: 92, readOnlyWidth: 69 },
368
+ xs: { control: 16, gap: 6, inputWidth: 82, readOnlyWidth: 59 }
369
+ };
370
+ var clampToRange = (value, min, max) => {
371
+ let next = value;
372
+ if (typeof min === "number" && next < min) next = min;
373
+ if (typeof max === "number" && next > max) next = max;
374
+ return next;
375
+ };
376
+ var sanitizeCounterInput = (raw, allowNegative) => {
377
+ const digitsAndSigns = raw.replace(/[^\d-]/g, "");
378
+ const digits = digitsAndSigns.replace(/-/g, "");
379
+ if (allowNegative && digitsAndSigns.startsWith("-")) {
380
+ return `-${digits}`;
381
+ }
382
+ return digits;
383
+ };
384
+ var parseCounterValue = (raw) => {
385
+ const trimmed = raw.trim();
386
+ if (trimmed === "" || trimmed === "-") return null;
387
+ const parsed = Number.parseInt(trimmed, 10);
388
+ return Number.isNaN(parsed) ? null : parsed;
389
+ };
390
+ var allowsNegative = (min) => typeof min !== "number" || min < 0;
391
+
392
+ // src/Counter.tsx
393
+ import { jsx as jsx4, jsxs } from "react/jsx-runtime";
394
+ var CounterButton = ({
395
+ icon,
396
+ label,
397
+ onPress,
398
+ available,
399
+ boxSize,
400
+ borderRadius,
401
+ color,
402
+ hoverColor,
403
+ testID
404
+ }) => /* @__PURE__ */ jsx4(
405
+ Box,
406
+ {
407
+ as: "button",
408
+ type: "button",
409
+ width: boxSize,
410
+ height: boxSize,
411
+ flexShrink: 0,
412
+ alignItems: "center",
413
+ justifyContent: "center",
414
+ padding: 0,
415
+ borderWidth: 0,
416
+ borderRadius,
417
+ backgroundColor: "transparent",
418
+ cursor: available ? "pointer" : "not-allowed",
419
+ disabled: !available,
420
+ "aria-disabled": !available || void 0,
421
+ "aria-label": label,
422
+ onPress,
423
+ hoverStyle: available ? { backgroundColor: hoverColor } : void 0,
424
+ ...isWeb && {
425
+ onMouseDown: (e) => e.preventDefault()
426
+ },
427
+ "data-testid": testID,
428
+ testID,
429
+ children: /* @__PURE__ */ jsx4(Icon, { size: boxSize, color, children: icon })
430
+ }
431
+ );
432
+ var Counter = forwardRef(
433
+ ({
434
+ value,
435
+ defaultValue = null,
436
+ onChange,
437
+ min,
438
+ max,
439
+ step = 1,
440
+ size = "md",
441
+ type = "input",
442
+ disabled = false,
443
+ label,
444
+ "aria-label": ariaLabel,
445
+ decrementLabel = "Decrease quantity",
446
+ incrementLabel = "Increase quantity",
447
+ name,
448
+ id: providedId,
449
+ testID,
450
+ themeMode,
451
+ themeProductContext
452
+ }, ref) => {
453
+ const { theme } = useResolvedTheme({ themeMode, themeProductContext });
454
+ const rawId = useId();
455
+ const safeId = rawId.replace(/:/g, "");
456
+ const counterId = providedId || `counter-${safeId}`;
457
+ const isControlled = value !== void 0;
458
+ const [uncontrolledValue, setUncontrolledValue] = useState(
459
+ defaultValue
460
+ );
461
+ const committedValue = isControlled ? value ?? null : uncontrolledValue;
462
+ const [draft, setDraft] = useState(null);
463
+ const inputRef = useRef(null);
464
+ React3.useImperativeHandle(
465
+ ref,
466
+ () => inputRef.current,
467
+ []
468
+ );
469
+ const sizeConfig = COUNTER_SIZE_CONFIG[size];
470
+ const inputSizing = theme.sizing.input(size);
471
+ const isReadOnly = type === "readOnly";
472
+ const pendingValue = draft !== null ? parseCounterValue(draft) : committedValue;
473
+ const stepBase = pendingValue ?? min ?? 0;
474
+ const canDecrement = !disabled && (typeof min !== "number" || stepBase > min);
475
+ const canIncrement = !disabled && (typeof max !== "number" || stepBase < max);
476
+ const commit = (next) => {
477
+ setDraft(null);
478
+ const resolved = next ?? committedValue;
479
+ if (resolved === null) return;
480
+ const clamped = clampToRange(resolved, min, max);
481
+ if (!isControlled) setUncontrolledValue(clamped);
482
+ if (clamped !== committedValue) onChange?.(clamped);
483
+ };
484
+ const stepBy = (direction) => {
485
+ commit(clampToRange(stepBase + direction * step, min, max));
486
+ };
487
+ const handleInputChange = (e) => {
488
+ setDraft(sanitizeCounterInput(e.target.value, allowsNegative(min)));
489
+ };
490
+ const handleBlur = () => {
491
+ commit(draft !== null ? parseCounterValue(draft) : committedValue);
492
+ };
493
+ const handleKeyDown = (e) => {
494
+ if (e.key === "ArrowUp") {
495
+ e.preventDefault();
496
+ if (canIncrement) stepBy(1);
497
+ return;
498
+ }
499
+ if (e.key === "ArrowDown") {
500
+ e.preventDefault();
501
+ if (canDecrement) stepBy(-1);
502
+ return;
503
+ }
504
+ if (e.key === "Home" && typeof min === "number") {
505
+ e.preventDefault();
506
+ commit(min);
507
+ return;
508
+ }
509
+ if (e.key === "End" && typeof max === "number") {
510
+ e.preventDefault();
511
+ commit(max);
512
+ return;
513
+ }
514
+ if (e.key === "Enter" || e.key === "\n") {
515
+ e.preventDefault();
516
+ commit(draft !== null ? parseCounterValue(draft) : committedValue);
517
+ }
518
+ };
519
+ const enabledIconColor = theme.colors.content.secondary;
520
+ const disabledIconColor = theme.colors.control.text.disable;
521
+ const hoverColor = theme.colors.overlay.mono;
522
+ const buttonRadius = theme.shape.input[size].borderRadius;
523
+ const decrementButton = /* @__PURE__ */ jsx4(
524
+ CounterButton,
525
+ {
526
+ icon: /* @__PURE__ */ jsx4(MinusSq, {}),
527
+ label: decrementLabel,
528
+ onPress: () => {
529
+ if (canDecrement) stepBy(-1);
530
+ },
531
+ available: canDecrement,
532
+ boxSize: sizeConfig.control,
533
+ borderRadius: buttonRadius,
534
+ color: canDecrement ? enabledIconColor : disabledIconColor,
535
+ hoverColor,
536
+ testID: testID ? `${testID}__decrement` : void 0
537
+ }
538
+ );
539
+ const incrementButton = /* @__PURE__ */ jsx4(
540
+ CounterButton,
541
+ {
542
+ icon: /* @__PURE__ */ jsx4(PlusSq, {}),
543
+ label: incrementLabel,
544
+ onPress: () => {
545
+ if (canIncrement) stepBy(1);
546
+ },
547
+ available: canIncrement,
548
+ boxSize: sizeConfig.control,
549
+ borderRadius: buttonRadius,
550
+ color: canIncrement ? enabledIconColor : disabledIconColor,
551
+ hoverColor,
552
+ testID: testID ? `${testID}__increment` : void 0
553
+ }
554
+ );
555
+ const labelId = `${counterId}-label`;
556
+ const showLabel = Boolean(label) && !isReadOnly;
557
+ const labelNode = showLabel ? /* @__PURE__ */ jsx4(Box, { as: "label", id: labelId, children: /* @__PURE__ */ jsx4(
558
+ Text,
559
+ {
560
+ color: theme.colors.content.secondary,
561
+ fontSize: inputSizing.fontSize - 2,
562
+ fontWeight: "500",
563
+ children: label
564
+ }
565
+ ) }) : null;
566
+ if (isReadOnly) {
567
+ const readOnlyValue = String(committedValue ?? min ?? 0);
568
+ return /* @__PURE__ */ jsxs(
569
+ Box,
570
+ {
571
+ flexDirection: "row",
572
+ alignItems: "center",
573
+ gap: sizeConfig.gap,
574
+ width: sizeConfig.readOnlyWidth,
575
+ role: "group",
576
+ "aria-label": ariaLabel || label,
577
+ "aria-disabled": disabled || void 0,
578
+ "data-testid": testID,
579
+ children: [
580
+ decrementButton,
581
+ /* @__PURE__ */ jsx4(Box, { flex: 1, minWidth: 0, "aria-live": "polite", alignItems: "center", children: /* @__PURE__ */ jsx4(
582
+ Text,
583
+ {
584
+ color: disabled ? theme.colors.control.input.textDisable : theme.colors.control.input.text,
585
+ fontSize: inputSizing.fontSize,
586
+ lineHeight: inputSizing.fontSize + 2,
587
+ textAlign: "center",
588
+ "data-testid": testID ? `${testID}__value` : void 0,
589
+ children: readOnlyValue
590
+ }
591
+ ) }),
592
+ incrementButton
593
+ ]
594
+ }
595
+ );
596
+ }
597
+ const displayValue = draft !== null ? draft : committedValue === null ? "" : String(committedValue);
598
+ return /* @__PURE__ */ jsxs(
599
+ Box,
600
+ {
601
+ flexDirection: "column",
602
+ gap: inputSizing.fieldGap,
603
+ alignItems: "flex-start",
604
+ role: "group",
605
+ "aria-label": showLabel ? void 0 : ariaLabel,
606
+ "aria-labelledby": showLabel ? labelId : void 0,
607
+ "aria-disabled": disabled || void 0,
608
+ "data-testid": testID,
609
+ children: [
610
+ labelNode,
611
+ /* @__PURE__ */ jsxs(
612
+ Box,
613
+ {
614
+ flexDirection: "row",
615
+ alignItems: "center",
616
+ gap: sizeConfig.gap,
617
+ width: sizeConfig.inputWidth,
618
+ children: [
619
+ decrementButton,
620
+ /* @__PURE__ */ jsx4(Box, { flex: 1, minWidth: 0, children: /* @__PURE__ */ jsx4(
621
+ Input,
622
+ {
623
+ ref: inputRef,
624
+ id: counterId,
625
+ name,
626
+ size,
627
+ value: displayValue,
628
+ disabled,
629
+ onChange: handleInputChange,
630
+ onBlur: handleBlur,
631
+ onKeyDown: handleKeyDown,
632
+ type: "text",
633
+ inputMode: "numeric",
634
+ role: "spinbutton",
635
+ "aria-valuenow": committedValue ?? void 0,
636
+ "aria-valuemin": min,
637
+ "aria-valuemax": max,
638
+ "aria-label": showLabel ? void 0 : ariaLabel,
639
+ ...showLabel ? { "aria-labelledby": labelId } : {},
640
+ style: { textAlign: "center" },
641
+ themeMode,
642
+ themeProductContext,
643
+ testID: testID ? `${testID}__input` : void 0
644
+ }
645
+ ) }),
646
+ incrementButton
647
+ ]
648
+ }
649
+ )
650
+ ]
651
+ }
652
+ );
653
+ }
654
+ );
655
+ Counter.displayName = "Counter";
656
+ export {
657
+ Counter
658
+ };
659
+ //# sourceMappingURL=index.mjs.map