@suphark/ui 0.31.0 → 0.34.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 CHANGED
@@ -2,6 +2,42 @@
2
2
 
3
3
  รูปแบบตาม [Keep a Changelog](https://keepachangelog.com/) · semver: patch = หน้าตา/บั๊ก, minor = component/prop ใหม่, major = เปลี่ยน/ลบ props
4
4
 
5
+ ## [0.34.0] — 2026-10-06
6
+
7
+ ### Fixed
8
+
9
+ - **`SortableProvider` ทำ hydration mismatch ทุกหน้าที่ SSR** (รายงานจาก Pi Config ตอนลากบนของจริง): ไม่ได้ส่ง `id` ให้ `DndContext` → dnd-kit สร้าง `aria-describedby="DndDescribedBy-N"` จากตัวนับที่เดินไม่ตรงกันระหว่าง server กับ client, React 19 ขึ้น error และ **ไม่ซ่อมให้** — attribute ค้างชี้ element ที่ไม่มีอยู่ ผู้ใช้ screen reader จึงไม่ได้ยินคำอธิบายวิธีลาก · ตอนนี้ค่าเริ่มต้นเป็น `useId()` (ตรงกันทั้งสองฝั่ง) และรับ prop **`id`** สำหรับ id ที่สื่อความหมาย — กระทบทุกแอปที่ใช้ `@suphark/ui/sortable` ใน Next.js
10
+ - `@suphark/ui/sortable`: เสียงประกาศระหว่างลากในกลุ่มเดียวกันบอกตำแหน่ง **เดิม** ซ้ำ ("ย้ายไปตำแหน่งที่ 1 จาก 2" ทั้งที่เลื่อนลงแล้ว) เพราะลำดับจริงเปลี่ยนตอนปล่อย — ตอนนี้ระหว่างลากประกาศตำแหน่งของเป้าหมาย และตอนปล่อยประกาศตำแหน่งจริงหลังย้าย (เจอระหว่างทดสอบลากด้วยคีย์บอร์ดตามที่ Pi Config ถาม; ลากด้วยคีย์บอร์ดเองทำงานถูกอยู่แล้ว)
11
+ - showcase: `/components/sortable-list` ย้ายแถว "ลากมาที่นี่เพื่อเอาออกจากกลุ่ม" ไปท้ายตาราง — เดิมโผล่เหนือกลุ่มตอนเริ่มลาก ดันแถวที่กำลังลากลง จนลากคีย์ในกลุ่มด้วยคีย์บอร์ดไม่ขยับ (README เพิ่มคำเตือนนี้ + เรื่อง `id`)
12
+
13
+ ## [0.33.0] — 2026-10-06
14
+
15
+ ### Fixed
16
+
17
+ - **คอนทราสต์ไม่ผ่าน WCAG AA 3 จุด** (DueBell วัดจากหน้าจริงทั้งสองธีม — ผมคำนวณจาก token ซ้ำได้ตัวเลขตรงกันทุกจุด):
18
+ - `--info` ธีมสว่าง: `text-info` บนพื้นขาว **4.02:1** → ลดความสว่าง L 0.588 → 0.53 = **5.05:1** (ตัวหนังสือขาวบน `bg-info` ก็ขึ้นเป็น 5.05:1 เท่ากัน) — สีฟ้าเข้มขึ้นเล็กน้อย
19
+ - `--muted-foreground` ธีมสว่าง: บนพื้น `--muted` **4.35:1** (กระทบ `CountBadge tone="muted"`, แท็บที่ไม่ได้เลือกบน `TabsList`, ป้ายนับใน `SegmentedControl` …) → L 0.554 → 0.52 = **5.02:1** (บนขาว 5.4:1) — ตัวหนังสือรองเข้มขึ้นเล็กน้อยทั้งระบบ
20
+ - ป้ายนับ `CountBadge tone="destructive"` (รวมปุ่มแจ้งเตือนใน `AppShell`/`MobileTabBar`) ธีมมืด: ตัวหนังสือขาวบน `--destructive` **2.89:1** → token ใหม่ **`--destructive-foreground`** (สว่าง = ขาว 4.76:1, มืด = สีพื้นเข้ม **6.97:1**) — ไม่ลดความสว่างของ `--destructive` ธีมมืดเพราะ `text-destructive` บนพื้นมืดจะไม่ผ่านแทน
21
+ - ใช้ `text-destructive-foreground` คู่ `bg-destructive` ได้ทุกที่ (ใหม่ใน `@theme`)
22
+
23
+ ## [0.32.0] — 2026-10-06
24
+
25
+ > แก้บั๊กที่แอปรายงานหลัง 0.30.0 — เดิมเตรียมไว้ใน 0.31.0 แต่ 0.31.0 ถูก publish (12:53 น.) ก่อน commit ทั้งสามนี้ (12:56–13:09 น.) ตรวจ tarball แล้วไม่มี จึงออกเป็น 0.32.0
26
+
27
+ ### Fixed
28
+
29
+ - **`@suphark/ui/sortable`: แถวกลุ่มไม่ได้อยู่ใน `SortableContext`** (รายงานจาก Pi Config ตอนย้าย `values-table`) — `SortableGroup` สร้าง context ให้เฉพาะรายการในกลุ่ม ส่วนแถวกลุ่มที่ลงทะเบียนด้วย `useSortableRow({ type: "group" })` ไม่มี context ไหนรับ ทำให้ลากกลุ่มแล้วไม่มีการเลื่อนแถวให้เห็นระหว่างลาก (ปล่อยแล้วลำดับยังถูก) — ตอนนี้ `SortableProvider` สร้าง `SortableContext` ชั้นนอกจาก `order.groups` ให้เอง **ไม่ต้องแก้ call site และลำดับใน context หลุดจากลำดับจริงไม่ได้**
30
+
31
+ - **`ActivityFeed relative={false}` พังทุกแถว** (รายงานจาก Pi Auth): วันที่กลายเป็น `-` และ console เต็มไปด้วย `TypeError: Invalid option` เพราะส่ง `timeStyle` ปนกับ `day`/`month`/`year` ซึ่ง `Intl.DateTimeFormat` ไม่ยอม — ต้นเหตุจริงอยู่ที่ **`ClientDateFormatter` เอา default (`day`/`month`/`year`) ไปผสมกับ options ของผู้เรียกเสมอ** ทำให้ทุกแอปที่ส่ง `dateStyle`/`timeStyle` พังเหมือนกัน; ตอนนี้ถ้าผู้เรียกใช้ `dateStyle`/`timeStyle` จะไม่ผสม default ให้ และ `ActivityFeed` เปลี่ยนไปใช้ `{ dateStyle: "medium", timeStyle: "short" }` (เทสต์ใหม่ครอบทั้งสองชั้น)
32
+
33
+ ### Added
34
+
35
+ - `prepublishOnly` เรียก `scripts/release-check.mjs` ก่อน publish: working tree ต้องสะอาด, HEAD ต้องตรงกับ `origin/main` หลัง fetch และ CHANGELOG ต้องมีหัวข้อของเวอร์ชันนั้น — กันปล่อยรุ่นที่ขาด commit (เกิดมาแล้ว 3 ครั้ง: 0.17.0, 0.30.0, 0.31.0; ข้อเสนอจาก Pi Auth/Pi Config)
36
+
37
+ ### Changed
38
+
39
+ - `ActivityFeed`: รับ `locale` แล้วส่งต่อให้ตัวฟอร์แมตวันที่ (ไม่ส่ง = `th-TH` เหมือนเดิม) — เดิมวันที่เป็นไทยตายตัวสำหรับทุกคน แอปสองภาษาจึงแสดงผิดเมื่อผู้ใช้เลือกอังกฤษ (รายงานจาก Pi Auth ตอนทำหน้า audit log); `emptyLabel` ค่าเริ่มต้นย้ายไปอ่านจาก `UiText.activity.empty` (คีย์ใหม่)
40
+
5
41
  ## [0.31.0] — 2026-10-06
6
42
 
7
43
  > 0.30.0 ถูก publish ก่อน commit ของ `NotificationCenter` (ตรวจ tarball แล้วไม่มีไฟล์ `shared/notification-center.tsx`) จึงเลื่อนมาออกเป็น 0.31.0 — ของอื่นใน 0.30.0 อยู่ครบตามรายการข้างล่าง
package/README.md CHANGED
@@ -217,7 +217,8 @@ const [order, setOrder] = useState(() =>
217
217
  </SortableProvider>
218
218
  ```
219
219
 
220
- - กลุ่มที่ไม่มีหัวกลุ่ม (เช่น "ยังไม่จัดกลุ่ม") ใช้ `useSortableDropZone(groupId)` แปะกับแถว placeholder ของตัวเอง และดูว่าควรแสดงเมื่อไหร่จาก `useSortableState().dragging`
220
+ - กลุ่มที่ไม่มีหัวกลุ่ม (เช่น "ยังไม่จัดกลุ่ม") ใช้ `useSortableDropZone(groupId)` แปะกับแถว placeholder ของตัวเอง และดูว่าควรแสดงเมื่อไหร่จาก `useSortableState().dragging` — **วางแถวนี้ไว้ใต้แถวที่ลากได้ทั้งหมด (ท้ายตาราง)** ถ้ามันโผล่ขึ้นเหนือแถวที่กำลังลาก layout จะเลื่อนตอนเริ่มลาก แล้วการลากด้วยคีย์บอร์ดจะไม่ขยับ
221
+ - ตั้ง `id` ของ `SortableProvider` ได้ (ไม่ตั้ง = `useId()`) — ต้องคงที่ระหว่าง SSR กับ client ไม่งั้น `aria-describedby` ของ dnd-kit จะ hydration mismatch
221
222
  - `onCommit` ล้มเหลว (throw หรือ `{ ok: false, error }`) = คืนลำดับเดิม + toast; ถ้าอยากจัดการ error เองใส่ `onCommitError`
222
223
  - `sortableOrderToPayload(order)` คืน `{ groups, items }` — `groups` คือลำดับของกลุ่ม, `items` คือรายการในแต่ละกลุ่มเรียงตามนั้น (ของที่ยังไม่มีกลุ่มมาก่อนด้วย `groupId: null`)
223
224
  - `SortableProvider disabled` (เช่นระหว่างกรอง ซึ่งลำดับที่เห็นไม่ใช่ลำดับจริง) ทำให้ `DragHandle` จางลงและกดไม่ได้ — ไม่อยากให้เห็นเลยใส่ `hideWhenDisabled`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suphark/ui",
3
- "version": "0.31.0",
3
+ "version": "0.34.0",
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 เอง",
@@ -64,7 +64,7 @@
64
64
  "check:typography": "node scripts/audit-tailwind-typography.mjs",
65
65
  "verify": "pnpm typecheck && pnpm check:barrel && pnpm lint:base-ui && pnpm check:typography && pnpm test",
66
66
  "check:barrel": "node ../../scripts/barrel-exports.mjs",
67
- "prepublishOnly": "node ../../scripts/barrel-exports.mjs && tsc --noEmit"
67
+ "prepublishOnly": "node ../../scripts/release-check.mjs && node ../../scripts/barrel-exports.mjs && tsc --noEmit"
68
68
  },
69
69
  "dependencies": {
70
70
  "@dnd-kit/core": "^6.3.1",
@@ -60,7 +60,7 @@ export function ShowcaseFoundations() {
60
60
  },
61
61
  {
62
62
  name: "Destructive",
63
- class: "bg-destructive text-white",
63
+ class: "bg-destructive text-destructive-foreground",
64
64
  token: "var(--destructive)",
65
65
  tailwind: "bg-destructive / text-destructive",
66
66
  usage: "Irreversible actions, Delete confirmation, Error callouts",
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { Initials } from "@suphark/ui/components/shared/initials";
4
4
  import { ClientDateFormatter } from "@suphark/ui/components/shared/time/client-date-formatter";
5
+ import { useUiText } from "@suphark/ui/components/shared/ui-text";
5
6
  import { cn } from "@suphark/ui/lib/utils";
6
7
  import type { ReactNode } from "react";
7
8
 
@@ -43,20 +44,27 @@ const DOT_TONE: Record<ActivityTone, string> = {
43
44
  export function ActivityFeed({
44
45
  items,
45
46
  relative = true,
47
+ locale,
46
48
  emptyLabel,
47
49
  className,
48
50
  }: {
49
51
  items: ReadonlyArray<ActivityItem>;
50
52
  /** true = "5 นาทีที่แล้ว" (ค่าเริ่มต้น) · false = วันที่/เวลาเต็ม */
51
53
  relative?: boolean;
52
- /** ไม่มีรายการ (ค่าเริ่มต้น "ยังไม่มีรายการ") */
54
+ /**
55
+ * ภาษาของวันที่ เช่น `"en-US"` หรือ `"th-TH-u-ca-gregory"` — ไม่ส่ง = `th-TH` (พ.ศ.)
56
+ * แอปสองภาษาต้องส่งตาม locale ของผู้ใช้ ไม่งั้นวันที่จะเป็นไทยสำหรับทุกคน
57
+ */
58
+ locale?: string;
59
+ /** ไม่มีรายการ (ไม่ส่ง = `UiText.activity.empty`) */
53
60
  emptyLabel?: ReactNode;
54
61
  className?: string;
55
62
  }) {
63
+ const text = useUiText();
56
64
  if (items.length === 0) {
57
65
  return (
58
66
  <p className={cn("py-6 text-center text-muted-foreground text-sm", className)}>
59
- {emptyLabel ?? "ยังไม่มีรายการ"}
67
+ {emptyLabel ?? text.activity?.empty ?? "ยังไม่มีรายการ"}
60
68
  </p>
61
69
  );
62
70
  }
@@ -95,12 +103,10 @@ export function ActivityFeed({
95
103
  )}
96
104
  <ClientDateFormatter
97
105
  date={item.at}
106
+ locale={locale}
98
107
  formatType={relative ? "relative" : "standard"}
99
- options={
100
- relative
101
- ? undefined
102
- : { day: "numeric", month: "short", year: "numeric", timeStyle: "short" }
103
- }
108
+ // Intl ห้ามใช้ dateStyle/timeStyle ปนกับ field รายตัว (day/month/year) — โยน TypeError
109
+ options={relative ? undefined : { dateStyle: "medium", timeStyle: "short" }}
104
110
  className="text-muted-foreground text-xs"
105
111
  />
106
112
  </div>
@@ -29,7 +29,7 @@ export function CountBadge({
29
29
  tone === "primary"
30
30
  ? "bg-primary text-primary-foreground"
31
31
  : tone === "destructive"
32
- ? "bg-destructive text-white"
32
+ ? "bg-destructive text-destructive-foreground"
33
33
  : "bg-muted text-muted-foreground",
34
34
  className,
35
35
  )}
@@ -66,10 +66,14 @@ export function ClientDateFormatter({
66
66
  if (formatType === "relative") {
67
67
  return formatDistance(dateObj, now, { addSuffix: true, locale: getLocaleObject(locale) });
68
68
  }
69
+ const user: Intl.DateTimeFormatOptions = optionsKey ? JSON.parse(optionsKey) : {};
70
+ // Intl ห้าม dateStyle/timeStyle ปนกับ field รายตัว (day/month/year/hour…) — โยน TypeError
71
+ // ผู้เรียกที่ใช้สไตล์จึงต้องไม่ถูกยัด DEFAULT_OPTIONS (day/month/year) เข้าไปด้วย
72
+ const usesStyle = user.dateStyle !== undefined || user.timeStyle !== undefined;
69
73
  const opts: Intl.DateTimeFormatOptions = {
70
- ...DEFAULT_OPTIONS,
74
+ ...(usesStyle ? {} : DEFAULT_OPTIONS),
71
75
  timeZone,
72
- ...(optionsKey ? (JSON.parse(optionsKey) as Intl.DateTimeFormatOptions) : {}),
76
+ ...user,
73
77
  };
74
78
  return getDateTimeFormat(locale, opts).format(dateObj);
75
79
  } catch (error) {
@@ -65,6 +65,8 @@ export interface UiText {
65
65
  }>;
66
66
  /** ErrorState เซสชันหมดอายุ (optional) */
67
67
  sessionExpired?: Partial<{ title: string; description: string; signIn: string }>;
68
+ /** ActivityFeed (optional) */
69
+ activity?: Partial<{ empty: string }>;
68
70
  /** NotificationCenter (optional) */
69
71
  notifications?: Partial<{ title: string; empty: string; viewAll: string }>;
70
72
  /** SelectionBar (optional) — `selected` ใช้ `{count}` */
@@ -239,6 +241,7 @@ export const UI_TEXT_TH: UiText = {
239
241
  "คุณถูกออกจากระบบระหว่างใช้งาน ข้อมูลที่ยังไม่ได้บันทึกจะหายไป กรุณาเข้าสู่ระบบอีกครั้ง แล้วระบบจะพากลับมาหน้านี้",
240
242
  signIn: "เข้าสู่ระบบ",
241
243
  },
244
+ activity: { empty: "ยังไม่มีรายการ" },
242
245
  notifications: {
243
246
  title: "การแจ้งเตือน",
244
247
  empty: "ไม่มีรายการที่ต้องจัดการ",
@@ -403,6 +406,7 @@ export const UI_TEXT_EN: UiText = {
403
406
  "You were signed out while working. Unsaved changes are lost — sign in again and we will bring you back to this page.",
404
407
  signIn: "Sign in",
405
408
  },
409
+ activity: { empty: "No activity yet" },
406
410
  notifications: {
407
411
  title: "Notifications",
408
412
  empty: "Nothing needs your attention",
@@ -41,6 +41,7 @@ import {
41
41
  type ReactNode,
42
42
  useCallback,
43
43
  useContext,
44
+ useId,
44
45
  useMemo,
45
46
  useRef,
46
47
  useState,
@@ -131,6 +132,13 @@ export interface SortableProviderProps {
131
132
  * ไม่ส่ง = แม่แบบจาก `UiText.sortable` (`{index}` / `{total}`)
132
133
  */
133
134
  announcements?: SortableAnnouncements;
135
+ /**
136
+ * id ของ `DndContext` — ต้องคงที่ระหว่าง SSR กับ client ไม่งั้น `aria-describedby="DndDescribedBy-N"`
137
+ * ที่ dnd-kit สร้างจากตัวนับจะไม่ตรงกัน (hydration mismatch ที่ React ไม่ซ่อมให้ → screen reader
138
+ * ไม่ได้ยินคำอธิบายวิธีลาก) · ไม่ส่ง = `useId()` ซึ่งตรงกันทั้งสองฝั่งอยู่แล้ว · ส่ง id ที่สื่อความหมาย
139
+ * (เช่น `values-${environmentId}`) ช่วยตอนไล่ปัญหา
140
+ */
141
+ id?: string;
134
142
  children: ReactNode;
135
143
  }
136
144
 
@@ -152,8 +160,10 @@ export function SortableProvider({
152
160
  onCommitError,
153
161
  screenReaderInstructions,
154
162
  announcements,
163
+ id,
155
164
  children,
156
165
  }: SortableProviderProps) {
166
+ const fallbackId = useId();
157
167
  const text = useUiText();
158
168
  const [dragging, setDragging] = useState<SortableKind | null>(null);
159
169
  const beforeDrag = useRef<SortableOrder | null>(null);
@@ -271,6 +281,31 @@ export function SortableProvider({
271
281
  };
272
282
  }, []);
273
283
 
284
+ /**
285
+ * ตำแหน่งที่ "จะไปวาง" ระหว่างลาก — ในกลุ่มเดียวกันลำดับจริงยังไม่เปลี่ยนจนกว่าจะปล่อย
286
+ * (เดิมประกาศตำแหน่งเดิมซ้ำ เช่น "ตำแหน่งที่ 1" ทั้งที่เลื่อนลงมาแล้ว) จึงใช้ตำแหน่งของเป้าหมายแทน
287
+ * ข้ามกลุ่มไม่ต้องทำอะไร — onDragOver ย้ายเข้ากลุ่มใหม่ใน order ไปแล้ว
288
+ */
289
+ const describeTarget = useCallback(
290
+ (activeId: UniqueIdentifier, overId: UniqueIdentifier | null): SortableAnnouncementContext => {
291
+ const base = describe(activeId);
292
+ if (overId === null || overId === activeId) return base;
293
+ const current = latest.current;
294
+ if (base.kind === "group" && kindOf(overId) === "g") {
295
+ const at = current.groups.indexOf(rawOf(overId));
296
+ return at >= 0 ? { ...base, index: at + 1 } : base;
297
+ }
298
+ if (base.kind === "item" && kindOf(overId) === "i") {
299
+ const group = groupOfItem(current, rawOf(overId));
300
+ if (!group || group !== base.groupId) return base;
301
+ const at = (current.items[group] ?? []).indexOf(rawOf(overId));
302
+ return at >= 0 ? { ...base, index: at + 1 } : base;
303
+ }
304
+ return base;
305
+ },
306
+ [describe],
307
+ );
308
+
274
309
  const dndAnnouncements = useMemo(() => {
275
310
  const fill = (
276
311
  template: string | undefined,
@@ -285,9 +320,19 @@ export function SortableProvider({
285
320
  own: ((context: SortableAnnouncementContext) => string | undefined) | undefined,
286
321
  template: string | undefined,
287
322
  fallback: string,
323
+ // ระหว่างลาก = ตำแหน่งของเป้าหมาย; ตอนปล่อย order อัปเดตแล้ว จึงใช้ตำแหน่งจริงของตัวที่ลาก
324
+ useTarget = false,
288
325
  ) =>
289
- ({ active }: { active: { id: UniqueIdentifier } }) => {
290
- const context = describe(active.id);
326
+ ({
327
+ active,
328
+ over,
329
+ }: {
330
+ active: { id: UniqueIdentifier };
331
+ over?: { id: UniqueIdentifier } | null;
332
+ }) => {
333
+ const context = useTarget
334
+ ? describeTarget(active.id, over?.id ?? null)
335
+ : describe(active.id);
291
336
  return own?.(context) ?? fill(template, fallback, context);
292
337
  };
293
338
  return {
@@ -300,6 +345,7 @@ export function SortableProvider({
300
345
  announcements?.onDragOver,
301
346
  text.sortable?.moved,
302
347
  "ย้ายไปตำแหน่งที่ {index} จาก {total}",
348
+ true,
303
349
  ),
304
350
  onDragEnd: announce(
305
351
  announcements?.onDragEnd,
@@ -308,7 +354,9 @@ export function SortableProvider({
308
354
  ),
309
355
  onDragCancel: announce(announcements?.onDragCancel, text.sortable?.cancelled, "ยกเลิกการลาก"),
310
356
  };
311
- }, [announcements, describe, text.sortable]);
357
+ }, [announcements, describeTarget, text.sortable]);
358
+
359
+ const groupIds = useMemo(() => order.groups.map((id) => dndId("group", id)), [order.groups]);
312
360
 
313
361
  const state = useMemo<SortableState>(
314
362
  () => ({
@@ -322,6 +370,7 @@ export function SortableProvider({
322
370
  return (
323
371
  <SortableStateContext.Provider value={state}>
324
372
  <DndContext
373
+ id={id ?? fallbackId}
325
374
  sensors={sensors}
326
375
  collisionDetection={collision}
327
376
  accessibility={{
@@ -333,7 +382,11 @@ export function SortableProvider({
333
382
  onDragEnd={onDragEnd}
334
383
  onDragCancel={onDragCancel}
335
384
  >
336
- {children}
385
+ {/* ชั้นนอก = ลำดับของกลุ่ม (provider รู้ `order.groups` อยู่แล้ว แอปจึงไม่ต้องสร้างเอง
386
+ และลำดับใน context หลุดจากลำดับจริงไม่ได้) · ชั้นใน = `SortableGroup` ของแต่ละกลุ่ม */}
387
+ <SortableContext items={groupIds} strategy={verticalListSortingStrategy}>
388
+ {children}
389
+ </SortableContext>
337
390
  </DndContext>
338
391
  </SortableStateContext.Provider>
339
392
  );
package/src/styles.css CHANGED
@@ -45,6 +45,7 @@
45
45
  --color-input: var(--input);
46
46
  --color-border: var(--border);
47
47
  --color-destructive: var(--destructive);
48
+ --color-destructive-foreground: var(--destructive-foreground);
48
49
  --color-success: var(--success);
49
50
  --color-success-foreground: var(--success-foreground);
50
51
  --color-warning: var(--warning);
@@ -148,17 +149,21 @@
148
149
  --secondary: oklch(0.968 0.007 247.896);
149
150
  --secondary-foreground: oklch(0.208 0.042 265.755);
150
151
  --muted: oklch(0.968 0.007 247.896);
151
- --muted-foreground: oklch(0.554 0.046 257.417);
152
+ /* 0.554 → 0.52: บนพื้น --muted (แท็บ, ป้ายนับ, ช่อง disabled) เดิมได้ 4.35:1 ไม่ผ่าน AA → 5.02:1 (บนขาว 5.4:1) */
153
+ --muted-foreground: oklch(0.52 0.046 257.417);
152
154
  --accent: oklch(0.968 0.007 247.896);
153
155
  --accent-foreground: oklch(0.208 0.042 265.755);
154
156
  --destructive: oklch(0.577 0.245 27.325);
157
+ /* ตัวหนังสือบนพื้น --destructive (ป้ายนับแจ้งเตือน) — ขาว 4.76:1 */
158
+ --destructive-foreground: oklch(1 0 0);
155
159
  --success: oklch(0.527 0.154 150.069);
156
160
  --success-foreground: oklch(0.985 0.002 247.839);
157
161
  --warning: oklch(0.852 0.199 91.936);
158
162
  --warning-foreground: oklch(0.286 0.066 53.813);
159
163
  --warning-muted-foreground: oklch(0.421 0.095 57.708);
160
164
  /* โทน: ค่าเท่า Tailwind sky/emerald/amber/red ที่ Notice/StatusBadge เคยใช้ตรง ๆ (50 พื้น · 200 ขอบ · 800 ตัวอักษร) */
161
- --info: oklch(0.588 0.158 241.966);
165
+ /* 0.588 → 0.53: `text-info` บนขาวเดิม 4.02:1 ไม่ผ่าน AA → 5.05:1 (ตัวหนังสือขาวบน bg-info ก็ผ่านเท่ากัน) */
166
+ --info: oklch(0.53 0.158 241.966);
162
167
  --info-foreground: oklch(0.984 0.003 247.858);
163
168
  --info-subtle: oklch(0.977 0.013 236.62);
164
169
  --info-subtle-foreground: oklch(0.443 0.11 240.79);
@@ -208,6 +213,9 @@
208
213
  --accent: oklch(0.279 0.041 260.031);
209
214
  --accent-foreground: oklch(0.984 0.003 247.858);
210
215
  --destructive: oklch(0.704 0.191 22.216);
216
+ /* ขาวบน --destructive ธีมมืด = 2.89:1 ไม่ผ่าน — ใช้สีพื้นหลังมืดเป็นตัวหนังสือ = 6.97:1
217
+ (ไม่ลดความสว่างของ --destructive เพราะ text-destructive บนพื้นมืดจะไม่ผ่านแทน) */
218
+ --destructive-foreground: oklch(0.129 0.042 264.695);
211
219
  --success: oklch(0.627 0.194 149.214);
212
220
  --success-foreground: oklch(0.145 0.028 147.727);
213
221
  --warning: oklch(0.852 0.199 91.936);