UX audit ทั้งแอป + typography audit 2026-09-16: toast แจ้ง error การบันทึก (H1 — เดิมรายการหายเงียบ + ReplyBox เก็บ draft ตอน fail), คำเตือน redaction แสดงทุกขนาดจอ (H2), toolbar แสดงชื่อ tool active + aria-pressed บนจอสัมผัส (H3/M9), ปุ่ม popover จิ้มง่าย 36px + focus ring เติมครบ (H4) + M1-M9 (ปิด sidebar ตอน navigate, bookmark หัวแถวจอเล็ก, hint โหมดมาร์กบนจอสัมผัส, การ์ดหน้ารายการคลิกได้ทั้งใบ, เช็ค 100MB ฝั่ง client, feedback ✓/pending ตอนบันทึก bookmark, dropdown วางที่ sort ตามหน้า+เลขหน้า, slate-500 แทน slate-400) + webfont Sarabun ผ่าน next/font self-host 400/600 (Option B — แสดงผลตรงกันทุกอุปกรณ์ รวม TEXTBOX ที่ผูก stack นี้) + NumberCombo preset เป็น chips แทน dropdown ล้นเมนู (L2) + leading-relaxed/13px ข้อความไทยที่ wrap + tabular-nums + prefers-reduced-motion (L7) + L1/L4/L5 + แถว bookmark แสดงชื่อผู้บันทึกเหมือนผลการค้นหา

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-09-16 14:28:53 +07:00
parent a020e96bda
commit 21b9958600
10 changed files with 351 additions and 158 deletions

View file

@ -84,17 +84,17 @@ export default async function SummaryPage({
<span className="block text-xs">
<span className="font-medium">{annotationTypeLabel(a)}</span>
{a.type === "COMMENT" && a.resolved && (
<span className="ml-2 rounded bg-emerald-50 px-1.5 py-0.5 text-[10px] text-emerald-600">
<span className="ml-2 rounded bg-emerald-50 px-1.5 py-0.5 text-[11px] text-emerald-600">
จัดการแล้ว
</span>
)}
<span className="ml-2 text-slate-400">{a.userName}</span>
</span>
<span className="mt-0.5 block whitespace-pre-wrap break-words text-sm">
<span className="mt-0.5 block whitespace-pre-wrap break-words text-sm leading-relaxed">
{annotationPreviewText(a)}
</span>
{annotationNote(a) && (
<span className="mt-0.5 block whitespace-pre-wrap break-words text-xs text-slate-500">
<span className="mt-0.5 block whitespace-pre-wrap break-words text-xs leading-relaxed text-slate-500">
โน้ต: {annotationNote(a)}
</span>
)}

View file

@ -108,10 +108,11 @@
/* NumberCombo (ความหนา/ความทึบ/ขนาด): ▲▼ ซ้อนแนวตั้ง 2 ปุ่ม — ถ้าโดน min 36px
คอลัมน์จะสูง 72px และ input (items-stretch) ถูกดึงบวมตามทั้งแถว — ปุ่ม ▲▼
จึงหลุดจาก rule ด้านบน (combo-step) แล้วตั้งความสูงแถวเองที่ 36px แทน:
แตะ input = เปิด preset list (แถวละ 36px จาก rule ด้านบน) คือ touch path
หลัก ส่วน ▲▼ (ครึ่งแถว) เป็น fine-tune สำรอง — ขนาดใกล้ desktop ไม่บวม */
[data-tool-menu] .number-combo > div {
จึงหลุดจาก rule ด้านบน (combo-step) แล้วตั้งความสูงแถว input เองที่ 36px
แทน (:first-child — แถว chips preset ของ L2 โดน rule ปุ่ม 36px ของเมนู
ครอบอยู่แล้ว ห้ามบังคับสูงซ้ำ): แตะ chips preset = touch path หลัก
ส่วน ▲▼ (ครึ่งแถว) เป็น fine-tune สำรอง — ขนาดใกล้ desktop ไม่บวม */
[data-tool-menu] .number-combo > div:first-child {
height: 2.25rem;
}
[data-tool-menu] .combo-step {
@ -197,14 +198,16 @@
}
}
/* UX review 2026-09-14 H2: แอปไม่เคยตั้ง font-family เลย ข้อความไทยเลยตกไปที่
default ของ OS แต่ละเครื่อง — วางฟอนต์ไทยนำหน้า stack (ไม่โหลด webfont
ตามที่เลือกไว้: Leelawadee UI = Windows, Noto Sans Thai = Android/Linux,
Thonburi = macOS) */
/* UX review 2026-09-14 H2 → typography audit 2026-09-16 (T1 + Option B): webfont
Sarabun (next/font จาก layout.tsx — variable มี adjusted fallback ของ next/font
ในตัวแล้ว) นำหน้า stack เพื่อแสดงผลเหมือนกันทุกอุปกรณ์; ที่เหลือเป็น fallback
ตอนฟอนต์ยังไม่ถึง (display:swap)/โหลดไม่ได้ — เรียงแบบ Latin-first ตาม T1:
Latin/ตัวเลขได้ UI font ของ OS (Segoe UI/SF) ส่วนไทยได้ Leelawadee UI
(Windows) / Thonburi (iPad) / Noto Sans Thai (Android/Linux) */
body {
font-family: "Leelawadee UI", "Noto Sans Thai", "Thonburi",
ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
font-family: var(--font-sarabun, "Sarabun"), -apple-system, "Segoe UI",
"Helvetica Neue", Arial, "Leelawadee UI", "Thonburi", "Noto Sans Thai",
sans-serif;
}
/* M6: touch hygiene — ตัดแถบเทากระพริบของ iOS ทุกครั้งที่แตะปุ่ม และกัน
@ -249,3 +252,18 @@ header {
.comment-composer {
padding-bottom: env(safe-area-inset-bottom);
}
/* UX audit 2026-09-16 L7: เคารพ prefers-reduced-motion — ตัด transition ของ
sidebar, animation กระพริบของปุ่ม export และ smooth scroll ทุกจุด
(scrollToBelowHeader ฝั่ง JS เช็ค media query เดียวกันตอนเลือก behavior) */
@media (prefers-reduced-motion: reduce) {
aside[data-sidebar] {
transition: none;
}
.animate-pulse {
animation: none;
}
* {
scroll-behavior: auto !important;
}
}

View file

@ -1,7 +1,21 @@
import type { Metadata } from "next";
import { Sarabun } from "next/font/google";
import "./globals.css";
/** typography audit 2026-09-16 (Option B): Sarabun — ฟอนต์ตระกูลมาตรฐานเอกสาร
* ราชการไทย (TH Sarabun) เจ้าหน้าที่คุ้นเคยที่สุด / next/font self-host ตอน build:
* ไฟล์ฟอนต์ serve จาก origin เดียวกับแอป ไม่พึ่ง Google ตอนรัน จึงใช้ใน intranet
* ได้ (แต่เครื่อง build ต้องเห็นอินเทอร์เน็ต) — โหลดเฉพาะ 2 weights ตามแผน
* ~100-160KB แลกกับการแสดงผลเหมือนกันทุกอุปกรณ์ รวมกล่อง TEXTBOX ที่ผูก
* stack นี้ (T1-C: wrap ข้อความตรงกันข้ามอุปกรณ์จริง) */
const sarabun = Sarabun({
subsets: ["thai", "latin"],
weight: ["400", "600"],
display: "swap",
variable: "--font-sarabun",
});
export const metadata: Metadata = {
title: "Judge's Note",
description: "ระบบ PDF Annotation สำหรับ e-Court",
@ -13,7 +27,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<html lang="th">
<html lang="th" className={sarabun.variable}>
<body className="bg-slate-100 text-slate-900 antialiased">{children}</body>
</html>
);

View file

@ -1,6 +1,6 @@
import Link from "next/link";
import { asc } from "drizzle-orm";
import { FileImage, FileText } from "lucide-react";
import { ChevronRight, FileImage, FileText } from "lucide-react";
import { db } from "@/db";
import { files } from "@/db/schema";
@ -48,7 +48,7 @@ export default async function HomePage() {
<div className="mt-8 flex items-baseline justify-between">
<h2 className="text-sm font-semibold text-slate-700">เอกสาร</h2>
{rows.length > 0 && (
<span className="text-xs text-slate-400">{rows.length} ไฟล์</span>
<span className="text-xs tabular-nums text-slate-500">{rows.length} ไฟล์</span>
)}
</div>
<UploadFileForm />
@ -59,7 +59,7 @@ export default async function HomePage() {
<p className="mt-3 text-sm font-medium text-slate-500">
ยังไม่มีไฟล์ในระบบ
</p>
<p className="mt-1 text-xs text-slate-400">
<p className="mt-1 text-xs text-slate-500">
รัน /sync-db เพื่อ seed ไฟล์ตัวอย่าง
</p>
</div>
@ -68,7 +68,7 @@ export default async function HomePage() {
{rows.map((f) => (
<li
key={f.id}
className="group flex items-center gap-3 rounded-xl border border-slate-200 bg-white p-4 shadow-sm transition hover:border-primary/30 hover:shadow-md"
className="group relative flex items-center gap-3 rounded-xl border border-slate-200 bg-white p-4 shadow-sm transition hover:border-primary/30 hover:shadow-md"
>
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10">
{imageFormatFromUrl(f.pdfUrl) ? (
@ -81,19 +81,21 @@ export default async function HomePage() {
<span className="block break-all line-clamp-2 text-sm font-medium text-slate-900">
{f.title}
</span>
<span className="mt-0.5 block text-xs text-slate-400">
<span className="mt-0.5 block text-xs text-slate-500">
<span className="mr-1.5 rounded bg-slate-100 px-1 py-0.5 font-medium text-slate-500">
{kindBadge(f.pdfUrl)}
</span>
{new Date(f.createdAt).toLocaleString("th-TH")}
</span>
</span>
<ChevronRight aria-hidden className="h-4 w-4 shrink-0 text-slate-400" />
{/* M4 (UX audit 2026-09-16): ลิงก์กางเต็มการ์ด — จอสัมผัสจิ้มตรงไหนก็เข้าได้
(เดิมปุ่ม "Annotate" ~30px อย่างเดียว และเป็นภาษาอังกฤษใน UI ไทย) */}
<Link
href={`/annotate/${f.id}`}
className="shrink-0 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-white transition hover:opacity-90"
>
Annotate
</Link>
className="absolute inset-0 rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1"
aria-label={`เปิด ${f.title}`}
/>
</li>
))}
</ul>