@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,4 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ console.log("@neuronix-tech/ui init \u2014 coming soon. The theme block format is defined by @neuronix/tokens.");
@@ -0,0 +1,18 @@
1
+ import * as react from 'react';
2
+ import { ButtonProps } from './button.types.js';
3
+ import '@base-ui/react/button';
4
+ import './button.variants.js';
5
+ import 'class-variance-authority/types';
6
+ import 'class-variance-authority';
7
+ import '../../lib/sealed.js';
8
+
9
+ /**
10
+ * The Neuronix button. Pick a `variant`; the height/typography come from the
11
+ * project's density tokens, so every button in the app is the same size.
12
+ * For links, pass your router's Link via `render`:
13
+ *
14
+ * <Button render={<Link href="/clients" />}>View clients</Button>
15
+ */
16
+ declare function Button({ variant, isLoading, fullWidth, disabled, children, tooltip, ...props }: ButtonProps): react.JSX.Element;
17
+
18
+ export { Button };
@@ -0,0 +1,40 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { Button as ButtonPrimitive } from "@base-ui/react/button";
4
+ import { buttonVariants, spinnerSizes } from "./button.variants.js";
5
+ import { cn } from "../../lib/cn.js";
6
+ import { SpinnerPrimitive } from "../../primitives/spinner.js";
7
+ import { TooltipContent, TooltipRoot, TooltipTrigger } from "../../primitives/tooltip.js";
8
+ function Button({
9
+ variant = "default",
10
+ isLoading = false,
11
+ fullWidth = false,
12
+ disabled,
13
+ children,
14
+ tooltip,
15
+ ...props
16
+ }) {
17
+ const button = /* @__PURE__ */ jsx(
18
+ ButtonPrimitive,
19
+ {
20
+ "data-slot": "button",
21
+ "aria-busy": isLoading || void 0,
22
+ disabled: disabled || isLoading,
23
+ className: cn(buttonVariants({ variant, size: "default" }), fullWidth && "nxui:w-full"),
24
+ ...props,
25
+ children: isLoading ? /* @__PURE__ */ jsxs(Fragment, { children: [
26
+ /* @__PURE__ */ jsx("span", { className: "nxui:invisible nxui:contents", children }),
27
+ /* @__PURE__ */ jsx(SpinnerPrimitive, { className: cn("nxui:absolute nxui:inset-0 nxui:m-auto", spinnerSizes.default) })
28
+ ] }) : children
29
+ }
30
+ );
31
+ if (!tooltip) return button;
32
+ return /* @__PURE__ */ jsxs(TooltipRoot, { children: [
33
+ /* @__PURE__ */ jsx(TooltipTrigger, { render: button }),
34
+ /* @__PURE__ */ jsx(TooltipContent, { children: tooltip })
35
+ ] });
36
+ }
37
+ export {
38
+ Button
39
+ };
40
+ //# sourceMappingURL=button.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/button/button.tsx"],"sourcesContent":["\"use client\"\n\nimport { Button as ButtonPrimitive } from \"@base-ui/react/button\"\n\nimport type { ButtonProps } from \"./button.types.js\"\nimport { buttonVariants, spinnerSizes } from \"./button.variants.js\"\nimport { cn } from \"../../lib/cn.js\"\nimport { SpinnerPrimitive } from \"../../primitives/spinner.js\"\nimport { TooltipContent, TooltipRoot, TooltipTrigger } from \"../../primitives/tooltip.js\"\n\n/**\n * The Neuronix button. Pick a `variant`; the height/typography come from the\n * project's density tokens, so every button in the app is the same size.\n * For links, pass your router's Link via `render`:\n *\n * <Button render={<Link href=\"/clients\" />}>View clients</Button>\n */\nfunction Button({\n variant = \"default\",\n isLoading = false,\n fullWidth = false,\n disabled,\n children,\n tooltip,\n ...props\n}: ButtonProps) {\n const button = (\n <ButtonPrimitive\n data-slot=\"button\"\n aria-busy={isLoading || undefined}\n disabled={disabled || isLoading}\n className={cn(buttonVariants({ variant, size: \"default\" }), fullWidth && \"nxui:w-full\")}\n {...props}\n >\n {isLoading ? (\n <>\n {/* `contents` keeps the children as direct flex items (gap, icon padding\n selectors, width all preserved); `invisible` hides them while their\n space is kept, so the button never resizes. */}\n <span className=\"nxui:invisible nxui:contents\">{children}</span>\n <SpinnerPrimitive className={cn(\"nxui:absolute nxui:inset-0 nxui:m-auto\", spinnerSizes.default)} />\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 { Button }\n"],"mappings":";AAmCQ,mBAIE,KAJF;AAjCR,SAAS,UAAU,uBAAuB;AAG1C,SAAS,gBAAgB,oBAAoB;AAC7C,SAAS,UAAU;AACnB,SAAS,wBAAwB;AACjC,SAAS,gBAAgB,aAAa,sBAAsB;AAS5D,SAAS,OAAO;AAAA,EACd,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAgB;AACd,QAAM,SACJ;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,aAAW,aAAa;AAAA,MACxB,UAAU,YAAY;AAAA,MACtB,WAAW,GAAG,eAAe,EAAE,SAAS,MAAM,UAAU,CAAC,GAAG,aAAa,aAAa;AAAA,MACrF,GAAG;AAAA,MAEH,sBACC,iCAIE;AAAA,4BAAC,UAAK,WAAU,gCAAgC,UAAS;AAAA,QACzD,oBAAC,oBAAiB,WAAW,GAAG,0CAA0C,aAAa,OAAO,GAAG;AAAA,SACnG,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,36 @@
1
+ import { Button } from '@base-ui/react/button';
2
+ import { ButtonVariantProps } from './button.variants.js';
3
+ import { Sealed } from '../../lib/sealed.js';
4
+ import 'class-variance-authority/types';
5
+ import 'class-variance-authority';
6
+
7
+ /**
8
+ * Note: there is deliberately NO size prop. Control size is a project-level
9
+ * decision (the density preset chosen at `init`), never a per-call one — that's
10
+ * how every button in an app stays the same height. Icon-only buttons are a
11
+ * separate shape: use <IconButton>.
12
+ */
13
+ interface ButtonProps extends Sealed<Button.Props>, Omit<ButtonVariantProps, "size"> {
14
+ /**
15
+ * Replaces the children with a centered spinner while KEEPING their layout
16
+ * space, so the button never resizes; also disables the button and sets
17
+ * aria-busy. Never render a conditional spinner as children yourself —
18
+ * `{isPending ? <Spinner/> : "Save"}` resizes the button. This prop exists
19
+ * so that pattern stays extinct.
20
+ */
21
+ isLoading?: boolean;
22
+ /**
23
+ * Text shown in a tooltip above the button. Tooltips are a prop, never a
24
+ * wrapper you compose — the library owns delay, positioning, and styling.
25
+ */
26
+ tooltip?: string;
27
+ /**
28
+ * Fills the parent's width. Use ONLY for full-bleed CTAs (login/onboarding),
29
+ * mobile dialog footers, and empty-state actions — never as a default look.
30
+ * Wanting a wide button via a stretch container (flex-col/grid) instead of
31
+ * this prop is an anti-pattern: width must be a visible, deliberate decision.
32
+ */
33
+ fullWidth?: boolean;
34
+ }
35
+
36
+ export type { ButtonProps };
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=button.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,27 @@
1
+ import * as class_variance_authority_types from 'class-variance-authority/types';
2
+ import { VariantProps } from 'class-variance-authority';
3
+
4
+ /**
5
+ * Ported from nx-automations ui/button.tsx with two systematic changes:
6
+ * - every class carries the `nxui:` prefix (sealed, collision-free stylesheet)
7
+ * - fixed heights/text sizes became density tokens (h-7 → h-control-md,
8
+ * text-xs → text-body) so the project's density preset applies globally.
9
+ */
10
+ declare const buttonVariants: (props?: ({
11
+ variant?: "default" | "outline" | "secondary" | "ghost" | "destructive" | "link" | null | undefined;
12
+ size?: "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
13
+ } & class_variance_authority_types.ClassProp) | undefined) => string;
14
+ /** Loading-overlay spinner size per button size (mirrors the icon auto-size). */
15
+ declare const spinnerSizes: {
16
+ readonly default: "nxui:size-3.5";
17
+ readonly xs: "nxui:size-2.5";
18
+ readonly sm: "nxui:size-3";
19
+ readonly lg: "nxui:size-4";
20
+ readonly icon: "nxui:size-3.5";
21
+ readonly "icon-xs": "nxui:size-2.5";
22
+ readonly "icon-sm": "nxui:size-3";
23
+ readonly "icon-lg": "nxui:size-4";
24
+ };
25
+ type ButtonVariantProps = VariantProps<typeof buttonVariants>;
26
+
27
+ export { type ButtonVariantProps, buttonVariants, spinnerSizes };
@@ -0,0 +1,45 @@
1
+ import { cva } from "class-variance-authority";
2
+ const buttonVariants = cva(
3
+ "nxui:group/button nxui:relative nxui:inline-flex nxui:shrink-0 nxui:items-center nxui:justify-center nxui:rounded-md nxui:border nxui:border-transparent nxui:bg-clip-padding nxui:text-body/relaxed nxui:font-medium nxui:whitespace-nowrap nxui:transition-all nxui:outline-none nxui:select-none nxui:focus-visible:border-ring nxui:focus-visible:ring-2 nxui:focus-visible:ring-ring/30 nxui:active:not-aria-[haspopup]:translate-y-px nxui:disabled:pointer-events-none nxui:disabled:opacity-50 nxui:aria-invalid:border-destructive nxui:aria-invalid:ring-2 nxui:aria-invalid:ring-destructive/20 nxui:dark:aria-invalid:border-destructive/50 nxui:dark:aria-invalid:ring-destructive/40 nxui:[&_svg]:pointer-events-none nxui:[&_svg]:shrink-0 nxui:[&_svg:not([class*=size-])]:size-4",
4
+ {
5
+ variants: {
6
+ variant: {
7
+ default: "nxui:bg-primary nxui:text-primary-foreground nxui:hover:bg-primary/80",
8
+ outline: "nxui:border-border nxui:hover:bg-input/50 nxui:hover:text-foreground nxui:aria-expanded:bg-muted nxui:aria-expanded:text-foreground nxui:dark:bg-input/30",
9
+ secondary: "nxui:bg-secondary nxui:text-secondary-foreground nxui:hover:bg-[color-mix(in_oklch,var(--nx-secondary),var(--nx-foreground)_5%)] nxui:aria-expanded:bg-secondary nxui:aria-expanded:text-secondary-foreground",
10
+ ghost: "nxui:hover:bg-muted nxui:hover:text-foreground nxui:aria-expanded:bg-muted nxui:aria-expanded:text-foreground nxui:dark:hover:bg-muted/50",
11
+ destructive: "nxui:bg-destructive/10 nxui:text-destructive nxui:hover:bg-destructive/20 nxui:focus-visible:border-destructive/40 nxui:focus-visible:ring-destructive/20 nxui:dark:bg-destructive/20 nxui:dark:hover:bg-destructive/30 nxui:dark:focus-visible:ring-destructive/40",
12
+ link: "nxui:text-primary nxui:underline-offset-4 nxui:hover:underline"
13
+ },
14
+ size: {
15
+ default: "nxui:h-control-md nxui:gap-1 nxui:px-2 nxui:text-body/relaxed nxui:has-data-[icon=inline-end]:pr-1.5 nxui:has-data-[icon=inline-start]:pl-1.5 nxui:[&_svg:not([class*=size-])]:size-3.5",
16
+ xs: "nxui:h-control-xs nxui:gap-1 nxui:rounded-sm nxui:px-2 nxui:text-body-xs nxui:has-data-[icon=inline-end]:pr-1.5 nxui:has-data-[icon=inline-start]:pl-1.5 nxui:[&_svg:not([class*=size-])]:size-2.5",
17
+ sm: "nxui:h-control-sm nxui:gap-1 nxui:px-2 nxui:text-body/relaxed nxui:has-data-[icon=inline-end]:pr-1.5 nxui:has-data-[icon=inline-start]:pl-1.5 nxui:[&_svg:not([class*=size-])]:size-3",
18
+ lg: "nxui:h-control-lg nxui:gap-1 nxui:px-2.5 nxui:text-body/relaxed nxui:has-data-[icon=inline-end]:pr-2 nxui:has-data-[icon=inline-start]:pl-2 nxui:[&_svg:not([class*=size-])]:size-4",
19
+ icon: "nxui:size-control-md nxui:[&_svg:not([class*=size-])]:size-3.5",
20
+ "icon-xs": "nxui:size-control-xs nxui:rounded-sm nxui:[&_svg:not([class*=size-])]:size-2.5",
21
+ "icon-sm": "nxui:size-control-sm nxui:[&_svg:not([class*=size-])]:size-3",
22
+ "icon-lg": "nxui:size-control-lg nxui:[&_svg:not([class*=size-])]:size-4"
23
+ }
24
+ },
25
+ defaultVariants: {
26
+ variant: "default",
27
+ size: "default"
28
+ }
29
+ }
30
+ );
31
+ const spinnerSizes = {
32
+ default: "nxui:size-3.5",
33
+ xs: "nxui:size-2.5",
34
+ sm: "nxui:size-3",
35
+ lg: "nxui:size-4",
36
+ icon: "nxui:size-3.5",
37
+ "icon-xs": "nxui:size-2.5",
38
+ "icon-sm": "nxui:size-3",
39
+ "icon-lg": "nxui:size-4"
40
+ };
41
+ export {
42
+ buttonVariants,
43
+ spinnerSizes
44
+ };
45
+ //# sourceMappingURL=button.variants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/button/button.variants.ts"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\"\n\n/**\n * Ported from nx-automations ui/button.tsx with two systematic changes:\n * - every class carries the `nxui:` prefix (sealed, collision-free stylesheet)\n * - fixed heights/text sizes became density tokens (h-7 → h-control-md,\n * text-xs → text-body) so the project's density preset applies globally.\n */\nexport const buttonVariants = cva(\n \"nxui:group/button nxui:relative nxui:inline-flex nxui:shrink-0 nxui:items-center nxui:justify-center nxui:rounded-md nxui:border nxui:border-transparent nxui:bg-clip-padding nxui:text-body/relaxed nxui:font-medium nxui:whitespace-nowrap nxui:transition-all nxui:outline-none nxui:select-none nxui:focus-visible:border-ring nxui:focus-visible:ring-2 nxui:focus-visible:ring-ring/30 nxui:active:not-aria-[haspopup]:translate-y-px nxui:disabled:pointer-events-none nxui:disabled:opacity-50 nxui:aria-invalid:border-destructive nxui:aria-invalid:ring-2 nxui:aria-invalid:ring-destructive/20 nxui:dark:aria-invalid:border-destructive/50 nxui:dark:aria-invalid:ring-destructive/40 nxui:[&_svg]:pointer-events-none nxui:[&_svg]:shrink-0 nxui:[&_svg:not([class*=size-])]:size-4\",\n {\n variants: {\n variant: {\n default: \"nxui:bg-primary nxui:text-primary-foreground nxui:hover:bg-primary/80\",\n outline:\n \"nxui:border-border nxui:hover:bg-input/50 nxui:hover:text-foreground nxui:aria-expanded:bg-muted nxui:aria-expanded:text-foreground nxui:dark:bg-input/30\",\n secondary:\n \"nxui:bg-secondary nxui:text-secondary-foreground nxui:hover:bg-[color-mix(in_oklch,var(--nx-secondary),var(--nx-foreground)_5%)] nxui:aria-expanded:bg-secondary nxui:aria-expanded:text-secondary-foreground\",\n ghost:\n \"nxui:hover:bg-muted nxui:hover:text-foreground nxui:aria-expanded:bg-muted nxui:aria-expanded:text-foreground nxui:dark:hover:bg-muted/50\",\n destructive:\n \"nxui:bg-destructive/10 nxui:text-destructive nxui:hover:bg-destructive/20 nxui:focus-visible:border-destructive/40 nxui:focus-visible:ring-destructive/20 nxui:dark:bg-destructive/20 nxui:dark:hover:bg-destructive/30 nxui:dark:focus-visible:ring-destructive/40\",\n link: \"nxui:text-primary nxui:underline-offset-4 nxui:hover:underline\",\n },\n size: {\n default:\n \"nxui:h-control-md nxui:gap-1 nxui:px-2 nxui:text-body/relaxed nxui:has-data-[icon=inline-end]:pr-1.5 nxui:has-data-[icon=inline-start]:pl-1.5 nxui:[&_svg:not([class*=size-])]:size-3.5\",\n xs: \"nxui:h-control-xs nxui:gap-1 nxui:rounded-sm nxui:px-2 nxui:text-body-xs nxui:has-data-[icon=inline-end]:pr-1.5 nxui:has-data-[icon=inline-start]:pl-1.5 nxui:[&_svg:not([class*=size-])]:size-2.5\",\n sm: \"nxui:h-control-sm nxui:gap-1 nxui:px-2 nxui:text-body/relaxed nxui:has-data-[icon=inline-end]:pr-1.5 nxui:has-data-[icon=inline-start]:pl-1.5 nxui:[&_svg:not([class*=size-])]:size-3\",\n lg: \"nxui:h-control-lg nxui:gap-1 nxui:px-2.5 nxui:text-body/relaxed nxui:has-data-[icon=inline-end]:pr-2 nxui:has-data-[icon=inline-start]:pl-2 nxui:[&_svg:not([class*=size-])]:size-4\",\n icon: \"nxui:size-control-md nxui:[&_svg:not([class*=size-])]:size-3.5\",\n \"icon-xs\": \"nxui:size-control-xs nxui:rounded-sm nxui:[&_svg:not([class*=size-])]:size-2.5\",\n \"icon-sm\": \"nxui:size-control-sm nxui:[&_svg:not([class*=size-])]:size-3\",\n \"icon-lg\": \"nxui:size-control-lg nxui:[&_svg:not([class*=size-])]:size-4\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n)\n\n/** Loading-overlay spinner size per button size (mirrors the icon auto-size). */\nexport const spinnerSizes = {\n default: \"nxui:size-3.5\",\n xs: \"nxui:size-2.5\",\n sm: \"nxui:size-3\",\n lg: \"nxui:size-4\",\n icon: \"nxui:size-3.5\",\n \"icon-xs\": \"nxui:size-2.5\",\n \"icon-sm\": \"nxui:size-3\",\n \"icon-lg\": \"nxui:size-4\",\n} as const\n\nexport type ButtonVariantProps = VariantProps<typeof buttonVariants>\n"],"mappings":"AAAA,SAAS,WAA8B;AAQhC,MAAM,iBAAiB;AAAA,EAC5B;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,SAAS;AAAA,QACP,SAAS;AAAA,QACT,SACE;AAAA,QACF,WACE;AAAA,QACF,OACE;AAAA,QACF,aACE;AAAA,QACF,MAAM;AAAA,MACR;AAAA,MACA,MAAM;AAAA,QACJ,SACE;AAAA,QACF,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,WAAW;AAAA,QACX,WAAW;AAAA,QACX,WAAW;AAAA,MACb;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,SAAS;AAAA,MACT,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAGO,MAAM,eAAe;AAAA,EAC1B,SAAS;AAAA,EACT,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,WAAW;AAAA,EACX,WAAW;AAAA,EACX,WAAW;AACb;","names":[]}
@@ -0,0 +1,8 @@
1
+ export { Button } from './button.js';
2
+ export { ButtonProps } from './button.types.js';
3
+ import 'react';
4
+ import '@base-ui/react/button';
5
+ import './button.variants.js';
6
+ import 'class-variance-authority/types';
7
+ import 'class-variance-authority';
8
+ import '../../lib/sealed.js';
@@ -0,0 +1,5 @@
1
+ import { Button } from "./button.js";
2
+ export {
3
+ Button
4
+ };
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/button/index.ts"],"sourcesContent":["export { Button } from \"./button.js\"\nexport type { ButtonProps } from \"./button.types.js\"\n"],"mappings":"AAAA,SAAS,cAAc;","names":[]}
@@ -0,0 +1,12 @@
1
+ import * as react from 'react';
2
+ import { FieldValues, FieldPath } from 'react-hook-form';
3
+ import { FormCurrencyInputProps } from './form-currency-input.types.js';
4
+
5
+ /**
6
+ * Money input. Form state stores the plain amount (`number | null`, e.g.
7
+ * 1234.5); the display groups thousands as the user types and pads to two
8
+ * decimals on blur ("1,234.50"). The currency code is a visual adornment only.
9
+ */
10
+ declare function FormCurrencyInput<T extends FieldValues, N extends FieldPath<T>>({ id, label, optional, field, error, isLoading, disabled, autoFocus, placeholder, labelHint, currency, }: FormCurrencyInputProps<T, N>): react.JSX.Element;
11
+
12
+ export { FormCurrencyInput };
@@ -0,0 +1,62 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useNumberMask } from "../../hooks/use-number-mask.js";
4
+ import { asFiniteNumber, sanitizeDecimal } from "../../lib/format/number.js";
5
+ import { Field, FieldError } from "../../primitives/field.js";
6
+ import { ControlLoadingOverlay, FormFieldLabel } from "../../primitives/form-field.js";
7
+ import { InputPrimitive } from "../../primitives/input.js";
8
+ const formatMoney = (value) => value.toFixed(2);
9
+ function FormCurrencyInput({
10
+ id,
11
+ label,
12
+ optional = false,
13
+ field,
14
+ error,
15
+ isLoading = false,
16
+ disabled = false,
17
+ autoFocus = false,
18
+ placeholder = "0.00",
19
+ labelHint,
20
+ currency = "MXN"
21
+ }) {
22
+ const value = asFiniteNumber(field.value);
23
+ const { ref, display, onChange, reformat } = useNumberMask({
24
+ value,
25
+ sanitize: sanitizeDecimal,
26
+ format: formatMoney,
27
+ onValueChange: field.onChange
28
+ });
29
+ return /* @__PURE__ */ jsxs(Field, { "data-invalid": !!error || void 0, className: "nxui:gap-1", children: [
30
+ /* @__PURE__ */ jsx(FormFieldLabel, { htmlFor: id, label, optional, labelHint }),
31
+ /* @__PURE__ */ jsxs("div", { className: "nxui:relative", children: [
32
+ /* @__PURE__ */ jsx(
33
+ InputPrimitive,
34
+ {
35
+ ref,
36
+ id,
37
+ type: "text",
38
+ inputMode: "decimal",
39
+ autoComplete: "off",
40
+ autoFocus,
41
+ placeholder,
42
+ disabled: disabled || isLoading,
43
+ "aria-invalid": !!error || void 0,
44
+ className: "nxui:pr-12",
45
+ value: display,
46
+ onChange,
47
+ onBlur: () => {
48
+ reformat();
49
+ field.onBlur();
50
+ }
51
+ }
52
+ ),
53
+ /* @__PURE__ */ jsx("span", { className: "nxui:pointer-events-none nxui:absolute nxui:top-1/2 nxui:right-2 nxui:-translate-y-1/2 nxui:text-body nxui:text-muted-foreground", children: currency }),
54
+ isLoading && /* @__PURE__ */ jsx(ControlLoadingOverlay, {})
55
+ ] }),
56
+ /* @__PURE__ */ jsx(FieldError, { errors: [error] })
57
+ ] });
58
+ }
59
+ export {
60
+ FormCurrencyInput
61
+ };
62
+ //# sourceMappingURL=form-currency-input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/form-currency-input/form-currency-input.tsx"],"sourcesContent":["\"use client\"\n\nimport type { FieldPath, FieldValues } from \"react-hook-form\"\n\nimport type { FormCurrencyInputProps } from \"./form-currency-input.types.js\"\nimport { useNumberMask } from \"../../hooks/use-number-mask.js\"\nimport { asFiniteNumber, sanitizeDecimal } from \"../../lib/format/number.js\"\nimport { Field, FieldError } from \"../../primitives/field.js\"\nimport { ControlLoadingOverlay, FormFieldLabel } from \"../../primitives/form-field.js\"\nimport { InputPrimitive } from \"../../primitives/input.js\"\n\nconst formatMoney = (value: number) => value.toFixed(2)\n\n/**\n * Money input. Form state stores the plain amount (`number | null`, e.g.\n * 1234.5); the display groups thousands as the user types and pads to two\n * decimals on blur (\"1,234.50\"). The currency code is a visual adornment only.\n */\nexport function FormCurrencyInput<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 placeholder = \"0.00\",\n labelHint,\n currency = \"MXN\",\n}: FormCurrencyInputProps<T, N>) {\n const value = asFiniteNumber(field.value)\n\n const { ref, display, onChange, reformat } = useNumberMask({\n value,\n sanitize: sanitizeDecimal,\n format: formatMoney,\n onValueChange: field.onChange,\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 <InputPrimitive\n ref={ref}\n id={id}\n type=\"text\"\n inputMode=\"decimal\"\n autoComplete=\"off\"\n autoFocus={autoFocus}\n placeholder={placeholder}\n disabled={disabled || isLoading}\n aria-invalid={!!error || undefined}\n className=\"nxui:pr-12\"\n value={display}\n onChange={onChange}\n onBlur={() => {\n reformat()\n field.onBlur()\n }}\n />\n <span className=\"nxui:pointer-events-none nxui:absolute nxui:top-1/2 nxui:right-2 nxui:-translate-y-1/2 nxui:text-body nxui:text-muted-foreground\">\n {currency}\n </span>\n {isLoading && <ControlLoadingOverlay />}\n </div>\n <FieldError errors={[error]} />\n </Field>\n )\n}\n"],"mappings":";AA0CM,cACA,YADA;AArCN,SAAS,qBAAqB;AAC9B,SAAS,gBAAgB,uBAAuB;AAChD,SAAS,OAAO,kBAAkB;AAClC,SAAS,uBAAuB,sBAAsB;AACtD,SAAS,sBAAsB;AAE/B,MAAM,cAAc,CAAC,UAAkB,MAAM,QAAQ,CAAC;AAO/C,SAAS,kBAAiE;AAAA,EAC/E;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,cAAc;AAAA,EACd;AAAA,EACA,WAAW;AACb,GAAiC;AAC/B,QAAM,QAAQ,eAAe,MAAM,KAAK;AAExC,QAAM,EAAE,KAAK,SAAS,UAAU,SAAS,IAAI,cAAc;AAAA,IACzD;AAAA,IACA,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,eAAe,MAAM;AAAA,EACvB,CAAC;AAED,SACE,qBAAC,SAAM,gBAAc,CAAC,CAAC,SAAS,QAAW,WAAU,cACnD;AAAA,wBAAC,kBAAe,SAAS,IAAI,OAAc,UAAoB,WAAsB;AAAA,IACrF,qBAAC,SAAI,WAAU,iBACb;AAAA;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA;AAAA,UACA,MAAK;AAAA,UACL,WAAU;AAAA,UACV,cAAa;AAAA,UACb;AAAA,UACA;AAAA,UACA,UAAU,YAAY;AAAA,UACtB,gBAAc,CAAC,CAAC,SAAS;AAAA,UACzB,WAAU;AAAA,UACV,OAAO;AAAA,UACP;AAAA,UACA,QAAQ,MAAM;AACZ,qBAAS;AACT,kBAAM,OAAO;AAAA,UACf;AAAA;AAAA,MACF;AAAA,MACA,oBAAC,UAAK,WAAU,oIACb,oBACH;AAAA,MACC,aAAa,oBAAC,yBAAsB;AAAA,OACvC;AAAA,IACA,oBAAC,cAAW,QAAQ,CAAC,KAAK,GAAG;AAAA,KAC/B;AAEJ;","names":[]}
@@ -0,0 +1,32 @@
1
+ import { FieldValues, FieldPath, ControllerRenderProps, FieldError } from 'react-hook-form';
2
+
3
+ /**
4
+ * Closed union — adding a currency is a deliberate library change (minor
5
+ * release), never a per-project string.
6
+ */
7
+ type CurrencyCode = "MXN" | "USD";
8
+ interface FormCurrencyInputProps<T extends FieldValues, N extends FieldPath<T>> {
9
+ id: string;
10
+ label: string;
11
+ /** Required is the DEFAULT: non-optional fields render "label *". */
12
+ optional?: boolean;
13
+ /**
14
+ * react-hook-form binding. Required: the mask owns the value round-trip.
15
+ * Form state stores the PLAIN AMOUNT as `number | null` (e.g. 1234.5) —
16
+ * the grouped/padded display string and the currency code never enter state.
17
+ */
18
+ field: ControllerRenderProps<T, N>;
19
+ /** react-hook-form field error; presence turns the field invalid. */
20
+ error?: FieldError;
21
+ /** Spinner overlay while an async source seeds the value; also disables the control. */
22
+ isLoading?: boolean;
23
+ disabled?: boolean;
24
+ autoFocus?: boolean;
25
+ placeholder?: string;
26
+ /** Explanation behind the label (dotted underline + tooltip). */
27
+ labelHint?: string;
28
+ /** Visual adornment only — never part of the value. */
29
+ currency?: CurrencyCode;
30
+ }
31
+
32
+ export type { CurrencyCode, FormCurrencyInputProps };
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=form-currency-input.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,4 @@
1
+ export { FormCurrencyInput } from './form-currency-input.js';
2
+ export { CurrencyCode, FormCurrencyInputProps } from './form-currency-input.types.js';
3
+ import 'react';
4
+ import 'react-hook-form';
@@ -0,0 +1,5 @@
1
+ import { FormCurrencyInput } from "./form-currency-input.js";
2
+ export {
3
+ FormCurrencyInput
4
+ };
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/form-currency-input/index.ts"],"sourcesContent":["export { FormCurrencyInput } from \"./form-currency-input.js\"\nexport type { FormCurrencyInputProps, CurrencyCode } from \"./form-currency-input.types.js\"\n"],"mappings":"AAAA,SAAS,yBAAyB;","names":[]}
@@ -0,0 +1,13 @@
1
+ import * as react from 'react';
2
+ import { FieldValues, FieldPath } from 'react-hook-form';
3
+ import { FormNumberInputProps } from './form-number-input.types.js';
4
+
5
+ /**
6
+ * Numeric input built on type="text" — avoids the rough edges of
7
+ * `<input type="number">` (spinner arrows, scroll-to-change, unclearable).
8
+ * Live-groups thousands with a caret-stable mask; the form stores
9
+ * `number | null`, never the display string.
10
+ */
11
+ declare function FormNumberInput<T extends FieldValues, N extends FieldPath<T>>({ id, label, optional, field, error, isLoading, disabled, autoFocus, placeholder, labelHint, allowDecimals, allowNegatives, grouped, }: FormNumberInputProps<T, N>): react.JSX.Element;
12
+
13
+ export { FormNumberInput };
@@ -0,0 +1,75 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useNumberMask } from "../../hooks/use-number-mask.js";
4
+ import { asFiniteNumber } from "../../lib/format/number.js";
5
+ import { Field, FieldError } from "../../primitives/field.js";
6
+ import { ControlLoadingOverlay, FormFieldLabel } from "../../primitives/form-field.js";
7
+ import { InputPrimitive } from "../../primitives/input.js";
8
+ function sanitizeNumber(raw, allowDecimals, allowNegatives) {
9
+ const negative = allowNegatives && raw.startsWith("-");
10
+ const digits = raw.replace(allowDecimals ? /[^\d.]/g : /[^\d]/g, "");
11
+ let body = digits;
12
+ if (allowDecimals) {
13
+ const dot = digits.indexOf(".");
14
+ if (dot !== -1) {
15
+ body = digits.slice(0, dot + 1) + digits.slice(dot + 1).replace(/\./g, "");
16
+ }
17
+ }
18
+ return `${negative ? "-" : ""}${body}`;
19
+ }
20
+ const formatNumber = (value) => String(value);
21
+ function FormNumberInput({
22
+ id,
23
+ label,
24
+ optional = false,
25
+ field,
26
+ error,
27
+ isLoading = false,
28
+ disabled = false,
29
+ autoFocus = false,
30
+ placeholder,
31
+ labelHint,
32
+ allowDecimals = false,
33
+ allowNegatives = false,
34
+ grouped = true
35
+ }) {
36
+ const value = asFiniteNumber(field.value);
37
+ const { ref, display, onChange, reformat } = useNumberMask({
38
+ value,
39
+ sanitize: (raw) => sanitizeNumber(raw, allowDecimals, allowNegatives),
40
+ format: formatNumber,
41
+ onValueChange: field.onChange,
42
+ group: grouped
43
+ });
44
+ return /* @__PURE__ */ jsxs(Field, { "data-invalid": !!error || void 0, className: "nxui:gap-1", children: [
45
+ /* @__PURE__ */ jsx(FormFieldLabel, { htmlFor: id, label, optional, labelHint }),
46
+ /* @__PURE__ */ jsxs("div", { className: "nxui:relative", children: [
47
+ /* @__PURE__ */ jsx(
48
+ InputPrimitive,
49
+ {
50
+ ref,
51
+ id,
52
+ type: "text",
53
+ inputMode: allowDecimals ? "decimal" : "numeric",
54
+ autoComplete: "off",
55
+ autoFocus,
56
+ placeholder,
57
+ disabled: disabled || isLoading,
58
+ "aria-invalid": !!error || void 0,
59
+ value: display,
60
+ onChange,
61
+ onBlur: () => {
62
+ reformat();
63
+ field.onBlur();
64
+ }
65
+ }
66
+ ),
67
+ isLoading && /* @__PURE__ */ jsx(ControlLoadingOverlay, {})
68
+ ] }),
69
+ /* @__PURE__ */ jsx(FieldError, { errors: [error] })
70
+ ] });
71
+ }
72
+ export {
73
+ FormNumberInput
74
+ };
75
+ //# sourceMappingURL=form-number-input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/form-number-input/form-number-input.tsx"],"sourcesContent":["\"use client\"\n\nimport type { FieldPath, FieldValues } from \"react-hook-form\"\n\nimport type { FormNumberInputProps } from \"./form-number-input.types.js\"\nimport { useNumberMask } from \"../../hooks/use-number-mask.js\"\nimport { asFiniteNumber } from \"../../lib/format/number.js\"\nimport { Field, FieldError } from \"../../primitives/field.js\"\nimport { ControlLoadingOverlay, FormFieldLabel } from \"../../primitives/form-field.js\"\nimport { InputPrimitive } from \"../../primitives/input.js\"\n\nfunction sanitizeNumber(raw: string, allowDecimals: boolean, allowNegatives: boolean): string {\n const negative = allowNegatives && raw.startsWith(\"-\")\n const digits = raw.replace(allowDecimals ? /[^\\d.]/g : /[^\\d]/g, \"\")\n let body = digits\n if (allowDecimals) {\n const dot = digits.indexOf(\".\")\n if (dot !== -1) {\n body = digits.slice(0, dot + 1) + digits.slice(dot + 1).replace(/\\./g, \"\")\n }\n }\n return `${negative ? \"-\" : \"\"}${body}`\n}\n\nconst formatNumber = (value: number) => String(value)\n\n/**\n * Numeric input built on type=\"text\" — avoids the rough edges of\n * `<input type=\"number\">` (spinner arrows, scroll-to-change, unclearable).\n * Live-groups thousands with a caret-stable mask; the form stores\n * `number | null`, never the display string.\n */\nexport function FormNumberInput<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 placeholder,\n labelHint,\n allowDecimals = false,\n allowNegatives = false,\n grouped = true,\n}: FormNumberInputProps<T, N>) {\n const value = asFiniteNumber(field.value)\n\n const { ref, display, onChange, reformat } = useNumberMask({\n value,\n sanitize: (raw) => sanitizeNumber(raw, allowDecimals, allowNegatives),\n format: formatNumber,\n onValueChange: field.onChange,\n group: grouped,\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 <InputPrimitive\n ref={ref}\n id={id}\n type=\"text\"\n inputMode={allowDecimals ? \"decimal\" : \"numeric\"}\n autoComplete=\"off\"\n autoFocus={autoFocus}\n placeholder={placeholder}\n disabled={disabled || isLoading}\n aria-invalid={!!error || undefined}\n value={display}\n onChange={onChange}\n onBlur={() => {\n reformat()\n field.onBlur()\n }}\n />\n {isLoading && <ControlLoadingOverlay />}\n </div>\n <FieldError errors={[error]} />\n </Field>\n )\n}\n"],"mappings":";AA2DM,cACA,YADA;AAtDN,SAAS,qBAAqB;AAC9B,SAAS,sBAAsB;AAC/B,SAAS,OAAO,kBAAkB;AAClC,SAAS,uBAAuB,sBAAsB;AACtD,SAAS,sBAAsB;AAE/B,SAAS,eAAe,KAAa,eAAwB,gBAAiC;AAC5F,QAAM,WAAW,kBAAkB,IAAI,WAAW,GAAG;AACrD,QAAM,SAAS,IAAI,QAAQ,gBAAgB,YAAY,UAAU,EAAE;AACnE,MAAI,OAAO;AACX,MAAI,eAAe;AACjB,UAAM,MAAM,OAAO,QAAQ,GAAG;AAC9B,QAAI,QAAQ,IAAI;AACd,aAAO,OAAO,MAAM,GAAG,MAAM,CAAC,IAAI,OAAO,MAAM,MAAM,CAAC,EAAE,QAAQ,OAAO,EAAE;AAAA,IAC3E;AAAA,EACF;AACA,SAAO,GAAG,WAAW,MAAM,EAAE,GAAG,IAAI;AACtC;AAEA,MAAM,eAAe,CAAC,UAAkB,OAAO,KAAK;AAQ7C,SAAS,gBAA+D;AAAA,EAC7E;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB,iBAAiB;AAAA,EACjB,UAAU;AACZ,GAA+B;AAC7B,QAAM,QAAQ,eAAe,MAAM,KAAK;AAExC,QAAM,EAAE,KAAK,SAAS,UAAU,SAAS,IAAI,cAAc;AAAA,IACzD;AAAA,IACA,UAAU,CAAC,QAAQ,eAAe,KAAK,eAAe,cAAc;AAAA,IACpE,QAAQ;AAAA,IACR,eAAe,MAAM;AAAA,IACrB,OAAO;AAAA,EACT,CAAC;AAED,SACE,qBAAC,SAAM,gBAAc,CAAC,CAAC,SAAS,QAAW,WAAU,cACnD;AAAA,wBAAC,kBAAe,SAAS,IAAI,OAAc,UAAoB,WAAsB;AAAA,IACrF,qBAAC,SAAI,WAAU,iBACb;AAAA;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA;AAAA,UACA,MAAK;AAAA,UACL,WAAW,gBAAgB,YAAY;AAAA,UACvC,cAAa;AAAA,UACb;AAAA,UACA;AAAA,UACA,UAAU,YAAY;AAAA,UACtB,gBAAc,CAAC,CAAC,SAAS;AAAA,UACzB,OAAO;AAAA,UACP;AAAA,UACA,QAAQ,MAAM;AACZ,qBAAS;AACT,kBAAM,OAAO;AAAA,UACf;AAAA;AAAA,MACF;AAAA,MACC,aAAa,oBAAC,yBAAsB;AAAA,OACvC;AAAA,IACA,oBAAC,cAAW,QAAQ,CAAC,KAAK,GAAG;AAAA,KAC/B;AAEJ;","names":[]}
@@ -0,0 +1,34 @@
1
+ import { FieldValues, FieldPath, ControllerRenderProps, FieldError } from 'react-hook-form';
2
+
3
+ interface FormNumberInputProps<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 from a <Controller>. Required here (unlike
10
+ * FormTextInput): the mask must own the value round-trip — form state stores
11
+ * `number | null`, the grouped display string never leaves the component.
12
+ */
13
+ field: ControllerRenderProps<T, N>;
14
+ /** react-hook-form field error; presence turns the field invalid. */
15
+ error?: FieldError;
16
+ /** Spinner overlay while an async source seeds the value; also disables the control. */
17
+ isLoading?: boolean;
18
+ disabled?: boolean;
19
+ autoFocus?: boolean;
20
+ placeholder?: string;
21
+ /** Explanation behind the label (dotted underline + tooltip). */
22
+ labelHint?: string;
23
+ /** Allow a single decimal point. Off by default (counts, quantities). */
24
+ allowDecimals?: boolean;
25
+ /** Allow a leading minus sign. Off by default. */
26
+ allowNegatives?: boolean;
27
+ /**
28
+ * Group thousands while typing (1,234,567). Pass false for years, indices,
29
+ * or any count where "2,026" would be wrong.
30
+ */
31
+ grouped?: boolean;
32
+ }
33
+
34
+ export type { FormNumberInputProps };
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=form-number-input.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,4 @@
1
+ export { FormNumberInput } from './form-number-input.js';
2
+ export { FormNumberInputProps } from './form-number-input.types.js';
3
+ import 'react';
4
+ import 'react-hook-form';
@@ -0,0 +1,5 @@
1
+ import { FormNumberInput } from "./form-number-input.js";
2
+ export {
3
+ FormNumberInput
4
+ };
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/form-number-input/index.ts"],"sourcesContent":["export { FormNumberInput } from \"./form-number-input.js\"\nexport type { FormNumberInputProps } from \"./form-number-input.types.js\"\n"],"mappings":"AAAA,SAAS,uBAAuB;","names":[]}
@@ -0,0 +1,12 @@
1
+ import * as react from 'react';
2
+ import { FieldValues, FieldPath } from 'react-hook-form';
3
+ import { FormPercentageInputProps } from './form-percentage-input.types.js';
4
+
5
+ /**
6
+ * Percentage input. Form state stores PERCENT POINTS (`number | null`: typing
7
+ * 15 stores 15, never 0.15) and clamps to [min, max] on blur. Convert to a
8
+ * fraction at the boundary with `percentToFraction` when the math needs it.
9
+ */
10
+ declare function FormPercentageInput<T extends FieldValues, N extends FieldPath<T>>({ id, label, optional, field, error, isLoading, disabled, autoFocus, placeholder, labelHint, min, max, }: FormPercentageInputProps<T, N>): react.JSX.Element;
11
+
12
+ export { FormPercentageInput };
@@ -0,0 +1,65 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useNumberMask } from "../../hooks/use-number-mask.js";
4
+ import { asFiniteNumber, sanitizeDecimal } from "../../lib/format/number.js";
5
+ import { Field, FieldError } from "../../primitives/field.js";
6
+ import { ControlLoadingOverlay, FormFieldLabel } from "../../primitives/form-field.js";
7
+ import { InputPrimitive } from "../../primitives/input.js";
8
+ const formatPercent = (value) => String(value);
9
+ function FormPercentageInput({
10
+ id,
11
+ label,
12
+ optional = false,
13
+ field,
14
+ error,
15
+ isLoading = false,
16
+ disabled = false,
17
+ autoFocus = false,
18
+ placeholder = "0",
19
+ labelHint,
20
+ min = 0,
21
+ max = 100
22
+ }) {
23
+ const value = asFiniteNumber(field.value);
24
+ const { ref, display, onChange, reformat } = useNumberMask({
25
+ value,
26
+ sanitize: sanitizeDecimal,
27
+ format: formatPercent,
28
+ onValueChange: field.onChange
29
+ });
30
+ return /* @__PURE__ */ jsxs(Field, { "data-invalid": !!error || void 0, className: "nxui:gap-1", children: [
31
+ /* @__PURE__ */ jsx(FormFieldLabel, { htmlFor: id, label, optional, labelHint }),
32
+ /* @__PURE__ */ jsxs("div", { className: "nxui:relative", children: [
33
+ /* @__PURE__ */ jsx(
34
+ InputPrimitive,
35
+ {
36
+ ref,
37
+ id,
38
+ type: "text",
39
+ inputMode: "decimal",
40
+ autoComplete: "off",
41
+ autoFocus,
42
+ placeholder,
43
+ disabled: disabled || isLoading,
44
+ "aria-invalid": !!error || void 0,
45
+ className: "nxui:pr-8",
46
+ value: display,
47
+ onChange,
48
+ onBlur: () => {
49
+ const clamped = value !== null ? Math.min(Math.max(value, min), max) : null;
50
+ if (clamped !== value) field.onChange(clamped);
51
+ reformat(clamped);
52
+ field.onBlur();
53
+ }
54
+ }
55
+ ),
56
+ /* @__PURE__ */ jsx("span", { className: "nxui:pointer-events-none nxui:absolute nxui:top-1/2 nxui:right-2 nxui:-translate-y-1/2 nxui:text-body nxui:text-muted-foreground", children: "%" }),
57
+ isLoading && /* @__PURE__ */ jsx(ControlLoadingOverlay, {})
58
+ ] }),
59
+ /* @__PURE__ */ jsx(FieldError, { errors: [error] })
60
+ ] });
61
+ }
62
+ export {
63
+ FormPercentageInput
64
+ };
65
+ //# sourceMappingURL=form-percentage-input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/form-percentage-input/form-percentage-input.tsx"],"sourcesContent":["\"use client\"\n\nimport type { FieldPath, FieldValues } from \"react-hook-form\"\n\nimport type { FormPercentageInputProps } from \"./form-percentage-input.types.js\"\nimport { useNumberMask } from \"../../hooks/use-number-mask.js\"\nimport { asFiniteNumber, sanitizeDecimal } from \"../../lib/format/number.js\"\nimport { Field, FieldError } from \"../../primitives/field.js\"\nimport { ControlLoadingOverlay, FormFieldLabel } from \"../../primitives/form-field.js\"\nimport { InputPrimitive } from \"../../primitives/input.js\"\n\nconst formatPercent = (value: number) => String(value)\n\n/**\n * Percentage input. Form state stores PERCENT POINTS (`number | null`: typing\n * 15 stores 15, never 0.15) and clamps to [min, max] on blur. Convert to a\n * fraction at the boundary with `percentToFraction` when the math needs it.\n */\nexport function FormPercentageInput<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 placeholder = \"0\",\n labelHint,\n min = 0,\n max = 100,\n}: FormPercentageInputProps<T, N>) {\n const value = asFiniteNumber(field.value)\n\n const { ref, display, onChange, reformat } = useNumberMask({\n value,\n sanitize: sanitizeDecimal,\n format: formatPercent,\n onValueChange: field.onChange,\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 <InputPrimitive\n ref={ref}\n id={id}\n type=\"text\"\n inputMode=\"decimal\"\n autoComplete=\"off\"\n autoFocus={autoFocus}\n placeholder={placeholder}\n disabled={disabled || isLoading}\n aria-invalid={!!error || undefined}\n className=\"nxui:pr-8\"\n value={display}\n onChange={onChange}\n onBlur={() => {\n const clamped = value !== null ? Math.min(Math.max(value, min), max) : null\n if (clamped !== value) field.onChange(clamped)\n reformat(clamped)\n field.onBlur()\n }}\n />\n <span className=\"nxui:pointer-events-none nxui:absolute nxui:top-1/2 nxui:right-2 nxui:-translate-y-1/2 nxui:text-body nxui:text-muted-foreground\">\n %\n </span>\n {isLoading && <ControlLoadingOverlay />}\n </div>\n <FieldError errors={[error]} />\n </Field>\n )\n}\n"],"mappings":";AA2CM,cACA,YADA;AAtCN,SAAS,qBAAqB;AAC9B,SAAS,gBAAgB,uBAAuB;AAChD,SAAS,OAAO,kBAAkB;AAClC,SAAS,uBAAuB,sBAAsB;AACtD,SAAS,sBAAsB;AAE/B,MAAM,gBAAgB,CAAC,UAAkB,OAAO,KAAK;AAO9C,SAAS,oBAAmE;AAAA,EACjF;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,cAAc;AAAA,EACd;AAAA,EACA,MAAM;AAAA,EACN,MAAM;AACR,GAAmC;AACjC,QAAM,QAAQ,eAAe,MAAM,KAAK;AAExC,QAAM,EAAE,KAAK,SAAS,UAAU,SAAS,IAAI,cAAc;AAAA,IACzD;AAAA,IACA,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,eAAe,MAAM;AAAA,EACvB,CAAC;AAED,SACE,qBAAC,SAAM,gBAAc,CAAC,CAAC,SAAS,QAAW,WAAU,cACnD;AAAA,wBAAC,kBAAe,SAAS,IAAI,OAAc,UAAoB,WAAsB;AAAA,IACrF,qBAAC,SAAI,WAAU,iBACb;AAAA;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA;AAAA,UACA,MAAK;AAAA,UACL,WAAU;AAAA,UACV,cAAa;AAAA,UACb;AAAA,UACA;AAAA,UACA,UAAU,YAAY;AAAA,UACtB,gBAAc,CAAC,CAAC,SAAS;AAAA,UACzB,WAAU;AAAA,UACV,OAAO;AAAA,UACP;AAAA,UACA,QAAQ,MAAM;AACZ,kBAAM,UAAU,UAAU,OAAO,KAAK,IAAI,KAAK,IAAI,OAAO,GAAG,GAAG,GAAG,IAAI;AACvE,gBAAI,YAAY,MAAO,OAAM,SAAS,OAAO;AAC7C,qBAAS,OAAO;AAChB,kBAAM,OAAO;AAAA,UACf;AAAA;AAAA,MACF;AAAA,MACA,oBAAC,UAAK,WAAU,oIAAmI,eAEnJ;AAAA,MACC,aAAa,oBAAC,yBAAsB;AAAA,OACvC;AAAA,IACA,oBAAC,cAAW,QAAQ,CAAC,KAAK,GAAG;AAAA,KAC/B;AAEJ;","names":[]}