@postxl/ui-components 1.3.1 → 1.3.2

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/dist/index.js CHANGED
@@ -6092,9 +6092,52 @@ function useIsMobile(mobileBreakpoint = 768) {
6092
6092
  //#endregion
6093
6093
  //#region src/input/number-input.tsx
6094
6094
  /**
6095
+ * Formats a number according to the provided formatter
6096
+ */
6097
+ function formatNumber(value, formatter, intlFormatter) {
6098
+ if (typeof formatter === "function") return formatter(value);
6099
+ if (intlFormatter) return intlFormatter.format(value);
6100
+ const { locale, options } = formatter;
6101
+ return new Intl.NumberFormat(locale, options).format(value);
6102
+ }
6103
+ /**
6104
+ * Parses a formatted string back to a number using the format configuration
6105
+ * This is more robust than heuristics as it uses the actual locale/format settings
6106
+ */
6107
+ function parseFormattedNumber(formatted, formatter, intlFormatter) {
6108
+ if (formatted === "") return void 0;
6109
+ if (typeof formatter === "function") return parseFormattedNumberHeuristic(formatted);
6110
+ const formatterToUse = intlFormatter ?? new Intl.NumberFormat(formatter.locale, formatter.options);
6111
+ const parts = formatterToUse.formatToParts(12345.6);
6112
+ const groupSeparator = parts.find((p) => p.type === "group")?.value ?? "";
6113
+ const decimalSeparator = parts.find((p) => p.type === "decimal")?.value ?? ".";
6114
+ const normalized = formatted.replaceAll(groupSeparator, "").replaceAll(decimalSeparator, ".").replaceAll(/[^\d.-]/g, "");
6115
+ let parsed = Number(normalized);
6116
+ if (Number.isNaN(parsed)) return void 0;
6117
+ if (formatter.options?.style === "percent") parsed = parsed / 100;
6118
+ return parsed;
6119
+ }
6120
+ /**
6121
+ * Fallback parser for custom format functions where we don't know the format rules
6122
+ * Uses heuristics to guess the decimal separator
6123
+ */
6124
+ function parseFormattedNumberHeuristic(formatted) {
6125
+ let cleaned = formatted.trim();
6126
+ const lastComma = cleaned.lastIndexOf(",");
6127
+ const lastPeriod = cleaned.lastIndexOf(".");
6128
+ if (lastComma > lastPeriod) {
6129
+ cleaned = cleaned.replaceAll(/[.\s']/g, "");
6130
+ cleaned = cleaned.replace(",", ".");
6131
+ } else if (lastPeriod > lastComma) cleaned = cleaned.replaceAll(/[,\s']/g, "");
6132
+ else cleaned = cleaned.replaceAll(/[,.\s']/g, "");
6133
+ cleaned = cleaned.replaceAll(/[^0-9.-]/g, "");
6134
+ const parsed = Number(cleaned);
6135
+ return Number.isNaN(parsed) ? void 0 : parsed;
6136
+ }
6137
+ /**
6095
6138
  * Wrapper variants that mirror inputVariants but use focus-within (for wrapper)
6096
6139
  * instead of focus-visible (for input element).
6097
- * NOTE: When modifying variants, also update inputVariants in input.tsx
6140
+ * Note: When modifying variants, also update inputVariants in input.tsx
6098
6141
  */
6099
6142
  const numberInputWrapperVariants = cva("border-input bg-background grid grid-cols-[auto_1fr_auto] items-center overflow-hidden rounded-md border shadow-xs transition-[color,box-shadow] has-[input:disabled]:pointer-events-none has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-50", {
6100
6143
  variants: { variant: {
@@ -6103,19 +6146,26 @@ const numberInputWrapperVariants = cva("border-input bg-background grid grid-col
6103
6146
  } },
6104
6147
  defaultVariants: { variant: "default" }
6105
6148
  });
6106
- const NumberInput = React$10.forwardRef(({ className, wrapperClassName, prefix, suffix, variant, showSpinButtons = false, __e2e_test_id__, onEnter, onChange,...props }, ref) => {
6149
+ const NumberInput = React$10.forwardRef(({ className, wrapperClassName, prefix, suffix, variant, showSpinButtons = false, __e2e_test_id__, onEnter, onChange, format: format$1, value: controlledValue,...props }, ref) => {
6150
+ const [isFocused, setIsFocused] = React$10.useState(false);
6151
+ const [inputString, setInputString] = React$10.useState("");
6152
+ const intlFormatter = React$10.useMemo(() => {
6153
+ if (!format$1 || typeof format$1 === "function") return void 0;
6154
+ return new Intl.NumberFormat(format$1.locale, format$1.options);
6155
+ }, [format$1]);
6107
6156
  const focusInputAtPosition = (element, cursor) => {
6108
6157
  const parent = element.parentElement;
6109
6158
  if (!parent) return;
6110
6159
  const input = parent.querySelector("input");
6111
6160
  if (!input) return;
6112
- input.type = "text";
6161
+ const originalType = input.type;
6162
+ if (originalType === "number") input.type = "text";
6113
6163
  if (cursor === "start") input.setSelectionRange(0, 0);
6114
6164
  else {
6115
6165
  const length = input.value.length;
6116
6166
  input.setSelectionRange(length, length);
6117
6167
  }
6118
- input.type = "number";
6168
+ input.type = originalType;
6119
6169
  input.click();
6120
6170
  input.focus();
6121
6171
  };
@@ -6131,9 +6181,44 @@ const NumberInput = React$10.forwardRef(({ className, wrapperClassName, prefix,
6131
6181
  };
6132
6182
  const handleChange = (e) => {
6133
6183
  const value = e.target.value;
6134
- const parsedValue = value === "" ? void 0 : Number(value);
6184
+ if (format$1) setInputString(value);
6185
+ let parsedValue;
6186
+ if (format$1) parsedValue = parseFormattedNumber(value, format$1, intlFormatter);
6187
+ else parsedValue = value === "" ? void 0 : Number(value);
6135
6188
  onChange?.(parsedValue);
6136
6189
  };
6190
+ const handleFocus = (e) => {
6191
+ setIsFocused(true);
6192
+ props.onFocus?.(e);
6193
+ };
6194
+ const handleBlur = (e) => {
6195
+ setIsFocused(false);
6196
+ setInputString("");
6197
+ props.onBlur?.(e);
6198
+ };
6199
+ const displayValue = React$10.useMemo(() => {
6200
+ if (isFocused && format$1) {
6201
+ if (inputString !== "") return inputString;
6202
+ if (controlledValue !== void 0) {
6203
+ const formatted = formatNumber(controlledValue, format$1, intlFormatter);
6204
+ if (typeof format$1 !== "function" && intlFormatter) {
6205
+ const parts = intlFormatter.formatToParts(controlledValue);
6206
+ const groupSeparator = parts.find((p) => p.type === "group")?.value ?? "";
6207
+ return formatted.replaceAll(groupSeparator, "");
6208
+ }
6209
+ return controlledValue.toString();
6210
+ }
6211
+ return "";
6212
+ }
6213
+ if (!isFocused && format$1 && controlledValue !== void 0) return formatNumber(controlledValue, format$1, intlFormatter);
6214
+ return controlledValue?.toString() ?? "";
6215
+ }, [
6216
+ isFocused,
6217
+ format$1,
6218
+ inputString,
6219
+ controlledValue,
6220
+ intlFormatter
6221
+ ]);
6137
6222
  return /* @__PURE__ */ jsxs("div", {
6138
6223
  className: cn(numberInputWrapperVariants({ variant }), wrapperClassName),
6139
6224
  children: [
@@ -6144,7 +6229,7 @@ const NumberInput = React$10.forwardRef(({ className, wrapperClassName, prefix,
6144
6229
  children: prefix
6145
6230
  }),
6146
6231
  /* @__PURE__ */ jsx("input", {
6147
- type: "number",
6232
+ type: format$1 ? "text" : "number",
6148
6233
  "data-slot": "input",
6149
6234
  className: cn(
6150
6235
  inputVariants({ variant }),
@@ -6155,12 +6240,15 @@ const NumberInput = React$10.forwardRef(({ className, wrapperClassName, prefix,
6155
6240
  "text-right",
6156
6241
  !prefix && "pl-2",
6157
6242
  !suffix && (showSpinButtons ? "pr-1" : "pr-2"),
6158
- !showSpinButtons && "appearance-none",
6243
+ !showSpinButtons && !format$1 && "appearance-none",
6159
6244
  className
6160
6245
  ),
6161
6246
  "data-test-id": __e2e_test_id__,
6162
6247
  ref,
6248
+ value: displayValue,
6163
6249
  onChange: handleChange,
6250
+ onFocus: handleFocus,
6251
+ onBlur: handleBlur,
6164
6252
  onKeyDown: (e) => {
6165
6253
  props.onKeyDown?.(e);
6166
6254
  if (e.key === "Enter") onEnter?.();