@kashi-ui/react 0.3.0 → 0.4.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/README.md CHANGED
@@ -22,6 +22,6 @@ export function SignUp() {
22
22
 
23
23
  کامپوننت‌ها: Button · Input · Textarea · Badge · Card · Alert · Avatar · AvatarGroup · Checkbox · Switch
24
24
 
25
- فارسی: DatePicker و Calendar شمسی · NationalIdInput · CardNumberInput
25
+ فارسی: DatePicker و Calendar شمسی · NationalIdInput · CardNumberInput · IbanInput · MobileInput · PriceInput
26
26
 
27
27
  نیازمند React ۱۸ یا بالاتر. مستندات کامل: [kashi.sepantapouya.com](https://kashi.sepantapouya.com) · کد: [GitHub](https://github.com/Sepantapouya/Kashi) · لایسنس MIT
@@ -0,0 +1,7 @@
1
+ import type { Bank } from "@kashi-ui/persian";
2
+ /** نام و لوگوی بانک داخل فیلد؛ نام با id به فیلد متصل می‌شود و لوگو تزئینی است */
3
+ export declare function BankBadge({ bank, id, showLogo }: {
4
+ bank: Bank | null;
5
+ id: string;
6
+ showLogo: boolean;
7
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ /** لوگوی بانک را فقط هنگام نیاز بارگذاری می‌کند (code-split)، تا حجم اولیهٔ برنامه زیاد نشود */
4
+ function useBankLogo(slug, enabled) {
5
+ const [uri, setUri] = useState(null);
6
+ useEffect(() => {
7
+ if (!slug || !enabled) {
8
+ setUri(null);
9
+ return;
10
+ }
11
+ let alive = true;
12
+ import("@kashi-ui/bank-logos")
13
+ .then((m) => alive && setUri(m.bankLogoDataUri(slug)))
14
+ .catch(() => alive && setUri(null));
15
+ return () => {
16
+ alive = false;
17
+ };
18
+ }, [slug, enabled]);
19
+ return uri;
20
+ }
21
+ /** نام و لوگوی بانک داخل فیلد؛ نام با id به فیلد متصل می‌شود و لوگو تزئینی است */
22
+ export function BankBadge({ bank, id, showLogo }) {
23
+ const logo = useBankLogo(bank?.slug, showLogo);
24
+ if (!bank)
25
+ return null;
26
+ return (_jsxs("span", { className: "k-bank", id: id, children: [logo && _jsx("img", { className: "k-bank__logo", src: logo, alt: "", width: 24, height: 24 }), bank.name] }));
27
+ }
@@ -11,9 +11,11 @@ export interface CardNumberInputProps extends Base {
11
11
  digits?: "fa" | "latin";
12
12
  /** نمایش نام بانک از روی شش رقم اول @default true */
13
13
  showBank?: boolean;
14
+ /** نمایش لوگوی بانک (فقط هنگام نیاز بارگذاری می‌شود) @default true */
15
+ showBankLogo?: boolean;
14
16
  /** @default "شماره کارت معتبر نیست." */
15
17
  invalidMessage?: ReactNode;
16
18
  }
17
- /** فیلد شماره کارت بانکی: گروه‌بندی چهارتایی، تشخیص بانک و بررسی Luhn */
19
+ /** فیلد شماره کارت بانکی: گروه‌بندی چهارتایی، تشخیص بانک با لوگو و بررسی Luhn */
18
20
  export declare const CardNumberInput: import("react").ForwardRefExoticComponent<CardNumberInputProps & import("react").RefAttributes<HTMLInputElement>>;
19
21
  export {};
@@ -1,50 +1,24 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { forwardRef, useId, useLayoutEffect, useRef, useState } from "react";
3
- import { formatCardNumber, getBankByCard, isValidCardNumber, onlyDigits, toFaDigits } from "@kashi-ui/persian";
2
+ import { forwardRef, useId, useState } from "react";
3
+ import { formatCardNumber, getBankByCard, isValidCardNumber } from "@kashi-ui/persian";
4
+ import { BankBadge } from "./BankBadge.js";
4
5
  import { Input } from "./Input.js";
6
+ import { useDigitsInput } from "./useDigitsInput.js";
5
7
  import { mergeRefs } from "./utils.js";
6
- /** شمارهٔ ارقام قبل از مکان‌نما را به موقعیت در متن قالب‌بندی‌شده تبدیل می‌کند */
7
- function caretFor(formatted, digitCount) {
8
- if (digitCount === 0)
9
- return 0;
10
- let seen = 0;
11
- for (let i = 0; i < formatted.length; i++) {
12
- if (/[0-9۰-۹]/.test(formatted[i]) && ++seen === digitCount)
13
- return i + 1;
14
- }
15
- return formatted.length;
16
- }
17
- /** فیلد شماره کارت بانکی: گروه‌بندی چهارتایی، تشخیص بانک و بررسی Luhn */
18
- export const CardNumberInput = forwardRef(function CardNumberInput({ value, defaultValue = "", onValueChange, digits = "fa", showBank = true, invalidMessage = "شماره کارت معتبر نیست.", error, name, id, onBlur, placeholder, ...rest }, ref) {
8
+ /** فیلد شماره کارت بانکی: گروه‌بندی چهارتایی، تشخیص بانک با لوگو و بررسی Luhn */
9
+ export const CardNumberInput = forwardRef(function CardNumberInput({ value, defaultValue, onValueChange, digits = "fa", showBank = true, showBankLogo = true, invalidMessage = "شماره کارت معتبر نیست.", error, name, id, onBlur, placeholder, ...rest }, ref) {
19
10
  const autoId = useId();
20
11
  const inputId = id ?? autoId;
21
- const inputRef = useRef(null);
22
- const caret = useRef(null);
23
- const [inner, setInner] = useState(() => onlyDigits(defaultValue).slice(0, 16));
24
12
  const [touched, setTouched] = useState(false);
25
- const raw = value !== undefined ? onlyDigits(value).slice(0, 16) : inner;
26
- const formatted = formatCardNumber(raw);
27
- const shown = digits === "fa" ? toFaDigits(formatted) : formatted;
13
+ const { raw, shown, inputRef, handleChange } = useDigitsInput({ value, defaultValue, maxDigits: 16, format: formatCardNumber, digits });
28
14
  const bank = showBank ? getBankByCard(raw) : null;
29
15
  const showInvalid = raw.length > 0 && (touched || raw.length === 16) && !isValidCardNumber(raw);
30
16
  const bankId = `${inputId}-bank`;
31
- // پس از قالب‌بندی دوباره، مکان‌نما را سر جای منطقی‌اش برمی‌گرداند
32
- useLayoutEffect(() => {
33
- if (caret.current !== null && inputRef.current === document.activeElement) {
34
- const pos = caretFor(shown, caret.current);
35
- inputRef.current?.setSelectionRange(pos, pos);
36
- caret.current = null;
37
- }
38
- });
39
17
  return (_jsxs(_Fragment, { children: [_jsx(Input, { ref: mergeRefs(ref, inputRef), id: inputId, type: "text", inputMode: "numeric", autoComplete: "cc-number", dir: "ltr", placeholder: placeholder ?? (digits === "fa" ? "۰۰۰۰ ۰۰۰۰ ۰۰۰۰ ۰۰۰۰" : "0000 0000 0000 0000"), value: shown, onChange: (e) => {
40
- const el = e.target;
41
- caret.current = onlyDigits(el.value.slice(0, el.selectionStart ?? el.value.length)).length;
42
- const next = onlyDigits(el.value).slice(0, 16);
43
- if (value === undefined)
44
- setInner(next);
18
+ const next = handleChange(e);
45
19
  onValueChange?.(next, isValidCardNumber(next), getBankByCard(next)?.name ?? null);
46
20
  }, onBlur: (e) => {
47
21
  setTouched(true);
48
22
  onBlur?.(e);
49
- }, addonEnd: bank ? _jsx("span", { className: "k-input__meta", id: bankId, children: bank.name }) : undefined, "aria-describedby": bank ? bankId : undefined, error: error ?? (showInvalid ? invalidMessage : undefined), ...rest }), name && _jsx("input", { type: "hidden", name: name, value: raw })] }));
23
+ }, addonEnd: bank ? _jsx(BankBadge, { bank: bank, id: bankId, showLogo: showBankLogo }) : undefined, "aria-describedby": bank ? bankId : undefined, error: error ?? (showInvalid ? invalidMessage : undefined), ...rest }), name && _jsx("input", { type: "hidden", name: name, value: raw })] }));
50
24
  });
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { forwardRef, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
2
+ import { forwardRef, useEffect, useId, useRef, useState } from "react";
3
3
  import { formatJalali, parseJalali, toISODate } from "@kashi-ui/persian";
4
4
  import { Calendar, sameDay } from "./Calendar.js";
5
5
  import { Input } from "./Input.js";
6
- import { mergeRefs } from "./utils.js";
6
+ import { mergeRefs, useIsomorphicLayoutEffect } from "./utils.js";
7
7
  /**
8
8
  * انتخاب تاریخ شمسی: تایپ مستقیم (۱۴۰۳/۰۵/۱۲) یا انتخاب از تقویم.
9
9
  * با name، مقدار میلادی به شکل YYYY-MM-DD در فرم ارسال می‌شود.
@@ -27,7 +27,7 @@ export const DatePicker = forwardRef(function DatePicker({ value, defaultValue =
27
27
  setInvalid(false);
28
28
  }, [selected?.getTime(), digits]);
29
29
  // تقویم درست زیر کادر فیلد باز می‌شود (نه زیر متن راهنما)
30
- useLayoutEffect(() => {
30
+ useIsomorphicLayoutEffect(() => {
31
31
  if (!open || !wrap.current)
32
32
  return;
33
33
  const box = wrap.current.querySelector(".k-input");
@@ -0,0 +1,21 @@
1
+ import { type ReactNode } from "react";
2
+ import { type InputProps } from "./Input.js";
3
+ type Base = Omit<InputProps, "value" | "defaultValue" | "onChange" | "type" | "inputMode" | "addonEnd">;
4
+ export interface IbanInputProps extends Base {
5
+ /** مقدار کنترل‌شده: «IR» + ۲۴ رقم یا فقط ۲۴ رقم */
6
+ value?: string;
7
+ defaultValue?: string;
8
+ /** با هر تغییر: شبای کامل «IR…» بدون فاصله، معتبر بودن و نام بانک */
9
+ onValueChange?: (value: string, valid: boolean, bank: string | null) => void;
10
+ /** شبا معمولاً با ارقام لاتین نوشته می‌شود @default "latin" */
11
+ digits?: "fa" | "latin";
12
+ /** @default true */
13
+ showBank?: boolean;
14
+ /** @default true */
15
+ showBankLogo?: boolean;
16
+ /** @default "شماره شبا معتبر نیست." */
17
+ invalidMessage?: ReactNode;
18
+ }
19
+ /** فیلد شماره شبا: IR خودکار، گروه‌بندی، تشخیص بانک با لوگو و بررسی رقم‌های کنترل */
20
+ export declare const IbanInput: import("react").ForwardRefExoticComponent<IbanInputProps & import("react").RefAttributes<HTMLInputElement>>;
21
+ export {};
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useId, useState } from "react";
3
+ import { formatIban, getBankByIban, isValidIban } from "@kashi-ui/persian";
4
+ import { BankBadge } from "./BankBadge.js";
5
+ import { Input } from "./Input.js";
6
+ import { useDigitsInput } from "./useDigitsInput.js";
7
+ import { mergeRefs } from "./utils.js";
8
+ const ibanOf = (d) => (d ? `IR${d}` : "");
9
+ /** فیلد شماره شبا: IR خودکار، گروه‌بندی، تشخیص بانک با لوگو و بررسی رقم‌های کنترل */
10
+ export const IbanInput = forwardRef(function IbanInput({ value, defaultValue, onValueChange, digits = "latin", showBank = true, showBankLogo = true, invalidMessage = "شماره شبا معتبر نیست.", error, name, id, onBlur, placeholder, ...rest }, ref) {
11
+ const autoId = useId();
12
+ const inputId = id ?? autoId;
13
+ const [touched, setTouched] = useState(false);
14
+ const strip = (v) => v?.replace(/^\s*IR/i, "");
15
+ const { raw, shown, inputRef, handleChange } = useDigitsInput({
16
+ value: strip(value),
17
+ defaultValue: strip(defaultValue),
18
+ maxDigits: 24,
19
+ format: (d) => formatIban(d),
20
+ digits,
21
+ });
22
+ const bank = showBank ? getBankByIban(raw) : null;
23
+ const showInvalid = raw.length > 0 && (touched || raw.length === 24) && !isValidIban(raw);
24
+ const bankId = `${inputId}-bank`;
25
+ return (_jsxs(_Fragment, { children: [_jsx(Input, { ref: mergeRefs(ref, inputRef), id: inputId, type: "text", inputMode: "numeric", autoComplete: "off", spellCheck: false, dir: "ltr", placeholder: placeholder ?? "IR00 0000 0000 0000 0000 0000 00", value: shown, onChange: (e) => {
26
+ // «IR» تایپ‌شده یا چسبانده‌شده حذف می‌شود و فقط ارقام می‌مانند
27
+ const next = handleChange(e);
28
+ onValueChange?.(ibanOf(next), isValidIban(next), getBankByIban(next)?.name ?? null);
29
+ }, onBlur: (e) => {
30
+ setTouched(true);
31
+ onBlur?.(e);
32
+ }, addonEnd: bank ? _jsx(BankBadge, { bank: bank, id: bankId, showLogo: showBankLogo }) : undefined, "aria-describedby": bank ? bankId : undefined, error: error ?? (showInvalid ? invalidMessage : undefined), ...rest }), name && _jsx("input", { type: "hidden", name: name, value: ibanOf(raw) })] }));
33
+ });
@@ -0,0 +1,19 @@
1
+ import { type ReactNode } from "react";
2
+ import { type InputProps } from "./Input.js";
3
+ type Base = Omit<InputProps, "value" | "defaultValue" | "onChange" | "type" | "inputMode" | "addonEnd">;
4
+ export interface MobileInputProps extends Base {
5
+ /** مقدار کنترل‌شده، مثل "09123456789" یا "+989123456789" */
6
+ value?: string;
7
+ defaultValue?: string;
8
+ /** با هر تغییر: شماره به شکل ۰۹xxxxxxxxx، معتبر بودن و نام اپراتور */
9
+ onValueChange?: (value: string, valid: boolean, operator: string | null) => void;
10
+ /** @default "fa" */
11
+ digits?: "fa" | "latin";
12
+ /** نمایش نام اپراتور @default true */
13
+ showOperator?: boolean;
14
+ /** @default "شماره موبایل معتبر نیست." */
15
+ invalidMessage?: ReactNode;
16
+ }
17
+ /** فیلد شماره موبایل: +98 و 0098 را به 09 تبدیل می‌کند، گروه‌بندی و تشخیص اپراتور */
18
+ export declare const MobileInput: import("react").ForwardRefExoticComponent<MobileInputProps & import("react").RefAttributes<HTMLInputElement>>;
19
+ export {};
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useId, useState } from "react";
3
+ import { formatMobile, getMobileOperator, isValidMobile, normalizeMobile } from "@kashi-ui/persian";
4
+ import { Input } from "./Input.js";
5
+ import { useDigitsInput } from "./useDigitsInput.js";
6
+ import { mergeRefs } from "./utils.js";
7
+ /** فیلد شماره موبایل: +98 و 0098 را به 09 تبدیل می‌کند، گروه‌بندی و تشخیص اپراتور */
8
+ export const MobileInput = forwardRef(function MobileInput({ value, defaultValue, onValueChange, digits = "fa", showOperator = true, invalidMessage = "شماره موبایل معتبر نیست.", error, name, id, onBlur, placeholder, ...rest }, ref) {
9
+ const autoId = useId();
10
+ const inputId = id ?? autoId;
11
+ const [touched, setTouched] = useState(false);
12
+ const { raw, shown, inputRef, handleChange } = useDigitsInput({
13
+ value,
14
+ defaultValue,
15
+ maxDigits: 11,
16
+ normalize: normalizeMobile,
17
+ format: formatMobile,
18
+ digits,
19
+ });
20
+ const operator = showOperator ? getMobileOperator(raw) : null;
21
+ const showInvalid = raw.length > 0 && (touched || raw.length === 11) && !isValidMobile(raw);
22
+ const opId = `${inputId}-operator`;
23
+ return (_jsxs(_Fragment, { children: [_jsx(Input, { ref: mergeRefs(ref, inputRef), id: inputId, type: "tel", inputMode: "tel", autoComplete: "tel-national", dir: "ltr", placeholder: placeholder ?? (digits === "fa" ? "۰۹۱۲ ۳۴۵ ۶۷۸۹" : "0912 345 6789"), value: shown, onChange: (e) => {
24
+ const next = handleChange(e);
25
+ onValueChange?.(next, isValidMobile(next), getMobileOperator(next)?.name ?? null);
26
+ }, onBlur: (e) => {
27
+ setTouched(true);
28
+ onBlur?.(e);
29
+ }, addonEnd: operator ? _jsx("span", { className: "k-input__meta", id: opId, children: operator.name }) : undefined, "aria-describedby": operator ? opId : undefined, error: error ?? (showInvalid ? invalidMessage : undefined), ...rest }), name && _jsx("input", { type: "hidden", name: name, value: raw })] }));
30
+ });
@@ -0,0 +1,21 @@
1
+ import { type ReactNode } from "react";
2
+ import { type InputProps } from "./Input.js";
3
+ type Base = Omit<InputProps, "value" | "defaultValue" | "onChange" | "type" | "inputMode" | "addonEnd" | "min" | "max">;
4
+ export interface PriceInputProps extends Base {
5
+ value?: number | null;
6
+ defaultValue?: number | null;
7
+ onValueChange?: (value: number | null) => void;
8
+ /** واحد پول کنار فیلد @default "تومان" */
9
+ unit?: ReactNode;
10
+ /** نمایش مبلغ به حروف زیر فیلد @default true */
11
+ showWords?: boolean;
12
+ /** @default "fa" */
13
+ digits?: "fa" | "latin";
14
+ min?: number;
15
+ max?: number;
16
+ /** پیام خطای بیرون از محدوده */
17
+ rangeMessage?: (min?: number, max?: number) => ReactNode;
18
+ }
19
+ /** فیلد مبلغ: جداکنندهٔ هزارگان، واحد پول و نمایش مبلغ به حروف برای اطمینان کاربر */
20
+ export declare const PriceInput: import("react").ForwardRefExoticComponent<PriceInputProps & import("react").RefAttributes<HTMLInputElement>>;
21
+ export {};
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useState } from "react";
3
+ import { formatNumber, numberToWords, toFaDigits } from "@kashi-ui/persian";
4
+ import { Input } from "./Input.js";
5
+ import { useDigitsInput } from "./useDigitsInput.js";
6
+ import { mergeRefs } from "./utils.js";
7
+ const toRaw = (n) => (n === null || n === undefined ? undefined : String(Math.trunc(Math.abs(n))));
8
+ /** فیلد مبلغ: جداکنندهٔ هزارگان، واحد پول و نمایش مبلغ به حروف برای اطمینان کاربر */
9
+ export const PriceInput = forwardRef(function PriceInput({ value, defaultValue, onValueChange, unit = "تومان", showWords = true, digits = "fa", min, max, rangeMessage = (lo, hi) => `مبلغ باید ${lo !== undefined ? `حداقل ${toFaDigits(formatNumber(lo))}` : ""}${lo !== undefined && hi !== undefined ? " و " : ""}${hi !== undefined ? `حداکثر ${toFaDigits(formatNumber(hi))}` : ""} باشد.`, error, hint, name, onBlur, placeholder, ...rest }, ref) {
10
+ const [touched, setTouched] = useState(false);
11
+ const { raw, shown, inputRef, handleChange } = useDigitsInput({
12
+ value: value === undefined ? undefined : (toRaw(value) ?? ""),
13
+ defaultValue: toRaw(defaultValue),
14
+ maxDigits: 15,
15
+ normalize: (d) => d.replace(/^0+(?=\d)/, ""),
16
+ format: (d) => (d ? formatNumber(d, { digits: "latin" }).replace(/,/g, "٬") : ""),
17
+ digits,
18
+ });
19
+ const n = raw ? Number(raw) : null;
20
+ const outOfRange = n !== null && ((min !== undefined && n < min) || (max !== undefined && n > max));
21
+ const words = showWords && n !== null ? `${numberToWords(n)}${typeof unit === "string" ? ` ${unit}` : ""}` : null;
22
+ return (_jsxs(_Fragment, { children: [_jsx(Input, { ref: mergeRefs(ref, inputRef), type: "text", inputMode: "numeric", autoComplete: "off", dir: "ltr", placeholder: placeholder ?? (digits === "fa" ? "۰" : "0"), value: shown, onChange: (e) => {
23
+ const next = handleChange(e);
24
+ onValueChange?.(next ? Number(next) : null);
25
+ }, onBlur: (e) => {
26
+ setTouched(true);
27
+ onBlur?.(e);
28
+ }, addonEnd: unit ? _jsx("span", { className: "k-input__meta", children: unit }) : undefined, hint: words ?? hint, error: error ?? (touched && outOfRange ? rangeMessage(min, max) : undefined), ...rest }), name && _jsx("input", { type: "hidden", name: name, value: raw })] }));
29
+ });
package/dist/index.d.ts CHANGED
@@ -22,3 +22,9 @@ export { Calendar } from "./Calendar.js";
22
22
  export type { CalendarProps, CalendarLabels } from "./Calendar.js";
23
23
  export { DatePicker } from "./DatePicker.js";
24
24
  export type { DatePickerProps } from "./DatePicker.js";
25
+ export { IbanInput } from "./IbanInput.js";
26
+ export type { IbanInputProps } from "./IbanInput.js";
27
+ export { MobileInput } from "./MobileInput.js";
28
+ export type { MobileInputProps } from "./MobileInput.js";
29
+ export { PriceInput } from "./PriceInput.js";
30
+ export type { PriceInputProps } from "./PriceInput.js";
package/dist/index.js CHANGED
@@ -11,3 +11,6 @@ export { NationalIdInput } from "./NationalIdInput.js";
11
11
  export { CardNumberInput } from "./CardNumberInput.js";
12
12
  export { Calendar } from "./Calendar.js";
13
13
  export { DatePicker } from "./DatePicker.js";
14
+ export { IbanInput } from "./IbanInput.js";
15
+ export { MobileInput } from "./MobileInput.js";
16
+ export { PriceInput } from "./PriceInput.js";
@@ -0,0 +1,23 @@
1
+ import { type ChangeEvent } from "react";
2
+ export interface DigitsInputOptions {
3
+ value?: string;
4
+ defaultValue?: string;
5
+ /** حداکثر تعداد رقم */
6
+ maxDigits: number;
7
+ /** متن نمایشی از روی ارقام خام (با ارقام لاتین) */
8
+ format: (raw: string) => string;
9
+ /** پیش‌پردازش ارقام، مثلاً تبدیل +98 به 0 */
10
+ normalize?: (digits: string) => string;
11
+ /** خط ارقام نمایشی */
12
+ digits: "fa" | "latin";
13
+ }
14
+ /**
15
+ * منطق مشترک فیلدهای عددی قالب‌بندی‌شده (کارت، شبا، موبایل، مبلغ):
16
+ * ارقام فارسی و لاتین را می‌پذیرد، بقیه را حذف می‌کند و مکان‌نما را پس از قالب‌بندی سر جایش نگه می‌دارد.
17
+ */
18
+ export declare function useDigitsInput({ value, defaultValue, maxDigits, format, normalize, digits }: DigitsInputOptions): {
19
+ raw: string;
20
+ shown: string;
21
+ inputRef: import("react").RefObject<HTMLInputElement>;
22
+ handleChange: (e: ChangeEvent<HTMLInputElement>) => string;
23
+ };
@@ -0,0 +1,46 @@
1
+ import { useRef, useState } from "react";
2
+ import { onlyDigits } from "@kashi-ui/persian";
3
+ import { useIsomorphicLayoutEffect } from "./utils.js";
4
+ const FA = "۰۱۲۳۴۵۶۷۸۹";
5
+ const fa = (s) => s.replace(/\d/g, (d) => FA[Number(d)]);
6
+ /** موقعیت مکان‌نما پس از n رقم در متن قالب‌بندی‌شده */
7
+ function caretAfter(text, n) {
8
+ if (n === 0)
9
+ return text.search(/[0-9۰-۹]/) === -1 ? text.length : text.search(/[0-9۰-۹]/);
10
+ let seen = 0;
11
+ for (let i = 0; i < text.length; i++)
12
+ if (/[0-9۰-۹]/.test(text[i]) && ++seen === n)
13
+ return i + 1;
14
+ return text.length;
15
+ }
16
+ /**
17
+ * منطق مشترک فیلدهای عددی قالب‌بندی‌شده (کارت، شبا، موبایل، مبلغ):
18
+ * ارقام فارسی و لاتین را می‌پذیرد، بقیه را حذف می‌کند و مکان‌نما را پس از قالب‌بندی سر جایش نگه می‌دارد.
19
+ */
20
+ export function useDigitsInput({ value, defaultValue = "", maxDigits, format, normalize = (d) => d, digits }) {
21
+ const clean = (input) => normalize(onlyDigits(input)).slice(0, maxDigits);
22
+ const [inner, setInner] = useState(() => clean(defaultValue));
23
+ const raw = value !== undefined ? clean(value) : inner;
24
+ const formatted = format(raw);
25
+ const shown = digits === "fa" ? fa(formatted) : formatted;
26
+ const inputRef = useRef(null);
27
+ const caret = useRef(null);
28
+ useIsomorphicLayoutEffect(() => {
29
+ if (caret.current !== null && inputRef.current === document.activeElement) {
30
+ const pos = caretAfter(shown, caret.current);
31
+ inputRef.current?.setSelectionRange(pos, pos);
32
+ caret.current = null;
33
+ }
34
+ });
35
+ /** ارقام خام جدید را برمی‌گرداند؛ حالت داخلی را هم به‌روز می‌کند */
36
+ const handleChange = (e) => {
37
+ const el = e.target;
38
+ const before = el.value.slice(0, el.selectionStart ?? el.value.length);
39
+ const next = clean(el.value);
40
+ caret.current = Math.min(onlyDigits(before).length, next.length);
41
+ if (value === undefined)
42
+ setInner(next);
43
+ return next;
44
+ };
45
+ return { raw, shown, inputRef, handleChange };
46
+ }
package/dist/utils.d.ts CHANGED
@@ -1,5 +1,7 @@
1
- import type { Ref, RefCallback } from "react";
1
+ import { useLayoutEffect, type Ref, type RefCallback } from "react";
2
2
  export { toFaDigits } from "@kashi-ui/persian";
3
3
  /** چند ref را یکی می‌کند */
4
4
  export declare function mergeRefs<T>(...refs: Array<Ref<T> | undefined>): RefCallback<T>;
5
5
  export declare function isDev(): boolean;
6
+ /** useLayoutEffect در مرورگر، useEffect در سرور؛ تا در Next.js و رندر سمت سرور هشدار ندهد */
7
+ export declare const useIsomorphicLayoutEffect: typeof useLayoutEffect;
package/dist/utils.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { useEffect, useLayoutEffect } from "react";
1
2
  export { toFaDigits } from "@kashi-ui/persian";
2
3
  /** چند ref را یکی می‌کند */
3
4
  export function mergeRefs(...refs) {
@@ -13,3 +14,5 @@ export function mergeRefs(...refs) {
13
14
  export function isDev() {
14
15
  return typeof process !== "undefined" && process.env.NODE_ENV !== "production";
15
16
  }
17
+ /** useLayoutEffect در مرورگر، useEffect در سرور؛ تا در Next.js و رندر سمت سرور هشدار ندهد */
18
+ export const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kashi-ui/react",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "کامپوننت‌های React دیزاین‌سیستم کاشی",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "test": "node --test test/*.test.mjs"
23
23
  },
24
24
  "peerDependencies": {
25
- "@kashi-ui/css": "^0.3.0",
25
+ "@kashi-ui/css": "^0.4.0",
26
26
  "react": ">=18"
27
27
  },
28
28
  "devDependencies": {
@@ -59,6 +59,7 @@
59
59
  }
60
60
  },
61
61
  "dependencies": {
62
- "@kashi-ui/persian": "^0.3.0"
62
+ "@kashi-ui/bank-logos": "^0.4.0",
63
+ "@kashi-ui/persian": "^0.4.0"
63
64
  }
64
65
  }