@kashi-ui/react 0.2.0 → 0.3.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/Calendar.d.ts +37 -0
- package/dist/Calendar.js +107 -0
- package/dist/CardNumberInput.d.ts +19 -0
- package/dist/CardNumberInput.js +50 -0
- package/dist/DatePicker.d.ts +28 -0
- package/dist/DatePicker.js +90 -0
- package/dist/NationalIdInput.d.ts +17 -0
- package/dist/NationalIdInput.js +21 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +4 -0
- package/dist/utils.d.ts +1 -2
- package/dist/utils.js +1 -4
- package/package.json +5 -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
|
|
26
|
+
|
|
25
27
|
نیازمند React ۱۸ یا بالاتر. مستندات کامل: [kashi.sepantapouya.com](https://kashi.sepantapouya.com) · کد: [GitHub](https://github.com/Sepantapouya/Kashi) · لایسنس MIT
|
|
@@ -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,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 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 "شماره کارت معتبر نیست." */
|
|
15
|
+
invalidMessage?: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
/** فیلد شماره کارت بانکی: گروهبندی چهارتایی، تشخیص بانک و بررسی Luhn */
|
|
18
|
+
export declare const CardNumberInput: import("react").ForwardRefExoticComponent<CardNumberInputProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
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";
|
|
4
|
+
import { Input } from "./Input.js";
|
|
5
|
+
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) {
|
|
19
|
+
const autoId = useId();
|
|
20
|
+
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
|
+
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;
|
|
28
|
+
const bank = showBank ? getBankByCard(raw) : null;
|
|
29
|
+
const showInvalid = raw.length > 0 && (touched || raw.length === 16) && !isValidCardNumber(raw);
|
|
30
|
+
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
|
+
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);
|
|
45
|
+
onValueChange?.(next, isValidCardNumber(next), getBankByCard(next)?.name ?? null);
|
|
46
|
+
}, onBlur: (e) => {
|
|
47
|
+
setTouched(true);
|
|
48
|
+
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 })] }));
|
|
50
|
+
});
|
|
@@ -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, useLayoutEffect, 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 } 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
|
+
useLayoutEffect(() => {
|
|
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,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
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -14,3 +14,11 @@ 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";
|
package/dist/index.js
CHANGED
|
@@ -7,3 +7,7 @@ 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";
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Ref, RefCallback } from "react";
|
|
2
|
-
|
|
3
|
-
export declare function toFaDigits(value: string | number): string;
|
|
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;
|
package/dist/utils.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export function toFaDigits(value) {
|
|
3
|
-
return String(value).replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[Number(d)]);
|
|
4
|
-
}
|
|
1
|
+
export { toFaDigits } from "@kashi-ui/persian";
|
|
5
2
|
/** چند ref را یکی میکند */
|
|
6
3
|
export function mergeRefs(...refs) {
|
|
7
4
|
return (node) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kashi-ui/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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.3.0",
|
|
26
26
|
"react": ">=18"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
@@ -57,5 +57,8 @@
|
|
|
57
57
|
"@kashi-ui/css": {
|
|
58
58
|
"optional": false
|
|
59
59
|
}
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"@kashi-ui/persian": "^0.3.0"
|
|
60
63
|
}
|
|
61
64
|
}
|