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