@sustaina/shared-ui 1.53.0 → 1.54.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.mjs CHANGED
@@ -15815,8 +15815,11 @@ var Input2 = React.forwardRef(
15815
15815
  }
15816
15816
  );
15817
15817
  Input2.displayName = "Input";
15818
- var createSourceInfo = () => ({
15819
- source: "prop"
15818
+
15819
+ // src/components/inputNumber/helper.ts
15820
+ var createSourceInfo = (event) => ({
15821
+ event,
15822
+ source: event ? "event" : "prop"
15820
15823
  });
15821
15824
  var parseToNumber = (val) => {
15822
15825
  if (typeof val === "number") return val;
@@ -15826,6 +15829,112 @@ var parseToNumber = (val) => {
15826
15829
  }
15827
15830
  return void 0;
15828
15831
  };
15832
+ var truncateToFixed = (num, scale) => {
15833
+ if (scale === 0) return Math.trunc(num).toString();
15834
+ const sign = num < 0 ? "-" : "";
15835
+ const abs = Math.abs(num);
15836
+ const str = abs.toString();
15837
+ const dotIndex = str.indexOf(".");
15838
+ let intPart;
15839
+ let fracPart;
15840
+ if (dotIndex === -1) {
15841
+ intPart = str;
15842
+ fracPart = "";
15843
+ } else {
15844
+ intPart = str.slice(0, dotIndex);
15845
+ fracPart = str.slice(dotIndex + 1, dotIndex + 1 + scale);
15846
+ }
15847
+ fracPart = fracPart.padEnd(scale, "0");
15848
+ return `${sign}${intPart}.${fracPart}`;
15849
+ };
15850
+ var truncateStringToFixed = (str, scale) => {
15851
+ const trimmed = str.trim();
15852
+ if (trimmed === "" || trimmed === "-") return "0" + (scale > 0 ? "." + "0".repeat(scale) : "");
15853
+ const negative = trimmed.startsWith("-");
15854
+ const unsigned = negative ? trimmed.slice(1) : trimmed;
15855
+ const dotIndex = unsigned.indexOf(".");
15856
+ let intPart;
15857
+ let fracPart;
15858
+ if (dotIndex === -1) {
15859
+ intPart = unsigned || "0";
15860
+ fracPart = "";
15861
+ } else {
15862
+ intPart = unsigned.slice(0, dotIndex) || "0";
15863
+ fracPart = unsigned.slice(dotIndex + 1, dotIndex + 1 + scale);
15864
+ }
15865
+ if (scale === 0) return (negative ? "-" : "") + intPart;
15866
+ fracPart = fracPart.padEnd(scale, "0");
15867
+ return (negative ? "-" : "") + intPart + "." + fracPart;
15868
+ };
15869
+ var clamp = (value, min, max) => {
15870
+ if (max !== void 0 && value > max) return max;
15871
+ if (min !== void 0 && value < min) return min;
15872
+ return value;
15873
+ };
15874
+ function useAutoFormatDecimal({
15875
+ enabled,
15876
+ decimalScale,
15877
+ value,
15878
+ defaultValue
15879
+ }) {
15880
+ const blurScale = enabled ? decimalScale ?? 2 : void 0;
15881
+ const isUserEditingRef = React.useRef(false);
15882
+ const [formattedValue, setFormattedValue] = React.useState(() => {
15883
+ if (!enabled) return void 0;
15884
+ const scale = decimalScale ?? 2;
15885
+ const initial = parseToNumber(value) ?? parseToNumber(defaultValue);
15886
+ if (initial !== void 0) return truncateToFixed(initial, scale);
15887
+ return void 0;
15888
+ });
15889
+ React.useEffect(() => {
15890
+ if (!enabled) return;
15891
+ const parsed = parseToNumber(value);
15892
+ if (parsed !== void 0) {
15893
+ if (blurScale !== void 0 && !isUserEditingRef.current) {
15894
+ setFormattedValue(truncateToFixed(parsed, blurScale));
15895
+ }
15896
+ } else if (value === void 0 || value === null || value === "") {
15897
+ setFormattedValue(void 0);
15898
+ }
15899
+ }, [value, enabled, blurScale]);
15900
+ const onEdit = React.useCallback(
15901
+ (values, sourceInfo) => {
15902
+ if (!enabled || sourceInfo.source !== "event") return;
15903
+ isUserEditingRef.current = true;
15904
+ setFormattedValue(values.value || void 0);
15905
+ },
15906
+ [enabled]
15907
+ );
15908
+ const onBlur = React.useCallback(
15909
+ (truncatedStr) => {
15910
+ isUserEditingRef.current = false;
15911
+ if (!enabled || blurScale === void 0) return;
15912
+ setFormattedValue(truncatedStr);
15913
+ },
15914
+ [enabled, blurScale]
15915
+ );
15916
+ const resetEditing = React.useCallback(() => {
15917
+ isUserEditingRef.current = false;
15918
+ }, []);
15919
+ return { formattedValue, blurScale, onEdit, onBlur, resetEditing, setFormattedValue };
15920
+ }
15921
+ function useStepper({ value, step, min, max, disabled, onStep }) {
15922
+ const [changed, setChanged] = React.useState(false);
15923
+ const changeValue = React.useCallback(
15924
+ (delta) => {
15925
+ const current = value ?? 0;
15926
+ const clamped = clamp(current + delta, min, max);
15927
+ setChanged(true);
15928
+ onStep(clamped);
15929
+ },
15930
+ [value, max, min, onStep]
15931
+ );
15932
+ const increment = React.useCallback(() => changeValue(step), [changeValue, step]);
15933
+ const decrement = React.useCallback(() => changeValue(-step), [changeValue, step]);
15934
+ const isIncrementDisabled = disabled || max !== void 0 && (value ?? 0) >= max;
15935
+ const isDecrementDisabled = disabled || min !== void 0 && (value ?? 0) <= min;
15936
+ return { changed, increment, decrement, isIncrementDisabled, isDecrementDisabled };
15937
+ }
15829
15938
  var InputNumber = ({
15830
15939
  customInputProps,
15831
15940
  showStepper = false,
@@ -15838,58 +15947,106 @@ var InputNumber = ({
15838
15947
  defaultValue,
15839
15948
  disabled,
15840
15949
  onBlur,
15950
+ isAllowed: isAllowedProp,
15951
+ autoFormatDecimal = false,
15952
+ decimalScale: decimalScaleProp,
15953
+ fixedDecimalScale: fixedDecimalScaleProp,
15841
15954
  ...props
15842
15955
  }) => {
15843
15956
  const [internalValue, setInternalValue] = React.useState(
15844
15957
  () => parseToNumber(value) ?? parseToNumber(defaultValue)
15845
15958
  );
15846
- const [stepperChanged, setStepperChanged] = React.useState(false);
15959
+ const internalValueRef = React.useRef(internalValue);
15960
+ const rawValueRef = React.useRef("");
15961
+ const isBlurClampedRef = React.useRef(false);
15847
15962
  React.useEffect(() => {
15848
15963
  const parsed = parseToNumber(value);
15849
- if (parsed !== void 0) setInternalValue(parsed);
15964
+ if (parsed !== void 0) {
15965
+ internalValueRef.current = parsed;
15966
+ setInternalValue(parsed);
15967
+ }
15968
+ isBlurClampedRef.current = false;
15850
15969
  }, [value]);
15851
- const notifyChange = (newValue) => {
15852
- onStepChange?.(newValue);
15853
- onValueChange?.(
15854
- {
15855
- floatValue: newValue,
15856
- formattedValue: String(newValue),
15857
- value: String(newValue)
15970
+ const autoFormat = useAutoFormatDecimal({
15971
+ enabled: autoFormatDecimal,
15972
+ decimalScale: decimalScaleProp,
15973
+ value,
15974
+ defaultValue
15975
+ });
15976
+ const notifyChange = React.useCallback(
15977
+ (newValue, event) => {
15978
+ internalValueRef.current = newValue;
15979
+ setInternalValue(newValue);
15980
+ onStepChange?.(newValue);
15981
+ onValueChange?.(
15982
+ { floatValue: newValue, formattedValue: String(newValue), value: String(newValue) },
15983
+ createSourceInfo(event)
15984
+ );
15985
+ },
15986
+ [onStepChange, onValueChange]
15987
+ );
15988
+ const stepper = useStepper({
15989
+ value: internalValue,
15990
+ step,
15991
+ min,
15992
+ max,
15993
+ disabled,
15994
+ onStep: React.useCallback(
15995
+ (clamped) => {
15996
+ setInternalValue(clamped);
15997
+ if (autoFormat.blurScale !== void 0) {
15998
+ autoFormat.setFormattedValue(truncateToFixed(clamped, autoFormat.blurScale));
15999
+ }
16000
+ notifyChange(clamped);
15858
16001
  },
15859
- createSourceInfo()
15860
- );
15861
- };
15862
- const handleValueChange = (values, sourceInfo) => {
15863
- setInternalValue(values.floatValue);
15864
- onValueChange?.(values, sourceInfo);
15865
- if (values.floatValue !== void 0) {
15866
- onStepChange?.(values.floatValue);
15867
- }
15868
- };
15869
- const handleBlur = (event) => {
15870
- onBlur?.(event);
15871
- if (internalValue === void 0) return;
15872
- const clamped = max !== void 0 && internalValue > max ? max : min !== void 0 && internalValue < min ? min : internalValue;
15873
- if (clamped !== internalValue) {
15874
- setInternalValue(clamped);
15875
- notifyChange(clamped);
15876
- }
15877
- };
15878
- const isAllowed = (values) => {
15879
- const { floatValue } = values;
15880
- if (floatValue !== void 0 && max !== void 0 && floatValue > max) return false;
15881
- if (props.isAllowed && !props.isAllowed(values)) return false;
15882
- return true;
15883
- };
15884
- const changeValue = (delta) => {
15885
- const current = internalValue ?? 0;
15886
- const clamped = delta > 0 && max !== void 0 ? Math.min(current + delta, max) : delta < 0 && min !== void 0 ? Math.max(current + delta, min) : current + delta;
15887
- setInternalValue(clamped);
15888
- setStepperChanged(true);
15889
- notifyChange(clamped);
15890
- };
15891
- const isIncrementDisabled = disabled || max !== void 0 && (internalValue ?? 0) >= max;
15892
- const isDecrementDisabled = disabled || min !== void 0 && (internalValue ?? 0) <= min;
16002
+ [autoFormat, notifyChange]
16003
+ )
16004
+ });
16005
+ const handleValueChange = React.useCallback(
16006
+ (values, sourceInfo) => {
16007
+ internalValueRef.current = values.floatValue;
16008
+ rawValueRef.current = values.value;
16009
+ setInternalValue(values.floatValue);
16010
+ if (sourceInfo.source === "event") {
16011
+ isBlurClampedRef.current = false;
16012
+ }
16013
+ onValueChange?.(values, sourceInfo);
16014
+ if (values.floatValue !== void 0) onStepChange?.(values.floatValue);
16015
+ autoFormat.onEdit(values, sourceInfo);
16016
+ },
16017
+ [onValueChange, onStepChange, autoFormat]
16018
+ );
16019
+ const handleBlur = React.useCallback(
16020
+ (event) => {
16021
+ onBlur?.(event);
16022
+ const latestValue = internalValueRef.current;
16023
+ if (latestValue === void 0) {
16024
+ autoFormat.resetEditing();
16025
+ return;
16026
+ }
16027
+ const clamped = clamp(latestValue, min, max);
16028
+ const wasClamped = clamped !== latestValue;
16029
+ if (wasClamped) {
16030
+ isBlurClampedRef.current = true;
16031
+ internalValueRef.current = clamped;
16032
+ rawValueRef.current = String(clamped);
16033
+ setInternalValue(clamped);
16034
+ onValueChange?.(
16035
+ { floatValue: clamped, formattedValue: String(clamped), value: String(clamped) },
16036
+ createSourceInfo()
16037
+ );
16038
+ onStepChange?.(clamped);
16039
+ }
16040
+ if (autoFormatDecimal && autoFormat.blurScale !== void 0) {
16041
+ const rawStr = wasClamped ? String(clamped) : rawValueRef.current || "0";
16042
+ autoFormat.onBlur(truncateStringToFixed(rawStr, autoFormat.blurScale));
16043
+ } else {
16044
+ autoFormat.resetEditing();
16045
+ }
16046
+ },
16047
+ [onBlur, autoFormatDecimal, autoFormat, min, max, onValueChange, onStepChange]
16048
+ );
16049
+ const effectiveValue = autoFormat.formattedValue !== void 0 ? autoFormat.formattedValue : isBlurClampedRef.current || stepper.changed || autoFormatDecimal ? internalValue : value;
15893
16050
  const buttonClass = cn(
15894
16051
  "flex items-center justify-center h-3 w-5 transition-colors outline-none",
15895
16052
  "text-neutral-400 hover:text-neutral-600 active:text-neutral-900",
@@ -15899,11 +16056,16 @@ var InputNumber = ({
15899
16056
  NumericFormat,
15900
16057
  {
15901
16058
  customInput: Input2,
15902
- value: stepperChanged ? internalValue : value,
16059
+ value: effectiveValue?.toString(),
15903
16060
  defaultValue,
15904
16061
  onValueChange: handleValueChange,
15905
16062
  onBlur: handleBlur,
15906
- isAllowed,
16063
+ ...isAllowedProp && { isAllowed: isAllowedProp },
16064
+ ...!autoFormatDecimal && {
16065
+ decimalScale: decimalScaleProp,
16066
+ fixedDecimalScale: fixedDecimalScaleProp
16067
+ },
16068
+ ...autoFormat.formattedValue !== void 0 && { valueIsNumericString: true },
15907
16069
  ...props,
15908
16070
  disabled,
15909
16071
  ...customInputProps,
@@ -15912,8 +16074,8 @@ var InputNumber = ({
15912
16074
  "button",
15913
16075
  {
15914
16076
  type: "button",
15915
- onClick: () => changeValue(step),
15916
- disabled: isIncrementDisabled,
16077
+ onClick: stepper.increment,
16078
+ disabled: stepper.isIncrementDisabled,
15917
16079
  className: buttonClass,
15918
16080
  tabIndex: -1,
15919
16081
  "aria-label": "Increment",
@@ -15924,8 +16086,8 @@ var InputNumber = ({
15924
16086
  "button",
15925
16087
  {
15926
16088
  type: "button",
15927
- onClick: () => changeValue(-step),
15928
- disabled: isDecrementDisabled,
16089
+ onClick: stepper.decrement,
16090
+ disabled: stepper.isDecrementDisabled,
15929
16091
  className: buttonClass,
15930
16092
  tabIndex: -1,
15931
16093
  "aria-label": "Decrement",