@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.
@@ -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-xs text-muted-foreground">
104
+ <span className="text-muted-foreground text-sm">
105
105
  Buttons, Badges, Cards, Avatars & Structure
106
106
  </span>
107
107
  </div>
108
- <h2 className="text-xl font-bold tracking-tight text-foreground">
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-base">Button Hierarchy & Variants</CardTitle>
122
- <CardDescription className="text-xs">
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-base">Shared Action Button Wrappers</CardTitle>
256
- <CardDescription className="text-xs">
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-base">Badge & Status Indicators</CardTitle>
280
- <CardDescription className="text-xs">
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 className="text-xs">
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-base">Accordion Primitive</CardTitle>
406
- <CardDescription className="text-xs">สำหรับคำถามที่พบบ่อยหรือเงื่อนไขสัญญา</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-base">Collapsible Primitive</CardTitle>
434
- <CardDescription className="text-xs">สำหรับกล่องฟิลเตอร์ขั้นสูงหรือข้อมูลทางเลือก</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 [copied, setCopied] = useState(false);
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
- try {
27
- await navigator.clipboard.writeText(`${content}\n`);
28
- setCopied(true);
29
- setTimeout(() => setCopied(false), 1500);
30
- } catch {
31
- // clipboard ไม่ได้ (http/permission) — ผู้ใช้ยังเลือกข้อความคัดลอกเองได้
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 className="overflow-x-auto px-3 py-2.5 font-mono text-xs leading-6">
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 className="break-all">{v}</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
- <DialogContent className={contentClassName ?? "sm:max-w-md"}>
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">{children}</div>
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
- try {
21
- await navigator.clipboard.writeText(value);
22
- setCopied(key);
23
- setTimeout(() => setCopied((c) => (c === key ? null : c)), 1500);
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";