@suphark/ui 0.31.0 → 0.33.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,34 @@
2
2
 
3
3
  รูปแบบตาม [Keep a Changelog](https://keepachangelog.com/) · semver: patch = หน้าตา/บั๊ก, minor = component/prop ใหม่, major = เปลี่ยน/ลบ props
4
4
 
5
+ ## [0.33.0] — 2026-10-06
6
+
7
+ ### Fixed
8
+
9
+ - **คอนทราสต์ไม่ผ่าน WCAG AA 3 จุด** (DueBell วัดจากหน้าจริงทั้งสองธีม — ผมคำนวณจาก token ซ้ำได้ตัวเลขตรงกันทุกจุด):
10
+ - `--info` ธีมสว่าง: `text-info` บนพื้นขาว **4.02:1** → ลดความสว่าง L 0.588 → 0.53 = **5.05:1** (ตัวหนังสือขาวบน `bg-info` ก็ขึ้นเป็น 5.05:1 เท่ากัน) — สีฟ้าเข้มขึ้นเล็กน้อย
11
+ - `--muted-foreground` ธีมสว่าง: บนพื้น `--muted` **4.35:1** (กระทบ `CountBadge tone="muted"`, แท็บที่ไม่ได้เลือกบน `TabsList`, ป้ายนับใน `SegmentedControl` …) → L 0.554 → 0.52 = **5.02:1** (บนขาว 5.4:1) — ตัวหนังสือรองเข้มขึ้นเล็กน้อยทั้งระบบ
12
+ - ป้ายนับ `CountBadge tone="destructive"` (รวมปุ่มแจ้งเตือนใน `AppShell`/`MobileTabBar`) ธีมมืด: ตัวหนังสือขาวบน `--destructive` **2.89:1** → token ใหม่ **`--destructive-foreground`** (สว่าง = ขาว 4.76:1, มืด = สีพื้นเข้ม **6.97:1**) — ไม่ลดความสว่างของ `--destructive` ธีมมืดเพราะ `text-destructive` บนพื้นมืดจะไม่ผ่านแทน
13
+ - ใช้ `text-destructive-foreground` คู่ `bg-destructive` ได้ทุกที่ (ใหม่ใน `@theme`)
14
+
15
+ ## [0.32.0] — 2026-10-06
16
+
17
+ > แก้บั๊กที่แอปรายงานหลัง 0.30.0 — เดิมเตรียมไว้ใน 0.31.0 แต่ 0.31.0 ถูก publish (12:53 น.) ก่อน commit ทั้งสามนี้ (12:56–13:09 น.) ตรวจ tarball แล้วไม่มี จึงออกเป็น 0.32.0
18
+
19
+ ### Fixed
20
+
21
+ - **`@suphark/ui/sortable`: แถวกลุ่มไม่ได้อยู่ใน `SortableContext`** (รายงานจาก Pi Config ตอนย้าย `values-table`) — `SortableGroup` สร้าง context ให้เฉพาะรายการในกลุ่ม ส่วนแถวกลุ่มที่ลงทะเบียนด้วย `useSortableRow({ type: "group" })` ไม่มี context ไหนรับ ทำให้ลากกลุ่มแล้วไม่มีการเลื่อนแถวให้เห็นระหว่างลาก (ปล่อยแล้วลำดับยังถูก) — ตอนนี้ `SortableProvider` สร้าง `SortableContext` ชั้นนอกจาก `order.groups` ให้เอง **ไม่ต้องแก้ call site และลำดับใน context หลุดจากลำดับจริงไม่ได้**
22
+
23
+ - **`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" }` (เทสต์ใหม่ครอบทั้งสองชั้น)
24
+
25
+ ### Added
26
+
27
+ - `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)
28
+
29
+ ### Changed
30
+
31
+ - `ActivityFeed`: รับ `locale` แล้วส่งต่อให้ตัวฟอร์แมตวันที่ (ไม่ส่ง = `th-TH` เหมือนเดิม) — เดิมวันที่เป็นไทยตายตัวสำหรับทุกคน แอปสองภาษาจึงแสดงผิดเมื่อผู้ใช้เลือกอังกฤษ (รายงานจาก Pi Auth ตอนทำหน้า audit log); `emptyLabel` ค่าเริ่มต้นย้ายไปอ่านจาก `UiText.activity.empty` (คีย์ใหม่)
32
+
5
33
  ## [0.31.0] — 2026-10-06
6
34
 
7
35
  > 0.30.0 ถูก publish ก่อน commit ของ `NotificationCenter` (ตรวจ tarball แล้วไม่มีไฟล์ `shared/notification-center.tsx`) จึงเลื่อนมาออกเป็น 0.31.0 — ของอื่นใน 0.30.0 อยู่ครบตามรายการข้างล่าง
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suphark/ui",
3
- "version": "0.31.0",
3
+ "version": "0.33.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",
@@ -310,6 +310,8 @@ export function SortableProvider({
310
310
  };
311
311
  }, [announcements, describe, text.sortable]);
312
312
 
313
+ const groupIds = useMemo(() => order.groups.map((id) => dndId("group", id)), [order.groups]);
314
+
313
315
  const state = useMemo<SortableState>(
314
316
  () => ({
315
317
  dragging,
@@ -333,7 +335,11 @@ export function SortableProvider({
333
335
  onDragEnd={onDragEnd}
334
336
  onDragCancel={onDragCancel}
335
337
  >
336
- {children}
338
+ {/* ชั้นนอก = ลำดับของกลุ่ม (provider รู้ `order.groups` อยู่แล้ว แอปจึงไม่ต้องสร้างเอง
339
+ และลำดับใน context หลุดจากลำดับจริงไม่ได้) · ชั้นใน = `SortableGroup` ของแต่ละกลุ่ม */}
340
+ <SortableContext items={groupIds} strategy={verticalListSortingStrategy}>
341
+ {children}
342
+ </SortableContext>
337
343
  </DndContext>
338
344
  </SortableStateContext.Provider>
339
345
  );
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);