@neuronix-tech/ui 0.0.0-stage → 0.1.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.
Files changed (106) hide show
  1. package/dist/cli/index.js +4 -0
  2. package/dist/components/button/button.d.ts +18 -0
  3. package/dist/components/button/button.js +40 -0
  4. package/dist/components/button/button.js.map +1 -0
  5. package/dist/components/button/button.types.d.ts +36 -0
  6. package/dist/components/button/button.types.js +1 -0
  7. package/dist/components/button/button.types.js.map +1 -0
  8. package/dist/components/button/button.variants.d.ts +27 -0
  9. package/dist/components/button/button.variants.js +45 -0
  10. package/dist/components/button/button.variants.js.map +1 -0
  11. package/dist/components/button/index.d.ts +8 -0
  12. package/dist/components/button/index.js +5 -0
  13. package/dist/components/button/index.js.map +1 -0
  14. package/dist/components/form-currency-input/form-currency-input.d.ts +12 -0
  15. package/dist/components/form-currency-input/form-currency-input.js +62 -0
  16. package/dist/components/form-currency-input/form-currency-input.js.map +1 -0
  17. package/dist/components/form-currency-input/form-currency-input.types.d.ts +32 -0
  18. package/dist/components/form-currency-input/form-currency-input.types.js +1 -0
  19. package/dist/components/form-currency-input/form-currency-input.types.js.map +1 -0
  20. package/dist/components/form-currency-input/index.d.ts +4 -0
  21. package/dist/components/form-currency-input/index.js +5 -0
  22. package/dist/components/form-currency-input/index.js.map +1 -0
  23. package/dist/components/form-number-input/form-number-input.d.ts +13 -0
  24. package/dist/components/form-number-input/form-number-input.js +75 -0
  25. package/dist/components/form-number-input/form-number-input.js.map +1 -0
  26. package/dist/components/form-number-input/form-number-input.types.d.ts +34 -0
  27. package/dist/components/form-number-input/form-number-input.types.js +1 -0
  28. package/dist/components/form-number-input/form-number-input.types.js.map +1 -0
  29. package/dist/components/form-number-input/index.d.ts +4 -0
  30. package/dist/components/form-number-input/index.js +5 -0
  31. package/dist/components/form-number-input/index.js.map +1 -0
  32. package/dist/components/form-percentage-input/form-percentage-input.d.ts +12 -0
  33. package/dist/components/form-percentage-input/form-percentage-input.js +65 -0
  34. package/dist/components/form-percentage-input/form-percentage-input.js.map +1 -0
  35. package/dist/components/form-percentage-input/form-percentage-input.types.d.ts +33 -0
  36. package/dist/components/form-percentage-input/form-percentage-input.types.js +1 -0
  37. package/dist/components/form-percentage-input/form-percentage-input.types.js.map +1 -0
  38. package/dist/components/form-percentage-input/index.d.ts +4 -0
  39. package/dist/components/form-percentage-input/index.js +5 -0
  40. package/dist/components/form-percentage-input/index.js.map +1 -0
  41. package/dist/components/form-text-input/form-text-input.d.ts +19 -0
  42. package/dist/components/form-text-input/form-text-input.js +64 -0
  43. package/dist/components/form-text-input/form-text-input.js.map +1 -0
  44. package/dist/components/form-text-input/form-text-input.types.d.ts +45 -0
  45. package/dist/components/form-text-input/form-text-input.types.js +1 -0
  46. package/dist/components/form-text-input/form-text-input.types.js.map +1 -0
  47. package/dist/components/form-text-input/index.d.ts +4 -0
  48. package/dist/components/form-text-input/index.js +5 -0
  49. package/dist/components/form-text-input/index.js.map +1 -0
  50. package/dist/components/icon-button/icon-button.d.ts +20 -0
  51. package/dist/components/icon-button/icon-button.js +32 -0
  52. package/dist/components/icon-button/icon-button.js.map +1 -0
  53. package/dist/components/icon-button/icon-button.types.d.ts +24 -0
  54. package/dist/components/icon-button/icon-button.types.js +1 -0
  55. package/dist/components/icon-button/icon-button.types.js.map +1 -0
  56. package/dist/components/icon-button/index.d.ts +8 -0
  57. package/dist/components/icon-button/index.js +5 -0
  58. package/dist/components/icon-button/index.js.map +1 -0
  59. package/dist/components/spinner/index.d.ts +2 -0
  60. package/dist/components/spinner/index.js +5 -0
  61. package/dist/components/spinner/index.js.map +1 -0
  62. package/dist/components/spinner/spinner.d.ts +15 -0
  63. package/dist/components/spinner/spinner.js +9 -0
  64. package/dist/components/spinner/spinner.js.map +1 -0
  65. package/dist/hooks/use-number-mask.d.ts +24 -0
  66. package/dist/hooks/use-number-mask.js +59 -0
  67. package/dist/hooks/use-number-mask.js.map +1 -0
  68. package/dist/index.d.ts +21 -0
  69. package/dist/index.js +28 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/lib/cn.d.ts +10 -0
  72. package/dist/lib/cn.js +8 -0
  73. package/dist/lib/cn.js.map +1 -0
  74. package/dist/lib/format/number.d.ts +6 -0
  75. package/dist/lib/format/number.js +26 -0
  76. package/dist/lib/format/number.js.map +1 -0
  77. package/dist/lib/format/percent.d.ts +15 -0
  78. package/dist/lib/format/percent.js +7 -0
  79. package/dist/lib/format/percent.js.map +1 -0
  80. package/dist/lib/sealed.d.ts +14 -0
  81. package/dist/lib/sealed.js +1 -0
  82. package/dist/lib/sealed.js.map +1 -0
  83. package/dist/primitives/field.d.ts +17 -0
  84. package/dist/primitives/field.js +74 -0
  85. package/dist/primitives/field.js.map +1 -0
  86. package/dist/primitives/form-field.d.ts +27 -0
  87. package/dist/primitives/form-field.js +27 -0
  88. package/dist/primitives/form-field.js.map +1 -0
  89. package/dist/primitives/input-group.d.ts +19 -0
  90. package/dist/primitives/input-group.js +87 -0
  91. package/dist/primitives/input-group.js.map +1 -0
  92. package/dist/primitives/input.d.ts +11 -0
  93. package/dist/primitives/input.js +22 -0
  94. package/dist/primitives/input.js.map +1 -0
  95. package/dist/primitives/label.d.ts +6 -0
  96. package/dist/primitives/label.js +19 -0
  97. package/dist/primitives/label.js.map +1 -0
  98. package/dist/primitives/spinner.d.ts +10 -0
  99. package/dist/primitives/spinner.js +19 -0
  100. package/dist/primitives/spinner.js.map +1 -0
  101. package/dist/primitives/tooltip.d.ts +17 -0
  102. package/dist/primitives/tooltip.js +51 -0
  103. package/dist/primitives/tooltip.js.map +1 -0
  104. package/dist/styles.css +2 -0
  105. package/package.json +70 -3
  106. package/README.md +0 -3
@@ -0,0 +1,33 @@
1
+ import { FieldValues, FieldPath, ControllerRenderProps, FieldError } from 'react-hook-form';
2
+
3
+ interface FormPercentageInputProps<T extends FieldValues, N extends FieldPath<T>> {
4
+ id: string;
5
+ label: string;
6
+ /** Required is the DEFAULT: non-optional fields render "label *". */
7
+ optional?: boolean;
8
+ /**
9
+ * react-hook-form binding. Required: the mask owns the value round-trip.
10
+ *
11
+ * OUTPUT CONTRACT — form state stores PERCENT POINTS as `number | null`:
12
+ * the user types 15 → state holds 15, NEVER 0.15. This is the UI convention
13
+ * (state matches the screen; Stripe/Shopify store rates the same way). When
14
+ * a backend or formula expects a fraction, convert explicitly at the
15
+ * boundary with `percentToFraction` (exported from @neuronix-tech/ui).
16
+ */
17
+ field: ControllerRenderProps<T, N>;
18
+ /** react-hook-form field error; presence turns the field invalid. */
19
+ error?: FieldError;
20
+ /** Spinner overlay while an async source seeds the value; also disables the control. */
21
+ isLoading?: boolean;
22
+ disabled?: boolean;
23
+ autoFocus?: boolean;
24
+ placeholder?: string;
25
+ /** Explanation behind the label (dotted underline + tooltip). */
26
+ labelHint?: string;
27
+ /** Inclusive lower bound applied on blur. Default 0. */
28
+ min?: number;
29
+ /** Inclusive upper bound applied on blur. Default 100. */
30
+ max?: number;
31
+ }
32
+
33
+ export type { FormPercentageInputProps };
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=form-percentage-input.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,4 @@
1
+ export { FormPercentageInput } from './form-percentage-input.js';
2
+ export { FormPercentageInputProps } from './form-percentage-input.types.js';
3
+ import 'react';
4
+ import 'react-hook-form';
@@ -0,0 +1,5 @@
1
+ import { FormPercentageInput } from "./form-percentage-input.js";
2
+ export {
3
+ FormPercentageInput
4
+ };
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/form-percentage-input/index.ts"],"sourcesContent":["export { FormPercentageInput } from \"./form-percentage-input.js\"\nexport type { FormPercentageInputProps } from \"./form-percentage-input.types.js\"\n"],"mappings":"AAAA,SAAS,2BAA2B;","names":[]}
@@ -0,0 +1,19 @@
1
+ import * as react from 'react';
2
+ import { FieldValues, FieldPath } from 'react-hook-form';
3
+ import { FormTextInputProps } from './form-text-input.types.js';
4
+
5
+ /**
6
+ * The standard text field: label (required by default, "*"), control, error —
7
+ * always the same skeleton. Bind it through a react-hook-form <Controller>:
8
+ *
9
+ * <Controller
10
+ * control={form.control}
11
+ * name="name"
12
+ * render={({ field, fieldState }) => (
13
+ * <FormTextInput id="name" label="Name" field={field} error={fieldState.error} />
14
+ * )}
15
+ * />
16
+ */
17
+ declare function FormTextInput<T extends FieldValues, N extends FieldPath<T>>({ id, label, optional, field, error, isLoading, disabled, autoFocus, maxLength, uppercase, type, placeholder, prefix, labelHint, }: FormTextInputProps<T, N>): react.JSX.Element;
18
+
19
+ export { FormTextInput };
@@ -0,0 +1,64 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { Field, FieldError } from "../../primitives/field.js";
4
+ import { ControlLoadingOverlay, FormFieldLabel } from "../../primitives/form-field.js";
5
+ import { InputPrimitive } from "../../primitives/input.js";
6
+ import {
7
+ InputGroup,
8
+ InputGroupAddon,
9
+ InputGroupInput,
10
+ InputGroupText
11
+ } from "../../primitives/input-group.js";
12
+ function FormTextInput({
13
+ id,
14
+ label,
15
+ optional = false,
16
+ field,
17
+ error,
18
+ isLoading = false,
19
+ disabled = false,
20
+ autoFocus = false,
21
+ maxLength,
22
+ uppercase = false,
23
+ type = "text",
24
+ placeholder,
25
+ prefix,
26
+ labelHint
27
+ }) {
28
+ const controlledValue = field && field.value !== null && field.value !== void 0 ? String(field.value) : "";
29
+ const inputProps = {
30
+ id,
31
+ name: field?.name,
32
+ ref: field?.ref,
33
+ value: field ? controlledValue : void 0,
34
+ maxLength,
35
+ type,
36
+ placeholder,
37
+ autoFocus,
38
+ autoComplete: "off",
39
+ disabled: disabled || isLoading,
40
+ "aria-invalid": !!error || void 0,
41
+ onBlur: field?.onBlur,
42
+ onChange: (e) => {
43
+ if (uppercase) {
44
+ e.target.value = e.target.value.toUpperCase();
45
+ }
46
+ field?.onChange?.(e);
47
+ }
48
+ };
49
+ return /* @__PURE__ */ jsxs(Field, { "data-invalid": !!error || void 0, className: "nxui:gap-1", children: [
50
+ /* @__PURE__ */ jsx(FormFieldLabel, { htmlFor: id, label, optional, labelHint }),
51
+ /* @__PURE__ */ jsxs("div", { className: "nxui:relative", children: [
52
+ prefix ? /* @__PURE__ */ jsxs(InputGroup, { children: [
53
+ /* @__PURE__ */ jsx(InputGroupAddon, { align: "inline-start", children: /* @__PURE__ */ jsx(InputGroupText, { children: prefix }) }),
54
+ /* @__PURE__ */ jsx(InputGroupInput, { ...inputProps })
55
+ ] }) : /* @__PURE__ */ jsx(InputPrimitive, { ...inputProps }),
56
+ isLoading && /* @__PURE__ */ jsx(ControlLoadingOverlay, {})
57
+ ] }),
58
+ /* @__PURE__ */ jsx(FieldError, { errors: [error] })
59
+ ] });
60
+ }
61
+ export {
62
+ FormTextInput
63
+ };
64
+ //# sourceMappingURL=form-text-input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/form-text-input/form-text-input.tsx"],"sourcesContent":["\"use client\"\n\nimport type { FieldPath, FieldValues } from \"react-hook-form\"\n\nimport type { FormTextInputProps } from \"./form-text-input.types.js\"\nimport { Field, FieldError } from \"../../primitives/field.js\"\nimport { ControlLoadingOverlay, FormFieldLabel } from \"../../primitives/form-field.js\"\nimport { InputPrimitive } from \"../../primitives/input.js\"\nimport {\n InputGroup,\n InputGroupAddon,\n InputGroupInput,\n InputGroupText,\n} from \"../../primitives/input-group.js\"\n\n/**\n * The standard text field: label (required by default, \"*\"), control, error —\n * always the same skeleton. Bind it through a react-hook-form <Controller>:\n *\n * <Controller\n * control={form.control}\n * name=\"name\"\n * render={({ field, fieldState }) => (\n * <FormTextInput id=\"name\" label=\"Name\" field={field} error={fieldState.error} />\n * )}\n * />\n */\nexport function FormTextInput<T extends FieldValues, N extends FieldPath<T>>({\n id,\n label,\n optional = false,\n field,\n error,\n isLoading = false,\n disabled = false,\n autoFocus = false,\n maxLength,\n uppercase = false,\n type = \"text\",\n placeholder,\n prefix,\n labelHint,\n}: FormTextInputProps<T, N>) {\n const controlledValue =\n field && field.value !== null && field.value !== undefined ? String(field.value) : \"\"\n\n const inputProps = {\n id,\n name: field?.name,\n ref: field?.ref,\n value: field ? controlledValue : undefined,\n maxLength,\n type,\n placeholder,\n autoFocus,\n autoComplete: \"off\",\n disabled: disabled || isLoading,\n \"aria-invalid\": !!error || undefined,\n onBlur: field?.onBlur,\n onChange: (e: React.ChangeEvent<HTMLInputElement>) => {\n if (uppercase) {\n e.target.value = e.target.value.toUpperCase()\n }\n field?.onChange?.(e)\n },\n }\n\n return (\n <Field data-invalid={!!error || undefined} className=\"nxui:gap-1\">\n <FormFieldLabel htmlFor={id} label={label} optional={optional} labelHint={labelHint} />\n <div className=\"nxui:relative\">\n {prefix ? (\n <InputGroup>\n <InputGroupAddon align=\"inline-start\">\n <InputGroupText>{prefix}</InputGroupText>\n </InputGroupAddon>\n <InputGroupInput {...inputProps} />\n </InputGroup>\n ) : (\n <InputPrimitive {...inputProps} />\n )}\n {isLoading && <ControlLoadingOverlay />}\n </div>\n <FieldError errors={[error]} />\n </Field>\n )\n}\n"],"mappings":";AAqEM,cAGI,YAHJ;AAhEN,SAAS,OAAO,kBAAkB;AAClC,SAAS,uBAAuB,sBAAsB;AACtD,SAAS,sBAAsB;AAC/B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAcA,SAAS,cAA6D;AAAA,EAC3E;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,YAAY;AAAA,EACZ;AAAA,EACA,YAAY;AAAA,EACZ,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AACF,GAA6B;AAC3B,QAAM,kBACJ,SAAS,MAAM,UAAU,QAAQ,MAAM,UAAU,SAAY,OAAO,MAAM,KAAK,IAAI;AAErF,QAAM,aAAa;AAAA,IACjB;AAAA,IACA,MAAM,OAAO;AAAA,IACb,KAAK,OAAO;AAAA,IACZ,OAAO,QAAQ,kBAAkB;AAAA,IACjC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc;AAAA,IACd,UAAU,YAAY;AAAA,IACtB,gBAAgB,CAAC,CAAC,SAAS;AAAA,IAC3B,QAAQ,OAAO;AAAA,IACf,UAAU,CAAC,MAA2C;AACpD,UAAI,WAAW;AACb,UAAE,OAAO,QAAQ,EAAE,OAAO,MAAM,YAAY;AAAA,MAC9C;AACA,aAAO,WAAW,CAAC;AAAA,IACrB;AAAA,EACF;AAEA,SACE,qBAAC,SAAM,gBAAc,CAAC,CAAC,SAAS,QAAW,WAAU,cACnD;AAAA,wBAAC,kBAAe,SAAS,IAAI,OAAc,UAAoB,WAAsB;AAAA,IACrF,qBAAC,SAAI,WAAU,iBACZ;AAAA,eACC,qBAAC,cACC;AAAA,4BAAC,mBAAgB,OAAM,gBACrB,8BAAC,kBAAgB,kBAAO,GAC1B;AAAA,QACA,oBAAC,mBAAiB,GAAG,YAAY;AAAA,SACnC,IAEA,oBAAC,kBAAgB,GAAG,YAAY;AAAA,MAEjC,aAAa,oBAAC,yBAAsB;AAAA,OACvC;AAAA,IACA,oBAAC,cAAW,QAAQ,CAAC,KAAK,GAAG;AAAA,KAC/B;AAEJ;","names":[]}
@@ -0,0 +1,45 @@
1
+ import { FieldValues, FieldPath, ControllerRenderProps, FieldError } from 'react-hook-form';
2
+
3
+ /**
4
+ * Deliberately a closed union — numeric entry must go through FormNumberInput
5
+ * (which formats and stores number | null); `type="number"` is not available.
6
+ */
7
+ type TextInputType = "text" | "email" | "password" | "url" | "tel" | "search";
8
+ interface FormTextInputProps<T extends FieldValues, N extends FieldPath<T>> {
9
+ id: string;
10
+ label: string;
11
+ /**
12
+ * Required is the DEFAULT: non-optional fields render "label *". Pass
13
+ * `optional` to drop the asterisk — inverted from the usual `required` flag
14
+ * so the common case needs no prop.
15
+ */
16
+ optional?: boolean;
17
+ /**
18
+ * react-hook-form binding from a <Controller>. Optional so the input can be
19
+ * driven externally/uncontrolled in rare non-form contexts.
20
+ */
21
+ field?: ControllerRenderProps<T, N>;
22
+ /** react-hook-form field error; presence turns the field invalid (red label, ring, message). */
23
+ error?: FieldError;
24
+ /** Overlays a spinner on the control while an async source fills it. */
25
+ isLoading?: boolean;
26
+ disabled?: boolean;
27
+ autoFocus?: boolean;
28
+ maxLength?: number;
29
+ /** Forces typed text to uppercase at the event level (value stored uppercase). */
30
+ uppercase?: boolean;
31
+ type?: TextInputType;
32
+ placeholder?: string;
33
+ /**
34
+ * Fixed adornment rendered before the value (e.g. "#" for a channel name).
35
+ * Purely visual — never part of the field's value.
36
+ */
37
+ prefix?: string;
38
+ /**
39
+ * Explanation behind the label (dotted underline + tooltip) for fields whose
40
+ * meaning is not obvious from the label alone.
41
+ */
42
+ labelHint?: string;
43
+ }
44
+
45
+ export type { FormTextInputProps, TextInputType };
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=form-text-input.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,4 @@
1
+ export { FormTextInput } from './form-text-input.js';
2
+ export { FormTextInputProps, TextInputType } from './form-text-input.types.js';
3
+ import 'react';
4
+ import 'react-hook-form';
@@ -0,0 +1,5 @@
1
+ import { FormTextInput } from "./form-text-input.js";
2
+ export {
3
+ FormTextInput
4
+ };
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/form-text-input/index.ts"],"sourcesContent":["export { FormTextInput } from \"./form-text-input.js\"\nexport type { FormTextInputProps, TextInputType } from \"./form-text-input.types.js\"\n"],"mappings":"AAAA,SAAS,qBAAqB;","names":[]}
@@ -0,0 +1,20 @@
1
+ import * as react from 'react';
2
+ import { IconButtonProps } from './icon-button.types.js';
3
+ import '@base-ui/react/button';
4
+ import '../../lib/sealed.js';
5
+ import '../button/button.variants.js';
6
+ import 'class-variance-authority/types';
7
+ import 'class-variance-authority';
8
+
9
+ /**
10
+ * Square, icon-only button at the project's control size. Pass one icon as the
11
+ * child; it is auto-sized. Give it a `tooltip` — an icon alone rarely explains
12
+ * itself.
13
+ *
14
+ * <IconButton aria-label="Delete" tooltip="Delete" variant="destructive">
15
+ * <IconTrash />
16
+ * </IconButton>
17
+ */
18
+ declare function IconButton({ variant, isLoading, disabled, children, tooltip, ...props }: IconButtonProps): react.JSX.Element;
19
+
20
+ export { IconButton };
@@ -0,0 +1,32 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { Button as ButtonPrimitive } from "@base-ui/react/button";
4
+ import { cn } from "../../lib/cn.js";
5
+ import { SpinnerPrimitive } from "../../primitives/spinner.js";
6
+ import { TooltipContent, TooltipRoot, TooltipTrigger } from "../../primitives/tooltip.js";
7
+ import { buttonVariants, spinnerSizes } from "../button/button.variants.js";
8
+ function IconButton({ variant = "ghost", isLoading = false, disabled, children, tooltip, ...props }) {
9
+ const button = /* @__PURE__ */ jsx(
10
+ ButtonPrimitive,
11
+ {
12
+ "data-slot": "icon-button",
13
+ "aria-busy": isLoading || void 0,
14
+ disabled: disabled || isLoading,
15
+ className: cn(buttonVariants({ variant, size: "icon" })),
16
+ ...props,
17
+ children: isLoading ? /* @__PURE__ */ jsxs(Fragment, { children: [
18
+ /* @__PURE__ */ jsx("span", { className: "nxui:invisible nxui:contents", children }),
19
+ /* @__PURE__ */ jsx(SpinnerPrimitive, { className: cn("nxui:absolute nxui:inset-0 nxui:m-auto", spinnerSizes.icon) })
20
+ ] }) : children
21
+ }
22
+ );
23
+ if (!tooltip) return button;
24
+ return /* @__PURE__ */ jsxs(TooltipRoot, { children: [
25
+ /* @__PURE__ */ jsx(TooltipTrigger, { render: button }),
26
+ /* @__PURE__ */ jsx(TooltipContent, { children: tooltip })
27
+ ] });
28
+ }
29
+ export {
30
+ IconButton
31
+ };
32
+ //# sourceMappingURL=icon-button.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/icon-button/icon-button.tsx"],"sourcesContent":["\"use client\"\n\nimport { Button as ButtonPrimitive } from \"@base-ui/react/button\"\n\nimport type { IconButtonProps } from \"./icon-button.types.js\"\nimport { cn } from \"../../lib/cn.js\"\nimport { SpinnerPrimitive } from \"../../primitives/spinner.js\"\nimport { TooltipContent, TooltipRoot, TooltipTrigger } from \"../../primitives/tooltip.js\"\nimport { buttonVariants, spinnerSizes } from \"../button/button.variants.js\"\n\n/**\n * Square, icon-only button at the project's control size. Pass one icon as the\n * child; it is auto-sized. Give it a `tooltip` — an icon alone rarely explains\n * itself.\n *\n * <IconButton aria-label=\"Delete\" tooltip=\"Delete\" variant=\"destructive\">\n * <IconTrash />\n * </IconButton>\n */\nfunction IconButton({ variant = \"ghost\", isLoading = false, disabled, children, tooltip, ...props }: IconButtonProps) {\n const button = (\n <ButtonPrimitive\n data-slot=\"icon-button\"\n aria-busy={isLoading || undefined}\n disabled={disabled || isLoading}\n className={cn(buttonVariants({ variant, size: \"icon\" }))}\n {...props}\n >\n {isLoading ? (\n <>\n <span className=\"nxui:invisible nxui:contents\">{children}</span>\n <SpinnerPrimitive className={cn(\"nxui:absolute nxui:inset-0 nxui:m-auto\", spinnerSizes.icon)} />\n </>\n ) : (\n children\n )}\n </ButtonPrimitive>\n )\n\n if (!tooltip) return button\n\n return (\n <TooltipRoot>\n <TooltipTrigger render={button} />\n <TooltipContent>{tooltip}</TooltipContent>\n </TooltipRoot>\n )\n}\n\nexport { IconButton }\n"],"mappings":";AA6BQ,mBACE,KADF;AA3BR,SAAS,UAAU,uBAAuB;AAG1C,SAAS,UAAU;AACnB,SAAS,wBAAwB;AACjC,SAAS,gBAAgB,aAAa,sBAAsB;AAC5D,SAAS,gBAAgB,oBAAoB;AAW7C,SAAS,WAAW,EAAE,UAAU,SAAS,YAAY,OAAO,UAAU,UAAU,SAAS,GAAG,MAAM,GAAoB;AACpH,QAAM,SACJ;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,aAAW,aAAa;AAAA,MACxB,UAAU,YAAY;AAAA,MACtB,WAAW,GAAG,eAAe,EAAE,SAAS,MAAM,OAAO,CAAC,CAAC;AAAA,MACtD,GAAG;AAAA,MAEH,sBACC,iCACE;AAAA,4BAAC,UAAK,WAAU,gCAAgC,UAAS;AAAA,QACzD,oBAAC,oBAAiB,WAAW,GAAG,0CAA0C,aAAa,IAAI,GAAG;AAAA,SAChG,IAEA;AAAA;AAAA,EAEJ;AAGF,MAAI,CAAC,QAAS,QAAO;AAErB,SACE,qBAAC,eACC;AAAA,wBAAC,kBAAe,QAAQ,QAAQ;AAAA,IAChC,oBAAC,kBAAgB,mBAAQ;AAAA,KAC3B;AAEJ;","names":[]}
@@ -0,0 +1,24 @@
1
+ import { Button } from '@base-ui/react/button';
2
+ import { Sealed } from '../../lib/sealed.js';
3
+ import { ButtonVariantProps } from '../button/button.variants.js';
4
+ import 'class-variance-authority/types';
5
+ import 'class-variance-authority';
6
+
7
+ /**
8
+ * Icon-only button: a SHAPE, not a size — it renders square at the project's
9
+ * control size. `aria-label` is required by the type because an icon-only
10
+ * control has no accessible name without it. Prefer also passing `tooltip`
11
+ * (usually the same text): an icon alone rarely explains itself.
12
+ */
13
+ interface IconButtonProps extends Sealed<Button.Props>, Omit<ButtonVariantProps, "size"> {
14
+ "aria-label": string;
15
+ /** Same no-resize loading overlay as Button. */
16
+ isLoading?: boolean;
17
+ /**
18
+ * Text shown in a tooltip above the button. Tooltips are a prop, never a
19
+ * wrapper you compose — the library owns delay, positioning, and styling.
20
+ */
21
+ tooltip?: string;
22
+ }
23
+
24
+ export type { IconButtonProps };
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=icon-button.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,8 @@
1
+ export { IconButton } from './icon-button.js';
2
+ export { IconButtonProps } from './icon-button.types.js';
3
+ import 'react';
4
+ import '@base-ui/react/button';
5
+ import '../../lib/sealed.js';
6
+ import '../button/button.variants.js';
7
+ import 'class-variance-authority/types';
8
+ import 'class-variance-authority';
@@ -0,0 +1,5 @@
1
+ import { IconButton } from "./icon-button.js";
2
+ export {
3
+ IconButton
4
+ };
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/icon-button/index.ts"],"sourcesContent":["export { IconButton } from \"./icon-button.js\"\nexport type { IconButtonProps } from \"./icon-button.types.js\"\n"],"mappings":"AAAA,SAAS,kBAAkB;","names":[]}
@@ -0,0 +1,2 @@
1
+ export { Spinner, SpinnerProps } from './spinner.js';
2
+ import 'react';
@@ -0,0 +1,5 @@
1
+ import { Spinner } from "./spinner.js";
2
+ export {
3
+ Spinner
4
+ };
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/spinner/index.ts"],"sourcesContent":["export { Spinner, type SpinnerProps } from \"./spinner.js\"\n"],"mappings":"AAAA,SAAS,eAAkC;","names":[]}
@@ -0,0 +1,15 @@
1
+ import * as react from 'react';
2
+
3
+ interface SpinnerProps {
4
+ /** Accessible label announced to screen readers. Defaults to "Loading". */
5
+ "aria-label"?: string;
6
+ }
7
+ /**
8
+ * Standalone loading spinner, one size — sizing is a library decision, not a
9
+ * caller decision. Components that embed a spinner (Button's isLoading, future
10
+ * DataLoading) size it internally. Inside a Button, use `isLoading` instead —
11
+ * never compose a Spinner into button children manually.
12
+ */
13
+ declare function Spinner({ "aria-label": ariaLabel }: SpinnerProps): react.JSX.Element;
14
+
15
+ export { Spinner, type SpinnerProps };
@@ -0,0 +1,9 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { SpinnerPrimitive } from "../../primitives/spinner.js";
3
+ function Spinner({ "aria-label": ariaLabel = "Loading" }) {
4
+ return /* @__PURE__ */ jsx(SpinnerPrimitive, { className: "nxui:size-4", "aria-label": ariaLabel });
5
+ }
6
+ export {
7
+ Spinner
8
+ };
9
+ //# sourceMappingURL=spinner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/spinner/spinner.tsx"],"sourcesContent":["import { SpinnerPrimitive } from \"../../primitives/spinner.js\"\n\nexport interface SpinnerProps {\n /** Accessible label announced to screen readers. Defaults to \"Loading\". */\n \"aria-label\"?: string\n}\n\n/**\n * Standalone loading spinner, one size — sizing is a library decision, not a\n * caller decision. Components that embed a spinner (Button's isLoading, future\n * DataLoading) size it internally. Inside a Button, use `isLoading` instead —\n * never compose a Spinner into button children manually.\n */\nfunction Spinner({ \"aria-label\": ariaLabel = \"Loading\" }: SpinnerProps) {\n return <SpinnerPrimitive className=\"nxui:size-4\" aria-label={ariaLabel} />\n}\n\nexport { Spinner }\n"],"mappings":"AAcS;AAdT,SAAS,wBAAwB;AAajC,SAAS,QAAQ,EAAE,cAAc,YAAY,UAAU,GAAiB;AACtE,SAAO,oBAAC,oBAAiB,WAAU,eAAc,cAAY,WAAW;AAC1E;","names":[]}
@@ -0,0 +1,24 @@
1
+ import * as react from 'react';
2
+
3
+ interface UseNumberMaskArgs {
4
+ value: number | null;
5
+ sanitize: (raw: string) => string;
6
+ format: (value: number) => string;
7
+ onValueChange: (value: number | null) => void;
8
+ group?: boolean;
9
+ }
10
+ /**
11
+ * Live-formatting numeric input mask (ported from nx-automations): groups
12
+ * thousands as the user types and keeps the caret stable when separators
13
+ * shift. The form state stores `number | null` — the grouped string is
14
+ * display only. Blur normalization (padding, clamping) is the caller's job
15
+ * via `reformat`.
16
+ */
17
+ declare function useNumberMask({ value, sanitize, format, onValueChange, group, }: UseNumberMaskArgs): {
18
+ ref: react.RefObject<HTMLInputElement | null>;
19
+ display: string;
20
+ onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
21
+ reformat: (v?: number | null) => void;
22
+ };
23
+
24
+ export { useNumberMask };
@@ -0,0 +1,59 @@
1
+ "use client";
2
+ import { useLayoutEffect, useRef, useState } from "react";
3
+ import { groupNumeric, stripGroups } from "../lib/format/number.js";
4
+ function parseNumeric(raw) {
5
+ if (raw === "" || raw === "-" || raw === ".") return null;
6
+ const n = Number(raw);
7
+ return Number.isFinite(n) ? n : null;
8
+ }
9
+ const countSignificant = (s) => (s.match(/[\d.-]/g) ?? []).length;
10
+ function caretAfterSignificant(formatted, anchor) {
11
+ if (anchor <= 0) return 0;
12
+ let seen = 0;
13
+ for (let i = 0; i < formatted.length; i += 1) {
14
+ if (formatted[i] !== ",") {
15
+ seen += 1;
16
+ if (seen === anchor) return i + 1;
17
+ }
18
+ }
19
+ return formatted.length;
20
+ }
21
+ function useNumberMask({
22
+ value,
23
+ sanitize,
24
+ format,
25
+ onValueChange,
26
+ group = true
27
+ }) {
28
+ const ref = useRef(null);
29
+ const caret = useRef(null);
30
+ const mask = (raw) => group ? groupNumeric(raw) : raw;
31
+ const toDisplay = (v) => mask(v === null ? "" : format(v));
32
+ const [display, setDisplay] = useState(() => toDisplay(value));
33
+ const [lastExternal, setLastExternal] = useState(value);
34
+ if (value !== lastExternal) {
35
+ setLastExternal(value);
36
+ if (parseNumeric(stripGroups(display)) !== value) setDisplay(toDisplay(value));
37
+ }
38
+ useLayoutEffect(() => {
39
+ if (caret.current !== null && ref.current) {
40
+ ref.current.setSelectionRange(caret.current, caret.current);
41
+ caret.current = null;
42
+ }
43
+ });
44
+ const onChange = (event) => {
45
+ const raw = event.target.value;
46
+ const anchor = countSignificant(raw.slice(0, event.target.selectionStart ?? raw.length));
47
+ const cleaned = sanitize(stripGroups(raw));
48
+ const next = mask(cleaned);
49
+ setDisplay(next);
50
+ caret.current = next === display ? null : caretAfterSignificant(next, anchor);
51
+ onValueChange(parseNumeric(cleaned));
52
+ };
53
+ const reformat = (v = value) => setDisplay(toDisplay(v));
54
+ return { ref, display, onChange, reformat };
55
+ }
56
+ export {
57
+ useNumberMask
58
+ };
59
+ //# sourceMappingURL=use-number-mask.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/use-number-mask.ts"],"sourcesContent":["\"use client\"\n\nimport { useLayoutEffect, useRef, useState } from \"react\"\n\nimport { groupNumeric, stripGroups } from \"../lib/format/number.js\"\n\nfunction parseNumeric(raw: string): number | null {\n if (raw === \"\" || raw === \"-\" || raw === \".\") return null\n const n = Number(raw)\n return Number.isFinite(n) ? n : null\n}\n\nconst countSignificant = (s: string) => (s.match(/[\\d.-]/g) ?? []).length\n\n// Caret index just after the `anchor`-th significant (non-comma) char.\nfunction caretAfterSignificant(formatted: string, anchor: number): number {\n if (anchor <= 0) return 0\n let seen = 0\n for (let i = 0; i < formatted.length; i += 1) {\n if (formatted[i] !== \",\") {\n seen += 1\n if (seen === anchor) return i + 1\n }\n }\n return formatted.length\n}\n\ninterface UseNumberMaskArgs {\n value: number | null\n sanitize: (raw: string) => string\n format: (value: number) => string\n onValueChange: (value: number | null) => void\n group?: boolean\n}\n\n/**\n * Live-formatting numeric input mask (ported from nx-automations): groups\n * thousands as the user types and keeps the caret stable when separators\n * shift. The form state stores `number | null` — the grouped string is\n * display only. Blur normalization (padding, clamping) is the caller's job\n * via `reformat`.\n */\nexport function useNumberMask({\n value,\n sanitize,\n format,\n onValueChange,\n group = true,\n}: UseNumberMaskArgs) {\n const ref = useRef<HTMLInputElement>(null)\n const caret = useRef<number | null>(null)\n\n const mask = (raw: string) => (group ? groupNumeric(raw) : raw)\n const toDisplay = (v: number | null) => mask(v === null ? \"\" : format(v))\n\n const [display, setDisplay] = useState(() => toDisplay(value))\n const [lastExternal, setLastExternal] = useState(value)\n\n // Re-sync from the form (reset / edit seeding) unless the user is mid-edit\n // of the same numeric value.\n if (value !== lastExternal) {\n setLastExternal(value)\n if (parseNumeric(stripGroups(display)) !== value) setDisplay(toDisplay(value))\n }\n\n useLayoutEffect(() => {\n if (caret.current !== null && ref.current) {\n ref.current.setSelectionRange(caret.current, caret.current)\n caret.current = null\n }\n })\n\n const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {\n const raw = event.target.value\n const anchor = countSignificant(raw.slice(0, event.target.selectionStart ?? raw.length))\n const cleaned = sanitize(stripGroups(raw))\n const next = mask(cleaned)\n setDisplay(next)\n // A rejected keystroke leaves `next` === `display`: no re-render happens,\n // so null the caret rather than strand a stale index for a later render.\n caret.current = next === display ? null : caretAfterSignificant(next, anchor)\n onValueChange(parseNumeric(cleaned))\n }\n\n const reformat = (v: number | null = value) => setDisplay(toDisplay(v))\n\n return { ref, display, onChange, reformat }\n}\n"],"mappings":";AAEA,SAAS,iBAAiB,QAAQ,gBAAgB;AAElD,SAAS,cAAc,mBAAmB;AAE1C,SAAS,aAAa,KAA4B;AAChD,MAAI,QAAQ,MAAM,QAAQ,OAAO,QAAQ,IAAK,QAAO;AACrD,QAAM,IAAI,OAAO,GAAG;AACpB,SAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AAClC;AAEA,MAAM,mBAAmB,CAAC,OAAe,EAAE,MAAM,SAAS,KAAK,CAAC,GAAG;AAGnE,SAAS,sBAAsB,WAAmB,QAAwB;AACxE,MAAI,UAAU,EAAG,QAAO;AACxB,MAAI,OAAO;AACX,WAAS,IAAI,GAAG,IAAI,UAAU,QAAQ,KAAK,GAAG;AAC5C,QAAI,UAAU,CAAC,MAAM,KAAK;AACxB,cAAQ;AACR,UAAI,SAAS,OAAQ,QAAO,IAAI;AAAA,IAClC;AAAA,EACF;AACA,SAAO,UAAU;AACnB;AAiBO,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AACV,GAAsB;AACpB,QAAM,MAAM,OAAyB,IAAI;AACzC,QAAM,QAAQ,OAAsB,IAAI;AAExC,QAAM,OAAO,CAAC,QAAiB,QAAQ,aAAa,GAAG,IAAI;AAC3D,QAAM,YAAY,CAAC,MAAqB,KAAK,MAAM,OAAO,KAAK,OAAO,CAAC,CAAC;AAExE,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,MAAM,UAAU,KAAK,CAAC;AAC7D,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,KAAK;AAItD,MAAI,UAAU,cAAc;AAC1B,oBAAgB,KAAK;AACrB,QAAI,aAAa,YAAY,OAAO,CAAC,MAAM,MAAO,YAAW,UAAU,KAAK,CAAC;AAAA,EAC/E;AAEA,kBAAgB,MAAM;AACpB,QAAI,MAAM,YAAY,QAAQ,IAAI,SAAS;AACzC,UAAI,QAAQ,kBAAkB,MAAM,SAAS,MAAM,OAAO;AAC1D,YAAM,UAAU;AAAA,IAClB;AAAA,EACF,CAAC;AAED,QAAM,WAAW,CAAC,UAA+C;AAC/D,UAAM,MAAM,MAAM,OAAO;AACzB,UAAM,SAAS,iBAAiB,IAAI,MAAM,GAAG,MAAM,OAAO,kBAAkB,IAAI,MAAM,CAAC;AACvF,UAAM,UAAU,SAAS,YAAY,GAAG,CAAC;AACzC,UAAM,OAAO,KAAK,OAAO;AACzB,eAAW,IAAI;AAGf,UAAM,UAAU,SAAS,UAAU,OAAO,sBAAsB,MAAM,MAAM;AAC5E,kBAAc,aAAa,OAAO,CAAC;AAAA,EACrC;AAEA,QAAM,WAAW,CAAC,IAAmB,UAAU,WAAW,UAAU,CAAC,CAAC;AAEtE,SAAO,EAAE,KAAK,SAAS,UAAU,SAAS;AAC5C;","names":[]}
@@ -0,0 +1,21 @@
1
+ export { Button } from './components/button/button.js';
2
+ export { ButtonProps } from './components/button/button.types.js';
3
+ export { FormCurrencyInput } from './components/form-currency-input/form-currency-input.js';
4
+ export { CurrencyCode, FormCurrencyInputProps } from './components/form-currency-input/form-currency-input.types.js';
5
+ export { FormNumberInput } from './components/form-number-input/form-number-input.js';
6
+ export { FormNumberInputProps } from './components/form-number-input/form-number-input.types.js';
7
+ export { FormPercentageInput } from './components/form-percentage-input/form-percentage-input.js';
8
+ export { FormPercentageInputProps } from './components/form-percentage-input/form-percentage-input.types.js';
9
+ export { fractionToPercent, percentToFraction } from './lib/format/percent.js';
10
+ export { FormTextInput } from './components/form-text-input/form-text-input.js';
11
+ export { FormTextInputProps, TextInputType } from './components/form-text-input/form-text-input.types.js';
12
+ export { IconButton } from './components/icon-button/icon-button.js';
13
+ export { IconButtonProps } from './components/icon-button/icon-button.types.js';
14
+ export { Spinner, SpinnerProps } from './components/spinner/spinner.js';
15
+ import 'react';
16
+ import '@base-ui/react/button';
17
+ import './components/button/button.variants.js';
18
+ import 'class-variance-authority/types';
19
+ import 'class-variance-authority';
20
+ import './lib/sealed.js';
21
+ import 'react-hook-form';
package/dist/index.js ADDED
@@ -0,0 +1,28 @@
1
+ import { Button } from "./components/button/index.js";
2
+ import {
3
+ FormCurrencyInput
4
+ } from "./components/form-currency-input/index.js";
5
+ import {
6
+ FormNumberInput
7
+ } from "./components/form-number-input/index.js";
8
+ import {
9
+ FormPercentageInput
10
+ } from "./components/form-percentage-input/index.js";
11
+ import { percentToFraction, fractionToPercent } from "./lib/format/percent.js";
12
+ import {
13
+ FormTextInput
14
+ } from "./components/form-text-input/index.js";
15
+ import { IconButton } from "./components/icon-button/index.js";
16
+ import { Spinner } from "./components/spinner/index.js";
17
+ export {
18
+ Button,
19
+ FormCurrencyInput,
20
+ FormNumberInput,
21
+ FormPercentageInput,
22
+ FormTextInput,
23
+ IconButton,
24
+ Spinner,
25
+ fractionToPercent,
26
+ percentToFraction
27
+ };
28
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @neuronix-tech/ui — sealed catalog barrel. Only finished catalog components are\n * exported here; primitives stay internal by design.\n */\nexport { Button, type ButtonProps } from \"./components/button/index.js\"\nexport {\n FormCurrencyInput,\n type FormCurrencyInputProps,\n type CurrencyCode,\n} from \"./components/form-currency-input/index.js\"\nexport {\n FormNumberInput,\n type FormNumberInputProps,\n} from \"./components/form-number-input/index.js\"\nexport {\n FormPercentageInput,\n type FormPercentageInputProps,\n} from \"./components/form-percentage-input/index.js\"\nexport { percentToFraction, fractionToPercent } from \"./lib/format/percent.js\"\nexport {\n FormTextInput,\n type FormTextInputProps,\n type TextInputType,\n} from \"./components/form-text-input/index.js\"\nexport { IconButton, type IconButtonProps } from \"./components/icon-button/index.js\"\nexport { Spinner, type SpinnerProps } from \"./components/spinner/index.js\"\n"],"mappings":"AAIA,SAAS,cAAgC;AACzC;AAAA,EACE;AAAA,OAGK;AACP;AAAA,EACE;AAAA,OAEK;AACP;AAAA,EACE;AAAA,OAEK;AACP,SAAS,mBAAmB,yBAAyB;AACrD;AAAA,EACE;AAAA,OAGK;AACP,SAAS,kBAAwC;AACjD,SAAS,eAAkC;","names":[]}
@@ -0,0 +1,10 @@
1
+ import { ClassValue } from 'clsx';
2
+
3
+ /**
4
+ * Internal class combiner. Deliberately clsx-only — no tailwind-merge: consumers
5
+ * cannot pass className, so every class string is authored in this package and
6
+ * conflicts are resolved at authoring time, not runtime.
7
+ */
8
+ declare function cn(...inputs: ClassValue[]): string;
9
+
10
+ export { cn };
package/dist/lib/cn.js ADDED
@@ -0,0 +1,8 @@
1
+ import { clsx } from "clsx";
2
+ function cn(...inputs) {
3
+ return clsx(inputs);
4
+ }
5
+ export {
6
+ cn
7
+ };
8
+ //# sourceMappingURL=cn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/cn.ts"],"sourcesContent":["import { clsx, type ClassValue } from \"clsx\"\n\n/**\n * Internal class combiner. Deliberately clsx-only — no tailwind-merge: consumers\n * cannot pass className, so every class string is authored in this package and\n * conflicts are resolved at authoring time, not runtime.\n */\nexport function cn(...inputs: ClassValue[]): string {\n return clsx(inputs)\n}\n"],"mappings":"AAAA,SAAS,YAA6B;AAO/B,SAAS,MAAM,QAA8B;AAClD,SAAO,KAAK,MAAM;AACpB;","names":[]}
@@ -0,0 +1,6 @@
1
+ declare const stripGroups: (s: string) => string;
2
+ declare function groupNumeric(raw: string): string;
3
+ declare function sanitizeDecimal(raw: string, maxDecimals?: number): string;
4
+ declare const asFiniteNumber: (value: unknown) => number | null;
5
+
6
+ export { asFiniteNumber, groupNumeric, sanitizeDecimal, stripGroups };
@@ -0,0 +1,26 @@
1
+ const stripGroups = (s) => s.replace(/,/g, "");
2
+ function groupNumeric(raw) {
3
+ if (raw === "" || raw === "-") return raw;
4
+ const negative = raw.startsWith("-");
5
+ const body = negative ? raw.slice(1) : raw;
6
+ const dot = body.indexOf(".");
7
+ const intPart = dot === -1 ? body : body.slice(0, dot);
8
+ const rest = dot === -1 ? "" : body.slice(dot);
9
+ const grouped = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
10
+ return `${negative ? "-" : ""}${grouped}${rest}`;
11
+ }
12
+ function sanitizeDecimal(raw, maxDecimals = 2) {
13
+ const s = raw.replace(/[^\d.]/g, "");
14
+ const dot = s.indexOf(".");
15
+ if (dot === -1) return s;
16
+ const dec = s.slice(dot + 1).replace(/\./g, "").slice(0, maxDecimals);
17
+ return `${s.slice(0, dot)}.${dec}`;
18
+ }
19
+ const asFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
20
+ export {
21
+ asFiniteNumber,
22
+ groupNumeric,
23
+ sanitizeDecimal,
24
+ stripGroups
25
+ };
26
+ //# sourceMappingURL=number.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/lib/format/number.ts"],"sourcesContent":["// Live-input helpers — operate on partial numeric strings while typing, which\n// Intl.NumberFormat can't (it needs a complete number). Used by the numeric\n// form inputs' mask (see hooks/use-number-mask). Ported from nx-automations.\n\nexport const stripGroups = (s: string) => s.replace(/,/g, \"\")\n\n// Assumes an already-sanitized string (digits, optional leading \"-\", one \".\").\nexport function groupNumeric(raw: string): string {\n if (raw === \"\" || raw === \"-\") return raw\n const negative = raw.startsWith(\"-\")\n const body = negative ? raw.slice(1) : raw\n const dot = body.indexOf(\".\")\n const intPart = dot === -1 ? body : body.slice(0, dot)\n const rest = dot === -1 ? \"\" : body.slice(dot)\n const grouped = intPart.replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\")\n return `${negative ? \"-\" : \"\"}${grouped}${rest}`\n}\n\nexport function sanitizeDecimal(raw: string, maxDecimals = 2): string {\n const s = raw.replace(/[^\\d.]/g, \"\")\n const dot = s.indexOf(\".\")\n if (dot === -1) return s\n const dec = s\n .slice(dot + 1)\n .replace(/\\./g, \"\")\n .slice(0, maxDecimals)\n return `${s.slice(0, dot)}.${dec}`\n}\n\nexport const asFiniteNumber = (value: unknown): number | null =>\n typeof value === \"number\" && Number.isFinite(value) ? value : null\n"],"mappings":"AAIO,MAAM,cAAc,CAAC,MAAc,EAAE,QAAQ,MAAM,EAAE;AAGrD,SAAS,aAAa,KAAqB;AAChD,MAAI,QAAQ,MAAM,QAAQ,IAAK,QAAO;AACtC,QAAM,WAAW,IAAI,WAAW,GAAG;AACnC,QAAM,OAAO,WAAW,IAAI,MAAM,CAAC,IAAI;AACvC,QAAM,MAAM,KAAK,QAAQ,GAAG;AAC5B,QAAM,UAAU,QAAQ,KAAK,OAAO,KAAK,MAAM,GAAG,GAAG;AACrD,QAAM,OAAO,QAAQ,KAAK,KAAK,KAAK,MAAM,GAAG;AAC7C,QAAM,UAAU,QAAQ,QAAQ,yBAAyB,GAAG;AAC5D,SAAO,GAAG,WAAW,MAAM,EAAE,GAAG,OAAO,GAAG,IAAI;AAChD;AAEO,SAAS,gBAAgB,KAAa,cAAc,GAAW;AACpE,QAAM,IAAI,IAAI,QAAQ,WAAW,EAAE;AACnC,QAAM,MAAM,EAAE,QAAQ,GAAG;AACzB,MAAI,QAAQ,GAAI,QAAO;AACvB,QAAM,MAAM,EACT,MAAM,MAAM,CAAC,EACb,QAAQ,OAAO,EAAE,EACjB,MAAM,GAAG,WAAW;AACvB,SAAO,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,IAAI,GAAG;AAClC;AAEO,MAAM,iBAAiB,CAAC,UAC7B,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,IAAI,QAAQ;","names":[]}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Percentage boundary converters.
3
+ *
4
+ * FormPercentageInput stores PERCENT POINTS in form state (user types 15 →
5
+ * state holds 15) — the UI convention (what you see is what's stored). When a
6
+ * backend or formula expects a FRACTION (0.15), convert explicitly at the
7
+ * boundary with these — one greppable call instead of scattered `/100`s.
8
+ *
9
+ * api.discountRate = percentToFraction(values.discount) // 15 → 0.15
10
+ * defaultValues: { discount: fractionToPercent(api.rate) } // 0.15 → 15
11
+ */
12
+ declare const percentToFraction: (points: number | null) => number | null;
13
+ declare const fractionToPercent: (fraction: number | null) => number | null;
14
+
15
+ export { fractionToPercent, percentToFraction };