@geomak/ui 1.8.0 → 1.9.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/dist/index.d.cts CHANGED
@@ -1457,11 +1457,14 @@ interface TextInputProps {
1457
1457
  declare function TextInput({ value, onChange, disabled, label, htmlFor, placeholder, name, inputStyle, style, layout, onBlur, errorMessage, labelColor, }: TextInputProps): react_jsx_runtime.JSX.Element;
1458
1458
 
1459
1459
  interface NumberInputProps {
1460
+ /** Step size for the up/down buttons and native arrow-key handling. Default `1`. */
1460
1461
  step?: number;
1461
- value?: any;
1462
+ /** Current value. `undefined` renders an empty field; a number renders that value. */
1463
+ value?: number | '';
1464
+ /** Fires with the next number. Empty input resolves to `undefined`. */
1462
1465
  onChange?: (e: {
1463
1466
  target: {
1464
- value: number;
1467
+ value: number | undefined;
1465
1468
  id?: string;
1466
1469
  name?: string;
1467
1470
  };
@@ -1470,8 +1473,8 @@ interface NumberInputProps {
1470
1473
  htmlFor?: string;
1471
1474
  name?: string;
1472
1475
  disabled?: boolean;
1473
- /** 'horizontal' | 'vertical' */
1474
- layout?: string;
1476
+ /** Label/input orientation. Defaults to `'horizontal'`. */
1477
+ layout?: 'horizontal' | 'vertical';
1475
1478
  errorMessage?: React$1.ReactNode;
1476
1479
  inputStyle?: React$1.CSSProperties;
1477
1480
  labelStyle?: React$1.CSSProperties;
@@ -1480,12 +1483,42 @@ interface NumberInputProps {
1480
1483
  min?: number;
1481
1484
  max?: number;
1482
1485
  readOnly?: boolean;
1483
- [key: string]: any;
1486
+ /** Optional precision for floating-point steps (number of decimal places to round to). */
1487
+ precision?: number;
1484
1488
  }
1485
1489
  /**
1486
- * Number input with increment / decrement controls.
1490
+ * Numeric input with keyboard-accessible increment / decrement buttons.
1491
+ *
1492
+ * **What's improved over the previous version**
1493
+ * - Step buttons are real `<button>` elements with `aria-label`, focus rings,
1494
+ * and proper keyboard activation (Enter / Space). The previous version used
1495
+ * `<span onClick>` which keyboard-only users could not reach.
1496
+ * - Floating-point drift on decimal steps (`0.1 + 0.2 = 0.30000000000000004`)
1497
+ * is rounded out via a `precision` prop or auto-inferred from the step.
1498
+ * - Empty input resolves to `undefined` instead of `NaN` — works with form
1499
+ * libraries (RHF, Formik) that treat empty as "no value".
1500
+ * - The decrement chevron actually points down (the previous SVG was the up
1501
+ * chevron rotated, with the up chevron itself wrongly using the same path).
1502
+ * - Width is a prop, not hardcoded `w-60`. Default is `w-full` so the input
1503
+ * flows with its parent.
1504
+ *
1505
+ * @example
1506
+ * ```tsx
1507
+ * const [qty, setQty] = useState<number | undefined>(1)
1508
+ * <NumberInput
1509
+ * label="Quantity"
1510
+ * value={qty ?? ''}
1511
+ * onChange={({ target }) => setQty(target.value)}
1512
+ * min={0} max={99}
1513
+ * />
1514
+ * ```
1515
+ *
1516
+ * @example Decimal step
1517
+ * ```tsx
1518
+ * <NumberInput label="Tonnage" step={0.25} precision={2} />
1519
+ * ```
1487
1520
  */
1488
- declare function NumberInput({ step, value, onChange, label, htmlFor, name, disabled, layout, errorMessage, inputStyle, labelStyle, placeholder, style, min, max, readOnly, }: NumberInputProps): react_jsx_runtime.JSX.Element;
1521
+ declare function NumberInput({ step, value, onChange, label, htmlFor, name, disabled, layout, errorMessage, inputStyle, labelStyle, placeholder, style, min, max, readOnly, precision, }: NumberInputProps): react_jsx_runtime.JSX.Element;
1489
1522
 
1490
1523
  interface PasswordProps {
1491
1524
  value?: string;
package/dist/index.d.ts CHANGED
@@ -1457,11 +1457,14 @@ interface TextInputProps {
1457
1457
  declare function TextInput({ value, onChange, disabled, label, htmlFor, placeholder, name, inputStyle, style, layout, onBlur, errorMessage, labelColor, }: TextInputProps): react_jsx_runtime.JSX.Element;
1458
1458
 
1459
1459
  interface NumberInputProps {
1460
+ /** Step size for the up/down buttons and native arrow-key handling. Default `1`. */
1460
1461
  step?: number;
1461
- value?: any;
1462
+ /** Current value. `undefined` renders an empty field; a number renders that value. */
1463
+ value?: number | '';
1464
+ /** Fires with the next number. Empty input resolves to `undefined`. */
1462
1465
  onChange?: (e: {
1463
1466
  target: {
1464
- value: number;
1467
+ value: number | undefined;
1465
1468
  id?: string;
1466
1469
  name?: string;
1467
1470
  };
@@ -1470,8 +1473,8 @@ interface NumberInputProps {
1470
1473
  htmlFor?: string;
1471
1474
  name?: string;
1472
1475
  disabled?: boolean;
1473
- /** 'horizontal' | 'vertical' */
1474
- layout?: string;
1476
+ /** Label/input orientation. Defaults to `'horizontal'`. */
1477
+ layout?: 'horizontal' | 'vertical';
1475
1478
  errorMessage?: React$1.ReactNode;
1476
1479
  inputStyle?: React$1.CSSProperties;
1477
1480
  labelStyle?: React$1.CSSProperties;
@@ -1480,12 +1483,42 @@ interface NumberInputProps {
1480
1483
  min?: number;
1481
1484
  max?: number;
1482
1485
  readOnly?: boolean;
1483
- [key: string]: any;
1486
+ /** Optional precision for floating-point steps (number of decimal places to round to). */
1487
+ precision?: number;
1484
1488
  }
1485
1489
  /**
1486
- * Number input with increment / decrement controls.
1490
+ * Numeric input with keyboard-accessible increment / decrement buttons.
1491
+ *
1492
+ * **What's improved over the previous version**
1493
+ * - Step buttons are real `<button>` elements with `aria-label`, focus rings,
1494
+ * and proper keyboard activation (Enter / Space). The previous version used
1495
+ * `<span onClick>` which keyboard-only users could not reach.
1496
+ * - Floating-point drift on decimal steps (`0.1 + 0.2 = 0.30000000000000004`)
1497
+ * is rounded out via a `precision` prop or auto-inferred from the step.
1498
+ * - Empty input resolves to `undefined` instead of `NaN` — works with form
1499
+ * libraries (RHF, Formik) that treat empty as "no value".
1500
+ * - The decrement chevron actually points down (the previous SVG was the up
1501
+ * chevron rotated, with the up chevron itself wrongly using the same path).
1502
+ * - Width is a prop, not hardcoded `w-60`. Default is `w-full` so the input
1503
+ * flows with its parent.
1504
+ *
1505
+ * @example
1506
+ * ```tsx
1507
+ * const [qty, setQty] = useState<number | undefined>(1)
1508
+ * <NumberInput
1509
+ * label="Quantity"
1510
+ * value={qty ?? ''}
1511
+ * onChange={({ target }) => setQty(target.value)}
1512
+ * min={0} max={99}
1513
+ * />
1514
+ * ```
1515
+ *
1516
+ * @example Decimal step
1517
+ * ```tsx
1518
+ * <NumberInput label="Tonnage" step={0.25} precision={2} />
1519
+ * ```
1487
1520
  */
1488
- declare function NumberInput({ step, value, onChange, label, htmlFor, name, disabled, layout, errorMessage, inputStyle, labelStyle, placeholder, style, min, max, readOnly, }: NumberInputProps): react_jsx_runtime.JSX.Element;
1521
+ declare function NumberInput({ step, value, onChange, label, htmlFor, name, disabled, layout, errorMessage, inputStyle, labelStyle, placeholder, style, min, max, readOnly, precision, }: NumberInputProps): react_jsx_runtime.JSX.Element;
1489
1522
 
1490
1523
  interface PasswordProps {
1491
1524
  value?: string;
package/dist/index.js CHANGED
@@ -2650,91 +2650,117 @@ function NumberInput({
2650
2650
  htmlFor,
2651
2651
  name,
2652
2652
  disabled,
2653
- layout,
2653
+ layout = "horizontal",
2654
2654
  errorMessage,
2655
2655
  inputStyle,
2656
2656
  labelStyle,
2657
2657
  placeholder,
2658
- style = {},
2658
+ style,
2659
2659
  min,
2660
2660
  max,
2661
- readOnly = false
2661
+ readOnly = false,
2662
+ precision
2662
2663
  }) {
2664
+ const errorId = useId();
2665
+ const hasError = errorMessage != null;
2666
+ const inferredPrecision = precision ?? (Number.isInteger(step) ? 0 : String(step).split(".")[1]?.length ?? 0);
2667
+ const round = (n) => {
2668
+ if (inferredPrecision === 0) return n;
2669
+ const factor = 10 ** inferredPrecision;
2670
+ return Math.round(n * factor) / factor;
2671
+ };
2672
+ const numeric = typeof value === "number" ? value : 0;
2663
2673
  const onIncrement = () => {
2664
- let newValue = value ? parseFloat(value) + step : 0 + step;
2665
- if (max !== void 0 && newValue > max) return;
2666
- onChange?.({ target: { value: newValue, id: htmlFor, name } });
2674
+ if (disabled || readOnly) return;
2675
+ const next = round(numeric + step);
2676
+ if (max !== void 0 && next > max) return;
2677
+ onChange?.({ target: { value: next, id: htmlFor, name } });
2667
2678
  };
2668
2679
  const onDecrement = () => {
2669
- let newValue = value ? parseFloat(value) - step : 0 - step;
2670
- if (min !== void 0 && newValue < min) return;
2671
- onChange?.({ target: { value: newValue, id: htmlFor, name } });
2680
+ if (disabled || readOnly) return;
2681
+ const next = round(numeric - step);
2682
+ if (min !== void 0 && next < min) return;
2683
+ onChange?.({ target: { value: next, id: htmlFor, name } });
2672
2684
  };
2673
- return /* @__PURE__ */ jsxs("div", { children: [
2674
- /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between pr-1 pl-1", children: /* @__PURE__ */ jsxs(
2675
- "div",
2676
- {
2677
- className: `flex ${layout === "vertical" ? "flex-col" : "flex-row items-center gap-2"}`,
2678
- children: [
2679
- /* @__PURE__ */ jsx(
2680
- "label",
2681
- {
2682
- className: "text-md font-bold ml-1 w-60 select-none text-prussian-blue dark:text-white",
2683
- style: labelStyle,
2684
- htmlFor,
2685
- children: label
2686
- }
2687
- ),
2688
- /* @__PURE__ */ jsxs(
2689
- "div",
2690
- {
2691
- style,
2692
- className: `${disabled ? "bg-disabled" : "bg-white"} rounded-lg flex items-center pr-1 pl-2 w-max`,
2693
- children: [
2694
- /* @__PURE__ */ jsx(
2695
- "input",
2696
- {
2697
- min,
2698
- max,
2699
- autoComplete: "off",
2700
- disabled,
2701
- name,
2702
- id: htmlFor,
2703
- step,
2704
- value,
2705
- onChange,
2706
- type: "number",
2707
- className: "focus:outline-0 focus-visible:outline-0 h-9 w-60 text-prussian-blue disabled:bg-disabled disabled:cursor-not-allowed transition-all",
2708
- style: inputStyle ?? {},
2709
- placeholder: placeholder ?? "",
2710
- readOnly
2711
- }
2712
- ),
2713
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
2714
- /* @__PURE__ */ jsx(
2715
- "span",
2716
- {
2717
- onClick: onIncrement,
2718
- className: "rotate-180 cursor-pointer transition-all duration-300 hover:bg-ice rounded-sm",
2719
- children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: colors_default.PALETTE["prussian-blue"], strokeWidth: 2, className: "h-3 w-3", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" }) })
2720
- }
2721
- ),
2722
- /* @__PURE__ */ jsx(
2723
- "span",
2724
- {
2725
- onClick: onDecrement,
2726
- className: "cursor-pointer transition-all duration-300 hover:bg-ice rounded-sm",
2727
- children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: colors_default.PALETTE["prussian-blue"], strokeWidth: 2, className: "h-3 w-3", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" }) })
2728
- }
2729
- )
2730
- ] })
2731
- ]
2732
- }
2733
- )
2734
- ]
2735
- }
2736
- ) }),
2737
- /* @__PURE__ */ jsx("div", { className: "text-center text-error min-h-0", children: errorMessage })
2685
+ const handleInputChange = (e) => {
2686
+ const raw = e.target.value;
2687
+ if (raw === "") {
2688
+ onChange?.({ target: { value: void 0, id: htmlFor, name } });
2689
+ return;
2690
+ }
2691
+ const parsed = Number(raw);
2692
+ if (Number.isNaN(parsed)) return;
2693
+ onChange?.({ target: { value: round(parsed), id: htmlFor, name } });
2694
+ };
2695
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
2696
+ /* @__PURE__ */ jsxs("div", { className: `flex ${layout === "vertical" ? "flex-col gap-1" : "flex-row items-center gap-2"}`, children: [
2697
+ label && /* @__PURE__ */ jsx(
2698
+ "label",
2699
+ {
2700
+ className: "text-sm font-medium ml-1 max-content select-none text-foreground",
2701
+ style: labelStyle,
2702
+ htmlFor,
2703
+ children: label
2704
+ }
2705
+ ),
2706
+ /* @__PURE__ */ jsxs(
2707
+ "div",
2708
+ {
2709
+ style,
2710
+ className: `flex items-center rounded-lg border ${hasError ? "border-status-error" : "border-border"} ${disabled ? "bg-surface-raised text-foreground-muted cursor-not-allowed" : "bg-surface text-foreground"} focus-within:ring-2 focus-within:ring-accent transition-colors`,
2711
+ children: [
2712
+ /* @__PURE__ */ jsx(
2713
+ "input",
2714
+ {
2715
+ min,
2716
+ max,
2717
+ autoComplete: "off",
2718
+ disabled,
2719
+ name,
2720
+ id: htmlFor,
2721
+ step,
2722
+ value: value ?? "",
2723
+ onChange: handleInputChange,
2724
+ type: "number",
2725
+ "aria-invalid": hasError || void 0,
2726
+ "aria-describedby": hasError ? errorId : void 0,
2727
+ className: "bg-transparent focus:outline-none h-9 w-full px-3 disabled:cursor-not-allowed [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none",
2728
+ style: inputStyle ?? {},
2729
+ placeholder: placeholder ?? "",
2730
+ readOnly
2731
+ }
2732
+ ),
2733
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col border-l border-border h-9", children: [
2734
+ /* @__PURE__ */ jsx(
2735
+ "button",
2736
+ {
2737
+ type: "button",
2738
+ tabIndex: -1,
2739
+ onClick: onIncrement,
2740
+ disabled: disabled || readOnly || max !== void 0 && numeric >= max,
2741
+ "aria-label": "Increase value",
2742
+ className: "flex-1 px-1.5 flex items-center justify-center hover:bg-surface-raised disabled:opacity-30 disabled:cursor-not-allowed transition-colors focus:outline-none focus-visible:bg-surface-raised",
2743
+ children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.5, className: "h-3 w-3", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 15l7-7 7 7" }) })
2744
+ }
2745
+ ),
2746
+ /* @__PURE__ */ jsx(
2747
+ "button",
2748
+ {
2749
+ type: "button",
2750
+ tabIndex: -1,
2751
+ onClick: onDecrement,
2752
+ disabled: disabled || readOnly || min !== void 0 && numeric <= min,
2753
+ "aria-label": "Decrease value",
2754
+ className: "flex-1 px-1.5 flex items-center justify-center hover:bg-surface-raised disabled:opacity-30 disabled:cursor-not-allowed transition-colors focus:outline-none focus-visible:bg-surface-raised border-t border-border",
2755
+ children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.5, className: "h-3 w-3", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" }) })
2756
+ }
2757
+ )
2758
+ ] })
2759
+ ]
2760
+ }
2761
+ )
2762
+ ] }),
2763
+ hasError && /* @__PURE__ */ jsx("div", { id: errorId, className: "text-xs text-status-error ml-1", children: errorMessage })
2738
2764
  ] });
2739
2765
  }
2740
2766
  function Password({