@neuronix-tech/ui 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +4 -0
- package/dist/components/button/button.d.ts +18 -0
- package/dist/components/button/button.js +40 -0
- package/dist/components/button/button.js.map +1 -0
- package/dist/components/button/button.types.d.ts +36 -0
- package/dist/components/button/button.types.js +1 -0
- package/dist/components/button/button.types.js.map +1 -0
- package/dist/components/button/button.variants.d.ts +32 -0
- package/dist/components/button/button.variants.js +45 -0
- package/dist/components/button/button.variants.js.map +1 -0
- package/dist/components/button/index.d.ts +8 -0
- package/dist/components/button/index.js +5 -0
- package/dist/components/button/index.js.map +1 -0
- package/dist/components/form-currency-input/form-currency-input.d.ts +12 -0
- package/dist/components/form-currency-input/form-currency-input.js +62 -0
- package/dist/components/form-currency-input/form-currency-input.js.map +1 -0
- package/dist/components/form-currency-input/form-currency-input.types.d.ts +32 -0
- package/dist/components/form-currency-input/form-currency-input.types.js +1 -0
- package/dist/components/form-currency-input/form-currency-input.types.js.map +1 -0
- package/dist/components/form-currency-input/index.d.ts +4 -0
- package/dist/components/form-currency-input/index.js +5 -0
- package/dist/components/form-currency-input/index.js.map +1 -0
- package/dist/components/form-number-input/form-number-input.d.ts +13 -0
- package/dist/components/form-number-input/form-number-input.js +75 -0
- package/dist/components/form-number-input/form-number-input.js.map +1 -0
- package/dist/components/form-number-input/form-number-input.types.d.ts +34 -0
- package/dist/components/form-number-input/form-number-input.types.js +1 -0
- package/dist/components/form-number-input/form-number-input.types.js.map +1 -0
- package/dist/components/form-number-input/index.d.ts +4 -0
- package/dist/components/form-number-input/index.js +5 -0
- package/dist/components/form-number-input/index.js.map +1 -0
- package/dist/components/form-percentage-input/form-percentage-input.d.ts +12 -0
- package/dist/components/form-percentage-input/form-percentage-input.js +65 -0
- package/dist/components/form-percentage-input/form-percentage-input.js.map +1 -0
- package/dist/components/form-percentage-input/form-percentage-input.types.d.ts +33 -0
- package/dist/components/form-percentage-input/form-percentage-input.types.js +1 -0
- package/dist/components/form-percentage-input/form-percentage-input.types.js.map +1 -0
- package/dist/components/form-percentage-input/index.d.ts +4 -0
- package/dist/components/form-percentage-input/index.js +5 -0
- package/dist/components/form-percentage-input/index.js.map +1 -0
- package/dist/components/form-text-input/form-text-input.d.ts +19 -0
- package/dist/components/form-text-input/form-text-input.js +64 -0
- package/dist/components/form-text-input/form-text-input.js.map +1 -0
- package/dist/components/form-text-input/form-text-input.types.d.ts +45 -0
- package/dist/components/form-text-input/form-text-input.types.js +1 -0
- package/dist/components/form-text-input/form-text-input.types.js.map +1 -0
- package/dist/components/form-text-input/index.d.ts +4 -0
- package/dist/components/form-text-input/index.js +5 -0
- package/dist/components/form-text-input/index.js.map +1 -0
- package/dist/components/icon-button/icon-button.d.ts +20 -0
- package/dist/components/icon-button/icon-button.js +32 -0
- package/dist/components/icon-button/icon-button.js.map +1 -0
- package/dist/components/icon-button/icon-button.types.d.ts +24 -0
- package/dist/components/icon-button/icon-button.types.js +1 -0
- package/dist/components/icon-button/icon-button.types.js.map +1 -0
- package/dist/components/icon-button/index.d.ts +8 -0
- package/dist/components/icon-button/index.js +5 -0
- package/dist/components/icon-button/index.js.map +1 -0
- package/dist/components/spinner/index.d.ts +2 -0
- package/dist/components/spinner/index.js +5 -0
- package/dist/components/spinner/index.js.map +1 -0
- package/dist/components/spinner/spinner.d.ts +15 -0
- package/dist/components/spinner/spinner.js +9 -0
- package/dist/components/spinner/spinner.js.map +1 -0
- package/dist/components/toast/index.d.ts +3 -0
- package/dist/components/toast/index.js +8 -0
- package/dist/components/toast/index.js.map +1 -0
- package/dist/components/toast/toast.d.ts +30 -0
- package/dist/components/toast/toast.js +75 -0
- package/dist/components/toast/toast.js.map +1 -0
- package/dist/components/toast/toast.types.d.ts +22 -0
- package/dist/components/toast/toast.types.js +1 -0
- package/dist/components/toast/toast.types.js.map +1 -0
- package/dist/components/toast/toast.variants.d.ts +23 -0
- package/dist/components/toast/toast.variants.js +19 -0
- package/dist/components/toast/toast.variants.js.map +1 -0
- package/dist/hooks/use-number-mask.d.ts +24 -0
- package/dist/hooks/use-number-mask.js +59 -0
- package/dist/hooks/use-number-mask.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/cn.d.ts +10 -0
- package/dist/lib/cn.js +8 -0
- package/dist/lib/cn.js.map +1 -0
- package/dist/lib/format/number.d.ts +6 -0
- package/dist/lib/format/number.js +26 -0
- package/dist/lib/format/number.js.map +1 -0
- package/dist/lib/format/percent.d.ts +15 -0
- package/dist/lib/format/percent.js +7 -0
- package/dist/lib/format/percent.js.map +1 -0
- package/dist/lib/sealed.d.ts +14 -0
- package/dist/lib/sealed.js +1 -0
- package/dist/lib/sealed.js.map +1 -0
- package/dist/primitives/field.d.ts +17 -0
- package/dist/primitives/field.js +74 -0
- package/dist/primitives/field.js.map +1 -0
- package/dist/primitives/form-field.d.ts +27 -0
- package/dist/primitives/form-field.js +27 -0
- package/dist/primitives/form-field.js.map +1 -0
- package/dist/primitives/input-group.d.ts +19 -0
- package/dist/primitives/input-group.js +87 -0
- package/dist/primitives/input-group.js.map +1 -0
- package/dist/primitives/input.d.ts +11 -0
- package/dist/primitives/input.js +22 -0
- package/dist/primitives/input.js.map +1 -0
- package/dist/primitives/label.d.ts +6 -0
- package/dist/primitives/label.js +19 -0
- package/dist/primitives/label.js.map +1 -0
- package/dist/primitives/spinner.d.ts +10 -0
- package/dist/primitives/spinner.js +19 -0
- package/dist/primitives/spinner.js.map +1 -0
- package/dist/primitives/toast.d.ts +31 -0
- package/dist/primitives/toast.js +102 -0
- package/dist/primitives/toast.js.map +1 -0
- package/dist/primitives/tooltip.d.ts +17 -0
- package/dist/primitives/tooltip.js +51 -0
- package/dist/primitives/tooltip.js.map +1 -0
- package/dist/styles.css +2 -0
- package/package.json +70 -3
- package/README.md +0 -3
|
@@ -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
|
+
...props,
|
|
24
|
+
className: cn(buttonVariants({ variant, size: "default" }), fullWidth && "nxui:w-full"),
|
|
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 {...props}\n // className comes AFTER the spread: a Base UI `render` merge (e.g. Toast\n // Action/Close rendering this component) injects its own className prop,\n // and the sealed classes must always win.\n className={cn(buttonVariants({ variant, size: \"default\" }), fullWidth && \"nxui:w-full\")}\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":";AAsCQ,mBAIE,KAJF;AApCR,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,MACrB,GAAG;AAAA,MAIJ,WAAW,GAAG,eAAe,EAAE,SAAS,MAAM,UAAU,CAAC,GAAG,aAAa,aAAa;AAAA,MAErF,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,32 @@
|
|
|
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
|
+
* Border COLOR lives per variant, never in the base: there is no tailwind-merge,
|
|
11
|
+
* so a base `border-transparent` plus outline's `border-border` would both apply
|
|
12
|
+
* and the stylesheet order (not the class order) would pick the winner — which
|
|
13
|
+
* silently erased the outline border. Every class axis is set exactly once.
|
|
14
|
+
*/
|
|
15
|
+
declare const buttonVariants: (props?: ({
|
|
16
|
+
variant?: "default" | "outline" | "secondary" | "ghost" | "destructive" | "link" | null | undefined;
|
|
17
|
+
size?: "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
|
|
18
|
+
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
19
|
+
/** Loading-overlay spinner size per button size (mirrors the icon auto-size). */
|
|
20
|
+
declare const spinnerSizes: {
|
|
21
|
+
readonly default: "nxui:size-3.5";
|
|
22
|
+
readonly xs: "nxui:size-2.5";
|
|
23
|
+
readonly sm: "nxui:size-3";
|
|
24
|
+
readonly lg: "nxui:size-4";
|
|
25
|
+
readonly icon: "nxui:size-3.5";
|
|
26
|
+
readonly "icon-xs": "nxui:size-2.5";
|
|
27
|
+
readonly "icon-sm": "nxui:size-3";
|
|
28
|
+
readonly "icon-lg": "nxui:size-4";
|
|
29
|
+
};
|
|
30
|
+
type ButtonVariantProps = VariantProps<typeof buttonVariants>;
|
|
31
|
+
|
|
32
|
+
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: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:border-transparent 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:border-transparent 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:border-transparent 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:border-transparent 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:border-transparent 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 *\n * Border COLOR lives per variant, never in the base: there is no tailwind-merge,\n * so a base `border-transparent` plus outline's `border-border` would both apply\n * and the stylesheet order (not the class order) would pick the winner — which\n * silently erased the outline border. Every class axis is set exactly once.\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: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:border-transparent 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:border-transparent 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:border-transparent 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:border-transparent 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:border-transparent 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;AAahC,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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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":[]}
|