@suphark/ui 0.3.1 → 0.3.3
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 +25 -0
- package/README.md +13 -0
- package/package.json +1 -1
- package/src/components/design-system/design-system-showcase.tsx +49 -46
- package/src/components/design-system/search-index.ts +219 -0
- package/src/components/design-system/sections/hero-specimen.tsx +112 -0
- package/src/components/design-system/sections/showcase-data-display.tsx +17 -17
- package/src/components/design-system/sections/showcase-feedback.tsx +14 -20
- package/src/components/design-system/sections/showcase-forms.tsx +12 -16
- package/src/components/design-system/sections/showcase-foundations.tsx +12 -18
- package/src/components/design-system/sections/showcase-guidelines-recipes.tsx +10 -14
- package/src/components/design-system/sections/showcase-overlays-navigation.tsx +16 -20
- package/src/components/design-system/sections/showcase-overview.tsx +95 -27
- package/src/components/design-system/sections/showcase-primitives.tsx +18 -22
- package/src/components/shared/env-snippet.tsx +28 -13
- package/src/components/shared/form-dialog.tsx +9 -3
- package/src/components/shared/secret-reveal.tsx +5 -7
- package/src/components/shared/ui-text.tsx +4 -0
- package/src/index.ts +3 -0
- package/src/lib/copy-text.ts +48 -0
- package/src/styles.css +4 -0
|
@@ -101,11 +101,11 @@ export function ShowcasePrimitives() {
|
|
|
101
101
|
<div className="flex flex-col gap-1">
|
|
102
102
|
<div className="flex items-center gap-2">
|
|
103
103
|
<Badge variant="outline">Core Primitives</Badge>
|
|
104
|
-
<span className="text-
|
|
104
|
+
<span className="text-muted-foreground text-sm">
|
|
105
105
|
Buttons, Badges, Cards, Avatars & Structure
|
|
106
106
|
</span>
|
|
107
107
|
</div>
|
|
108
|
-
<h2 className="
|
|
108
|
+
<h2 className="font-bold text-2xl text-foreground tracking-tight">
|
|
109
109
|
คอมโพเนนต์พื้นฐานและการแสดงผลโครงสร้าง
|
|
110
110
|
</h2>
|
|
111
111
|
<p className="text-sm text-muted-foreground">
|
|
@@ -114,14 +114,12 @@ export function ShowcasePrimitives() {
|
|
|
114
114
|
</div>
|
|
115
115
|
|
|
116
116
|
{/* Button Matrix */}
|
|
117
|
-
<Card>
|
|
117
|
+
<Card id="primitives-button">
|
|
118
118
|
<CardHeader>
|
|
119
119
|
<div className="flex items-center justify-between">
|
|
120
120
|
<div>
|
|
121
|
-
<CardTitle className="text-
|
|
122
|
-
<CardDescription
|
|
123
|
-
ลำดับความสำคัญของปุ่มและขนาดตามความเหมาะสมของหน้าจอ
|
|
124
|
-
</CardDescription>
|
|
121
|
+
<CardTitle className="text-lg">Button Hierarchy & Variants</CardTitle>
|
|
122
|
+
<CardDescription>ลำดับความสำคัญของปุ่มและขนาดตามความเหมาะสมของหน้าจอ</CardDescription>
|
|
125
123
|
</div>
|
|
126
124
|
<Badge variant="secondary">shadcn/ui Button</Badge>
|
|
127
125
|
</div>
|
|
@@ -248,12 +246,12 @@ export function ShowcasePrimitives() {
|
|
|
248
246
|
</Card>
|
|
249
247
|
|
|
250
248
|
{/* Shared Button Wrappers */}
|
|
251
|
-
<Card>
|
|
249
|
+
<Card id="primitives-shared-buttons">
|
|
252
250
|
<CardHeader>
|
|
253
251
|
<div className="flex items-center justify-between">
|
|
254
252
|
<div>
|
|
255
|
-
<CardTitle className="text-
|
|
256
|
-
<CardDescription
|
|
253
|
+
<CardTitle className="text-lg">Shared Action Button Wrappers</CardTitle>
|
|
254
|
+
<CardDescription>
|
|
257
255
|
คอมโพเนนต์ปุ่มมาตรฐานพร้อมสไตล์และไอคอนจาก <code>src/components/shared/button/*</code>
|
|
258
256
|
</CardDescription>
|
|
259
257
|
</div>
|
|
@@ -274,12 +272,10 @@ export function ShowcasePrimitives() {
|
|
|
274
272
|
</Card>
|
|
275
273
|
|
|
276
274
|
{/* Badges & Status Indicators */}
|
|
277
|
-
<Card>
|
|
275
|
+
<Card id="primitives-badge">
|
|
278
276
|
<CardHeader>
|
|
279
|
-
<CardTitle className="text-
|
|
280
|
-
<CardDescription
|
|
281
|
-
ป้ายสถานะสำหรับ workflow การจัดซื้อและกระบวนการอนุมัติ
|
|
282
|
-
</CardDescription>
|
|
277
|
+
<CardTitle className="text-lg">Badge & Status Indicators</CardTitle>
|
|
278
|
+
<CardDescription>ป้ายสถานะสำหรับ workflow การจัดซื้อและกระบวนการอนุมัติ</CardDescription>
|
|
283
279
|
</CardHeader>
|
|
284
280
|
<CardContent className="flex flex-col gap-4">
|
|
285
281
|
<div className="flex flex-wrap items-center gap-2">
|
|
@@ -310,7 +306,7 @@ export function ShowcasePrimitives() {
|
|
|
310
306
|
</Card>
|
|
311
307
|
|
|
312
308
|
{/* Full Card Composition & Avatar */}
|
|
313
|
-
<div className="grid gap-6 lg:grid-cols-2">
|
|
309
|
+
<div id="primitives-card" className="grid gap-6 lg:grid-cols-2">
|
|
314
310
|
{/* Full Card Composition */}
|
|
315
311
|
<Card>
|
|
316
312
|
<CardHeader>
|
|
@@ -319,7 +315,7 @@ export function ShowcasePrimitives() {
|
|
|
319
315
|
<span className="text-xs text-muted-foreground">Updated today</span>
|
|
320
316
|
</div>
|
|
321
317
|
<CardTitle className="text-lg">Package Evaluation Summary</CardTitle>
|
|
322
|
-
<CardDescription
|
|
318
|
+
<CardDescription>
|
|
323
319
|
โครงสร้าง Card แบบเต็มรูปแบบ (CardHeader / Title / Description / Content / Footer)
|
|
324
320
|
</CardDescription>
|
|
325
321
|
</CardHeader>
|
|
@@ -398,12 +394,12 @@ export function ShowcasePrimitives() {
|
|
|
398
394
|
</div>
|
|
399
395
|
|
|
400
396
|
{/* Accordion & Collapsible */}
|
|
401
|
-
<div className="grid gap-6 lg:grid-cols-2">
|
|
397
|
+
<div id="primitives-accordion" className="grid gap-6 lg:grid-cols-2">
|
|
402
398
|
{/* Accordion */}
|
|
403
399
|
<Card>
|
|
404
400
|
<CardHeader className="pb-2">
|
|
405
|
-
<CardTitle className="text-
|
|
406
|
-
<CardDescription
|
|
401
|
+
<CardTitle className="text-lg">Accordion Primitive</CardTitle>
|
|
402
|
+
<CardDescription>สำหรับคำถามที่พบบ่อยหรือเงื่อนไขสัญญา</CardDescription>
|
|
407
403
|
</CardHeader>
|
|
408
404
|
<CardContent>
|
|
409
405
|
<Accordion className="w-full">
|
|
@@ -430,8 +426,8 @@ export function ShowcasePrimitives() {
|
|
|
430
426
|
{/* Collapsible */}
|
|
431
427
|
<Card>
|
|
432
428
|
<CardHeader className="pb-2">
|
|
433
|
-
<CardTitle className="text-
|
|
434
|
-
<CardDescription
|
|
429
|
+
<CardTitle className="text-lg">Collapsible Primitive</CardTitle>
|
|
430
|
+
<CardDescription>สำหรับกล่องฟิลเตอร์ขั้นสูงหรือข้อมูลทางเลือก</CardDescription>
|
|
435
431
|
</CardHeader>
|
|
436
432
|
<CardContent>
|
|
437
433
|
<Collapsible
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Button } from "@suphark/ui/components/ui/button";
|
|
4
|
+
import { copyText, selectContents } from "@suphark/ui/lib/copy-text";
|
|
4
5
|
import { CheckIcon, CopyIcon } from "lucide-react";
|
|
5
|
-
import { useState } from "react";
|
|
6
|
+
import { useRef, useState } from "react";
|
|
6
7
|
import { useUiText } from "./ui-text";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* บล็อก .env สำเร็จรูป: คัดลอกทั้งก้อนปุ่มเดียว (Clerk-style "quick copy")
|
|
10
11
|
* ค่าที่เป็น secret แสดงจริง (หน้าที่เรียกต้องเป็นหน้า "แสดงครั้งเดียว" อยู่แล้ว)
|
|
12
|
+
* - ค่ายาว (secret) ตัดบรรทัดในกรอบ ไม่ล้น dialog
|
|
13
|
+
* - clipboard ถูกบล็อก (webview/embedded browser) → fallback execCommand; ถ้ายังไม่ได้ เลือกข้อความให้กด Ctrl+C
|
|
11
14
|
*/
|
|
12
15
|
export function EnvSnippet({
|
|
13
16
|
title = ".env.local",
|
|
@@ -20,34 +23,46 @@ export function EnvSnippet({
|
|
|
20
23
|
copyLabel?: string;
|
|
21
24
|
}) {
|
|
22
25
|
const text = useUiText();
|
|
23
|
-
const [
|
|
26
|
+
const [state, setState] = useState<"idle" | "copied" | "failed">("idle");
|
|
27
|
+
const preRef = useRef<HTMLPreElement>(null);
|
|
24
28
|
const content = lines.map(([k, v]) => `${k}=${v}`).join("\n");
|
|
29
|
+
|
|
25
30
|
async function copy() {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
setTimeout(() =>
|
|
30
|
-
}
|
|
31
|
-
|
|
31
|
+
const ok = await copyText(`${content}\n`);
|
|
32
|
+
if (ok) {
|
|
33
|
+
setState("copied");
|
|
34
|
+
setTimeout(() => setState((s) => (s === "copied" ? "idle" : s)), 1500);
|
|
35
|
+
} else {
|
|
36
|
+
selectContents(preRef.current);
|
|
37
|
+
setState("failed");
|
|
32
38
|
}
|
|
33
39
|
}
|
|
40
|
+
|
|
34
41
|
return (
|
|
35
|
-
<div className="overflow-hidden rounded-lg border bg-card">
|
|
42
|
+
<div className="min-w-0 overflow-hidden rounded-lg border bg-card">
|
|
36
43
|
<div className="flex items-center justify-between gap-2 border-b bg-muted px-3 py-1.5">
|
|
37
44
|
<span className="font-mono text-xs text-muted-foreground">{title}</span>
|
|
38
45
|
<Button type="button" variant="outline" size="xs" onClick={copy}>
|
|
39
|
-
{copied ? <CheckIcon /> : <CopyIcon />}
|
|
40
|
-
{copied ? text.copied : (copyLabel ?? text.copy)}
|
|
46
|
+
{state === "copied" ? <CheckIcon /> : <CopyIcon />}
|
|
47
|
+
{state === "copied" ? text.copied : (copyLabel ?? text.copy)}
|
|
41
48
|
</Button>
|
|
42
49
|
</div>
|
|
43
|
-
<pre
|
|
50
|
+
<pre
|
|
51
|
+
ref={preRef}
|
|
52
|
+
className="whitespace-pre-wrap break-all px-3 py-2.5 font-mono text-xs leading-6"
|
|
53
|
+
>
|
|
44
54
|
{lines.map(([k, v]) => (
|
|
45
55
|
<div key={k}>
|
|
46
56
|
<span className="text-muted-foreground">{k}=</span>
|
|
47
|
-
<span
|
|
57
|
+
<span>{v}</span>
|
|
48
58
|
</div>
|
|
49
59
|
))}
|
|
50
60
|
</pre>
|
|
61
|
+
{state === "failed" ? (
|
|
62
|
+
<p className="border-t bg-muted/50 px-3 py-1.5 text-muted-foreground text-xs">
|
|
63
|
+
{text.copyFailed ?? "คัดลอกอัตโนมัติไม่ได้ — เลือกข้อความให้แล้ว กด Ctrl+C / ⌘C"}
|
|
64
|
+
</p>
|
|
65
|
+
) : null}
|
|
51
66
|
</div>
|
|
52
67
|
);
|
|
53
68
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
DialogTitle,
|
|
11
11
|
DialogTrigger,
|
|
12
12
|
} from "@suphark/ui/components/ui/dialog";
|
|
13
|
+
import { cn } from "@suphark/ui/lib/utils";
|
|
13
14
|
import { useRouter } from "next/navigation";
|
|
14
15
|
import {
|
|
15
16
|
type ReactElement,
|
|
@@ -76,7 +77,10 @@ export function FormDialog({
|
|
|
76
77
|
return (
|
|
77
78
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
78
79
|
{trigger && <DialogTrigger render={trigger} />}
|
|
79
|
-
|
|
80
|
+
{/* จำกัดความสูงไว้ในจอ: header/footer ตรึง เฉพาะเนื้อฟอร์มเลื่อน (ฟอร์มยาวไม่ล้นในหน้าต่างเตี้ย) */}
|
|
81
|
+
<DialogContent
|
|
82
|
+
className={cn("flex max-h-[calc(100dvh-2rem)] flex-col", contentClassName ?? "sm:max-w-md")}
|
|
83
|
+
>
|
|
80
84
|
<form
|
|
81
85
|
// ไม่ใช้ `action=` — React 19 รีเซ็ตฟอร์ม uncontrolled หลัง action จบทุกครั้ง (แม้คืน error)
|
|
82
86
|
// ทำให้ค่าที่พิมพ์หาย; เรียก action ใน transition เองแทน ฟอร์มจึงคงค่าไว้ให้แก้ต่อได้
|
|
@@ -86,13 +90,15 @@ export function FormDialog({
|
|
|
86
90
|
setSubmitted((n) => n + 1);
|
|
87
91
|
startTransition(() => formAction(fd));
|
|
88
92
|
}}
|
|
89
|
-
className="flex flex-col gap-4"
|
|
93
|
+
className="flex min-h-0 flex-1 flex-col gap-4"
|
|
90
94
|
>
|
|
91
95
|
<DialogHeader>
|
|
92
96
|
<DialogTitle>{title}</DialogTitle>
|
|
93
97
|
{description && <DialogDescription>{description}</DialogDescription>}
|
|
94
98
|
</DialogHeader>
|
|
95
|
-
<div className="flex flex-col gap-3">
|
|
99
|
+
<div className="-mx-1 flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto px-1">
|
|
100
|
+
{children}
|
|
101
|
+
</div>
|
|
96
102
|
<FormMessage state={state} />
|
|
97
103
|
<DialogFooter>
|
|
98
104
|
<Button type="button" variant="ghost" onClick={() => setOpen(false)}>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Button } from "@suphark/ui/components/ui/button";
|
|
4
|
+
import { copyText } from "@suphark/ui/lib/copy-text";
|
|
4
5
|
import { CheckIcon, CopyIcon } from "lucide-react";
|
|
5
6
|
import { useState } from "react";
|
|
6
7
|
import { Notice } from "./notice";
|
|
@@ -17,13 +18,10 @@ export function SecretReveal({
|
|
|
17
18
|
const text = useUiText();
|
|
18
19
|
const [copied, setCopied] = useState<string | null>(null);
|
|
19
20
|
async function copy(key: string, value: string) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} catch {
|
|
25
|
-
// clipboard ไม่ได้ (http/permission) — ผู้ใช้ยังคัดลอกจากข้อความได้
|
|
26
|
-
}
|
|
21
|
+
// clipboard API ถูกบล็อก (webview/embedded browser) → copyText ลอง execCommand ให้; ไม่ได้จริง ๆ ผู้ใช้ยังเลือกจากข้อความได้
|
|
22
|
+
if (!(await copyText(value))) return;
|
|
23
|
+
setCopied(key);
|
|
24
|
+
setTimeout(() => setCopied((c) => (c === key ? null : c)), 1500);
|
|
27
25
|
}
|
|
28
26
|
return (
|
|
29
27
|
<Notice tone="warning" title={title ?? text.showOnce}>
|
|
@@ -12,6 +12,8 @@ export interface UiText {
|
|
|
12
12
|
copied: string;
|
|
13
13
|
showOnce: string;
|
|
14
14
|
success: string;
|
|
15
|
+
/** EnvSnippet: คัดลอกอัตโนมัติไม่ได้ (optional — มีค่าเริ่มต้นในตัว) */
|
|
16
|
+
copyFailed?: string;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export const UI_TEXT_TH: UiText = {
|
|
@@ -23,6 +25,7 @@ export const UI_TEXT_TH: UiText = {
|
|
|
23
25
|
copied: "คัดลอกแล้ว",
|
|
24
26
|
showOnce: "แสดงครั้งเดียว — คัดลอกเก็บไว้ทันที",
|
|
25
27
|
success: "สำเร็จ",
|
|
28
|
+
copyFailed: "คัดลอกอัตโนมัติไม่ได้ — เลือกข้อความให้แล้ว กด Ctrl+C / ⌘C",
|
|
26
29
|
};
|
|
27
30
|
|
|
28
31
|
export const UI_TEXT_EN: UiText = {
|
|
@@ -34,6 +37,7 @@ export const UI_TEXT_EN: UiText = {
|
|
|
34
37
|
copied: "Copied",
|
|
35
38
|
showOnce: "Shown once — copy it now",
|
|
36
39
|
success: "Done",
|
|
40
|
+
copyFailed: "Automatic copy is blocked — text selected, press Ctrl+C / ⌘C",
|
|
37
41
|
};
|
|
38
42
|
|
|
39
43
|
const UiTextContext = createContext<UiText>(UI_TEXT_TH);
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* `chart` (recharts) ไม่อยู่ใน barrel: import ตรง `@suphark/ui/components/ui/chart` เมื่อแอปติดตั้ง recharts เอง
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
// ชนิดช่วงวันที่ของ react-day-picker — แอปไม่ต้องติดตั้ง react-day-picker เอง
|
|
13
|
+
export type { DateRange } from "react-day-picker";
|
|
12
14
|
// sonner: `toast()` คู่กับ <Toaster /> — แอปไม่ต้องติดตั้ง sonner เอง
|
|
13
15
|
export { toast } from "sonner";
|
|
14
16
|
export * from "./components/shared/button";
|
|
@@ -136,6 +138,7 @@ export * from "./components/ui/tooltip";
|
|
|
136
138
|
// ---------- lib / hooks ----------
|
|
137
139
|
export { useCopyToClipboard } from "./hooks/use-copy-to-clipboard";
|
|
138
140
|
export { useIsMobile } from "./hooks/use-mobile";
|
|
141
|
+
export { copyText, selectContents } from "./lib/copy-text";
|
|
139
142
|
export { formatRelativeThaiDate, formatThaiFullDate } from "./lib/formatters";
|
|
140
143
|
export {
|
|
141
144
|
type AtomicPermissionRequirement,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* คัดลอกข้อความลง clipboard ให้ได้มากที่สุด:
|
|
3
|
+
* 1. navigator.clipboard (ต้อง secure context + permission — บาง embedded browser/webview บล็อก)
|
|
4
|
+
* 2. fallback: textarea ชั่วคราว + document.execCommand("copy") (deprecated แต่ยังใช้ได้ทุก browser)
|
|
5
|
+
* คืน true เมื่อสำเร็จ — ผู้เรียกควรมีทางหนี (เลือกข้อความให้ผู้ใช้กด Ctrl+C เอง) เมื่อได้ false
|
|
6
|
+
*/
|
|
7
|
+
export async function copyText(text: string): Promise<boolean> {
|
|
8
|
+
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
|
|
9
|
+
try {
|
|
10
|
+
await navigator.clipboard.writeText(text);
|
|
11
|
+
return true;
|
|
12
|
+
} catch {
|
|
13
|
+
// ลอง fallback ต่อ
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (typeof document === "undefined") return false;
|
|
17
|
+
const ta = document.createElement("textarea");
|
|
18
|
+
ta.value = text;
|
|
19
|
+
ta.setAttribute("readonly", "");
|
|
20
|
+
ta.style.position = "fixed";
|
|
21
|
+
ta.style.top = "0";
|
|
22
|
+
ta.style.left = "0";
|
|
23
|
+
ta.style.opacity = "0";
|
|
24
|
+
document.body.appendChild(ta);
|
|
25
|
+
const active = document.activeElement as HTMLElement | null;
|
|
26
|
+
try {
|
|
27
|
+
ta.focus();
|
|
28
|
+
ta.select();
|
|
29
|
+
ta.setSelectionRange(0, text.length);
|
|
30
|
+
return document.execCommand("copy");
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
} finally {
|
|
34
|
+
ta.remove();
|
|
35
|
+
active?.focus?.();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** เลือกข้อความทั้งก้อนใน element (ให้ผู้ใช้กด Ctrl+C ได้ทันทีเมื่อ copy อัตโนมัติไม่ได้) */
|
|
40
|
+
export function selectContents(el: Element | null): void {
|
|
41
|
+
if (!el || typeof window === "undefined") return;
|
|
42
|
+
const sel = window.getSelection();
|
|
43
|
+
if (!sel) return;
|
|
44
|
+
const range = document.createRange();
|
|
45
|
+
range.selectNodeContents(el);
|
|
46
|
+
sel.removeAllRanges();
|
|
47
|
+
sel.addRange(range);
|
|
48
|
+
}
|
package/src/styles.css
CHANGED
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
*
|
|
10
10
|
* ฟอนต์: Noto Sans Thai Looped (@fontsource) โหลดที่นี่ — แอปที่ต้องการฟอนต์อื่น override --font-sans ใน @theme ของตัวเอง
|
|
11
11
|
* ส่วน print/A4 และสไตล์เฉพาะ feature ของ supplychain ไม่ได้ยกมา (อยู่ที่แอปนั้น)
|
|
12
|
+
*
|
|
13
|
+
* Override contract (ดู docs/agent-rules.md):
|
|
14
|
+
* แอปแก้ได้: --brand / --brand-foreground (บน wrapper), --font-sans / --font-mono, --radius
|
|
15
|
+
* ห้ามแก้: semantic token (--primary --background --muted --destructive …), --sidebar-*, --chart-*, วิธีสลับธีม
|
|
12
16
|
*/
|
|
13
17
|
@import "tw-animate-css";
|
|
14
18
|
@import "@fontsource/noto-sans-thai-looped/400.css";
|