@suphark/ui 0.47.0 → 0.57.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.
@@ -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
+ }
@@ -24,8 +24,11 @@ export interface SelectionBarProps {
24
24
  children?: ReactNode;
25
25
  /** ข้อความนับ ใช้ `{count}` เช่น "เลือก {count} รายการ" (ไม่ส่ง = `UiText.selection.selected`) */
26
26
  label?: string;
27
- /** floating = แถบลอยกลางล่างจอ (ค่าเริ่มต้น) · inline = แถบในหน้า เหนือตาราง */
28
- variant?: "floating" | "inline";
27
+ /**
28
+ * floating = แถบลอยกลางล่างจอ (ค่าเริ่มต้น) · inline = แถบในหน้า เหนือตาราง ·
29
+ * pill = แคปซูลสีหลักลอยกลางล่างจอ (กลับสีเข้ม เด่นกว่า floating, ปุ่มโปร่งบนพื้นสีหลัก)
30
+ */
31
+ variant?: "floating" | "inline" | "pill";
29
32
  className?: string;
30
33
  }
31
34
 
@@ -48,6 +51,7 @@ export function SelectionBar({
48
51
  }: SelectionBarProps) {
49
52
  const text = useUiText();
50
53
  if (count <= 0) return null;
54
+ const pill = variant === "pill";
51
55
 
52
56
  const template = label ?? text.selection?.selected ?? "เลือก {count} รายการ";
53
57
  const message = template.replace("{count}", String(count));
@@ -58,8 +62,11 @@ export function SelectionBar({
58
62
  // อ่านออกเสียงจำนวนที่เลือกให้ screen reader โดยไม่ขัดจังหวะ
59
63
  aria-live="polite"
60
64
  className={cn(
61
- "flex items-center gap-2 rounded-xl border bg-card px-3 py-2 shadow-lg",
62
- variant === "floating" &&
65
+ "flex items-center gap-2 px-3 py-2 shadow-lg",
66
+ pill
67
+ ? "h-11 rounded-full bg-primary py-0 ps-4 pe-2 text-primary-foreground"
68
+ : "rounded-xl border bg-card",
69
+ variant !== "inline" &&
63
70
  "-translate-x-1/2 fixed bottom-[calc(1rem+env(safe-area-inset-bottom))] left-1/2 z-40 max-w-[calc(100vw-2rem)]",
64
71
  className,
65
72
  )}
@@ -70,7 +77,11 @@ export function SelectionBar({
70
77
  <Button
71
78
  key={action.label}
72
79
  size="sm"
73
- variant={action.destructive ? "destructive" : "outline"}
80
+ variant={pill ? "ghost" : action.destructive ? "destructive" : "outline"}
81
+ className={cn(
82
+ pill && "rounded-full hover:bg-primary-foreground/15 hover:text-primary-foreground",
83
+ pill && action.destructive && "text-primary-foreground",
84
+ )}
74
85
  disabled={action.disabled}
75
86
  onClick={action.onClick}
76
87
  >
@@ -85,7 +96,10 @@ export function SelectionBar({
85
96
  size="icon-sm"
86
97
  aria-label={text.selection?.clear ?? "ล้างการเลือก"}
87
98
  onClick={onClear}
88
- className="ml-auto shrink-0"
99
+ className={cn(
100
+ "ml-auto shrink-0",
101
+ pill && "rounded-full hover:bg-primary-foreground/15 hover:text-primary-foreground",
102
+ )}
89
103
  >
90
104
  <XIcon />
91
105
  </Button>
@@ -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
+ }
@@ -12,13 +12,27 @@ const TONE: Record<Tone, string> = {
12
12
  info: "border-info-border bg-info-subtle text-info-subtle-foreground",
13
13
  };
14
14
 
15
- /** Badge ของ shadcn (variant outline) + สีตามความหมายของสถานะ */
15
+ /**
16
+ * Badge ของ shadcn (variant outline) + สีตามความหมายของสถานะ
17
+ * อยู่ใน `RecordCard variant="quiet"` (data-variant="quiet") จะกลายเป็น "จุดสี + ข้อความสี" ไม่มีกรอบ/พื้น อัตโนมัติ
18
+ */
16
19
  export function StatusBadge({
17
20
  tone = "neutral",
18
21
  className,
19
22
  ...props
20
23
  }: Omit<ComponentProps<typeof Badge>, "variant"> & { tone?: Tone }) {
21
- return <Badge variant="outline" className={cn(TONE[tone], className)} {...props} />;
24
+ return (
25
+ <Badge
26
+ variant="outline"
27
+ className={cn(
28
+ TONE[tone],
29
+ "in-data-[variant=quiet]:h-auto in-data-[variant=quiet]:border-transparent in-data-[variant=quiet]:bg-transparent in-data-[variant=quiet]:px-0 in-data-[variant=quiet]:py-0 in-data-[variant=quiet]:font-semibold",
30
+ "in-data-[variant=quiet]:before:size-1.5 in-data-[variant=quiet]:before:shrink-0 in-data-[variant=quiet]:before:rounded-full in-data-[variant=quiet]:before:bg-current in-data-[variant=quiet]:before:content-['']",
31
+ className,
32
+ )}
33
+ {...props}
34
+ />
35
+ );
22
36
  }
23
37
 
24
38
  /** สถานะที่ใช้บ่อยในระบบ → tone */
@@ -204,6 +204,20 @@ export interface UiText {
204
204
  helpRange: string;
205
205
  helpRangeShortcut: string;
206
206
  pickEnd: string;
207
+ /** ช่องพิมพ์ (0.49.0) — `{example}` = ตัวอย่างที่พิมพ์ได้, `{count}` = จำนวนวัน */
208
+ cannotRead: string;
209
+ shortcutsLabel: string;
210
+ showAll: string;
211
+ backToCalendar: string;
212
+ openCalendar: string;
213
+ dayCount: string;
214
+ allShortcuts: string;
215
+ tapToFill: string;
216
+ /** `minDate`/`maxDate` — `{date}` = วันที่ขอบเขต (ตาม `locale` ของช่อง) */
217
+ beforeMin: string;
218
+ afterMax: string;
219
+ /** `DateRangePicker` (ตัวกรอง) */
220
+ clearRange: string;
207
221
  last7Days: string;
208
222
  last30Days: string;
209
223
  thisMonth: string;
@@ -401,6 +415,17 @@ export const UI_TEXT_TH: UiText = {
401
415
  helpRange: "1–5 ก.ค.",
402
416
  helpRangeShortcut: "ช่วงจากตัวย่อ (คั่นด้วย .. ~ ถึง to)",
403
417
  pickEnd: "เลือกวันสุดท้ายของช่วง",
418
+ cannotRead: "อ่านไม่ออก — ลองพิมพ์ {example}",
419
+ shortcutsLabel: "พิมพ์ลัด",
420
+ showAll: "ทั้งหมด",
421
+ backToCalendar: "ปฏิทิน",
422
+ openCalendar: "เปิดปฏิทิน",
423
+ dayCount: "{count} วัน",
424
+ allShortcuts: "พิมพ์ลัดทั้งหมด",
425
+ tapToFill: "กดเพื่อเติมในช่อง",
426
+ beforeMin: "เลือกได้ตั้งแต่วันที่ {date}",
427
+ afterMax: "เลือกได้ถึงวันที่ {date}",
428
+ clearRange: "ล้างช่วงวันที่",
404
429
  last7Days: "7 วันล่าสุด",
405
430
  last30Days: "30 วันล่าสุด",
406
431
  thisMonth: "เดือนนี้",
@@ -604,6 +629,17 @@ export const UI_TEXT_EN: UiText = {
604
629
  helpRange: "1–5 Jul",
605
630
  helpRangeShortcut: "Range from shortcuts (separate with .. ~ to)",
606
631
  pickEnd: "Pick the last day of the range",
632
+ cannotRead: "Can't read that — try {example}",
633
+ shortcutsLabel: "Shortcuts",
634
+ showAll: "All",
635
+ backToCalendar: "Calendar",
636
+ openCalendar: "Open calendar",
637
+ dayCount: "{count} days",
638
+ allShortcuts: "All shortcuts",
639
+ tapToFill: "Tap to fill the field",
640
+ beforeMin: "Pick {date} or later",
641
+ afterMax: "Pick {date} or earlier",
642
+ clearRange: "Clear selection",
607
643
  last7Days: "Last 7 days",
608
644
  last30Days: "Last 30 days",
609
645
  thisMonth: "This month",
@@ -1048,6 +1048,8 @@ export interface SortableRow {
1048
1048
  isDragging: boolean;
1049
1049
  /** รายการนี้ถูกเลือกอยู่ (`multiSelect`) */
1050
1050
  selected: boolean;
1051
+ /** เลือก/ไม่เลือกแถวนี้ (สำหรับช่องติ๊กในแถว) — ไม่ได้เปิด `multiSelect` = ไม่ทำอะไร */
1052
+ toggleSelected: () => void;
1051
1053
  /**
1052
1054
  * กระจายลงแถวเมื่อเปิด `multiSelect`: คลิกพร้อม ⌘/Ctrl/Shift เพื่อเลือก + `data-selected`
1053
1055
  * สำหรับทำสี (`data-[selected]:bg-primary/10`) — ปิด `multiSelect` หรือเป็นแถวกลุ่ม = object ว่าง
@@ -1188,6 +1190,9 @@ export function useSortableRow({
1188
1190
  handleProps,
1189
1191
  isDragging: sortable.isDragging,
1190
1192
  selected: isSelected,
1193
+ toggleSelected: () => {
1194
+ if (selectable) select(id, "toggle");
1195
+ },
1191
1196
  selectProps,
1192
1197
  };
1193
1198
  }
@@ -1240,6 +1245,7 @@ export function DragHandle({
1240
1245
  className,
1241
1246
  disabled,
1242
1247
  hideWhenDisabled = false,
1248
+ reveal = false,
1243
1249
  ...handleProps
1244
1250
  }: {
1245
1251
  label?: string;
@@ -1247,6 +1253,11 @@ export function DragHandle({
1247
1253
  disabled?: boolean;
1248
1254
  /** ปิดการลากแล้วไม่ต้องแสดงที่จับเลย (ค่าเริ่มต้นคือแสดงแบบจาง ๆ ให้รู้ว่ามีแต่ตอนนี้ลากไม่ได้) */
1249
1255
  hideWhenDisabled?: boolean;
1256
+ /**
1257
+ * "สงบจนกว่าจะแตะ": ซ่อนที่จับไว้ โผล่เมื่อชี้แถว / โฟกัสในแถว (คีย์บอร์ด) / แถวถูกเลือก · จอสัมผัส (ไม่มี hover) เห็นตลอด
1258
+ * ต้องใส่ `group/row` ที่แถว (`<TableRow className="group/row">`) · ที่จับยังโฟกัสด้วย Tab ได้เสมอ
1259
+ */
1260
+ reveal?: boolean;
1250
1261
  } & Record<string, unknown>) {
1251
1262
  const state = useContext(SortableStateContext);
1252
1263
  const off = disabled ?? state.disabled;
@@ -1257,7 +1268,12 @@ export function DragHandle({
1257
1268
  size="icon-sm"
1258
1269
  aria-label={label ?? state.handleLabel}
1259
1270
  disabled={off}
1260
- className={cn("cursor-grab touch-none active:cursor-grabbing", className)}
1271
+ className={cn(
1272
+ "cursor-grab touch-none active:cursor-grabbing",
1273
+ reveal &&
1274
+ "opacity-0 transition-opacity focus-visible:opacity-100 group-focus-within/row:opacity-100 group-hover/row:opacity-100 group-data-[selected]/row:opacity-100 [@media(hover:none)]:opacity-100",
1275
+ className,
1276
+ )}
1261
1277
  data-sortable-handle=""
1262
1278
  {...handleProps}
1263
1279
  >
package/src/index.ts CHANGED
@@ -77,6 +77,7 @@ export {
77
77
  } from "./components/shared/confirm-dialog";
78
78
  export { CountBadge } from "./components/shared/count-badge";
79
79
  export { DataList, DataRow } from "./components/shared/data-list";
80
+ export { AccessibleCalendar } from "./components/shared/date/accessible-calendar";
80
81
  export {
81
82
  DatePicker,
82
83
  type DatePickerProps,
@@ -131,7 +132,7 @@ export {
131
132
  type FormInvalidNotice,
132
133
  useFormInvalidNotice,
133
134
  } from "./components/shared/form/form-invalid-notice";
134
- export { CheckboxField, LabeledField } from "./components/shared/form/labeled-field";
135
+ export { CheckboxField, LabeledField, SwitchField } from "./components/shared/form/labeled-field";
135
136
  export { NumberInput } from "./components/shared/form/number-input";
136
137
  export { SearchInput, type SearchInputProps } from "./components/shared/form/search-input";
137
138
  export {
@@ -175,6 +176,11 @@ export {
175
176
  type LanguageSwitchProps,
176
177
  } from "./components/shared/language/language-switch";
177
178
  export { ErrorState, type RouteError } from "./components/shared/layout/error-state";
179
+ export {
180
+ ListSection,
181
+ ListSectionHeader,
182
+ type ListSectionTone,
183
+ } from "./components/shared/list-section";
178
184
  export {
179
185
  MonthStepper,
180
186
  type MonthStepperProps,
@@ -211,6 +217,24 @@ export {
211
217
  type RecordTone,
212
218
  recordAccentColor,
213
219
  } from "./components/shared/record-card";
220
+ export {
221
+ ResponsivePopover,
222
+ ResponsivePopoverContent,
223
+ type ResponsivePopoverContentProps,
224
+ ResponsivePopoverItem,
225
+ type ResponsivePopoverProps,
226
+ ResponsivePopoverTrigger,
227
+ type ResponsivePopoverTriggerProps,
228
+ useIsResponsiveSheet,
229
+ } from "./components/shared/responsive-popover";
230
+ export {
231
+ RowTable,
232
+ RowTableActions,
233
+ RowTableCell,
234
+ RowTableGroup,
235
+ RowTableHeader,
236
+ RowTableRow,
237
+ } from "./components/shared/row-table";
214
238
  export {
215
239
  describeCondition,
216
240
  emptyGroup,
@@ -242,6 +266,7 @@ export { SelectionButton } from "./components/shared/selection-button";
242
266
  export { SettingsLayout, type SettingsSection } from "./components/shared/settings-layout";
243
267
  export { DangerZone, SettingsRow, SwitchRow } from "./components/shared/settings-row";
244
268
  export { SetupChecklist, type SetupChecklistStep } from "./components/shared/setup-checklist";
269
+ export { SidebarCountBadge, SidebarNavItem } from "./components/shared/sidebar-nav-item";
245
270
  export { SimpleDialog, SimpleSheet } from "./components/shared/simple-dialog";
246
271
  export { paginationRange, SimplePagination } from "./components/shared/simple-pagination";
247
272
  export {
package/src/pwa.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @suphark/ui/pwa — ชุด Progressive Web App: ลงทะเบียน/อัปเดต service worker, การ์ดชวนติดตั้ง, ดึงลงเพื่อรีเฟรช
2
+ * @suphark/ui/pwa — ชุด Progressive Web App: ลงทะเบียน/อัปเดต service worker, การ์ดชวนติดตั้ง, ดึงลงเพื่อรีเฟรช, สวิตช์แจ้งเตือน Web Push
3
3
  * `PullToRefresh` ใช้ next/navigation จึงแยกจาก barrel หลักเหมือน `@suphark/ui/next`
4
4
  */
5
5
  export {
@@ -22,3 +22,4 @@ export {
22
22
  } from "./components/pwa/pwa-install-card";
23
23
  export { PwaServiceWorkerRegistration } from "./components/pwa/pwa-service-worker-registration";
24
24
  export { canReloadNow, watchServiceWorkerUpdates } from "./components/pwa/service-worker-updates";
25
+ export { WebPushRow, type WebPushRowProps } from "./components/pwa/web-push-row";