@suphark/ui 0.3.1 → 0.3.2
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
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
รูปแบบตาม [Keep a Changelog](https://keepachangelog.com/) · semver: patch = หน้าตา/บั๊ก, minor = component/prop ใหม่, major = เปลี่ยน/ลบ props
|
|
4
4
|
|
|
5
|
+
## [0.3.2] — 2026-09-18
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `copyText`/`selectContents` export จาก barrel หลักด้วย (แอปใช้ทำปุ่มคัดลอกเองได้)
|
|
10
|
+
- `EnvSnippet`: ค่ายาว (client secret) ล้นออกนอก dialog — ตัดบรรทัดในกรอบ (`whitespace-pre-wrap break-all` + `min-w-0`) แทน scroll แนวนอน (รายงานจากหน้า Register app ของ Pi Auth)
|
|
11
|
+
- `EnvSnippet` / `SecretReveal`: ปุ่มคัดลอกเงียบเมื่อ `navigator.clipboard` ถูกบล็อก (webview/embedded browser เช่น browser ใน Claude desktop) — เพิ่ม `copyText()` (`@suphark/ui/lib/copy-text`) ที่ fallback ไป `execCommand("copy")`; ถ้ายังไม่ได้ EnvSnippet จะเลือกข้อความให้และบอกให้กด Ctrl+C (`UiText.copyFailed` optional)
|
|
12
|
+
|
|
5
13
|
## [0.3.1] — 2026-09-18
|
|
6
14
|
|
|
7
15
|
### Changed
|
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ Design system กลางของ Suphark apps (Pi Auth, DueBell, Pi PDF, supp
|
|
|
4
4
|
|
|
5
5
|
แพ็กเกจส่ง **source (.ts/.tsx) ไม่ build** — แอป Next.js transpile เองผ่าน `transpilePackages`
|
|
6
6
|
|
|
7
|
+
ดูตัวอย่างสด: **https://ui.suphark.com** (design system, `/shell` AppShell, `/brand`)
|
|
8
|
+
|
|
7
9
|
## ติดตั้ง
|
|
8
10
|
|
|
9
11
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suphark/ui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Design system กลางของ Suphark apps: shadcn/ui (base-nova, Base UI, Tailwind v4) + shared components + token/ฟอนต์ Noto Sans Thai Looped — ส่งเป็น source ให้แอป Next transpile เอง",
|
|
@@ -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
|
}
|
|
@@ -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
|
@@ -136,6 +136,7 @@ export * from "./components/ui/tooltip";
|
|
|
136
136
|
// ---------- lib / hooks ----------
|
|
137
137
|
export { useCopyToClipboard } from "./hooks/use-copy-to-clipboard";
|
|
138
138
|
export { useIsMobile } from "./hooks/use-mobile";
|
|
139
|
+
export { copyText, selectContents } from "./lib/copy-text";
|
|
139
140
|
export { formatRelativeThaiDate, formatThaiFullDate } from "./lib/formatters";
|
|
140
141
|
export {
|
|
141
142
|
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
|
+
}
|