@kashi-ui/react 0.0.0-stage → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kashi contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,27 @@
1
- # Temporary Holding Version
1
+ # @kashi-ui/react
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ کامپوننت‌های React [کاشی](https://github.com/Sepantapouya/Kashi)، دیزاین‌سیستم فارسی، دسترس‌پذیر و متن‌باز.
4
+
5
+ ```bash
6
+ npm i @kashi-ui/react @kashi-ui/css
7
+ ```
8
+
9
+ ```tsx
10
+ import "@kashi-ui/css";
11
+ import { Button, Input } from "@kashi-ui/react";
12
+
13
+ export function SignUp() {
14
+ return (
15
+ <form>
16
+ <Input label="ایمیل" type="email" required />
17
+ <Button variant="primary" type="submit">ثبت‌نام</Button>
18
+ </form>
19
+ );
20
+ }
21
+ ```
22
+
23
+ کامپوننت‌ها: Button · Input · Textarea · Badge · Card · Alert · Avatar · AvatarGroup · Checkbox · Switch
24
+
25
+ فارسی: DatePicker و Calendar شمسی · NationalIdInput · CardNumberInput
26
+
27
+ نیازمند React ۱۸ یا بالاتر. مستندات کامل: [kashi.sepantapouya.com](https://kashi.sepantapouya.com) · کد: [GitHub](https://github.com/Sepantapouya/Kashi) · لایسنس MIT
@@ -0,0 +1,18 @@
1
+ import { type HTMLAttributes, type ReactNode } from "react";
2
+ export type AlertTone = "neutral" | "brand" | "info" | "success" | "warning" | "error";
3
+ export interface AlertProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
4
+ /** @default "neutral" */
5
+ tone?: AlertTone;
6
+ title?: ReactNode;
7
+ /** آیکون سفارشی؛ false برای حذف */
8
+ icon?: ReactNode | false;
9
+ /** دکمه‌ها یا لینک‌های زیر متن */
10
+ actions?: ReactNode;
11
+ /** با دادن این تابع دکمهٔ بستن نمایش داده می‌شود */
12
+ onClose?: () => void;
13
+ /** @default "بستن" */
14
+ closeLabel?: string;
15
+ /** پس‌زمینهٔ رنگی کم‌رنگ */
16
+ tinted?: boolean;
17
+ }
18
+ export declare const Alert: import("react").ForwardRefExoticComponent<AlertProps & import("react").RefAttributes<HTMLDivElement>>;
package/dist/Alert.js ADDED
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef } from "react";
3
+ import { cx } from "./cx.js";
4
+ import { CloseIcon, ErrorIcon, InfoIcon, SuccessIcon, WarningIcon } from "./icons.js";
5
+ const ICONS = {
6
+ neutral: InfoIcon,
7
+ brand: InfoIcon,
8
+ info: InfoIcon,
9
+ success: SuccessIcon,
10
+ warning: WarningIcon,
11
+ error: ErrorIcon,
12
+ };
13
+ export const Alert = forwardRef(function Alert({ tone = "neutral", title, icon, actions, onClose, closeLabel = "بستن", tinted = false, role, className, children, ...rest }, ref) {
14
+ const Icon = ICONS[tone];
15
+ return (_jsxs("div", { ref: ref,
16
+ // خطا و هشدار فوراً خوانده می‌شوند؛ بقیه مؤدبانه
17
+ role: role ?? (tone === "error" || tone === "warning" ? "alert" : "status"), className: cx("k-alert", tone !== "neutral" && `k-alert--${tone}`, tinted && "k-alert--tinted", className), ...rest, children: [icon !== false && _jsx("span", { className: "k-alert__icon", "aria-hidden": "true", children: icon ?? _jsx(Icon, {}) }), _jsxs("div", { className: "k-alert__content", children: [title && _jsx("p", { className: "k-alert__title", children: title }), children && _jsx("div", { className: "k-alert__description", children: children }), actions && _jsx("div", { className: "k-alert__actions", children: actions })] }), onClose && (_jsx("button", { type: "button", className: "k-btn k-btn--tertiary k-btn--icon k-btn--sm k-alert__close", "aria-label": closeLabel, onClick: onClose, children: _jsx(CloseIcon, {}) }))] }));
18
+ });
@@ -0,0 +1,29 @@
1
+ import { type HTMLAttributes, type ReactNode } from "react";
2
+ export type AvatarSize = "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
3
+ export type AvatarStatus = "online" | "offline" | "busy" | "away";
4
+ /** حروف اول دو کلمهٔ اول؛ با نیم‌فاصله تا حروف فارسی به هم نچسبند */
5
+ export declare function initials(name: string): string;
6
+ export interface AvatarProps extends HTMLAttributes<HTMLSpanElement> {
7
+ /** نام شخص — برای alt تصویر و حروف اول جایگزین */
8
+ name: string;
9
+ src?: string;
10
+ /** @default "md" */
11
+ size?: AvatarSize;
12
+ square?: boolean;
13
+ /** رنگ برند برای حالت حروف اول */
14
+ brand?: boolean;
15
+ status?: AvatarStatus;
16
+ /** برچسب‌های وضعیت برای صفحه‌خوان (پیش‌فرض فارسی) */
17
+ statusLabels?: Partial<Record<AvatarStatus, string>>;
18
+ }
19
+ export declare const Avatar: import("react").ForwardRefExoticComponent<AvatarProps & import("react").RefAttributes<HTMLSpanElement>>;
20
+ export interface AvatarGroupProps extends HTMLAttributes<HTMLDivElement> {
21
+ /** حداکثر تعداد نمایش؛ بقیه به شکل «+۳» */
22
+ max?: number;
23
+ /** اندازهٔ آواتار «+n» @default "md" */
24
+ size?: AvatarSize;
25
+ /** @default (n) => `${n} نفر دیگر` */
26
+ moreLabel?: (count: number) => string;
27
+ children: ReactNode;
28
+ }
29
+ export declare function AvatarGroup({ max, size, moreLabel, className, children, ...rest }: AvatarGroupProps): import("react").JSX.Element;
package/dist/Avatar.js ADDED
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Children, forwardRef, useState } from "react";
3
+ import { cx } from "./cx.js";
4
+ import { toFaDigits } from "./utils.js";
5
+ const STATUS_LABEL = {
6
+ online: "آنلاین",
7
+ offline: "آفلاین",
8
+ busy: "مشغول",
9
+ away: "غایب",
10
+ };
11
+ /** حروف اول دو کلمهٔ اول؛ با نیم‌فاصله تا حروف فارسی به هم نچسبند */
12
+ export function initials(name) {
13
+ return name
14
+ .trim()
15
+ .split(/\s+/)
16
+ .slice(0, 2)
17
+ .map((w) => w[0])
18
+ .join("‌")
19
+ .toUpperCase();
20
+ }
21
+ export const Avatar = forwardRef(function Avatar({ name, src, size = "md", square = false, brand = false, status, statusLabels, className, ...rest }, ref) {
22
+ const [failed, setFailed] = useState(false);
23
+ const showImage = src && !failed;
24
+ const statusText = status ? (statusLabels?.[status] ?? STATUS_LABEL[status]) : undefined;
25
+ return (_jsxs("span", { ref: ref, role: "img", "aria-label": statusText ? `${name}، ${statusText}` : name, className: cx("k-avatar", size !== "md" && `k-avatar--${size}`, square && "k-avatar--square", brand && "k-avatar--brand", className), ...rest, children: [showImage ? (_jsx("img", { className: "k-avatar__image", src: src, alt: "", onError: () => setFailed(true) })) : (_jsx("span", { "aria-hidden": "true", children: initials(name) })), status && _jsx("span", { className: `k-avatar__status k-avatar__status--${status}`, "aria-hidden": "true" })] }));
26
+ });
27
+ export function AvatarGroup({ max, size = "md", moreLabel = (n) => `${toFaDigits(n)} نفر دیگر`, className, children, ...rest }) {
28
+ const items = Children.toArray(children);
29
+ const visible = max ? items.slice(0, max) : items;
30
+ const extra = items.length - visible.length;
31
+ return (_jsxs("div", { role: "group", className: cx("k-avatar-group", className), ...rest, children: [visible, extra > 0 && (_jsx("span", { role: "img", "aria-label": moreLabel(extra), className: cx("k-avatar", size !== "md" && `k-avatar--${size}`), children: _jsxs("span", { "aria-hidden": "true", children: ["+", toFaDigits(extra)] }) }))] }));
32
+ }
@@ -0,0 +1,13 @@
1
+ import { type HTMLAttributes } from "react";
2
+ export type BadgeTone = "neutral" | "brand" | "error" | "warning" | "success" | "info";
3
+ export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
4
+ /** @default "neutral" */
5
+ tone?: BadgeTone;
6
+ /** @default "md" */
7
+ size?: "sm" | "md" | "lg";
8
+ /** نقطهٔ رنگی ابتدای متن */
9
+ dot?: boolean;
10
+ /** گوشهٔ کم به‌جای گرد */
11
+ square?: boolean;
12
+ }
13
+ export declare const Badge: import("react").ForwardRefExoticComponent<BadgeProps & import("react").RefAttributes<HTMLSpanElement>>;
package/dist/Badge.js ADDED
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef } from "react";
3
+ import { cx } from "./cx.js";
4
+ export const Badge = forwardRef(function Badge({ tone = "neutral", size = "md", dot = false, square = false, className, children, ...rest }, ref) {
5
+ return (_jsxs("span", { ref: ref, className: cx("k-badge", tone !== "neutral" && `k-badge--${tone}`, size !== "md" && `k-badge--${size}`, square && "k-badge--square", className), ...rest, children: [dot && _jsx("span", { className: "k-badge__dot", "aria-hidden": "true" }), children] }));
6
+ });
@@ -0,0 +1,22 @@
1
+ import { type ButtonHTMLAttributes, type ReactNode } from "react";
2
+ export type ButtonVariant = "primary" | "secondary" | "tertiary" | "link" | "danger" | "danger-secondary";
3
+ export type ButtonSize = "sm" | "md" | "lg" | "xl";
4
+ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
5
+ /** @default "secondary" */
6
+ variant?: ButtonVariant;
7
+ /** @default "md" */
8
+ size?: ButtonSize;
9
+ /** نمایش اسپینر؛ دکمه فوکوس‌پذیر می‌ماند ولی کلیک نمی‌گیرد */
10
+ loading?: boolean;
11
+ /** متنی که صفحه‌خوان هنگام بارگذاری می‌خواند @default "در حال بارگذاری" */
12
+ loadingLabel?: string;
13
+ /** دکمهٔ تمام‌عرض */
14
+ block?: boolean;
15
+ /** دکمهٔ مربعی فقط‌آیکون — حتماً aria-label بدهید */
16
+ iconOnly?: boolean;
17
+ /** آیکون ابتدای متن (در RTL سمت راست) */
18
+ iconStart?: ReactNode;
19
+ /** آیکون انتهای متن (در RTL سمت چپ) */
20
+ iconEnd?: ReactNode;
21
+ }
22
+ export declare const Button: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
package/dist/Button.js ADDED
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef } from "react";
3
+ import { cx } from "./cx.js";
4
+ import { isDev } from "./utils.js";
5
+ export const Button = forwardRef(function Button({ variant = "secondary", size = "md", loading = false, loadingLabel = "در حال بارگذاری", block = false, iconOnly = false, iconStart, iconEnd, disabled, type = "button", className, children, onClick, ...rest }, ref) {
6
+ if (isDev() && iconOnly && !rest["aria-label"] && !rest["aria-labelledby"]) {
7
+ console.warn("[kashi] <Button iconOnly> بدون aria-label برای صفحه‌خوان‌ها بی‌نام است.");
8
+ }
9
+ return (_jsxs("button", { ref: ref, type: type, disabled: disabled, "aria-disabled": loading || undefined, "aria-busy": loading || undefined, onClick: loading ? (e) => e.preventDefault() : onClick, className: cx("k-btn", `k-btn--${variant}`, size !== "md" && `k-btn--${size}`, block && "k-btn--block", iconOnly && "k-btn--icon", className), ...rest, children: [iconStart, children, iconEnd, loading && (_jsx("span", { className: "k-sr-only", role: "status", children: loadingLabel }))] }));
10
+ });
@@ -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;
@@ -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
+ }
package/dist/Card.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { type ElementType, type HTMLAttributes } from "react";
2
+ export interface CardProps extends HTMLAttributes<HTMLElement> {
3
+ /** @default "article" */
4
+ as?: ElementType;
5
+ /** بدون سایه */
6
+ flat?: boolean;
7
+ /** کارت قابل‌کلیک؛ یک لینک یا دکمه با کلاس k-card__link داخلش بگذارید */
8
+ interactive?: boolean;
9
+ }
10
+ export declare const Card: import("react").ForwardRefExoticComponent<CardProps & import("react").RefAttributes<HTMLElement>>;
11
+ export declare const CardHeader: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLElement> & {
12
+ as?: ElementType;
13
+ } & import("react").RefAttributes<HTMLElement>>;
14
+ /** پیش‌فرض h3؛ با as سطح عنوان را با ساختار صفحه هماهنگ کنید */
15
+ export declare const CardTitle: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLElement> & {
16
+ as?: ElementType;
17
+ } & import("react").RefAttributes<HTMLElement>>;
18
+ export declare const CardDescription: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLElement> & {
19
+ as?: ElementType;
20
+ } & import("react").RefAttributes<HTMLElement>>;
21
+ export declare const CardBody: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLElement> & {
22
+ as?: ElementType;
23
+ } & import("react").RefAttributes<HTMLElement>>;
24
+ export declare const CardFooter: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLElement> & {
25
+ as?: ElementType;
26
+ } & import("react").RefAttributes<HTMLElement>>;
package/dist/Card.js ADDED
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef } from "react";
3
+ import { cx } from "./cx.js";
4
+ export const Card = forwardRef(function Card({ as: Tag = "article", flat = false, interactive = false, className, ...rest }, ref) {
5
+ return _jsx(Tag, { ref: ref, className: cx("k-card", flat && "k-card--flat", interactive && "k-card--interactive", className), ...rest });
6
+ });
7
+ const part = (tag, base, name) => {
8
+ const Part = forwardRef(function Part({ as: Tag = tag, className, ...rest }, ref) {
9
+ return _jsx(Tag, { ref: ref, className: cx(base, className), ...rest });
10
+ });
11
+ Part.displayName = name;
12
+ return Part;
13
+ };
14
+ export const CardHeader = part("header", "k-card__header", "CardHeader");
15
+ /** پیش‌فرض h3؛ با as سطح عنوان را با ساختار صفحه هماهنگ کنید */
16
+ export const CardTitle = part("h3", "k-card__title", "CardTitle");
17
+ export const CardDescription = part("p", "k-card__description", "CardDescription");
18
+ export const CardBody = part("div", "k-card__body", "CardBody");
19
+ export const CardFooter = part("footer", "k-card__footer", "CardFooter");
@@ -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,22 @@
1
+ import { type InputHTMLAttributes, type ReactNode } from "react";
2
+ interface CheckOwnProps {
3
+ /** برچسب کنار کنترل */
4
+ label?: ReactNode;
5
+ /** توضیح زیر برچسب */
6
+ description?: ReactNode;
7
+ /** حالت خطا */
8
+ invalid?: boolean;
9
+ }
10
+ export interface CheckboxProps extends CheckOwnProps, Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "size"> {
11
+ /** حالت نیمه‌انتخاب (مثلاً برای «انتخاب همه») */
12
+ indeterminate?: boolean;
13
+ /** @default "md" */
14
+ size?: "md" | "lg";
15
+ }
16
+ export declare const Checkbox: import("react").ForwardRefExoticComponent<CheckboxProps & import("react").RefAttributes<HTMLInputElement>>;
17
+ export interface SwitchProps extends CheckOwnProps, Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "size" | "role"> {
18
+ /** @default "md" */
19
+ size?: "sm" | "md" | "lg";
20
+ }
21
+ export declare const Switch: import("react").ForwardRefExoticComponent<SwitchProps & import("react").RefAttributes<HTMLInputElement>>;
22
+ export {};
@@ -0,0 +1,26 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useEffect, useId, useRef } from "react";
3
+ import { cx } from "./cx.js";
4
+ import { mergeRefs } from "./utils.js";
5
+ /** برچسب و توضیح مشترک بین Checkbox و Switch */
6
+ function CheckLabel({ id, label, description, className, children }) {
7
+ if (!label && !description)
8
+ return _jsx(_Fragment, { children: children });
9
+ return (_jsxs("label", { className: cx("k-check", className), htmlFor: id, children: [children, _jsxs("span", { className: "k-check__text", children: [label && _jsx("span", { className: "k-check__label", children: label }), description && _jsx("span", { className: "k-check__description", id: `${id}-desc`, children: description })] })] }));
10
+ }
11
+ export const Checkbox = forwardRef(function Checkbox({ label, description, invalid, indeterminate = false, size = "md", id, className, ...rest }, ref) {
12
+ const autoId = useId();
13
+ const inputId = id ?? autoId;
14
+ const inner = useRef(null);
15
+ // indeterminate فقط از طریق DOM قابل‌تنظیم است
16
+ useEffect(() => {
17
+ if (inner.current)
18
+ inner.current.indeterminate = indeterminate;
19
+ }, [indeterminate]);
20
+ return (_jsx(CheckLabel, { id: inputId, label: label, description: description, className: className, children: _jsx("input", { ref: mergeRefs(ref, inner), type: "checkbox", id: inputId, className: cx("k-checkbox", size === "lg" && "k-checkbox--lg", !label && !description && className), "aria-invalid": invalid || undefined, "aria-describedby": description ? `${inputId}-desc` : undefined, "data-indeterminate": indeterminate || undefined, ...rest }) }));
21
+ });
22
+ export const Switch = forwardRef(function Switch({ label, description, invalid, size = "md", id, className, ...rest }, ref) {
23
+ const autoId = useId();
24
+ const inputId = id ?? autoId;
25
+ return (_jsx(CheckLabel, { id: inputId, label: label, description: description, className: className, children: _jsx("input", { ref: ref, type: "checkbox", role: "switch", id: inputId, className: cx("k-switch", size !== "md" && `k-switch--${size}`, !label && !description && className), "aria-invalid": invalid || undefined, "aria-describedby": description ? `${inputId}-desc` : undefined, ...rest }) }));
26
+ });
@@ -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,16 @@
1
+ import { type ReactNode } from "react";
2
+ export interface FieldProps {
3
+ id: string;
4
+ label?: ReactNode;
5
+ hint?: ReactNode;
6
+ error?: ReactNode;
7
+ required?: boolean;
8
+ className?: string;
9
+ children: ReactNode;
10
+ }
11
+ /** شناسهٔ متن‌های راهنما و خطا برای aria-describedby */
12
+ export declare const hintId: (id: string) => string;
13
+ export declare const errorId: (id: string) => string;
14
+ export declare function describedBy(id: string, hint: unknown, error: unknown, extra?: string): string | undefined;
15
+ /** قالب مشترک برچسب + کنترل + راهنما/خطا */
16
+ export declare function Field({ id, label, hint, error, required, className, children }: FieldProps): import("react").JSX.Element;
package/dist/Field.js ADDED
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** شناسهٔ متن‌های راهنما و خطا برای aria-describedby */
3
+ export const hintId = (id) => `${id}-hint`;
4
+ export const errorId = (id) => `${id}-error`;
5
+ export function describedBy(id, hint, error, extra) {
6
+ return [extra, error ? errorId(id) : null, hint ? hintId(id) : null].filter(Boolean).join(" ") || undefined;
7
+ }
8
+ /** قالب مشترک برچسب + کنترل + راهنما/خطا */
9
+ export function Field({ id, label, hint, error, required, className, children }) {
10
+ return (_jsxs("div", { className: className ? `k-field ${className}` : "k-field", children: [label && (_jsxs("label", { className: "k-label", htmlFor: id, children: [label, required && _jsx("span", { className: "k-label__required", "aria-hidden": "true", children: "*" })] })), children, error && _jsx("p", { className: "k-hint k-hint--error", id: errorId(id), children: error }), hint && _jsx("p", { className: "k-hint", id: hintId(id), children: hint })] }));
11
+ }
@@ -0,0 +1,25 @@
1
+ import { type InputHTMLAttributes, type ReactNode, type TextareaHTMLAttributes } from "react";
2
+ export type InputSize = "sm" | "md" | "lg";
3
+ interface FieldOwnProps {
4
+ /** برچسب بالای فیلد — اگر نمی‌دهید حتماً aria-label بدهید */
5
+ label?: ReactNode;
6
+ /** متن راهنمای زیر فیلد */
7
+ hint?: ReactNode;
8
+ /** پیام خطا؛ فیلد را aria-invalid می‌کند */
9
+ error?: ReactNode;
10
+ /** کلاس روی ظرف بیرونی (k-field) */
11
+ fieldClassName?: string;
12
+ }
13
+ export interface InputProps extends FieldOwnProps, Omit<InputHTMLAttributes<HTMLInputElement>, "size"> {
14
+ /** @default "md" */
15
+ size?: InputSize;
16
+ /** آیکون یا متن ابتدای فیلد */
17
+ addonStart?: ReactNode;
18
+ /** آیکون یا متن انتهای فیلد */
19
+ addonEnd?: ReactNode;
20
+ }
21
+ export declare const Input: import("react").ForwardRefExoticComponent<InputProps & import("react").RefAttributes<HTMLInputElement>>;
22
+ export interface TextareaProps extends FieldOwnProps, TextareaHTMLAttributes<HTMLTextAreaElement> {
23
+ }
24
+ export declare const Textarea: import("react").ForwardRefExoticComponent<TextareaProps & import("react").RefAttributes<HTMLTextAreaElement>>;
25
+ export {};
package/dist/Input.js ADDED
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useId } from "react";
3
+ import { cx } from "./cx.js";
4
+ import { Field, describedBy } from "./Field.js";
5
+ export const Input = forwardRef(function Input({ label, hint, error, size = "md", addonStart, addonEnd, required, id, className, fieldClassName, "aria-describedby": ariaDescribedBy, ...rest }, ref) {
6
+ const autoId = useId();
7
+ const inputId = id ?? autoId;
8
+ return (_jsx(Field, { id: inputId, label: label, hint: hint, error: error, required: required, className: fieldClassName, children: _jsxs("div", { className: cx("k-input", size !== "md" && `k-input--${size}`, className), children: [addonStart && _jsx("span", { className: "k-input__addon", children: addonStart }), _jsx("input", { ref: ref, id: inputId, className: "k-input__control", required: required, "aria-invalid": error ? true : undefined, "aria-describedby": describedBy(inputId, hint, error, ariaDescribedBy), ...rest }), addonEnd && _jsx("span", { className: "k-input__addon", children: addonEnd })] }) }));
9
+ });
10
+ export const Textarea = forwardRef(function Textarea({ label, hint, error, required, id, className, fieldClassName, "aria-describedby": ariaDescribedBy, ...rest }, ref) {
11
+ const autoId = useId();
12
+ const inputId = id ?? autoId;
13
+ return (_jsx(Field, { id: inputId, label: label, hint: hint, error: error, required: required, className: fieldClassName, children: _jsx("div", { className: cx("k-input", "k-input--textarea", className), children: _jsx("textarea", { ref: ref, id: inputId, className: "k-input__control", required: required, "aria-invalid": error ? true : undefined, "aria-describedby": describedBy(inputId, hint, error, ariaDescribedBy), ...rest }) }) }));
14
+ });
@@ -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/cx.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** کلاس‌ها را به هم می‌چسباند و مقادیر خالی را نادیده می‌گیرد */
2
+ export declare function cx(...parts: Array<string | false | null | undefined>): string;
package/dist/cx.js ADDED
@@ -0,0 +1,4 @@
1
+ /** کلاس‌ها را به هم می‌چسباند و مقادیر خالی را نادیده می‌گیرد */
2
+ export function cx(...parts) {
3
+ return parts.filter(Boolean).join(" ");
4
+ }
@@ -0,0 +1,5 @@
1
+ export declare const InfoIcon: () => import("react").JSX.Element;
2
+ export declare const SuccessIcon: () => import("react").JSX.Element;
3
+ export declare const WarningIcon: () => import("react").JSX.Element;
4
+ export declare const ErrorIcon: () => import("react").JSX.Element;
5
+ export declare const CloseIcon: () => import("react").JSX.Element;
package/dist/icons.js ADDED
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // آیکون‌های داخلی کوچک — همه aria-hidden هستند
3
+ const base = {
4
+ viewBox: "0 0 20 20",
5
+ fill: "none",
6
+ stroke: "currentColor",
7
+ strokeWidth: 1.7,
8
+ strokeLinecap: "round",
9
+ strokeLinejoin: "round",
10
+ "aria-hidden": true,
11
+ };
12
+ export const InfoIcon = () => (_jsxs("svg", { ...base, children: [_jsx("circle", { cx: "10", cy: "10", r: "7.5" }), _jsx("path", { d: "M10 13.5V9.5M10 6.5h.01" })] }));
13
+ export const SuccessIcon = () => (_jsxs("svg", { ...base, children: [_jsx("circle", { cx: "10", cy: "10", r: "7.5" }), _jsx("path", { d: "m6.75 10 2.25 2.25 4.25-4.5" })] }));
14
+ export const WarningIcon = () => (_jsxs("svg", { ...base, children: [_jsx("path", { d: "M8.6 3.4 2.4 14a1.6 1.6 0 0 0 1.4 2.4h12.4a1.6 1.6 0 0 0 1.4-2.4L11.4 3.4a1.6 1.6 0 0 0-2.8 0Z" }), _jsx("path", { d: "M10 7.5v3.25M10 13.5h.01" })] }));
15
+ export const ErrorIcon = () => (_jsxs("svg", { ...base, children: [_jsx("circle", { cx: "10", cy: "10", r: "7.5" }), _jsx("path", { d: "M10 6.5v4M10 13.5h.01" })] }));
16
+ export const CloseIcon = () => (_jsx("svg", { ...base, children: _jsx("path", { d: "m5 5 10 10M15 5 5 15" }) }));
@@ -0,0 +1,24 @@
1
+ export { Button } from "./Button.js";
2
+ export type { ButtonProps, ButtonVariant, ButtonSize } from "./Button.js";
3
+ export { Input, Textarea } from "./Input.js";
4
+ export type { InputProps, InputSize, TextareaProps } from "./Input.js";
5
+ export { Badge } from "./Badge.js";
6
+ export type { BadgeProps, BadgeTone } from "./Badge.js";
7
+ export { Card, CardHeader, CardTitle, CardDescription, CardBody, CardFooter } from "./Card.js";
8
+ export type { CardProps } from "./Card.js";
9
+ export { Alert } from "./Alert.js";
10
+ export type { AlertProps, AlertTone } from "./Alert.js";
11
+ export { Avatar, AvatarGroup, initials } from "./Avatar.js";
12
+ export type { AvatarProps, AvatarGroupProps, AvatarSize, AvatarStatus } from "./Avatar.js";
13
+ export { Checkbox, Switch } from "./Checkbox.js";
14
+ export type { CheckboxProps, SwitchProps } from "./Checkbox.js";
15
+ export { cx } from "./cx.js";
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 ADDED
@@ -0,0 +1,13 @@
1
+ export { Button } from "./Button.js";
2
+ export { Input, Textarea } from "./Input.js";
3
+ export { Badge } from "./Badge.js";
4
+ export { Card, CardHeader, CardTitle, CardDescription, CardBody, CardFooter } from "./Card.js";
5
+ export { Alert } from "./Alert.js";
6
+ export { Avatar, AvatarGroup, initials } from "./Avatar.js";
7
+ export { Checkbox, Switch } from "./Checkbox.js";
8
+ export { cx } from "./cx.js";
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";
@@ -0,0 +1,5 @@
1
+ import type { Ref, RefCallback } from "react";
2
+ export { toFaDigits } from "@kashi-ui/persian";
3
+ /** چند ref را یکی می‌کند */
4
+ export declare function mergeRefs<T>(...refs: Array<Ref<T> | undefined>): RefCallback<T>;
5
+ export declare function isDev(): boolean;
package/dist/utils.js ADDED
@@ -0,0 +1,15 @@
1
+ export { toFaDigits } from "@kashi-ui/persian";
2
+ /** چند ref را یکی می‌کند */
3
+ export function mergeRefs(...refs) {
4
+ return (node) => {
5
+ for (const ref of refs) {
6
+ if (typeof ref === "function")
7
+ ref(node);
8
+ else if (ref)
9
+ ref.current = node;
10
+ }
11
+ };
12
+ }
13
+ export function isDev() {
14
+ return typeof process !== "undefined" && process.env.NODE_ENV !== "production";
15
+ }
package/package.json CHANGED
@@ -1,6 +1,64 @@
1
1
  {
2
2
  "name": "@kashi-ui/react",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.0",
4
+ "description": "کامپوننت‌های React دیزاین‌سیستم کاشی",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "sideEffects": false,
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.json",
22
+ "test": "node --test test/*.test.mjs"
23
+ },
24
+ "peerDependencies": {
25
+ "@kashi-ui/css": "^0.3.0",
26
+ "react": ">=18"
27
+ },
28
+ "devDependencies": {
29
+ "@types/react": "^18.3.0",
30
+ "react": "^18.3.1",
31
+ "react-dom": "^18.3.1",
32
+ "typescript": "^5.6.0"
33
+ },
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/Sepantapouya/Kashi.git",
38
+ "directory": "packages/react"
39
+ },
40
+ "homepage": "https://kashi.sepantapouya.com",
41
+ "bugs": "https://github.com/Sepantapouya/Kashi/issues",
42
+ "author": "Sepanta Pouya (https://sepantapouya.com)",
43
+ "keywords": [
44
+ "design-system",
45
+ "persian",
46
+ "farsi",
47
+ "rtl",
48
+ "accessibility",
49
+ "ui",
50
+ "css",
51
+ "react"
52
+ ],
53
+ "publishConfig": {
54
+ "access": "public"
55
+ },
56
+ "peerDependenciesMeta": {
57
+ "@kashi-ui/css": {
58
+ "optional": false
59
+ }
60
+ },
61
+ "dependencies": {
62
+ "@kashi-ui/persian": "^0.3.0"
63
+ }
64
+ }