@suphark/ui 0.6.0 → 0.6.1

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,17 @@
2
2
 
3
3
  รูปแบบตาม [Keep a Changelog](https://keepachangelog.com/) · semver: patch = หน้าตา/บั๊ก, minor = component/prop ใหม่, major = เปลี่ยน/ลบ props
4
4
 
5
+ ## [0.6.1] — 2026-09-20
6
+
7
+ ### Added
8
+
9
+ - **`npx suphark-ui-audit`** (`bin` ของแพ็กเกจ, `scripts/audit.mjs`) — ตรวจโค้ดแอปปลายทางตามกติกา design system ในคำสั่งเดียว: error = `window-dialog` (window.confirm/alert/prompt), `radix-state` (`data-[state=…]` ที่ Base UI ไม่มี), `arbitrary-text` (`text-[Npx]`), `base-ui-props` (สคริปต์เดิม); warning = `raw-palette` (bg-red-500 …), `important` (utility ลงท้าย `!` — เฉพาะ `--important` สำหรับโค้ดแพ็กเกจ); `--strict` ให้ warning เป็น error; ยกเว้น `// ui-audit-ok: เหตุผล` / `/* ui-audit-ok-file: เหตุผล */`; ข้าม `components/ui/**`, tests, generated; README + `docs/agent-rules.md` มีวิธีใช้ — repo นี้รัน `pnpm audit:ui` ใน `verify` และ CI (0 error 0 warning)
10
+ - `FormFieldRadioCards`: prop `aria-label` ส่งต่อไป RadioGroup เมื่อไม่แสดง `label` (ข้อสังเกตจาก Pi Auth หลังย้ายมาใช้ — ของเดิมมี aria-label)
11
+
12
+ ### Changed
13
+
14
+ - ล้าง raw palette ในแพ็กเกจเป็น semantic token: labs badge ใน `AppNav` → `text-warning-muted-foreground`, `FloatingSaveButton` สำเร็จ → `bg-success`, `FormMessage` สำเร็จ → `text-success`, ดอกจัน/ข้อความ error ใน `FormField*` → `text-destructive`, ไอคอน `ThemeToggle` → `text-warning`/`text-primary`/`text-muted-foreground`, ตัวอย่างใน design-system; ที่ยังใช้ palette โดยตั้งใจและจดไว้ (`ui-audit-ok-file`): `Notice`/`StatusBadge` (โทน 50/900 คู่ dark — รอตัดสินใจ token ระดับ tone), `lib/status-colors.ts` (ชุดสีให้ผู้ใช้เลือก)
15
+
5
16
  ## [0.6.0] — 2026-09-20
6
17
 
7
18
  component สำเร็จรูปสำหรับฟอร์ม server action ที่ Pi Auth ประกอบเองอยู่ (DueBell/Mail/Files ใช้ได้เหมือนกัน) — ไม่เปลี่ยน API เดิม; **รวม 0.5.2 ที่ยังไม่ได้ publish**
package/README.md CHANGED
@@ -182,6 +182,26 @@ webview/automation บางตัวปิด `window.confirm` ทิ้ง (
182
182
 
183
183
  `createKanbanCollisionDetection({ columnItems, resolveColumnId })` (จาก `@suphark/ui/kanban`) ใช้เป็น `collisionDetection` ของ `DndContext` เมื่อมีหลายคอลัมน์ (SortableContext ต่อคอลัมน์) — ตัวอย่างบอร์ดเต็มที่ ui.suphark.com/components/kanban-board; wrapper ของการ์ด**ห้าม** `overflow-hidden` (dnd-kit ใส่ transform ที่ node ที่ `setNodeRef` ถ้าถูกครอบด้วย overflow-hidden การ์ดที่ขยับหลบจะถูก clip)
184
184
 
185
+ ### ตรวจกติกาในแอป — `npx suphark-ui-audit`
186
+
187
+ แพ็กเกจมี `bin` ให้รันในแอปปลายทาง (ตรวจ `src` ค่าเริ่มต้น; ข้าม `components/ui/**`, tests, generated):
188
+
189
+ ```bash
190
+ npx suphark-ui-audit # หรือใส่ใน package.json: "audit:ui": "suphark-ui-audit src"
191
+ npx suphark-ui-audit --strict # warning นับเป็น error (ใช้ใน CI เมื่อล้างหมดแล้ว)
192
+ ```
193
+
194
+ | กฎ | ระดับ | เจอ → ใช้แทน |
195
+ | --- | --- | --- |
196
+ | `window-dialog` | error | `window.confirm/alert/prompt(` → `ConfirmDialog` / `useConfirm()` / `useDialogs()` |
197
+ | `radix-state` | error | `data-[state=open\|checked\|active…]` → `data-open` / `data-checked` / `data-active` / `data-popup-open` (Base UI ไม่มี data-state) |
198
+ | `arbitrary-text` | error | `text-[Npx]` → `text-xs … text-3xl` |
199
+ | `base-ui-props` | error | `default*` ที่ค่าเปลี่ยนได้ / `onSelect` บนเมนู (ดู `scripts/audit-base-ui-props.mjs`) |
200
+ | `raw-palette` | warning | `bg-red-500`, `text-slate-600` … → `bg-destructive`, `text-muted-foreground`, `bg-success`, `bg-warning`, `text-primary` |
201
+ | `important` | warning (เฉพาะ `--important`) | utility ลงท้าย `!` ในโค้ดแพ็กเกจ |
202
+
203
+ ยกเว้นรายบรรทัด `// ui-audit-ok: <เหตุผล>` (บรรทัดเดียวกันหรือบรรทัดก่อน) — ยกเว้นทั้งไฟล์ `/* ui-audit-ok-file: <เหตุผล> */`
204
+
185
205
  ### สิทธิ์ (ActionButton/LinkButton `permission` prop)
186
206
 
187
207
  แพ็กเกจไม่รู้จักระบบสิทธิ์ของแอป — ครอบด้วย `UiPermissionProvider` แล้วส่งฟังก์ชัน `check(requirement, options)` ของแอปเข้ามา ถ้าไม่ครอบ = อนุญาตทุกอย่าง
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suphark/ui",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
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 เอง",
@@ -22,6 +22,9 @@
22
22
  "sideEffects": [
23
23
  "*.css"
24
24
  ],
25
+ "bin": {
26
+ "suphark-ui-audit": "scripts/audit.mjs"
27
+ },
25
28
  "files": [
26
29
  "src",
27
30
  "!src/**/__tests__",
@@ -116,6 +119,7 @@
116
119
  },
117
120
  "scripts": {
118
121
  "typecheck": "tsc --noEmit",
122
+ "audit": "node scripts/audit.mjs src --important",
119
123
  "test": "vitest run",
120
124
  "test:watch": "vitest",
121
125
  "lint:base-ui": "node scripts/audit-base-ui-props.mjs",
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * suphark-ui-audit — ตรวจโค้ดแอปปลายทางตามกติกา design system (docs/agent-rules.md) ในคำสั่งเดียว
4
+ *
5
+ * npx suphark-ui-audit # ตรวจ src (ค่าเริ่มต้น)
6
+ * npx suphark-ui-audit src apps/web # หลายโฟลเดอร์
7
+ * npx suphark-ui-audit --strict # warning นับเป็น error ด้วย (ใช้ใน CI เมื่อล้างหมดแล้ว)
8
+ * npx suphark-ui-audit --no-palette # ปิดกฎ raw palette ชั่วคราว
9
+ * npx suphark-ui-audit --important # เปิดกฎ utility ลงท้าย ! (ค่าเริ่มต้นตรวจเฉพาะโค้ดของแพ็กเกจเอง)
10
+ *
11
+ * กฎ (error = พังบน Base UI / ผิดกติกาชัดเจน, warning = ควรแก้แต่ไม่บล็อก):
12
+ * window-dialog error window.confirm/alert/prompt → ConfirmDialog / useConfirm() / useDialogs()
13
+ * radix-state error data-[state=open|checked|active|…] ไม่มีบน Base UI → data-open / data-checked / data-active / data-popup-open
14
+ * arbitrary-text error text-[Npx] → ใช้ scale (text-xs … text-3xl)
15
+ * important warning utility ที่ลงท้าย ! (size-3.5!) — เฉพาะโค้ดแพ็กเกจ (--important): Tailwind ของแอปที่ scan ผ่าน @source อาจไม่ generate
16
+ * raw-palette warning bg-red-500 / text-slate-600 … → semantic token (bg-destructive, text-muted-foreground, bg-success …)
17
+ * base-ui-props error default* ที่ค่าเปลี่ยนได้ / onSelect บน Base UI (สคริปต์ audit-base-ui-props)
18
+ *
19
+ * ยกเว้นเป็นบรรทัด: ใส่ `// ui-audit-ok: <เหตุผล>` ท้ายบรรทัดหรือบรรทัดก่อนหน้า
20
+ * ยกเว้นทั้งไฟล์: `/* ui-audit-ok-file: <เหตุผล> *\/` ที่หัวไฟล์
21
+ * ข้ามอัตโนมัติ: node_modules, .next, dist, build, generated, __tests__, *.test.*, components/ui/** (ไฟล์ shadcn)
22
+ */
23
+ import { spawnSync } from "node:child_process";
24
+ import fs from "node:fs";
25
+ import path from "node:path";
26
+ import { fileURLToPath } from "node:url";
27
+
28
+ const args = process.argv.slice(2);
29
+ const flags = new Set(args.filter((a) => a.startsWith("--")));
30
+ const roots = args.filter((a) => !a.startsWith("--"));
31
+ if (roots.length === 0) roots.push("src");
32
+ const STRICT = flags.has("--strict");
33
+ const PALETTE = !flags.has("--no-palette");
34
+ const SKIP_BASE_UI = flags.has("--no-base-ui");
35
+ const IMPORTANT = flags.has("--important");
36
+
37
+ const SKIP_DIRS = new Set([
38
+ "node_modules",
39
+ ".next",
40
+ "dist",
41
+ "build",
42
+ "generated",
43
+ "__tests__",
44
+ ".git",
45
+ ]);
46
+ const LINE_MARKER = "ui-audit-ok:";
47
+ const FILE_MARKER = "ui-audit-ok-file:";
48
+
49
+ const PALETTE_COLORS =
50
+ "red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|slate|gray|zinc|neutral|stone";
51
+ const RULES = [
52
+ {
53
+ id: "window-dialog",
54
+ level: "error",
55
+ regex: /\bwindow\.(confirm|alert|prompt)\s*\(/g,
56
+ message:
57
+ "ใช้ ConfirmDialog / useConfirm() / useDialogs() ของ @suphark/ui แทน window dialog (webview บางตัวปิดทิ้ง)",
58
+ },
59
+ {
60
+ id: "radix-state",
61
+ level: "error",
62
+ regex:
63
+ /data-\[state=(?:open|closed|checked|unchecked|active|inactive|on|off|visible|hidden|delayed-open|instant-open)\]/g,
64
+ message:
65
+ "Base UI ไม่ใส่ data-state — ใช้ data-open / data-closed / data-checked / data-active / data-popup-open",
66
+ },
67
+ {
68
+ id: "arbitrary-text",
69
+ level: "error",
70
+ regex: /\btext-\[\d+(?:px|rem|em)\]/g,
71
+ message: "ห้าม text-[Npx] — ใช้ typography scale (text-xs … text-3xl) ตาม docs/design-system.md",
72
+ },
73
+ {
74
+ id: "important",
75
+ level: "warning",
76
+ regex: /(?<=["'`\s])[a-z][\w:/[\]().%=,-]*!(?=["'`\s])/g,
77
+ message:
78
+ "utility ที่ลงท้าย ! — แอปที่ scan แพ็กเกจผ่าน @source อาจไม่ generate class นี้ ปรับ specificity แทน",
79
+ },
80
+ {
81
+ id: "raw-palette",
82
+ level: "warning",
83
+ regex: new RegExp(
84
+ `(?<![\\w-])(?:[a-z-]+:)*(?:bg|text|border|ring|fill|stroke|from|to|via|outline|decoration|divide|shadow|accent|caret|placeholder)-(?:${PALETTE_COLORS})-(?:50|100|200|300|400|500|600|700|800|900|950)(?:/\\d+)?(?![\\w-])`,
85
+ "g",
86
+ ),
87
+ message:
88
+ "raw palette → semantic token (bg-muted, text-muted-foreground, bg-destructive, bg-success, bg-warning, text-primary …)",
89
+ },
90
+ ];
91
+
92
+ /** ปิดส่วนที่ไม่ใช่โค้ด (comment, <code>/<pre>, template ตัวอย่างในเอกสาร) ด้วยช่องว่างเพื่อคง offset */
93
+ function mask(content) {
94
+ return content
95
+ .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "))
96
+ .replace(/(^|[^:\\])\/\/.*$/gm, (m, p1) => p1 + " ".repeat(m.length - p1.length))
97
+ .replace(/<code[\s\S]*?<\/code>/g, (m) => m.replace(/[^\n]/g, " "))
98
+ .replace(/<pre[\s\S]*?<\/pre>/g, (m) => m.replace(/[^\n]/g, " "));
99
+ }
100
+
101
+ function walk(dir, out) {
102
+ let entries;
103
+ try {
104
+ entries = fs.readdirSync(dir, { withFileTypes: true });
105
+ } catch {
106
+ return out;
107
+ }
108
+ for (const e of entries) {
109
+ const full = path.join(dir, e.name);
110
+ if (e.isDirectory()) {
111
+ if (SKIP_DIRS.has(e.name)) continue;
112
+ walk(full, out);
113
+ } else if (/\.(tsx?|jsx?|mdx?)$/.test(e.name) && !/\.test\.[jt]sx?$/.test(e.name)) {
114
+ // ไฟล์ shadcn ของแอป (สำเนา CLI) ไม่ตรวจ — ควรใช้ของแพ็กเกจแทนอยู่แล้ว
115
+ if (/[\\/]components[\\/]ui[\\/]/.test(full)) continue;
116
+ out.push(full);
117
+ }
118
+ }
119
+ return out;
120
+ }
121
+
122
+ const files = roots.flatMap((r) => walk(path.resolve(process.cwd(), r), []));
123
+ const findings = [];
124
+ let skippedFiles = 0;
125
+
126
+ for (const file of files) {
127
+ const raw = fs.readFileSync(file, "utf8");
128
+ if (raw.includes(FILE_MARKER)) {
129
+ skippedFiles++;
130
+ continue;
131
+ }
132
+ const lines = raw.split("\n");
133
+ const masked = mask(raw).split("\n");
134
+ for (const rule of RULES) {
135
+ if (rule.id === "raw-palette" && !PALETTE) continue;
136
+ if (rule.id === "important" && !IMPORTANT) continue;
137
+ for (let i = 0; i < masked.length; i++) {
138
+ const line = masked[i] ?? "";
139
+ rule.regex.lastIndex = 0;
140
+ let m = rule.regex.exec(line);
141
+ if (!m) continue;
142
+ const rawLine = lines[i] ?? "";
143
+ const prev = lines[i - 1] ?? "";
144
+ if (
145
+ rawLine.includes(LINE_MARKER) ||
146
+ (prev.trim().startsWith("//") && prev.includes(LINE_MARKER))
147
+ )
148
+ continue;
149
+ const hits = [];
150
+ while (m) {
151
+ hits.push(m[0]);
152
+ m = rule.regex.exec(line);
153
+ }
154
+ findings.push({
155
+ file: path.relative(process.cwd(), file),
156
+ line: i + 1,
157
+ rule: rule.id,
158
+ level: STRICT ? "error" : rule.level,
159
+ hits: [...new Set(hits)],
160
+ message: rule.message,
161
+ });
162
+ }
163
+ }
164
+ }
165
+
166
+ // base-ui props audit ของแพ็กเกจ (รันเป็นโปรเซสลูกเพื่อคง UI_DIR ชี้ที่ shadcn ของแพ็กเกจ)
167
+ let baseUiExit = 0;
168
+ let baseUiOutput = "";
169
+ if (!SKIP_BASE_UI) {
170
+ const here = path.dirname(fileURLToPath(import.meta.url));
171
+ const uiDir = path.resolve(here, "..", "src", "components", "ui");
172
+ const r = spawnSync(process.execPath, [path.join(here, "audit-base-ui-props.mjs"), ...roots], {
173
+ encoding: "utf8",
174
+ env: { ...process.env, UI_DIR: uiDir },
175
+ });
176
+ baseUiExit = r.status ?? 1;
177
+ baseUiOutput = `${r.stdout ?? ""}${r.stderr ?? ""}`.trim();
178
+ }
179
+
180
+ // ---------- รายงาน ----------
181
+ const byRule = new Map();
182
+ for (const f of findings) byRule.set(f.rule, (byRule.get(f.rule) ?? 0) + 1);
183
+ const errors = findings.filter((f) => f.level === "error");
184
+ const warnings = findings.filter((f) => f.level === "warning");
185
+
186
+ findings.sort((a, b) => (a.file === b.file ? a.line - b.line : a.file.localeCompare(b.file)));
187
+ let lastFile = "";
188
+ for (const f of findings) {
189
+ if (f.file !== lastFile) {
190
+ console.log(`\n${f.file}`);
191
+ lastFile = f.file;
192
+ }
193
+ const tag = f.level === "error" ? "✗" : "△";
194
+ console.log(` ${tag} :${f.line} ${f.rule} ${f.hits.join(", ")}`);
195
+ }
196
+ if (baseUiOutput && baseUiExit !== 0) console.log(`\n${baseUiOutput}`);
197
+
198
+ console.log(
199
+ `\nsuphark-ui-audit — ตรวจ ${files.length} ไฟล์ใน ${roots.join(", ")}${skippedFiles ? ` (ข้าม ${skippedFiles} ไฟล์ที่ทำเครื่องหมาย ${FILE_MARKER})` : ""}`,
200
+ );
201
+ for (const rule of RULES) {
202
+ const n = byRule.get(rule.id) ?? 0;
203
+ if (rule.id === "raw-palette" && !PALETTE) continue;
204
+ if (rule.id === "important" && !IMPORTANT) continue;
205
+ console.log(
206
+ ` ${n === 0 ? "✓" : rule.level === "error" || STRICT ? "✗" : "△"} ${rule.id.padEnd(15)} ${n}${n ? ` — ${rule.message}` : ""}`,
207
+ );
208
+ }
209
+ console.log(
210
+ ` ${baseUiExit === 0 ? "✓" : "✗"} base-ui-props ${baseUiExit === 0 ? "ผ่าน" : "พบปัญหา (ดูข้างบน)"}`,
211
+ );
212
+ console.log(`\n${errors.length} error, ${warnings.length} warning${STRICT ? " (--strict)" : ""}`);
213
+ if (errors.length || warnings.length) {
214
+ console.log(
215
+ `ยกเว้นรายบรรทัด: // ${LINE_MARKER} <เหตุผล> ยกเว้นทั้งไฟล์: /* ${FILE_MARKER} <เหตุผล> */`,
216
+ );
217
+ }
218
+ process.exit(errors.length > 0 || baseUiExit !== 0 ? 1 : 0);
@@ -65,6 +65,7 @@ export function AppBrand({
65
65
  <SidebarMenuItem className={cn("flex items-center", className)}>
66
66
  <SidebarMenuButton
67
67
  size="lg"
68
+ // ui-audit-ok: class เดียวกับ size=lg ของ sidebar.tsx (shadcn) จึงถูก generate แน่นอน
68
69
  className="min-w-0 flex-1 group-data-[collapsible=icon]:p-0!"
69
70
  render={<Link href={href} onClick={closeMobile} />}
70
71
  >
@@ -49,12 +49,10 @@ function MenuTitle({ title, isLabs }: { title: string; isLabs?: boolean }) {
49
49
  <TooltipTrigger
50
50
  render={<span className="flex w-full items-center gap-1.5 overflow-hidden" />}
51
51
  >
52
- <span
53
- className={cn("truncate", isLabs && "font-medium text-amber-600 dark:text-amber-500")}
54
- >
52
+ <span className={cn("truncate", isLabs && "font-medium text-warning-muted-foreground")}>
55
53
  {title}
56
54
  </span>
57
- {isLabs && <FlaskConical className="h-3 w-3 shrink-0 text-amber-600 dark:text-amber-500" />}
55
+ {isLabs && <FlaskConical className="h-3 w-3 shrink-0 text-warning-muted-foreground" />}
58
56
  </TooltipTrigger>
59
57
  <TooltipContent side="right" align="center" className="max-w-xs break-words">
60
58
  {title}
@@ -414,7 +414,7 @@ export function ShowcaseDataDisplay() {
414
414
 
415
415
  <Item variant="outline" className="p-3">
416
416
  <ItemMedia variant="icon">
417
- <AlertTriangle className="size-4 text-amber-500" />
417
+ <AlertTriangle className="size-4 text-warning" />
418
418
  </ItemMedia>
419
419
  <ItemContent>
420
420
  <ItemTitle className="text-xs">หนังสือค้ำประกันผลงาน (Performance Bond)</ItemTitle>
@@ -444,7 +444,7 @@ export function ShowcaseDataDisplay() {
444
444
  <AttachmentGroup>
445
445
  <Attachment state="done" size="default">
446
446
  <AttachmentMedia variant="icon">
447
- <FileSpreadsheet className="size-4 text-emerald-600" />
447
+ <FileSpreadsheet className="size-4 text-success" />
448
448
  </AttachmentMedia>
449
449
  <AttachmentContent>
450
450
  <AttachmentTitle>BOQ_Facade_Rev2.xlsx</AttachmentTitle>
@@ -296,7 +296,7 @@ export function ApprovePackageButton({ id }: { id: string }) {
296
296
  <span className="font-bold">✕ หลีกเลี่ยง:</span>
297
297
  <span className="line-through">{item.bad}</span>
298
298
  </div>
299
- <div className="flex items-start gap-2 text-emerald-600 font-medium">
299
+ <div className="flex items-start gap-2 text-success font-medium">
300
300
  <span className="font-bold">✓ แนะนำ:</span>
301
301
  <span>{item.good}</span>
302
302
  </div>
@@ -252,11 +252,11 @@ export function ShowcaseOverlaysNavigation() {
252
252
  <div className="p-4 flex flex-col gap-2 text-xs">
253
253
  <div className="flex justify-between border-b pb-2">
254
254
  <span>ส่งมอบตรงเวลา (On-Time Delivery)</span>
255
- <span className="font-semibold text-emerald-600">96.5%</span>
255
+ <span className="font-semibold text-success">96.5%</span>
256
256
  </div>
257
257
  <div className="flex justify-between border-b pb-2">
258
258
  <span>มาตรฐานคุณภาพวัสดุ (Quality Pass Rate)</span>
259
- <span className="font-semibold text-emerald-600">98.2%</span>
259
+ <span className="font-semibold text-success">98.2%</span>
260
260
  </div>
261
261
  </div>
262
262
  <DrawerFooter>
@@ -91,7 +91,7 @@ export function ShowcasePrimitives() {
91
91
  label: "Pending Vendor / รอผู้ขาย",
92
92
  variant: "outline",
93
93
  icon: AlertCircle,
94
- className: "border-amber-500/40 text-amber-600 bg-amber-50/50 dark:bg-amber-950/20",
94
+ className: "border-warning/40 text-warning-muted-foreground bg-warning/10",
95
95
  },
96
96
  ];
97
97
 
@@ -68,7 +68,7 @@ export function FloatingSaveButton({
68
68
  className={cn(
69
69
  "h-14 gap-3 rounded-full border-4 border-background px-8 text-base shadow-2xl transition-all duration-300",
70
70
  showSuccess
71
- ? "scale-105 bg-emerald-500 text-white shadow-[0_0_20px_rgba(16,185,129,0.4)] hover:bg-emerald-500"
71
+ ? "scale-105 bg-success text-success-foreground shadow-2xl shadow-success/40 hover:bg-success"
72
72
  : "bg-primary text-primary-foreground hover:bg-primary/90",
73
73
  buttonClassName,
74
74
  )}
@@ -55,7 +55,7 @@ export function FormFieldColorSelect<T extends FieldValues>({
55
55
  render={({ field }) => (
56
56
  <FormItem>
57
57
  <FormLabel>
58
- {label} {props.required && <span className="text-red-500">*</span>}
58
+ {label} {props.required && <span className="text-destructive">*</span>}
59
59
  </FormLabel>
60
60
  <Select onValueChange={field.onChange} value={field.value} {...props}>
61
61
  <FormControl>
@@ -103,7 +103,7 @@ export function FormFieldCombobox<T extends FieldValues>({
103
103
  render={({ field }) => (
104
104
  <FormItem className={cn("flex flex-col gap-1.5", className)}>
105
105
  <FormLabel>
106
- {label} {required && <span className="text-red-500">*</span>}
106
+ {label} {required && <span className="text-destructive">*</span>}
107
107
  </FormLabel>
108
108
  <FormControl>
109
109
  <Popover open={open} onOpenChange={setOpen}>
@@ -36,7 +36,7 @@ export function FormFieldDate<T extends FieldValues>({
36
36
  render={({ field }) => (
37
37
  <FormItem className="flex flex-col">
38
38
  <FormLabel>
39
- {label} {required && <span className="text-red-500">*</span>}
39
+ {label} {required && <span className="text-destructive">*</span>}
40
40
  </FormLabel>
41
41
  <FormControl>
42
42
  <DatePicker
@@ -182,16 +182,16 @@ function UniqueCheckInput<T extends FieldValues>({
182
182
 
183
183
  return (
184
184
  <div>
185
- <InputGroup className={cn(isDuplicate ? "border-red-500 ring-red-500" : "")}>
185
+ <InputGroup className={cn(isDuplicate ? "border-destructive ring-destructive" : "")}>
186
186
  <InputGroupInput
187
187
  {...field}
188
188
  {...props}
189
189
  value={value || ""}
190
- className={cn(isDuplicate ? "text-red-600" : "")}
190
+ className={cn(isDuplicate ? "text-destructive" : "")}
191
191
  />
192
192
  {isDuplicate && (
193
193
  <InputGroupAddon>
194
- <CircleAlert className="h-4 w-4 text-red-500" />
194
+ <CircleAlert className="h-4 w-4 text-destructive" />
195
195
  </InputGroupAddon>
196
196
  )}
197
197
  {enableCopy && (
@@ -200,12 +200,12 @@ function UniqueCheckInput<T extends FieldValues>({
200
200
  type="button"
201
201
  title="Copy to clipboard"
202
202
  >
203
- {isCopied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
203
+ {isCopied ? <Check className="h-4 w-4 text-success" /> : <Copy className="h-4 w-4" />}
204
204
  </InputGroupButton>
205
205
  )}
206
206
  </InputGroup>
207
207
  {isDuplicate && (
208
- <p className="mt-1 font-medium text-[0.8rem] text-red-500">{uniqueErrorMessage}</p>
208
+ <p className="mt-1 font-medium text-[0.8rem] text-destructive">{uniqueErrorMessage}</p>
209
209
  )}
210
210
  </div>
211
211
  );
@@ -224,7 +224,7 @@ function CopyableInput(props: ComponentProps<typeof InputGroupInput>) {
224
224
  type="button"
225
225
  title="Copy to clipboard"
226
226
  >
227
- {isCopied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
227
+ {isCopied ? <Check className="h-4 w-4 text-success" /> : <Copy className="h-4 w-4" />}
228
228
  </InputGroupButton>
229
229
  </InputGroup>
230
230
  );
@@ -33,6 +33,8 @@ interface FormFieldRadioCardsProps<T extends FieldValues = any> {
33
33
  * `row` = การ์ดแนวนอน: ปุ่มวิทยุจริงซ้าย, ไอคอน+ชื่อบรรทัดบน, คำอธิบายบรรทัดล่าง (แบบ Pi Auth เลือกประเภทแอป)
34
34
  */
35
35
  layout?: "grid" | "row";
36
+ /** ชื่อกลุ่มสำหรับ screen reader เมื่อไม่แสดง `label` (ส่งต่อไป RadioGroup) */
37
+ "aria-label"?: string;
36
38
  }
37
39
 
38
40
  export function FormFieldRadioCards<T extends FieldValues = any>({
@@ -47,6 +49,7 @@ export function FormFieldRadioCards<T extends FieldValues = any>({
47
49
  className,
48
50
  size = "md",
49
51
  layout = "grid",
52
+ "aria-label": ariaLabel,
50
53
  }: FormFieldRadioCardsProps<T>) {
51
54
  const baseId = useId();
52
55
 
@@ -59,6 +62,7 @@ export function FormFieldRadioCards<T extends FieldValues = any>({
59
62
  {label && <Label className="block font-semibold text-base">{label}</Label>}
60
63
  <RadioGroup
61
64
  name={groupName}
65
+ aria-label={label ? undefined : ariaLabel}
62
66
  onValueChange={(val) => {
63
67
  if (val !== null) onChange(String(val));
64
68
  }}
@@ -21,11 +21,7 @@ export function FormMessage({
21
21
  return (
22
22
  <p
23
23
  role={state.ok ? "status" : "alert"}
24
- className={cn(
25
- "text-xs",
26
- state.ok ? "text-emerald-700 dark:text-emerald-300" : "text-destructive",
27
- className,
28
- )}
24
+ className={cn("text-xs", state.ok ? "text-success" : "text-destructive", className)}
29
25
  >
30
26
  {state.ok ? (state.message ?? text.success) : state.error}
31
27
  </p>
@@ -1,3 +1,4 @@
1
+ /* ui-audit-ok-file: โทน info/success/warning/danger ใช้ palette 50/900 คู่ dark โดยตั้งใจ — รอตัดสินใจ token ระดับ tone (todo) */
1
2
  import { Alert, AlertDescription, AlertTitle } from "@suphark/ui/components/ui/alert";
2
3
  import { cn } from "@suphark/ui/lib/utils";
3
4
  import type { ComponentProps, ReactNode } from "react";
@@ -1,3 +1,4 @@
1
+ /* ui-audit-ok-file: โทน badge ใช้ palette 50/800 คู่ dark โดยตั้งใจ — รอตัดสินใจ token ระดับ tone (todo) */
1
2
  import { Badge } from "@suphark/ui/components/ui/badge";
2
3
  import { cn } from "@suphark/ui/lib/utils";
3
4
  import type { ComponentProps } from "react";
@@ -56,21 +56,21 @@ export function ThemeToggle({ persistedTheme, onThemeChange }: ThemeToggleProps)
56
56
  onClick={() => handleThemeChange("light")}
57
57
  className="flex cursor-pointer items-center gap-2"
58
58
  >
59
- <Sun className="h-4 w-4 text-amber-500" />
59
+ <Sun className="h-4 w-4 text-warning" />
60
60
  <span>Light</span>
61
61
  </DropdownMenuItem>
62
62
  <DropdownMenuItem
63
63
  onClick={() => handleThemeChange("dark")}
64
64
  className="flex cursor-pointer items-center gap-2"
65
65
  >
66
- <Moon className="h-4 w-4 text-blue-500" />
66
+ <Moon className="h-4 w-4 text-primary" />
67
67
  <span>Dark</span>
68
68
  </DropdownMenuItem>
69
69
  <DropdownMenuItem
70
70
  onClick={() => handleThemeChange("system")}
71
71
  className="flex cursor-pointer items-center gap-2"
72
72
  >
73
- <Laptop className="h-4 w-4 text-slate-500" />
73
+ <Laptop className="h-4 w-4 text-muted-foreground" />
74
74
  <span>System</span>
75
75
  </DropdownMenuItem>
76
76
  </DropdownMenuContent>
@@ -1,3 +1,4 @@
1
+ /* ui-audit-ok-file: ชุดสีให้ผู้ใช้เลือก (ColorName) เป็น palette โดยนิยาม — ข้อยกเว้นที่จดไว้ใน docs/design-system.md */
1
2
  export type ColorName = "gray" | "red" | "yellow" | "green" | "blue" | "indigo" | "purple" | "pink";
2
3
 
3
4
  export type ColorTheme = {