@suphark/ui 0.53.0 → 0.59.1

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.
@@ -1,6 +1,7 @@
1
1
  import { Checkbox } from "@suphark/ui/components/ui/checkbox";
2
2
  import { Field, FieldDescription, FieldError, FieldLabel } from "@suphark/ui/components/ui/field";
3
3
  import { Label } from "@suphark/ui/components/ui/label";
4
+ import { Switch } from "@suphark/ui/components/ui/switch";
4
5
  import { cn } from "@suphark/ui/lib/utils";
5
6
  import type { ComponentProps, ReactNode } from "react";
6
7
 
@@ -97,3 +98,39 @@ export function CheckboxField({
97
98
  </Label>
98
99
  );
99
100
  }
101
+
102
+ /**
103
+ * สวิตช์ + ป้ายในตัว (คลิกป้ายก็สลับ) — ใช้แทน `<label><Switch />…</label>` ที่ต้อง biome-ignore เพราะ Switch เป็น span
104
+ * `size` ส่งต่อให้ Switch · `className` ทับที่ป้ายทั้งก้อน (ใส่สไตล์ hover/ตัวหนังสือได้) · ป้ายใหญ่ขึ้นเป็น 44px บนจอสัมผัส
105
+ *
106
+ * <SwitchField size="sm" checked={showDone} onCheckedChange={setShowDone} label="แสดงที่เสร็จแล้ว" />
107
+ */
108
+ export function SwitchField({
109
+ label,
110
+ hint,
111
+ className,
112
+ labelClassName,
113
+ ...props
114
+ }: ComponentProps<typeof Switch> & {
115
+ label: ReactNode;
116
+ /** คำอธิบายใต้ label (เยื้องตรงกับ label) */
117
+ hint?: ReactNode;
118
+ /** ทับสไตล์ข้อความป้าย (ค่าเริ่มต้น `text-sm`) เช่น `text-caption font-semibold` */
119
+ labelClassName?: string;
120
+ }) {
121
+ return (
122
+ <Label
123
+ className={cn(
124
+ "flex cursor-pointer items-center gap-2 font-normal pointer-coarse:min-h-11",
125
+ hint && "items-start",
126
+ className,
127
+ )}
128
+ >
129
+ <Switch className={cn(hint && "mt-0.5")} {...props} />
130
+ <span className="flex flex-col gap-0.5">
131
+ <span className={cn("text-sm", labelClassName)}>{label}</span>
132
+ {hint && <span className="text-muted-foreground text-xs">{hint}</span>}
133
+ </span>
134
+ </Label>
135
+ );
136
+ }
@@ -0,0 +1,85 @@
1
+ import { cn } from "@suphark/ui/lib/utils";
2
+ import type { ComponentProps, ReactNode } from "react";
3
+
4
+ export type ListSectionTone = "default" | "danger" | "success" | "accent" | "muted";
5
+
6
+ const TONE: Record<ListSectionTone, string> = {
7
+ default: "text-foreground/90",
8
+ danger: "text-destructive",
9
+ success: "text-success",
10
+ accent: "text-primary",
11
+ muted: "text-muted-foreground",
12
+ };
13
+
14
+ /** หัวกลุ่มของรายการ: ชื่อ (สีตาม tone) + จำนวน + ปุ่มขวา + เส้นบางใต้หัว — ใช้เดี่ยว ๆ หรือผ่าน `ListSection` */
15
+ export function ListSectionHeader({
16
+ title,
17
+ count,
18
+ action,
19
+ tone = "default",
20
+ as: Heading = "h2",
21
+ className,
22
+ }: {
23
+ title: ReactNode;
24
+ count?: number;
25
+ action?: ReactNode;
26
+ tone?: ListSectionTone;
27
+ as?: "h2" | "h3" | "h4" | "p";
28
+ className?: string;
29
+ }) {
30
+ return (
31
+ <div
32
+ data-slot="list-section-header"
33
+ className={cn("mb-1 flex min-h-9 items-center justify-between border-b pb-1", className)}
34
+ >
35
+ <div className="flex min-w-0 items-center gap-2">
36
+ <Heading
37
+ className={cn(
38
+ "flex min-w-0 items-center gap-2 truncate font-semibold text-caption",
39
+ TONE[tone],
40
+ )}
41
+ >
42
+ {title}
43
+ </Heading>
44
+ {typeof count === "number" && (
45
+ <span className="rounded-md bg-muted px-1.5 py-0.5 font-semibold text-caption text-muted-foreground tabular-nums">
46
+ {count}
47
+ </span>
48
+ )}
49
+ </div>
50
+ {action}
51
+ </div>
52
+ );
53
+ }
54
+
55
+ /**
56
+ * กลุ่มของรายการแบบ "ไม่มีกล่อง" (รูปแบบ List): หัวกลุ่ม + เส้นบาง + แถวคั่นด้วยเส้น — ยกจาก `SectionPanel plain` ของ pi-done
57
+ * แถวข้างในคั่นเส้นให้เอง (`divide-y`) · อยากได้กล่องใช้ `SectionCard`
58
+ *
59
+ * <ListSection title="เกินกำหนด" tone="danger" count={3} action={<Button variant="link" size="xs">ทำทั้งหมด</Button>}>
60
+ * {tasks.map((t) => <TaskRow key={t.id} … />)}
61
+ * </ListSection>
62
+ */
63
+ export function ListSection({
64
+ title,
65
+ count,
66
+ action,
67
+ tone,
68
+ headingAs,
69
+ children,
70
+ className,
71
+ ...props
72
+ }: Omit<ComponentProps<"section">, "title"> & {
73
+ title: ReactNode;
74
+ count?: number;
75
+ action?: ReactNode;
76
+ tone?: ListSectionTone;
77
+ headingAs?: "h2" | "h3" | "h4" | "p";
78
+ }) {
79
+ return (
80
+ <section data-slot="list-section" className={cn("py-1", className)} {...props}>
81
+ <ListSectionHeader title={title} count={count} action={action} tone={tone} as={headingAs} />
82
+ <div className="divide-y divide-border/50">{children}</div>
83
+ </section>
84
+ );
85
+ }
@@ -122,6 +122,8 @@ export interface NotificationCenterProps {
122
122
  description?: ReactNode;
123
123
  /** ไม่มีรายการ — ส่ง element เองได้ทั้งก้อน */
124
124
  empty?: ReactNode;
125
+ /** ปุ่มคำสั่งของทั้งกล่องที่หัว เช่น "อ่านทั้งหมดแล้ว" (ซ้ายของ "ดูทั้งหมด") — ไม่ต้องยืม `viewAll` มาใช้ผิดความหมาย */
126
+ headerAction?: ReactNode;
125
127
  /** ลิงก์/ปุ่ม "ดูทั้งหมด" มุมขวาบน */
126
128
  viewAll?: { label?: ReactNode; onSelect: () => void };
127
129
  /** กำลังทำคำสั่งลัดอยู่ — ปิดปุ่มทั้งหมดกันกดซ้ำ */
@@ -159,6 +161,7 @@ export function NotificationCenter({
159
161
  description,
160
162
  empty,
161
163
  viewAll,
164
+ headerAction,
162
165
  busy = false,
163
166
  variant = "card",
164
167
  renderItem,
@@ -186,11 +189,16 @@ export function NotificationCenter({
186
189
  </div>
187
190
  )}
188
191
  </div>
189
- {viewAll && (
190
- <Button variant="ghost" size="sm" onClick={viewAll.onSelect} className="shrink-0">
191
- {viewAll.label ?? text.notifications?.viewAll ?? "ดูทั้งหมด"}
192
- <ChevronRightIcon />
193
- </Button>
192
+ {(headerAction || viewAll) && (
193
+ <div className="flex shrink-0 items-center gap-1">
194
+ {headerAction}
195
+ {viewAll && (
196
+ <Button variant="ghost" size="sm" onClick={viewAll.onSelect} className="shrink-0">
197
+ {viewAll.label ?? text.notifications?.viewAll ?? "ดูทั้งหมด"}
198
+ <ChevronRightIcon />
199
+ </Button>
200
+ )}
201
+ </div>
194
202
  )}
195
203
  </div>
196
204
 
@@ -44,7 +44,8 @@ export function ProgressBar({
44
44
  showValue?: boolean;
45
45
  /** ปรับข้อความตัวเลข เช่น (v) => `${v} / ${max}` */
46
46
  format?: (value: number | null, max: number) => ReactNode;
47
- tone?: ProgressTone;
47
+ /** `auto` = เขียวเมื่อครบ (value ≥ max) นอกนั้นสีหลัก — แถบงานย่อยที่เต็มแล้วเปลี่ยนสีเอง */
48
+ tone?: ProgressTone | "auto";
48
49
  /** sm = h-1 (ค่าเริ่มต้น), md = h-2, lg = h-3 */
49
50
  size?: "sm" | "md" | "lg";
50
51
  trackClassName?: string;
@@ -76,7 +77,13 @@ export function ProgressBar({
76
77
  <ProgressTrack
77
78
  className={cn(size === "md" ? "h-2" : size === "lg" ? "h-3" : "h-1", trackClassName)}
78
79
  >
79
- <ProgressIndicator className={cn(TONE[tone], indicatorClassName)} style={indicatorStyle} />
80
+ <ProgressIndicator
81
+ className={cn(
82
+ TONE[tone === "auto" ? (value != null && value >= max ? "success" : "default") : tone],
83
+ indicatorClassName,
84
+ )}
85
+ style={indicatorStyle}
86
+ />
80
87
  </ProgressTrack>
81
88
  </ProgressPrimitive.Root>
82
89
  );
@@ -0,0 +1,174 @@
1
+ "use client";
2
+
3
+ import {
4
+ Drawer,
5
+ DrawerContent,
6
+ DrawerDescription,
7
+ DrawerTitle,
8
+ DrawerTrigger,
9
+ } from "@suphark/ui/components/ui/drawer";
10
+ import { Popover, PopoverContent, PopoverTrigger } from "@suphark/ui/components/ui/popover";
11
+ import { useIsMobile } from "@suphark/ui/hooks/use-mobile";
12
+ import { cn } from "@suphark/ui/lib/utils";
13
+ import {
14
+ type ComponentProps,
15
+ createContext,
16
+ type KeyboardEventHandler,
17
+ type MouseEventHandler,
18
+ type PointerEventHandler,
19
+ type ReactElement,
20
+ type ReactNode,
21
+ useContext,
22
+ } from "react";
23
+
24
+ const ResponsivePopoverContext = createContext(false);
25
+
26
+ /**
27
+ * อยู่ในโหมดแผ่นล่าง (จอ < 768px) หรือไม่ — ใช้ขยายพื้นที่แตะของ picker ใน `ResponsivePopover`
28
+ * (นอก `ResponsivePopover` = false)
29
+ */
30
+ export function useIsResponsiveSheet() {
31
+ return useContext(ResponsivePopoverContext);
32
+ }
33
+
34
+ export interface ResponsivePopoverProps {
35
+ open?: boolean;
36
+ onOpenChange?: (open: boolean) => void;
37
+ children: ReactNode;
38
+ }
39
+
40
+ /**
41
+ * Popover บนจอกว้าง (≥ 768px) · แผ่นล่าง (Drawer) บนมือถือ: สูงตามเนื้อหา, footer ค้างล่าง, ปัดลงเพื่อปิด
42
+ * ใช้กับ picker ที่เปิดจากปุ่ม (สถานะ/ลำดับความสำคัญ/วันที่ …) — ในแผ่นใช้ `useIsResponsiveSheet()` ขยายแถวเป็น 44px
43
+ *
44
+ * <ResponsivePopover open={open} onOpenChange={setOpen}>
45
+ * <ResponsivePopoverTrigger render={<Button variant="outline">สถานะ</Button>} />
46
+ * <ResponsivePopoverContent title="เลือกสถานะ" className="w-56 p-1">
47
+ * <ResponsivePopoverItem onClick={() => pick("todo")}>ต้องทำ</ResponsivePopoverItem>
48
+ * </ResponsivePopoverContent>
49
+ * </ResponsivePopover>
50
+ */
51
+ export function ResponsivePopover({ open, onOpenChange, children }: ResponsivePopoverProps) {
52
+ const isMobile = useIsMobile();
53
+ const handleOpenChange = onOpenChange ? (next: boolean) => onOpenChange(next) : undefined;
54
+
55
+ return (
56
+ <ResponsivePopoverContext.Provider value={isMobile}>
57
+ {isMobile ? (
58
+ <Drawer open={open} onOpenChange={handleOpenChange} showSwipeHandle>
59
+ {children}
60
+ </Drawer>
61
+ ) : (
62
+ <Popover open={open} onOpenChange={handleOpenChange}>
63
+ {children}
64
+ </Popover>
65
+ )}
66
+ </ResponsivePopoverContext.Provider>
67
+ );
68
+ }
69
+
70
+ export interface ResponsivePopoverTriggerProps {
71
+ render: ReactElement;
72
+ nativeButton?: boolean;
73
+ disabled?: boolean;
74
+ children?: ReactNode;
75
+ }
76
+
77
+ export function ResponsivePopoverTrigger(props: ResponsivePopoverTriggerProps) {
78
+ const isSheet = useIsResponsiveSheet();
79
+ return isSheet ? <DrawerTrigger {...props} /> : <PopoverTrigger {...props} />;
80
+ }
81
+
82
+ export interface ResponsivePopoverContentProps {
83
+ /** หัวแผ่นบนมือถือ และเป็นชื่อสำหรับ screen reader เสมอ */
84
+ title: string;
85
+ description?: string;
86
+ /** ซ่อนหัวแผ่น (ยังอ่านออกเสียง) เมื่อเนื้อหาบอกชัดอยู่แล้ว */
87
+ hideTitle?: boolean;
88
+ /** class ของ Popover เท่านั้น (ความกว้าง, padding …) */
89
+ className?: string;
90
+ /** class ของส่วนเนื้อหาที่เลื่อนได้ในแผ่น */
91
+ sheetClassName?: string;
92
+ /** class ของตัวแผ่นเอง (ทับ `bg-background text-left`) */
93
+ sheetContentClassName?: string;
94
+ /** ค้างใต้เนื้อหาที่เลื่อน (ในแผ่น) หรือต่อท้ายเนื้อหา (ใน Popover) */
95
+ footer?: ReactNode;
96
+ align?: "start" | "center" | "end";
97
+ side?: "top" | "bottom" | "left" | "right";
98
+ sideOffset?: number;
99
+ onClick?: MouseEventHandler<HTMLDivElement>;
100
+ onPointerDown?: PointerEventHandler<HTMLDivElement>;
101
+ onKeyDown?: KeyboardEventHandler<HTMLDivElement>;
102
+ children: ReactNode;
103
+ }
104
+
105
+ export function ResponsivePopoverContent({
106
+ title,
107
+ description,
108
+ hideTitle = false,
109
+ className,
110
+ sheetClassName,
111
+ sheetContentClassName,
112
+ footer,
113
+ align = "start",
114
+ side,
115
+ sideOffset = 4,
116
+ children,
117
+ ...eventProps
118
+ }: ResponsivePopoverContentProps) {
119
+ const isSheet = useIsResponsiveSheet();
120
+
121
+ if (!isSheet) {
122
+ return (
123
+ <PopoverContent
124
+ align={align}
125
+ side={side}
126
+ sideOffset={sideOffset}
127
+ className={className}
128
+ {...eventProps}
129
+ >
130
+ {children}
131
+ {footer}
132
+ </PopoverContent>
133
+ );
134
+ }
135
+
136
+ return (
137
+ <DrawerContent className={cn("bg-background text-left", sheetContentClassName)} {...eventProps}>
138
+ <DrawerTitle className={cn("px-4 pt-1 pb-1 font-semibold text-sm", hideTitle && "sr-only")}>
139
+ {title}
140
+ </DrawerTitle>
141
+ <DrawerDescription className="sr-only">{description ?? title}</DrawerDescription>
142
+ <div
143
+ className={cn(
144
+ "min-h-0 flex-1 overflow-y-auto overflow-x-hidden overscroll-contain",
145
+ !footer && "pb-[max(0.5rem,env(safe-area-inset-bottom))]",
146
+ sheetClassName,
147
+ )}
148
+ >
149
+ {children}
150
+ </div>
151
+ {footer ? <div className="shrink-0 pb-[env(safe-area-inset-bottom)]">{footer}</div> : null}
152
+ </DrawerContent>
153
+ );
154
+ }
155
+
156
+ /** แถวเมนูที่สูงเป็นพื้นที่แตะ 44px เมื่ออยู่ในแผ่นล่าง (36px ใน Popover) */
157
+ export function ResponsivePopoverItem({
158
+ className,
159
+ type = "button",
160
+ ...props
161
+ }: ComponentProps<"button">) {
162
+ const isSheet = useIsResponsiveSheet();
163
+ return (
164
+ <button
165
+ type={type}
166
+ className={cn(
167
+ "flex w-full cursor-pointer items-center gap-2 rounded-md px-2 text-left text-sm outline-none hover:bg-muted focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50",
168
+ isSheet ? "h-11" : "h-9",
169
+ className,
170
+ )}
171
+ {...props}
172
+ />
173
+ );
174
+ }
@@ -0,0 +1,133 @@
1
+ import { cn } from "@suphark/ui/lib/utils";
2
+ import type { ComponentProps, CSSProperties } from "react";
3
+
4
+ /**
5
+ * ตารางแถว (grid) ที่หัว/แถว/แถวกลุ่มใช้คอลัมน์ชุดเดียวกัน — หัวซ่อนบนมือถือ แถวเหลือคอลัมน์ที่เลือก (ปกติชื่อ + ปุ่ม)
6
+ * ปุ่มท้ายแถวโผล่ตอนชี้ (`RowTableActions`) แต่เห็นตลอดบนมือถือ/จอสัมผัส · ยกจาก pi-done (เขียนซ้ำ 4 ที่)
7
+ * ต่างจาก `DataList` (รายการแถวไม่มีหัวคอลัมน์) และ `SimpleTable` (`<table>` จริงที่เรียง/แบ่งหน้าได้): ตัวนี้เหมาะกับรายการที่ไม่กี่คอลัมน์ ไม่ต้องเรียงข้อมูล
8
+ *
9
+ * <RowTable columns="minmax(0,1fr) 3.5rem 7rem" mobileColumns="minmax(0,1fr) auto">
10
+ * <RowTableHeader><span>ชื่อ</span><span className="text-right">เปิด</span><span className="sr-only">คำสั่ง</span></RowTableHeader>
11
+ * <RowTableRow>
12
+ * <RowTableCell>โปรเจกต์ A</RowTableCell>
13
+ * <RowTableCell hideOnMobile align="end">12</RowTableCell>
14
+ * <RowTableActions><Button size="icon-sm" aria-label="แก้ไข">…</Button></RowTableActions>
15
+ * </RowTableRow>
16
+ * </RowTable>
17
+ */
18
+ export function RowTable({
19
+ columns,
20
+ mobileColumns = "minmax(0,1fr) auto",
21
+ className,
22
+ style,
23
+ ...props
24
+ }: ComponentProps<"div"> & {
25
+ /** `grid-template-columns` ตั้งแต่จอ md ขึ้นไป เช่น "minmax(0,1fr) 3.5rem 7rem" (จำนวนช่องต้องเท่าลูกของหัว/แถว) */
26
+ columns: string;
27
+ /** `grid-template-columns` ของมือถือ (ค่าเริ่มต้น "minmax(0,1fr) auto") — ซ่อนช่องที่เหลือด้วย `hideOnMobile` */
28
+ mobileColumns?: string;
29
+ }) {
30
+ return (
31
+ <div
32
+ data-slot="row-table"
33
+ style={
34
+ {
35
+ "--row-cols": columns,
36
+ "--row-cols-mobile": mobileColumns,
37
+ ...style,
38
+ } as CSSProperties
39
+ }
40
+ className={cn("overflow-hidden rounded-xl border bg-card text-card-foreground", className)}
41
+ {...props}
42
+ />
43
+ );
44
+ }
45
+
46
+ const GRID = "grid grid-cols-(--row-cols-mobile) items-center gap-x-4 md:grid-cols-(--row-cols)";
47
+
48
+ /** หัวคอลัมน์ — ซ่อนบนมือถือ */
49
+ export function RowTableHeader({ className, ...props }: ComponentProps<"div">) {
50
+ return (
51
+ <div
52
+ data-slot="row-table-header"
53
+ className={cn(
54
+ GRID,
55
+ "hidden h-10 border-b bg-muted/40 px-2 font-semibold text-label text-muted-foreground md:grid",
56
+ className,
57
+ )}
58
+ {...props}
59
+ />
60
+ );
61
+ }
62
+
63
+ export function RowTableRow({ className, ...props }: ComponentProps<"div">) {
64
+ return (
65
+ <div
66
+ data-slot="row-table-row"
67
+ className={cn(
68
+ GRID,
69
+ "group/row min-h-13 border-b px-2 last:border-b-0 hover:bg-muted/40",
70
+ className,
71
+ )}
72
+ {...props}
73
+ />
74
+ );
75
+ }
76
+
77
+ /** ช่องของแถว — `hideOnMobile` ซ่อนบนมือถือ (ต้องตรงกับ `mobileColumns`) */
78
+ export function RowTableCell({
79
+ hideOnMobile = false,
80
+ align = "start",
81
+ className,
82
+ ...props
83
+ }: ComponentProps<"div"> & {
84
+ hideOnMobile?: boolean;
85
+ align?: "start" | "end";
86
+ }) {
87
+ return (
88
+ <div
89
+ data-slot="row-table-cell"
90
+ className={cn(
91
+ "min-w-0 text-sm",
92
+ align === "end" && "text-right tabular-nums",
93
+ hideOnMobile && "hidden md:block",
94
+ className,
95
+ )}
96
+ {...props}
97
+ />
98
+ );
99
+ }
100
+
101
+ /** ปุ่มท้ายแถว — โผล่ตอนชี้/โฟกัสในแถว (md ขึ้นไป) · มือถือและจอสัมผัสเห็นตลอด (`reveal={false}` = เห็นตลอดทุกจอ) */
102
+ export function RowTableActions({
103
+ reveal = true,
104
+ className,
105
+ ...props
106
+ }: ComponentProps<"div"> & { reveal?: boolean }) {
107
+ return (
108
+ <div
109
+ data-slot="row-table-actions"
110
+ className={cn(
111
+ "flex shrink-0 items-center justify-end gap-0.5",
112
+ reveal &&
113
+ "transition-opacity md:opacity-0 md:group-focus-within/row:opacity-100 md:group-hover/row:opacity-100 pointer-coarse:opacity-100",
114
+ className,
115
+ )}
116
+ {...props}
117
+ />
118
+ );
119
+ }
120
+
121
+ /** แถวหัวกลุ่ม (โฟลเดอร์) คั่นระหว่างแถว — ครอบทั้งความกว้าง ไม่ใช้คอลัมน์ของตาราง */
122
+ export function RowTableGroup({ className, ...props }: ComponentProps<"div">) {
123
+ return (
124
+ <div
125
+ data-slot="row-table-group"
126
+ className={cn(
127
+ "flex h-10 items-center gap-1.5 border-b bg-muted/20 px-3 font-semibold text-caption text-muted-foreground",
128
+ className,
129
+ )}
130
+ {...props}
131
+ />
132
+ );
133
+ }
@@ -0,0 +1,62 @@
1
+ import { cn } from "@suphark/ui/lib/utils";
2
+ import type { ComponentProps } from "react";
3
+
4
+ /**
5
+ * แถวเมนูข้างแบบเรียบ (ใช้เมื่อประกอบ sidebar เอง ไม่ใช่ `AppNav`): หน้าปัจจุบันอ่านเป็นสีหลัก (`bg-primary/10 text-primary`)
6
+ * สูงขั้นต่ำ 32px และ 44px บนจอสัมผัส · `draggingOver` ไฮไลต์ตอนลากรายการมาวาง · ยกจาก pi-done
7
+ * เป็น `div` (วาง `Link`/ปุ่มไว้ข้างใน) เพื่อให้ใช้เป็นที่วางของการลากได้
8
+ *
9
+ * <SidebarNavItem active={path === "/inbox"}>
10
+ * <Link href="/inbox" className="flex-1">กล่องเข้า</Link>
11
+ * <SidebarCountBadge count={unread} active={path === "/inbox"} />
12
+ * </SidebarNavItem>
13
+ */
14
+ export function SidebarNavItem({
15
+ active = false,
16
+ draggingOver = false,
17
+ className,
18
+ ...props
19
+ }: ComponentProps<"div"> & { active?: boolean; draggingOver?: boolean }) {
20
+ return (
21
+ <div
22
+ data-slot="sidebar-nav-item"
23
+ data-active={active || undefined}
24
+ className={cn(
25
+ "relative flex min-h-8 items-center justify-between rounded-md border border-transparent px-2.5 py-1 text-sm transition-[background-color,transform,border-color,color] duration-150 ease-out pointer-coarse:min-h-11",
26
+ active
27
+ ? "bg-primary/10 font-semibold text-primary"
28
+ : "font-medium text-sidebar-foreground/85 hover:bg-sidebar-accent/60 hover:text-foreground",
29
+ draggingOver && "scale-[1.02] border-primary/20 bg-primary/10 text-primary shadow-2xs",
30
+ className,
31
+ )}
32
+ {...props}
33
+ />
34
+ );
35
+ }
36
+
37
+ /** ตัวเลขท้ายแถวเมนู — ข้อความเทา ไม่ใช่ pill (เมนูอ่านชื่อก่อน) · 0 ซ่อน · เกิน `max` แสดง "999+" */
38
+ export function SidebarCountBadge({
39
+ count,
40
+ active = false,
41
+ max = 999,
42
+ className,
43
+ }: {
44
+ count: number;
45
+ active?: boolean;
46
+ max?: number;
47
+ className?: string;
48
+ }) {
49
+ if (count <= 0) return null;
50
+ return (
51
+ <span
52
+ data-slot="sidebar-count-badge"
53
+ className={cn(
54
+ "shrink-0 text-caption tabular-nums",
55
+ active ? "font-semibold text-primary" : "text-muted-foreground/80",
56
+ className,
57
+ )}
58
+ >
59
+ {count > max ? `${max}+` : count}
60
+ </span>
61
+ );
62
+ }
@@ -81,6 +81,10 @@ export interface UiText {
81
81
  table?: Partial<{
82
82
  rowsPerPage: string;
83
83
  pageOf: string;
84
+ /** `variant="quiet"` — `{from}`/`{to}`/`{total}` และ `{shown}`/`{total}` */
85
+ rangeOf: string;
86
+ shownOf: string;
87
+ density: string;
84
88
  rowsSelected: string;
85
89
  noResults: string;
86
90
  deleteTitle: string;
@@ -213,6 +217,11 @@ export interface UiText {
213
217
  dayCount: string;
214
218
  allShortcuts: string;
215
219
  tapToFill: string;
220
+ /** `minDate`/`maxDate` — `{date}` = วันที่ขอบเขต (ตาม `locale` ของช่อง) */
221
+ beforeMin: string;
222
+ afterMax: string;
223
+ /** `DateRangePicker` (ตัวกรอง) */
224
+ clearRange: string;
216
225
  last7Days: string;
217
226
  last30Days: string;
218
227
  thisMonth: string;
@@ -311,6 +320,9 @@ export const UI_TEXT_TH: UiText = {
311
320
  table: {
312
321
  rowsPerPage: "แถวต่อหน้า",
313
322
  pageOf: "หน้า {page} จาก {pages}",
323
+ rangeOf: "{from}–{to} จาก {total}",
324
+ shownOf: "{shown} จาก {total} รายการ",
325
+ density: "ความหนาแน่นของแถว",
314
326
  rowsSelected: "เลือก {selected} จาก {total} แถว",
315
327
  noResults: "ไม่พบข้อมูล",
316
328
  deleteTitle: "ยืนยันการลบ",
@@ -418,6 +430,9 @@ export const UI_TEXT_TH: UiText = {
418
430
  dayCount: "{count} วัน",
419
431
  allShortcuts: "พิมพ์ลัดทั้งหมด",
420
432
  tapToFill: "กดเพื่อเติมในช่อง",
433
+ beforeMin: "เลือกได้ตั้งแต่วันที่ {date}",
434
+ afterMax: "เลือกได้ถึงวันที่ {date}",
435
+ clearRange: "ล้างช่วงวันที่",
421
436
  last7Days: "7 วันล่าสุด",
422
437
  last30Days: "30 วันล่าสุด",
423
438
  thisMonth: "เดือนนี้",
@@ -522,6 +537,9 @@ export const UI_TEXT_EN: UiText = {
522
537
  table: {
523
538
  rowsPerPage: "Rows per page",
524
539
  pageOf: "Page {page} of {pages}",
540
+ rangeOf: "{from}–{to} of {total}",
541
+ shownOf: "{shown} of {total} items",
542
+ density: "Row density",
525
543
  rowsSelected: "{selected} of {total} row(s) selected",
526
544
  noResults: "No results",
527
545
  deleteTitle: "Are you sure?",
@@ -629,6 +647,9 @@ export const UI_TEXT_EN: UiText = {
629
647
  dayCount: "{count} days",
630
648
  allShortcuts: "All shortcuts",
631
649
  tapToFill: "Tap to fill the field",
650
+ beforeMin: "Pick {date} or later",
651
+ afterMax: "Pick {date} or earlier",
652
+ clearRange: "Clear selection",
632
653
  last7Days: "Last 7 days",
633
654
  last30Days: "Last 30 days",
634
655
  thisMonth: "This month",
package/src/data-table.ts CHANGED
@@ -12,6 +12,7 @@ export {
12
12
  DataTableColumnHeader,
13
13
  DataTableDateRangeFilter,
14
14
  DataTableFacetedFilter,
15
+ type DataTableMobileSlot,
15
16
  DataTablePagination,
16
17
  type DataTableProps,
17
18
  DataTableToolbar,
@@ -8,12 +8,14 @@ export function useIsMobile() {
8
8
  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
9
9
 
10
10
  React.useEffect(() => {
11
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
12
+ // เบราว์เซอร์เก่า/jsdom ที่ไม่มี matchMedia: ใช้ค่าตอน mount อย่างเดียว (ไม่ติดตามการปรับขนาด) แทนที่จะพังทั้งหน้า
13
+ if (typeof window.matchMedia !== "function") return;
11
14
  const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
12
15
  const onChange = () => {
13
16
  setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
14
17
  };
15
18
  mql.addEventListener("change", onChange);
16
- setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
17
19
  return () => mql.removeEventListener("change", onChange);
18
20
  }, []);
19
21