@kashi-ui/react 0.2.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 +2 -0
- package/dist/BankBadge.d.ts +7 -0
- package/dist/BankBadge.js +27 -0
- package/dist/Calendar.d.ts +37 -0
- package/dist/Calendar.js +107 -0
- package/dist/CardNumberInput.d.ts +21 -0
- package/dist/CardNumberInput.js +24 -0
- package/dist/DatePicker.d.ts +28 -0
- package/dist/DatePicker.js +90 -0
- package/dist/IbanInput.d.ts +21 -0
- package/dist/IbanInput.js +33 -0
- package/dist/MobileInput.d.ts +19 -0
- package/dist/MobileInput.js +30 -0
- package/dist/NationalIdInput.d.ts +17 -0
- package/dist/NationalIdInput.js +21 -0
- package/dist/PriceInput.d.ts +21 -0
- package/dist/PriceInput.js +29 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +7 -0
- package/dist/useDigitsInput.d.ts +23 -0
- package/dist/useDigitsInput.js +46 -0
- package/dist/utils.d.ts +4 -3
- package/dist/utils.js +4 -4
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -22,4 +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 · IbanInput · MobileInput · PriceInput
|
|
26
|
+
|
|
25
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
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export declare const sameDay: (a?: Date | null, b?: Date | null) => boolean;
|
|
3
|
+
export interface CalendarLabels {
|
|
4
|
+
/** @default "ماه قبل" */
|
|
5
|
+
prevMonth?: string;
|
|
6
|
+
/** @default "ماه بعد" */
|
|
7
|
+
nextMonth?: string;
|
|
8
|
+
/** @default "ماه" */
|
|
9
|
+
month?: string;
|
|
10
|
+
/** @default "سال" */
|
|
11
|
+
year?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface CalendarProps {
|
|
14
|
+
/** تاریخ انتخابشده (کنترلشده) */
|
|
15
|
+
value?: Date | null;
|
|
16
|
+
defaultValue?: Date | null;
|
|
17
|
+
onChange?: (date: Date) => void;
|
|
18
|
+
/** کمترین تاریخ قابلانتخاب */
|
|
19
|
+
min?: Date;
|
|
20
|
+
/** بیشترین تاریخ قابلانتخاب */
|
|
21
|
+
max?: Date;
|
|
22
|
+
/** غیرفعال کردن روزهای خاص، مثلاً جمعهها */
|
|
23
|
+
isDateDisabled?: (date: Date) => boolean;
|
|
24
|
+
/** @default "fa" */
|
|
25
|
+
digits?: "fa" | "latin";
|
|
26
|
+
/** فوکوس روی روز انتخابشده (یا امروز) هنگام نمایش */
|
|
27
|
+
autoFocus?: boolean;
|
|
28
|
+
/** محدودهٔ سالهای فهرست سال؛ پیشفرض از min/max یا ۱۰۰ سال قبل تا ۲۰ سال بعد */
|
|
29
|
+
yearRange?: [number, number];
|
|
30
|
+
labels?: CalendarLabels;
|
|
31
|
+
/** محتوای پایین تقویم، مثل دکمهٔ «امروز» */
|
|
32
|
+
footer?: ReactNode;
|
|
33
|
+
className?: string;
|
|
34
|
+
"aria-label"?: string;
|
|
35
|
+
}
|
|
36
|
+
/** تقویم شمسی با الگوی grid استاندارد WAI-ARIA و پیمایش کامل با کیبورد */
|
|
37
|
+
export declare function Calendar({ value, defaultValue, onChange, min, max, isDateDisabled, digits, autoFocus, yearRange, labels, footer, className, "aria-label": ariaLabel, }: CalendarProps): import("react").JSX.Element;
|
package/dist/Calendar.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
3
|
+
import { JALALI_MONTHS, WEEKDAYS, WEEKDAYS_SHORT, formatJalali, jalaliMonthLength, jalaliWeekday, toFaDigits, toGregorian, toJalali, } from "@kashi-ui/persian";
|
|
4
|
+
import { cx } from "./cx.js";
|
|
5
|
+
export const sameDay = (a, b) => !!a && !!b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
6
|
+
const startOfDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
7
|
+
const addDays = (d, n) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
|
|
8
|
+
const dayKey = (d) => `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`;
|
|
9
|
+
/** چند ماه شمسی جلو یا عقب؛ روز در ماههای کوتاهتر به آخر ماه میچسبد */
|
|
10
|
+
function addJalaliMonths(d, n) {
|
|
11
|
+
const { jy, jm, jd } = toJalali(d);
|
|
12
|
+
const total = jy * 12 + (jm - 1) + n;
|
|
13
|
+
const ny = Math.floor(total / 12);
|
|
14
|
+
const nm = total - ny * 12 + 1;
|
|
15
|
+
return toGregorian(ny, nm, Math.min(jd, jalaliMonthLength(ny, nm)));
|
|
16
|
+
}
|
|
17
|
+
function clamp(d, min, max) {
|
|
18
|
+
if (min && d < startOfDay(min))
|
|
19
|
+
return startOfDay(min);
|
|
20
|
+
if (max && d > startOfDay(max))
|
|
21
|
+
return startOfDay(max);
|
|
22
|
+
return d;
|
|
23
|
+
}
|
|
24
|
+
/** تقویم شمسی با الگوی grid استاندارد WAI-ARIA و پیمایش کامل با کیبورد */
|
|
25
|
+
export function Calendar({ value, defaultValue = null, onChange, min, max, isDateDisabled, digits = "fa", autoFocus = false, yearRange, labels = {}, footer, className, "aria-label": ariaLabel = "تقویم", }) {
|
|
26
|
+
const id = useId();
|
|
27
|
+
const [inner, setInner] = useState(defaultValue);
|
|
28
|
+
const selected = value !== undefined ? value : inner;
|
|
29
|
+
const [today] = useState(() => startOfDay(new Date()));
|
|
30
|
+
const [focused, setFocused] = useState(() => clamp(startOfDay(selected ?? today), min, max));
|
|
31
|
+
const moveFocus = useRef(autoFocus);
|
|
32
|
+
const grid = useRef(null);
|
|
33
|
+
const n = (v) => (digits === "fa" ? toFaDigits(v) : String(v));
|
|
34
|
+
// اگر مقدار از بیرون عوض شد، ماه نمایشدادهشده هم دنبالش برود
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (selected)
|
|
37
|
+
setFocused(startOfDay(selected));
|
|
38
|
+
}, [selected?.getTime()]);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!moveFocus.current)
|
|
41
|
+
return;
|
|
42
|
+
moveFocus.current = false;
|
|
43
|
+
grid.current?.querySelector(`[data-day="${dayKey(focused)}"]`)?.focus();
|
|
44
|
+
});
|
|
45
|
+
const { jy, jm } = toJalali(focused);
|
|
46
|
+
const monthStart = toGregorian(jy, jm, 1);
|
|
47
|
+
const offset = jalaliWeekday(monthStart);
|
|
48
|
+
const length = jalaliMonthLength(jy, jm);
|
|
49
|
+
const cells = [
|
|
50
|
+
...Array(offset).fill(null),
|
|
51
|
+
...Array.from({ length }, (_, i) => addDays(monthStart, i)),
|
|
52
|
+
];
|
|
53
|
+
while (cells.length % 7)
|
|
54
|
+
cells.push(null);
|
|
55
|
+
const weeks = Array.from({ length: cells.length / 7 }, (_, i) => cells.slice(i * 7, i * 7 + 7));
|
|
56
|
+
const disabled = (d) => (min !== undefined && d < startOfDay(min)) || (max !== undefined && d > startOfDay(max)) || !!isDateDisabled?.(d);
|
|
57
|
+
const [fromYear, toYear] = yearRange ?? [
|
|
58
|
+
min ? toJalali(min).jy : toJalali(today).jy - 100,
|
|
59
|
+
max ? toJalali(max).jy : toJalali(today).jy + 20,
|
|
60
|
+
];
|
|
61
|
+
const years = Array.from({ length: Math.max(0, toYear - fromYear + 1) }, (_, i) => fromYear + i);
|
|
62
|
+
if (!years.includes(jy))
|
|
63
|
+
years.push(jy);
|
|
64
|
+
const go = (d, focus = true) => {
|
|
65
|
+
moveFocus.current = focus;
|
|
66
|
+
setFocused(clamp(d, min, max));
|
|
67
|
+
};
|
|
68
|
+
const select = (d) => {
|
|
69
|
+
if (disabled(d))
|
|
70
|
+
return;
|
|
71
|
+
if (value === undefined)
|
|
72
|
+
setInner(d);
|
|
73
|
+
onChange?.(d);
|
|
74
|
+
};
|
|
75
|
+
const onKeyDown = (e) => {
|
|
76
|
+
// در راستبهچپ، فلش چپ یعنی روز بعد
|
|
77
|
+
const rtl = getComputedStyle(e.currentTarget).direction === "rtl";
|
|
78
|
+
const wd = jalaliWeekday(focused);
|
|
79
|
+
const next = {
|
|
80
|
+
ArrowLeft: () => addDays(focused, rtl ? 1 : -1),
|
|
81
|
+
ArrowRight: () => addDays(focused, rtl ? -1 : 1),
|
|
82
|
+
ArrowUp: () => addDays(focused, -7),
|
|
83
|
+
ArrowDown: () => addDays(focused, 7),
|
|
84
|
+
Home: () => addDays(focused, -wd),
|
|
85
|
+
End: () => addDays(focused, 6 - wd),
|
|
86
|
+
PageUp: () => addJalaliMonths(focused, e.shiftKey ? -12 : -1),
|
|
87
|
+
PageDown: () => addJalaliMonths(focused, e.shiftKey ? 12 : 1),
|
|
88
|
+
};
|
|
89
|
+
if (!next[e.key])
|
|
90
|
+
return;
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
go(next[e.key]());
|
|
93
|
+
};
|
|
94
|
+
const { prevMonth = "ماه قبل", nextMonth = "ماه بعد", month: monthLabel = "ماه", year: yearLabel = "سال" } = labels;
|
|
95
|
+
const prevDisabled = min !== undefined && toGregorian(jy, jm, 1) <= startOfDay(min);
|
|
96
|
+
const nextDisabled = max !== undefined && toGregorian(jy, jm, length) >= startOfDay(max);
|
|
97
|
+
return (_jsxs("div", { className: cx("k-calendar", className), role: "group", "aria-label": ariaLabel, children: [_jsxs("div", { className: "k-calendar__header", children: [_jsx("button", { type: "button", className: "k-btn k-btn--tertiary k-btn--icon k-btn--sm", "aria-label": prevMonth, disabled: prevDisabled, onClick: () => go(addJalaliMonths(focused, -1), false), children: _jsx("svg", { "data-k-mirror": "", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.7, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "m12 5-5 5 5 5" }) }) }), _jsxs("div", { className: "k-calendar__selects", children: [_jsx("select", { className: "k-calendar__select", "aria-label": monthLabel, value: jm, onChange: (e) => go(toGregorian(jy, Number(e.target.value), Math.min(toJalali(focused).jd, jalaliMonthLength(jy, Number(e.target.value)))), false), children: JALALI_MONTHS.map((name, i) => _jsx("option", { value: i + 1, children: name }, name)) }), _jsx("select", { className: "k-calendar__select", "aria-label": yearLabel, value: jy, onChange: (e) => go(toGregorian(Number(e.target.value), jm, Math.min(toJalali(focused).jd, jalaliMonthLength(Number(e.target.value), jm))), false), children: years.sort((a, b) => a - b).map((y) => _jsx("option", { value: y, children: n(y) }, y)) })] }), _jsx("button", { type: "button", className: "k-btn k-btn--tertiary k-btn--icon k-btn--sm", "aria-label": nextMonth, disabled: nextDisabled, onClick: () => go(addJalaliMonths(focused, 1), false), children: _jsx("svg", { "data-k-mirror": "", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.7, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "m8 5 5 5-5 5" }) }) })] }), _jsx("h2", { className: "k-sr-only", id: `${id}-title`, "aria-live": "polite", children: `${JALALI_MONTHS[jm - 1]} ${n(jy)}` }), _jsxs("table", { className: "k-calendar__grid", role: "grid", "aria-labelledby": `${id}-title`, ref: grid, children: [_jsx("thead", { children: _jsx("tr", { children: WEEKDAYS_SHORT.map((s, i) => (_jsx("th", { scope: "col", abbr: WEEKDAYS[i], children: s }, s))) }) }), _jsx("tbody", { children: weeks.map((week, wi) => (_jsx("tr", { children: week.map((d, di) => {
|
|
98
|
+
if (!d)
|
|
99
|
+
return _jsx("td", { role: "gridcell" }, di);
|
|
100
|
+
const isSelected = sameDay(d, selected);
|
|
101
|
+
const isDisabled = disabled(d);
|
|
102
|
+
return (_jsx("td", { role: "gridcell", "aria-selected": isSelected, children: _jsx("button", { type: "button", className: "k-calendar__day", "data-day": dayKey(d), tabIndex: sameDay(d, focused) ? 0 : -1, "aria-label": formatJalali(d, { style: "full", digits }), "aria-current": sameDay(d, today) ? "date" : undefined, "aria-disabled": isDisabled || undefined, onClick: () => {
|
|
103
|
+
setFocused(d);
|
|
104
|
+
select(d);
|
|
105
|
+
}, onKeyDown: onKeyDown, children: n(toJalali(d).jd) }) }, di));
|
|
106
|
+
}) }, wi))) })] }), footer && _jsx("div", { className: "k-calendar__footer", children: footer })] }));
|
|
107
|
+
}
|
|
@@ -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 CardNumberInputProps extends Base {
|
|
5
|
+
/** مقدار کنترلشده: ۱۶ رقم لاتین بدون فاصله */
|
|
6
|
+
value?: string;
|
|
7
|
+
defaultValue?: string;
|
|
8
|
+
/** با هر تغییر: ارقام لاتین، معتبر بودن و بانک تشخیصدادهشده */
|
|
9
|
+
onValueChange?: (value: string, valid: boolean, bank: string | null) => void;
|
|
10
|
+
/** @default "fa" */
|
|
11
|
+
digits?: "fa" | "latin";
|
|
12
|
+
/** نمایش نام بانک از روی شش رقم اول @default true */
|
|
13
|
+
showBank?: boolean;
|
|
14
|
+
/** نمایش لوگوی بانک (فقط هنگام نیاز بارگذاری میشود) @default true */
|
|
15
|
+
showBankLogo?: boolean;
|
|
16
|
+
/** @default "شماره کارت معتبر نیست." */
|
|
17
|
+
invalidMessage?: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
/** فیلد شماره کارت بانکی: گروهبندی چهارتایی، تشخیص بانک با لوگو و بررسی Luhn */
|
|
20
|
+
export declare const CardNumberInput: import("react").ForwardRefExoticComponent<CardNumberInputProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useId, useState } from "react";
|
|
3
|
+
import { formatCardNumber, getBankByCard, isValidCardNumber } 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
|
+
/** فیلد شماره کارت بانکی: گروهبندی چهارتایی، تشخیص بانک با لوگو و بررسی 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) {
|
|
10
|
+
const autoId = useId();
|
|
11
|
+
const inputId = id ?? autoId;
|
|
12
|
+
const [touched, setTouched] = useState(false);
|
|
13
|
+
const { raw, shown, inputRef, handleChange } = useDigitsInput({ value, defaultValue, maxDigits: 16, format: formatCardNumber, digits });
|
|
14
|
+
const bank = showBank ? getBankByCard(raw) : null;
|
|
15
|
+
const showInvalid = raw.length > 0 && (touched || raw.length === 16) && !isValidCardNumber(raw);
|
|
16
|
+
const bankId = `${inputId}-bank`;
|
|
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) => {
|
|
18
|
+
const next = handleChange(e);
|
|
19
|
+
onValueChange?.(next, isValidCardNumber(next), getBankByCard(next)?.name ?? null);
|
|
20
|
+
}, onBlur: (e) => {
|
|
21
|
+
setTouched(true);
|
|
22
|
+
onBlur?.(e);
|
|
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 })] }));
|
|
24
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type CalendarLabels } from "./Calendar.js";
|
|
3
|
+
import { type InputProps } from "./Input.js";
|
|
4
|
+
type Base = Omit<InputProps, "value" | "defaultValue" | "onChange" | "type" | "addonEnd" | "min" | "max">;
|
|
5
|
+
export interface DatePickerProps extends Base {
|
|
6
|
+
value?: Date | null;
|
|
7
|
+
defaultValue?: Date | null;
|
|
8
|
+
onChange?: (date: Date | null) => void;
|
|
9
|
+
min?: Date;
|
|
10
|
+
max?: Date;
|
|
11
|
+
isDateDisabled?: (date: Date) => boolean;
|
|
12
|
+
/** @default "fa" */
|
|
13
|
+
digits?: "fa" | "latin";
|
|
14
|
+
yearRange?: [number, number];
|
|
15
|
+
/** @default "این تاریخ معتبر نیست؛ آن را به شکل ۱۴۰۳/۰۵/۱۲ وارد کنید." */
|
|
16
|
+
invalidMessage?: ReactNode;
|
|
17
|
+
/** نام دکمهٔ باز کردن تقویم @default "انتخاب تاریخ از تقویم" */
|
|
18
|
+
openLabel?: string;
|
|
19
|
+
/** @default "امروز" */
|
|
20
|
+
todayLabel?: string;
|
|
21
|
+
calendarLabels?: CalendarLabels;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* انتخاب تاریخ شمسی: تایپ مستقیم (۱۴۰۳/۰۵/۱۲) یا انتخاب از تقویم.
|
|
25
|
+
* با name، مقدار میلادی به شکل YYYY-MM-DD در فرم ارسال میشود.
|
|
26
|
+
*/
|
|
27
|
+
export declare const DatePicker: import("react").ForwardRefExoticComponent<DatePickerProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useId, useRef, useState } from "react";
|
|
3
|
+
import { formatJalali, parseJalali, toISODate } from "@kashi-ui/persian";
|
|
4
|
+
import { Calendar, sameDay } from "./Calendar.js";
|
|
5
|
+
import { Input } from "./Input.js";
|
|
6
|
+
import { mergeRefs, useIsomorphicLayoutEffect } from "./utils.js";
|
|
7
|
+
/**
|
|
8
|
+
* انتخاب تاریخ شمسی: تایپ مستقیم (۱۴۰۳/۰۵/۱۲) یا انتخاب از تقویم.
|
|
9
|
+
* با name، مقدار میلادی به شکل YYYY-MM-DD در فرم ارسال میشود.
|
|
10
|
+
*/
|
|
11
|
+
export const DatePicker = forwardRef(function DatePicker({ value, defaultValue = null, onChange, min, max, isDateDisabled, digits = "fa", yearRange, invalidMessage = "این تاریخ معتبر نیست؛ آن را به شکل ۱۴۰۳/۰۵/۱۲ وارد کنید.", openLabel = "انتخاب تاریخ از تقویم", todayLabel = "امروز", calendarLabels, error, name, onBlur, placeholder, ...rest }, ref) {
|
|
12
|
+
const id = useId();
|
|
13
|
+
const popoverId = `${id}-calendar`;
|
|
14
|
+
const [inner, setInner] = useState(defaultValue);
|
|
15
|
+
const selected = value !== undefined ? value : inner;
|
|
16
|
+
const fmt = (d) => (d ? formatJalali(d, { digits }) : "");
|
|
17
|
+
const [text, setText] = useState(() => fmt(selected));
|
|
18
|
+
const [invalid, setInvalid] = useState(false);
|
|
19
|
+
const [open, setOpen] = useState(false);
|
|
20
|
+
const [top, setTop] = useState(0);
|
|
21
|
+
const wrap = useRef(null);
|
|
22
|
+
const inputRef = useRef(null);
|
|
23
|
+
const button = useRef(null);
|
|
24
|
+
// همگامسازی متن با مقدار کنترلشده
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
setText(fmt(selected));
|
|
27
|
+
setInvalid(false);
|
|
28
|
+
}, [selected?.getTime(), digits]);
|
|
29
|
+
// تقویم درست زیر کادر فیلد باز میشود (نه زیر متن راهنما)
|
|
30
|
+
useIsomorphicLayoutEffect(() => {
|
|
31
|
+
if (!open || !wrap.current)
|
|
32
|
+
return;
|
|
33
|
+
const box = wrap.current.querySelector(".k-input");
|
|
34
|
+
if (box)
|
|
35
|
+
setTop(box.offsetTop + box.offsetHeight + 6);
|
|
36
|
+
}, [open]);
|
|
37
|
+
// بستن با کلیک بیرون
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!open)
|
|
40
|
+
return;
|
|
41
|
+
const onDown = (e) => {
|
|
42
|
+
if (!wrap.current?.contains(e.target))
|
|
43
|
+
setOpen(false);
|
|
44
|
+
};
|
|
45
|
+
document.addEventListener("pointerdown", onDown);
|
|
46
|
+
return () => document.removeEventListener("pointerdown", onDown);
|
|
47
|
+
}, [open]);
|
|
48
|
+
const commit = (d) => {
|
|
49
|
+
if (value === undefined)
|
|
50
|
+
setInner(d);
|
|
51
|
+
setText(fmt(d));
|
|
52
|
+
setInvalid(false);
|
|
53
|
+
if (!sameDay(d, selected) || (d === null) !== (selected === null))
|
|
54
|
+
onChange?.(d);
|
|
55
|
+
};
|
|
56
|
+
const close = () => {
|
|
57
|
+
setOpen(false);
|
|
58
|
+
button.current?.focus();
|
|
59
|
+
};
|
|
60
|
+
return (_jsxs("div", { className: "k-datepicker", ref: wrap, children: [_jsx(Input, { ref: mergeRefs(ref, inputRef), type: "text", dir: "ltr", autoComplete: "off", placeholder: placeholder ?? (digits === "fa" ? "۱۴۰۳/۰۱/۰۱" : "1403/01/01"), value: text, onChange: (e) => setText(e.target.value), onBlur: (e) => {
|
|
61
|
+
const t = e.target.value.trim();
|
|
62
|
+
if (!t)
|
|
63
|
+
commit(null);
|
|
64
|
+
else {
|
|
65
|
+
const d = parseJalali(t);
|
|
66
|
+
if (d && !(min && d < min) && !(max && d > max) && !isDateDisabled?.(d))
|
|
67
|
+
commit(d);
|
|
68
|
+
else
|
|
69
|
+
setInvalid(true);
|
|
70
|
+
}
|
|
71
|
+
onBlur?.(e);
|
|
72
|
+
}, error: error ?? (invalid ? invalidMessage : undefined), addonEnd: _jsx("button", { ref: button, type: "button", className: "k-btn k-btn--tertiary k-btn--icon k-btn--sm k-datepicker__toggle", "aria-label": selected ? `${openLabel}، ${formatJalali(selected, { style: "full", digits })}` : openLabel, "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": open ? popoverId : undefined, onClick: () => setOpen((o) => !o), children: _jsxs("svg", { viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.7, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("rect", { x: "3", y: "4.5", width: "14", height: "12.5", rx: "2" }), _jsx("path", { d: "M3 8.5h14M7 3v3M13 3v3" })] }) }), ...rest }), name && _jsx("input", { type: "hidden", name: name, value: selected ? toISODate(selected) : "" }), open && (_jsx("div", { id: popoverId, className: "k-datepicker__popover", role: "dialog", "aria-label": openLabel, style: { insetBlockStart: top }, onKeyDown: (e) => {
|
|
73
|
+
if (e.key === "Escape") {
|
|
74
|
+
e.stopPropagation();
|
|
75
|
+
close();
|
|
76
|
+
}
|
|
77
|
+
}, onBlur: (e) => {
|
|
78
|
+
// اگر فوکوس (با Tab) از تقویم و کل فیلد بیرون رفت، ببند؛ کلیک بیرون را pointerdown میگیرد
|
|
79
|
+
const next = e.relatedTarget;
|
|
80
|
+
if (next && !wrap.current?.contains(next))
|
|
81
|
+
setOpen(false);
|
|
82
|
+
}, children: _jsx(Calendar, { value: selected, onChange: (d) => {
|
|
83
|
+
commit(d);
|
|
84
|
+
close();
|
|
85
|
+
}, min: min, max: max, isDateDisabled: isDateDisabled, digits: digits, yearRange: yearRange, labels: calendarLabels, autoFocus: true, footer: _jsx("button", { type: "button", className: "k-btn k-btn--link", onClick: () => {
|
|
86
|
+
const t = new Date();
|
|
87
|
+
commit(new Date(t.getFullYear(), t.getMonth(), t.getDate()));
|
|
88
|
+
close();
|
|
89
|
+
}, children: todayLabel }) }) }))] }));
|
|
90
|
+
});
|
|
@@ -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,17 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type InputProps } from "./Input.js";
|
|
3
|
+
type Base = Omit<InputProps, "value" | "defaultValue" | "onChange" | "type" | "inputMode">;
|
|
4
|
+
export interface NationalIdInputProps extends Base {
|
|
5
|
+
/** مقدار کنترلشده با ارقام لاتین، مثل "0013542419" */
|
|
6
|
+
value?: string;
|
|
7
|
+
defaultValue?: string;
|
|
8
|
+
/** با هر تغییر: مقدار با ارقام لاتین و معتبر بودن آن */
|
|
9
|
+
onValueChange?: (value: string, valid: boolean) => void;
|
|
10
|
+
/** خط ارقام نمایشی @default "fa" */
|
|
11
|
+
digits?: "fa" | "latin";
|
|
12
|
+
/** @default "کد ملی معتبر نیست." */
|
|
13
|
+
invalidMessage?: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
/** فیلد کد ملی: ارقام فارسی و لاتین را میپذیرد، ۱۰ رقم را محدود و رقم کنترل را بررسی میکند. */
|
|
16
|
+
export declare const NationalIdInput: import("react").ForwardRefExoticComponent<NationalIdInputProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useState } from "react";
|
|
3
|
+
import { isValidNationalId, onlyDigits, toFaDigits } from "@kashi-ui/persian";
|
|
4
|
+
import { Input } from "./Input.js";
|
|
5
|
+
/** فیلد کد ملی: ارقام فارسی و لاتین را میپذیرد، ۱۰ رقم را محدود و رقم کنترل را بررسی میکند. */
|
|
6
|
+
export const NationalIdInput = forwardRef(function NationalIdInput({ value, defaultValue = "", onValueChange, digits = "fa", invalidMessage = "کد ملی معتبر نیست.", error, name, onBlur, placeholder, ...rest }, ref) {
|
|
7
|
+
const [inner, setInner] = useState(() => onlyDigits(defaultValue).slice(0, 10));
|
|
8
|
+
const [touched, setTouched] = useState(false);
|
|
9
|
+
const raw = value !== undefined ? onlyDigits(value).slice(0, 10) : inner;
|
|
10
|
+
// خطا فقط پس از خروج از فیلد یا کامل شدن ۱۰ رقم نمایش داده میشود
|
|
11
|
+
const showInvalid = raw.length > 0 && (touched || raw.length === 10) && !isValidNationalId(raw);
|
|
12
|
+
return (_jsxs(_Fragment, { children: [_jsx(Input, { ref: ref, type: "text", inputMode: "numeric", autoComplete: "off", dir: "ltr", placeholder: placeholder ?? (digits === "fa" ? "۰۰۰۰۰۰۰۰۰۰" : "0000000000"), value: digits === "fa" ? toFaDigits(raw) : raw, onChange: (e) => {
|
|
13
|
+
const next = onlyDigits(e.target.value).slice(0, 10);
|
|
14
|
+
if (value === undefined)
|
|
15
|
+
setInner(next);
|
|
16
|
+
onValueChange?.(next, isValidNationalId(next));
|
|
17
|
+
}, onBlur: (e) => {
|
|
18
|
+
setTouched(true);
|
|
19
|
+
onBlur?.(e);
|
|
20
|
+
}, error: error ?? (showInvalid ? invalidMessage : undefined), ...rest }), name && _jsx("input", { type: "hidden", name: name, value: raw })] }));
|
|
21
|
+
});
|
|
@@ -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
|
@@ -14,3 +14,17 @@ export { Checkbox, Switch } from "./Checkbox.js";
|
|
|
14
14
|
export type { CheckboxProps, SwitchProps } from "./Checkbox.js";
|
|
15
15
|
export { cx } from "./cx.js";
|
|
16
16
|
export { toFaDigits } from "./utils.js";
|
|
17
|
+
export { NationalIdInput } from "./NationalIdInput.js";
|
|
18
|
+
export type { NationalIdInputProps } from "./NationalIdInput.js";
|
|
19
|
+
export { CardNumberInput } from "./CardNumberInput.js";
|
|
20
|
+
export type { CardNumberInputProps } from "./CardNumberInput.js";
|
|
21
|
+
export { Calendar } from "./Calendar.js";
|
|
22
|
+
export type { CalendarProps, CalendarLabels } from "./Calendar.js";
|
|
23
|
+
export { DatePicker } from "./DatePicker.js";
|
|
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
|
@@ -7,3 +7,10 @@ export { Avatar, AvatarGroup, initials } from "./Avatar.js";
|
|
|
7
7
|
export { Checkbox, Switch } from "./Checkbox.js";
|
|
8
8
|
export { cx } from "./cx.js";
|
|
9
9
|
export { toFaDigits } from "./utils.js";
|
|
10
|
+
export { NationalIdInput } from "./NationalIdInput.js";
|
|
11
|
+
export { CardNumberInput } from "./CardNumberInput.js";
|
|
12
|
+
export { Calendar } from "./Calendar.js";
|
|
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,6 +1,7 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
3
|
-
export declare function toFaDigits(value: string | number): string;
|
|
1
|
+
import { useLayoutEffect, type Ref, type RefCallback } from "react";
|
|
2
|
+
export { toFaDigits } from "@kashi-ui/persian";
|
|
4
3
|
/** چند ref را یکی میکند */
|
|
5
4
|
export declare function mergeRefs<T>(...refs: Array<Ref<T> | undefined>): RefCallback<T>;
|
|
6
5
|
export declare function isDev(): boolean;
|
|
6
|
+
/** useLayoutEffect در مرورگر، useEffect در سرور؛ تا در Next.js و رندر سمت سرور هشدار ندهد */
|
|
7
|
+
export declare const useIsomorphicLayoutEffect: typeof useLayoutEffect;
|
package/dist/utils.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
return String(value).replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[Number(d)]);
|
|
4
|
-
}
|
|
1
|
+
import { useEffect, useLayoutEffect } from "react";
|
|
2
|
+
export { toFaDigits } from "@kashi-ui/persian";
|
|
5
3
|
/** چند ref را یکی میکند */
|
|
6
4
|
export function mergeRefs(...refs) {
|
|
7
5
|
return (node) => {
|
|
@@ -16,3 +14,5 @@ export function mergeRefs(...refs) {
|
|
|
16
14
|
export function isDev() {
|
|
17
15
|
return typeof process !== "undefined" && process.env.NODE_ENV !== "production";
|
|
18
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
|
+
"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.
|
|
25
|
+
"@kashi-ui/css": "^0.4.0",
|
|
26
26
|
"react": ">=18"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
@@ -57,5 +57,9 @@
|
|
|
57
57
|
"@kashi-ui/css": {
|
|
58
58
|
"optional": false
|
|
59
59
|
}
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"@kashi-ui/bank-logos": "^0.4.0",
|
|
63
|
+
"@kashi-ui/persian": "^0.4.0"
|
|
60
64
|
}
|
|
61
65
|
}
|