@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.
- package/CHANGELOG.md +107 -0
- package/README.md +1 -0
- package/package.json +2 -2
- package/src/components/app-shell/header-breadcrumbs.tsx +24 -0
- package/src/components/pwa/pull-to-refresh.tsx +44 -8
- package/src/components/pwa/web-push-row.tsx +126 -0
- package/src/components/shared/date/accessible-calendar.tsx +40 -0
- package/src/components/shared/date/date-picker.tsx +844 -321
- package/src/components/shared/date/date-range-picker.tsx +8 -5
- package/src/components/shared/form/date-time-picker.tsx +2 -2
- package/src/components/shared/form/labeled-field.tsx +34 -0
- package/src/components/shared/list-section.tsx +85 -0
- package/src/components/shared/notification-center.tsx +99 -11
- package/src/components/shared/progress-bar.tsx +9 -2
- package/src/components/shared/record-card.tsx +158 -0
- package/src/components/shared/responsive-popover.tsx +174 -0
- package/src/components/shared/row-table.tsx +133 -0
- package/src/components/shared/selection-bar.tsx +20 -6
- package/src/components/shared/sidebar-nav-item.tsx +62 -0
- package/src/components/shared/status-badge.tsx +16 -2
- package/src/components/shared/ui-text.tsx +36 -0
- package/src/components/sortable/sortable-list.tsx +17 -1
- package/src/index.ts +26 -1
- package/src/pwa.ts +2 -1
|
@@ -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
|
-
/**
|
|
28
|
-
|
|
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
|
|
62
|
-
|
|
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=
|
|
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
|
-
/**
|
|
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
|
|
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(
|
|
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";
|