@kashi-ui/react 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,7 +20,7 @@ export function SignUp() {
20
20
  }
21
21
  ```
22
22
 
23
- کامپوننت‌ها: Button · Input · Textarea · Badge · Card · Alert · Avatar · AvatarGroup · Checkbox · Switch
23
+ کامپوننت‌ها: Button · Input · Textarea · Select · Checkbox · RadioGroup · Switch · Badge · Card · Alert · Avatar · AvatarGroup · Tabs · Dialog · Toast · Tooltip · RelativeTime
24
24
 
25
25
  فارسی: DatePicker و Calendar شمسی · NationalIdInput · CardNumberInput · IbanInput · MobileInput · PriceInput
26
26
 
@@ -0,0 +1,27 @@
1
+ import { type ReactNode } from "react";
2
+ export interface DialogProps {
3
+ open: boolean;
4
+ onOpenChange: (open: boolean) => void;
5
+ title: ReactNode;
6
+ description?: ReactNode;
7
+ /** آیکون شاخص بالای عنوان */
8
+ icon?: ReactNode;
9
+ /** «danger» برای تأیید عملیات مخرب */
10
+ tone?: "default" | "danger";
11
+ /** @default "md" */
12
+ size?: "sm" | "md" | "lg";
13
+ /** دکمه‌های پایین پنجره */
14
+ footer?: ReactNode;
15
+ /** بستن با کلیک روی پس‌زمینه @default true */
16
+ dismissible?: boolean;
17
+ /** @default "بستن" */
18
+ closeLabel?: string;
19
+ className?: string;
20
+ children?: ReactNode;
21
+ }
22
+ /**
23
+ * پنجرهٔ گفتگو روی <dialog> بومی با showModal:
24
+ * فوکوس داخل پنجره می‌ماند، Esc می‌بندد، بقیهٔ صفحه از دسترس صفحه‌خوان خارج می‌شود
25
+ * و پس از بستن، فوکوس به عنصری که پنجره را باز کرده برمی‌گردد.
26
+ */
27
+ export declare function Dialog({ open, onOpenChange, title, description, icon, tone, size, footer, dismissible, closeLabel, className, children, }: DialogProps): import("react").JSX.Element;
package/dist/Dialog.js ADDED
@@ -0,0 +1,38 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useId, useRef } from "react";
3
+ import { cx } from "./cx.js";
4
+ import { CloseIcon } from "./icons.js";
5
+ /**
6
+ * پنجرهٔ گفتگو روی <dialog> بومی با showModal:
7
+ * فوکوس داخل پنجره می‌ماند، Esc می‌بندد، بقیهٔ صفحه از دسترس صفحه‌خوان خارج می‌شود
8
+ * و پس از بستن، فوکوس به عنصری که پنجره را باز کرده برمی‌گردد.
9
+ */
10
+ export function Dialog({ open, onOpenChange, title, description, icon, tone = "default", size = "md", footer, dismissible = true, closeLabel = "بستن", className, children, }) {
11
+ const ref = useRef(null);
12
+ const opener = useRef(null);
13
+ const id = useId();
14
+ useEffect(() => {
15
+ const d = ref.current;
16
+ if (!d)
17
+ return;
18
+ if (open && !d.open) {
19
+ opener.current = document.activeElement;
20
+ d.showModal();
21
+ }
22
+ else if (!open && d.open) {
23
+ d.close();
24
+ }
25
+ }, [open]);
26
+ return (_jsxs("dialog", { ref: ref, className: cx("k-dialog", size !== "md" && `k-dialog--${size}`, tone === "danger" && "k-dialog--danger", className), "aria-labelledby": `${id}-title`, "aria-describedby": description ? `${id}-desc` : undefined, onClose: () => {
27
+ onOpenChange(false);
28
+ opener.current?.focus?.();
29
+ }, onCancel: (e) => {
30
+ // Esc: بستن از طریق state تا والد همگام بماند
31
+ e.preventDefault();
32
+ onOpenChange(false);
33
+ }, onClick: (e) => {
34
+ // کلیک روی خود <dialog> یعنی کلیک روی پس‌زمینه
35
+ if (dismissible && e.target === ref.current)
36
+ onOpenChange(false);
37
+ }, children: [_jsxs("div", { className: "k-dialog__header", children: [icon && _jsx("span", { className: "k-dialog__icon", "aria-hidden": "true", children: icon }), _jsxs("div", { className: "k-dialog__heading", children: [_jsx("h2", { className: "k-dialog__title", id: `${id}-title`, children: title }), description && _jsx("p", { className: "k-dialog__description", id: `${id}-desc`, children: description })] }), _jsx("button", { type: "button", className: "k-btn k-btn--tertiary k-btn--icon k-btn--sm k-dialog__close", "aria-label": closeLabel, onClick: () => onOpenChange(false), children: _jsx(CloseIcon, {}) })] }), children && _jsx("div", { className: "k-dialog__body", children: children }), footer && _jsx("div", { className: "k-dialog__footer", children: footer })] }));
38
+ }
@@ -0,0 +1,26 @@
1
+ import { type InputHTMLAttributes, type ReactNode } from "react";
2
+ export interface RadioGroupProps {
3
+ /** عنوان گروه (legend) */
4
+ label: ReactNode;
5
+ /** نام فیلد فرم؛ پیش‌فرض خودکار */
6
+ name?: string;
7
+ value?: string;
8
+ defaultValue?: string;
9
+ onChange?: (value: string) => void;
10
+ /** «list» (پیش‌فرض)، «inline» افقی، «cards» کارت‌های قابل‌انتخاب */
11
+ variant?: "list" | "inline" | "cards";
12
+ hint?: ReactNode;
13
+ error?: ReactNode;
14
+ required?: boolean;
15
+ disabled?: boolean;
16
+ className?: string;
17
+ children: ReactNode;
18
+ }
19
+ /** گروه رادیو با fieldset و legend؛ پیمایش با کلیدهای جهت‌دار را مرورگر انجام می‌دهد */
20
+ export declare function RadioGroup({ label, name, value, defaultValue, onChange, variant, hint, error, required, disabled, className, children, }: RadioGroupProps): import("react").JSX.Element;
21
+ export interface RadioProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "value"> {
22
+ value: string;
23
+ label: ReactNode;
24
+ description?: ReactNode;
25
+ }
26
+ export declare const Radio: import("react").ForwardRefExoticComponent<RadioProps & import("react").RefAttributes<HTMLInputElement>>;
package/dist/Radio.js ADDED
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createContext, forwardRef, useContext, useId, useState } from "react";
3
+ import { cx } from "./cx.js";
4
+ import { isDev } from "./utils.js";
5
+ const RadioCtx = createContext(null);
6
+ /** گروه رادیو با fieldset و legend؛ پیمایش با کلیدهای جهت‌دار را مرورگر انجام می‌دهد */
7
+ export function RadioGroup({ label, name, value, defaultValue, onChange, variant = "list", hint, error, required, disabled, className, children, }) {
8
+ const id = useId();
9
+ const [inner, setInner] = useState(defaultValue);
10
+ const current = value !== undefined ? value : inner;
11
+ const describedBy = [error && `${id}-error`, hint && `${id}-hint`].filter(Boolean).join(" ") || undefined;
12
+ return (_jsxs("fieldset", { className: cx("k-radio-group", variant !== "list" && `k-radio-group--${variant}`, className), "aria-describedby": describedBy, "aria-required": required || undefined, "aria-invalid": error ? true : undefined, disabled: disabled, children: [_jsxs("legend", { className: "k-label", children: [label, required && _jsx("span", { className: "k-label__required", "aria-hidden": "true", children: "*" })] }), _jsx(RadioCtx.Provider, { value: {
13
+ name: name ?? id,
14
+ value: current,
15
+ invalid: !!error,
16
+ disabled,
17
+ setValue: (v) => {
18
+ if (value === undefined)
19
+ setInner(v);
20
+ onChange?.(v);
21
+ },
22
+ }, children: children }), error && _jsx("p", { className: "k-hint k-hint--error", id: `${id}-error`, children: error }), hint && _jsx("p", { className: "k-hint", id: `${id}-hint`, children: hint })] }));
23
+ }
24
+ export const Radio = forwardRef(function Radio({ value, label, description, id, className, disabled, name, ...rest }, ref) {
25
+ const ctx = useContext(RadioCtx);
26
+ // بدون name مشترک، مرورگر رادیوها را یک گروه نمی‌داند و چند گزینه هم‌زمان انتخاب می‌شود
27
+ if (!ctx && !name && isDev()) {
28
+ console.warn("[kashi] <Radio> باید داخل <RadioGroup> باشد یا name داشته باشد؛ وگرنه چند گزینه هم‌زمان قابل‌انتخاب است.");
29
+ }
30
+ const autoId = useId();
31
+ const inputId = id ?? autoId;
32
+ return (_jsxs("label", { className: cx("k-check", className), htmlFor: inputId, children: [_jsx("input", { ref: ref, type: "radio", className: "k-radio", id: inputId, name: ctx?.name ?? name, value: value, checked: ctx ? ctx.value === value : undefined, onChange: () => ctx?.setValue(value), disabled: disabled, "aria-describedby": description ? `${inputId}-desc` : undefined, ...rest }), _jsxs("span", { className: "k-check__text", children: [_jsx("span", { className: "k-check__label", children: label }), description && _jsx("span", { className: "k-check__description", id: `${inputId}-desc`, children: description })] })] }));
33
+ });
@@ -0,0 +1,10 @@
1
+ import { type HTMLAttributes } from "react";
2
+ export interface RelativeTimeProps extends Omit<HTMLAttributes<HTMLTimeElement>, "children"> {
3
+ date: Date;
4
+ /** @default "fa" */
5
+ digits?: "fa" | "latin";
6
+ /** فاصلهٔ به‌روزرسانی بر حسب میلی‌ثانیه @default 60000 */
7
+ refresh?: number;
8
+ }
9
+ /** «۵ دقیقه پیش» در یک <time> معنایی؛ تاریخ کامل شمسی در title و برای صفحه‌خوان در دسترس است */
10
+ export declare function RelativeTime({ date, digits, refresh, ...rest }: RelativeTimeProps): import("react").JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { formatJalali, formatRelative } from "@kashi-ui/persian";
4
+ /** «۵ دقیقه پیش» در یک <time> معنایی؛ تاریخ کامل شمسی در title و برای صفحه‌خوان در دسترس است */
5
+ export function RelativeTime({ date, digits = "fa", refresh = 60000, ...rest }) {
6
+ const [now, setNow] = useState(() => new Date());
7
+ useEffect(() => {
8
+ const t = setInterval(() => setNow(new Date()), refresh);
9
+ return () => clearInterval(t);
10
+ }, [refresh]);
11
+ return (_jsx("time", { dateTime: date.toISOString(), title: formatJalali(date, { style: "full", digits }), ...rest, children: formatRelative(date, { now, digits }) }));
12
+ }
@@ -0,0 +1,20 @@
1
+ import { type ReactNode, type SelectHTMLAttributes } from "react";
2
+ export interface SelectOption {
3
+ value: string;
4
+ label: ReactNode;
5
+ disabled?: boolean;
6
+ }
7
+ export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "size"> {
8
+ label?: ReactNode;
9
+ hint?: ReactNode;
10
+ error?: ReactNode;
11
+ /** گزینهٔ خالی اول، مثل «انتخاب کنید» */
12
+ placeholder?: string;
13
+ /** گزینه‌ها؛ یا به‌جای آن <option> را به‌عنوان children بدهید */
14
+ options?: SelectOption[];
15
+ /** @default "md" */
16
+ size?: "sm" | "md" | "lg";
17
+ fieldClassName?: string;
18
+ }
19
+ /** فهرست انتخابی روی <select> بومی: در گوشی انتخاب‌گر سیستم باز می‌شود و صفحه‌خوان کامل پشتیبانی می‌کند */
20
+ export declare const Select: import("react").ForwardRefExoticComponent<SelectProps & import("react").RefAttributes<HTMLSelectElement>>;
package/dist/Select.js ADDED
@@ -0,0 +1,11 @@
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
+ /** فهرست انتخابی روی <select> بومی: در گوشی انتخاب‌گر سیستم باز می‌شود و صفحه‌خوان کامل پشتیبانی می‌کند */
6
+ export const Select = forwardRef(function Select({ label, hint, error, placeholder, options, size = "md", id, required, className, fieldClassName, children, "aria-describedby": extra, ...rest }, ref) {
7
+ const autoId = useId();
8
+ const selectId = id ?? autoId;
9
+ const placeholderSelected = placeholder !== undefined && rest.value === undefined && rest.defaultValue === undefined;
10
+ return (_jsx(Field, { id: selectId, label: label, hint: hint, error: error, required: required, className: fieldClassName, children: _jsx("div", { className: cx("k-select", size !== "md" && `k-select--${size}`, className), children: _jsxs("select", { ref: ref, id: selectId, className: "k-select__control", required: required, "aria-invalid": error ? true : undefined, "aria-describedby": describedBy(selectId, hint, error, extra), defaultValue: placeholderSelected ? "" : undefined, ...rest, children: [placeholder !== undefined && _jsx("option", { value: "", disabled: true, children: placeholder }), options?.map((o) => (_jsx("option", { value: o.value, disabled: o.disabled, children: o.label }, o.value))), children] }) }) }));
11
+ });
package/dist/Tabs.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { type HTMLAttributes, type ReactNode } from "react";
2
+ export interface TabsProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "defaultValue"> {
3
+ value?: string;
4
+ defaultValue?: string;
5
+ onChange?: (value: string) => void;
6
+ /** @default "line" */
7
+ variant?: "line" | "pill" | "segmented";
8
+ /** زبانه‌ها تمام عرض را پر کنند */
9
+ block?: boolean;
10
+ }
11
+ /** زبانه‌ها با الگوی Tabs در WAI-ARIA؛ کلیدهای جهت‌دار در راست‌به‌چپ برعکس عمل می‌کنند */
12
+ export declare function Tabs({ value, defaultValue, onChange, variant, block, className, children, ...rest }: TabsProps): import("react").JSX.Element;
13
+ export interface TabListProps extends HTMLAttributes<HTMLDivElement> {
14
+ /** نام فهرست برای صفحه‌خوان، مثل «تنظیمات حساب» */
15
+ "aria-label": string;
16
+ }
17
+ export declare function TabList({ className, children, ...rest }: TabListProps): import("react").JSX.Element;
18
+ export interface TabProps extends Omit<HTMLAttributes<HTMLButtonElement>, "value"> {
19
+ value: string;
20
+ disabled?: boolean;
21
+ }
22
+ export declare function Tab({ value, className, children, disabled, ...rest }: TabProps): import("react").JSX.Element;
23
+ export interface TabPanelProps extends HTMLAttributes<HTMLDivElement> {
24
+ value: string;
25
+ children: ReactNode;
26
+ }
27
+ export declare function TabPanel({ value, className, children, ...rest }: TabPanelProps): import("react").JSX.Element;
package/dist/Tabs.js ADDED
@@ -0,0 +1,57 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useId, useRef, useState } from "react";
3
+ import { cx } from "./cx.js";
4
+ const TabsCtx = createContext(null);
5
+ const useTabs = () => {
6
+ const ctx = useContext(TabsCtx);
7
+ if (!ctx)
8
+ throw new Error("[kashi] Tab و TabPanel باید داخل <Tabs> باشند.");
9
+ return ctx;
10
+ };
11
+ /** زبانه‌ها با الگوی Tabs در WAI-ARIA؛ کلیدهای جهت‌دار در راست‌به‌چپ برعکس عمل می‌کنند */
12
+ export function Tabs({ value, defaultValue = "", onChange, variant = "line", block, className, children, ...rest }) {
13
+ const id = useId();
14
+ const [inner, setInner] = useState(defaultValue);
15
+ const selected = value !== undefined ? value : inner;
16
+ return (_jsx(TabsCtx.Provider, { value: {
17
+ id,
18
+ selected,
19
+ select: (v) => {
20
+ if (value === undefined)
21
+ setInner(v);
22
+ onChange?.(v);
23
+ },
24
+ }, children: _jsx("div", { className: cx("k-tabs", `k-tabs--${variant}`, block && "k-tabs--block", className), ...rest, children: children }) }));
25
+ }
26
+ export function TabList({ className, children, ...rest }) {
27
+ const ref = useRef(null);
28
+ const onKeyDown = (e) => {
29
+ const tabs = [...(ref.current?.querySelectorAll('[role="tab"]:not(:disabled)') ?? [])];
30
+ const i = tabs.indexOf(document.activeElement);
31
+ if (i < 0)
32
+ return;
33
+ const rtl = getComputedStyle(ref.current).direction === "rtl";
34
+ const moves = {
35
+ ArrowLeft: rtl ? i + 1 : i - 1,
36
+ ArrowRight: rtl ? i - 1 : i + 1,
37
+ Home: 0,
38
+ End: tabs.length - 1,
39
+ };
40
+ if (!(e.key in moves))
41
+ return;
42
+ e.preventDefault();
43
+ const next = tabs[(moves[e.key] + tabs.length) % tabs.length];
44
+ next.focus();
45
+ next.click(); // فعال‌سازی خودکار با فوکوس
46
+ };
47
+ return (_jsx("div", { ref: ref, role: "tablist", className: cx("k-tabs__list", className), onKeyDown: onKeyDown, ...rest, children: children }));
48
+ }
49
+ export function Tab({ value, className, children, disabled, ...rest }) {
50
+ const { id, selected, select } = useTabs();
51
+ const on = selected === value;
52
+ return (_jsx("button", { type: "button", role: "tab", id: `${id}-tab-${value}`, "aria-selected": on, "aria-controls": `${id}-panel-${value}`, tabIndex: on ? 0 : -1, disabled: disabled, className: cx("k-tabs__tab", className), onClick: () => select(value), ...rest, children: children }));
53
+ }
54
+ export function TabPanel({ value, className, children, ...rest }) {
55
+ const { id, selected } = useTabs();
56
+ return (_jsx("div", { role: "tabpanel", id: `${id}-panel-${value}`, "aria-labelledby": `${id}-tab-${value}`, hidden: selected !== value, tabIndex: 0, className: cx("k-tabs__panel", className), ...rest, children: children }));
57
+ }
@@ -0,0 +1,32 @@
1
+ import { type ReactNode } from "react";
2
+ export type ToastTone = "neutral" | "success" | "error" | "warning" | "info";
3
+ export interface ToastOptions {
4
+ title: ReactNode;
5
+ description?: ReactNode;
6
+ tone?: ToastTone;
7
+ /** میلی‌ثانیه؛ Infinity برای ماندن تا بستن دستی. پیش‌فرض ۵ ثانیه، و بی‌نهایت اگر action داشته باشد */
8
+ duration?: number;
9
+ action?: {
10
+ label: string;
11
+ onClick: () => void;
12
+ };
13
+ }
14
+ /** نمایش اعلان کوتاه؛ شناسهٔ آن را برمی‌گرداند */
15
+ export declare function toast(options: ToastOptions | string): number;
16
+ export declare namespace toast {
17
+ var success: (title: ReactNode, o?: Omit<ToastOptions, "title" | "tone">) => number;
18
+ var error: (title: ReactNode, o?: Omit<ToastOptions, "title" | "tone">) => number;
19
+ var warning: (title: ReactNode, o?: Omit<ToastOptions, "title" | "tone">) => number;
20
+ var info: (title: ReactNode, o?: Omit<ToastOptions, "title" | "tone">) => number;
21
+ var dismiss: (id: number) => void;
22
+ }
23
+ export interface ToasterProps {
24
+ /** @default "bottom" */
25
+ position?: "top" | "bottom";
26
+ /** @default "اعلان‌ها" */
27
+ label?: string;
28
+ /** @default "بستن اعلان" */
29
+ closeLabel?: string;
30
+ }
31
+ /** محل نمایش اعلان‌ها؛ یک بار در ریشهٔ برنامه قرار دهید */
32
+ export declare function Toaster({ position, label, closeLabel }: ToasterProps): import("react").JSX.Element;
package/dist/Toast.js ADDED
@@ -0,0 +1,65 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState, useSyncExternalStore } from "react";
3
+ import { cx } from "./cx.js";
4
+ import { CloseIcon, ErrorIcon, InfoIcon, SuccessIcon, WarningIcon } from "./icons.js";
5
+ // مخزن سادهٔ سراسری؛ هر جای برنامه می‌تواند toast() را صدا بزند
6
+ let items = [];
7
+ let nextId = 1;
8
+ const listeners = new Set();
9
+ const emit = () => listeners.forEach((l) => l());
10
+ const subscribe = (l) => {
11
+ listeners.add(l);
12
+ return () => listeners.delete(l);
13
+ };
14
+ function dismiss(id) {
15
+ items = items.map((t) => (t.id === id ? { ...t, closing: true } : t));
16
+ emit();
17
+ setTimeout(() => {
18
+ items = items.filter((t) => t.id !== id);
19
+ emit();
20
+ }, 200);
21
+ }
22
+ /** نمایش اعلان کوتاه؛ شناسهٔ آن را برمی‌گرداند */
23
+ export function toast(options) {
24
+ const o = typeof options === "string" ? { title: options } : options;
25
+ const id = nextId++;
26
+ items = [...items, { ...o, id }];
27
+ emit();
28
+ return id;
29
+ }
30
+ toast.success = (title, o = {}) => toast({ ...o, title, tone: "success" });
31
+ toast.error = (title, o = {}) => toast({ ...o, title, tone: "error" });
32
+ toast.warning = (title, o = {}) => toast({ ...o, title, tone: "warning" });
33
+ toast.info = (title, o = {}) => toast({ ...o, title, tone: "info" });
34
+ toast.dismiss = dismiss;
35
+ const ICONS = {
36
+ neutral: InfoIcon, info: InfoIcon, success: SuccessIcon, warning: WarningIcon, error: ErrorIcon,
37
+ };
38
+ function ToastView({ t, closeLabel }) {
39
+ const duration = t.duration ?? (t.action ? Infinity : 5000);
40
+ const remaining = useRef(duration);
41
+ const started = useRef(0);
42
+ const timer = useRef();
43
+ const [paused, setPaused] = useState(false);
44
+ // زمان‌سنج با توقف هنگام hover یا فوکوس (WCAG 2.2.1)
45
+ useEffect(() => {
46
+ if (!Number.isFinite(duration) || paused || t.closing)
47
+ return;
48
+ started.current = Date.now();
49
+ timer.current = setTimeout(() => dismiss(t.id), remaining.current);
50
+ return () => {
51
+ clearTimeout(timer.current);
52
+ remaining.current -= Date.now() - started.current;
53
+ };
54
+ }, [paused, t.closing]);
55
+ const Icon = ICONS[t.tone ?? "neutral"];
56
+ return (_jsxs("li", { className: cx("k-toast", t.tone && t.tone !== "neutral" && `k-toast--${t.tone}`), "data-state": t.closing ? "closing" : "open", role: t.tone === "error" ? "alert" : "status", onMouseEnter: () => setPaused(true), onMouseLeave: () => setPaused(false), onFocus: () => setPaused(true), onBlur: () => setPaused(false), onKeyDown: (e) => e.key === "Escape" && dismiss(t.id), children: [_jsx("span", { className: "k-toast__icon", "aria-hidden": "true", children: _jsx(Icon, {}) }), _jsxs("div", { className: "k-toast__content", children: [_jsx("p", { className: "k-toast__title", children: t.title }), t.description && _jsx("p", { className: "k-toast__description", children: t.description }), t.action && (_jsx("button", { type: "button", className: "k-btn k-btn--link k-toast__action", onClick: () => {
57
+ t.action.onClick();
58
+ dismiss(t.id);
59
+ }, children: t.action.label }))] }), _jsx("button", { type: "button", className: "k-btn k-btn--tertiary k-btn--icon k-btn--sm k-toast__close", "aria-label": closeLabel, onClick: () => dismiss(t.id), children: _jsx(CloseIcon, {}) }), Number.isFinite(duration) && _jsx("span", { className: "k-toast__progress", "aria-hidden": "true", style: { animationDuration: `${duration}ms` } })] }));
60
+ }
61
+ /** محل نمایش اعلان‌ها؛ یک بار در ریشهٔ برنامه قرار دهید */
62
+ export function Toaster({ position = "bottom", label = "اعلان‌ها", closeLabel = "بستن اعلان" }) {
63
+ const list = useSyncExternalStore(subscribe, () => items, () => items);
64
+ return (_jsx("section", { "aria-label": label, children: _jsx("ol", { className: cx("k-toaster", position === "top" && "k-toaster--top"), "aria-live": "polite", children: list.map((t) => _jsx(ToastView, { t: t, closeLabel: closeLabel }, t.id)) }) }));
65
+ }
@@ -0,0 +1,16 @@
1
+ import { type ReactElement, type ReactNode } from "react";
2
+ export interface TooltipProps {
3
+ /** متن راهنما؛ فقط توضیح تکمیلی، نه اطلاعات ضروری */
4
+ content: ReactNode;
5
+ /** عنصر فوکوس‌پذیر، معمولاً یک دکمه */
6
+ children: ReactElement;
7
+ /** @default "top" */
8
+ placement?: "top" | "bottom";
9
+ /** تأخیر نمایش با hover بر حسب میلی‌ثانیه @default 300 */
10
+ delay?: number;
11
+ }
12
+ /**
13
+ * راهنمای کوتاه طبق WCAG 1.4.13: با hover و فوکوس نمایش داده می‌شود، روی خودش هم hover پذیر است
14
+ * و با Esc بسته می‌شود. با aria-describedby به عنصر متصل است.
15
+ */
16
+ export declare function Tooltip({ content, children, placement, delay }: TooltipProps): import("react").JSX.Element;
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cloneElement, isValidElement, useEffect, useId, useRef, useState } from "react";
3
+ import { cx } from "./cx.js";
4
+ /**
5
+ * راهنمای کوتاه طبق WCAG 1.4.13: با hover و فوکوس نمایش داده می‌شود، روی خودش هم hover پذیر است
6
+ * و با Esc بسته می‌شود. با aria-describedby به عنصر متصل است.
7
+ */
8
+ export function Tooltip({ content, children, placement = "top", delay = 300 }) {
9
+ const id = useId();
10
+ const [open, setOpen] = useState(false);
11
+ const timer = useRef();
12
+ const show = (wait) => {
13
+ clearTimeout(timer.current);
14
+ timer.current = setTimeout(() => setOpen(true), wait);
15
+ };
16
+ const hide = () => {
17
+ clearTimeout(timer.current);
18
+ timer.current = setTimeout(() => setOpen(false), 100);
19
+ };
20
+ useEffect(() => {
21
+ if (!open)
22
+ return;
23
+ const onKey = (e) => e.key === "Escape" && setOpen(false);
24
+ document.addEventListener("keydown", onKey);
25
+ return () => document.removeEventListener("keydown", onKey);
26
+ }, [open]);
27
+ useEffect(() => () => clearTimeout(timer.current), []);
28
+ const child = isValidElement(children)
29
+ ? cloneElement(children, {
30
+ "aria-describedby": [children.props["aria-describedby"], id].filter(Boolean).join(" "),
31
+ })
32
+ : children;
33
+ return (_jsxs("span", { className: "k-tooltip-anchor", onMouseEnter: () => show(delay), onMouseLeave: hide, onFocus: () => show(0), onBlur: hide, children: [child, _jsx("span", { role: "tooltip", id: id, className: cx("k-tooltip", placement === "bottom" && "k-tooltip--bottom"), hidden: !open, children: content })] }));
34
+ }
package/dist/index.d.ts CHANGED
@@ -28,3 +28,17 @@ export { MobileInput } from "./MobileInput.js";
28
28
  export type { MobileInputProps } from "./MobileInput.js";
29
29
  export { PriceInput } from "./PriceInput.js";
30
30
  export type { PriceInputProps } from "./PriceInput.js";
31
+ export { RadioGroup, Radio } from "./Radio.js";
32
+ export type { RadioGroupProps, RadioProps } from "./Radio.js";
33
+ export { Select } from "./Select.js";
34
+ export type { SelectProps, SelectOption } from "./Select.js";
35
+ export { Tabs, TabList, Tab, TabPanel } from "./Tabs.js";
36
+ export type { TabsProps, TabListProps, TabProps, TabPanelProps } from "./Tabs.js";
37
+ export { Dialog } from "./Dialog.js";
38
+ export type { DialogProps } from "./Dialog.js";
39
+ export { Toaster, toast } from "./Toast.js";
40
+ export type { ToasterProps, ToastOptions, ToastTone } from "./Toast.js";
41
+ export { Tooltip } from "./Tooltip.js";
42
+ export type { TooltipProps } from "./Tooltip.js";
43
+ export { RelativeTime } from "./RelativeTime.js";
44
+ export type { RelativeTimeProps } from "./RelativeTime.js";
package/dist/index.js CHANGED
@@ -14,3 +14,10 @@ export { DatePicker } from "./DatePicker.js";
14
14
  export { IbanInput } from "./IbanInput.js";
15
15
  export { MobileInput } from "./MobileInput.js";
16
16
  export { PriceInput } from "./PriceInput.js";
17
+ export { RadioGroup, Radio } from "./Radio.js";
18
+ export { Select } from "./Select.js";
19
+ export { Tabs, TabList, Tab, TabPanel } from "./Tabs.js";
20
+ export { Dialog } from "./Dialog.js";
21
+ export { Toaster, toast } from "./Toast.js";
22
+ export { Tooltip } from "./Tooltip.js";
23
+ export { RelativeTime } from "./RelativeTime.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kashi-ui/react",
3
- "version": "0.4.0",
3
+ "version": "0.5.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.4.0",
25
+ "@kashi-ui/css": "^0.5.0",
26
26
  "react": ">=18"
27
27
  },
28
28
  "devDependencies": {
@@ -59,7 +59,7 @@
59
59
  }
60
60
  },
61
61
  "dependencies": {
62
- "@kashi-ui/bank-logos": "^0.4.0",
63
- "@kashi-ui/persian": "^0.4.0"
62
+ "@kashi-ui/bank-logos": "^0.5.0",
63
+ "@kashi-ui/persian": "^0.5.0"
64
64
  }
65
65
  }