genesis+worker: design system Genesis ทั้งชุด + patch pdf.worker แก้ fake-space ไทย + ปิด review design/responsive/quality รอบ 2026-09-24 — (1) Genesis: ฟอนต์ General Sans self-host + DM Sans + Sarabun, AppNav 56px backdrop-blur แทน SessionBar, DotGrid หน้า login, palette/รัศมี/เงา/focus ตาม docs/DESIGN.md ครบทุกหน้า (2) postinstall scripts/patch-pdf-worker.mjs ถอด if (scaledDim) รอบ prevTransform ให้อัปเดตทุก glyph — กัน addFakeSpaces แทรก space กลางคำไทยใน PDF ที่ justify ด้วย letter-spacing + test กัน regression (tests/scripts/ + fixture thai_letterspacing.pdf, invariant #6) (3) ฟอร์มอัปโหลด + ช่องกลุ่ม typeId ทางเลือก — normDocumentGroup ที่ utils/external-file.ts เป็น single source ร่วมกับ N8 (4) token fixes: focus ring ปุ่ม pin + แถว bookmark ตามหน้า, ปุ่ม action ใน popup/composer/dialog เป็น rounded-md ทั้งคู่, selection preview เป็น tint ของ primary แทน blue-500 ดิบ, default สี shape/highlight รวมที่ utils/annotation-display, loading animate-pulse + จดข้อยกเว้น (เงา pin, font weights ของ viewer) ลง DESIGN.md (5) responsive: Export PDF max-lg:order-first ให้เห็นบน iPad, แถวบน header แถวเดียวเลื่อนได้บนจอแคบ, re-fit ความกว้างตอน toggle sidebar เฉพาะเมื่อยังไม่ซูมเอง, pin popover flip ขึ้นบนเมื่อใกล้ท้ายหน้า, toast hint ตอนสลับ tool กลางเอกสาร, ป้าย "ชนิด · ผู้ทำ" ของรายการผู้อื่นสำหรับจอสัมผัส (6) คุณภาพ: @layer ครอบ @media กันกฎ touch รั่ยไป desktop (Tailwind v3 ยก @layer ออกจาก media ทิ้ง wrapper — ยืนยันด้วย build จริง + วัดบนจอ), กฎ M5 ครอบ contenteditable กัน Safari zoom ตอนแก้ไข TEXTBOX บน iPad, COMMENT ใช้ box มาตราฐานเดียวตาม pin px จริง (render/scroll target/move-clamp/bookmark), ตัดหัวการ์ดซ้ำกับ h2 หน้ารายการ — พร้อมไฟล์ untracked ที่ postinstall/โค้ดอ้างถึงครบชุด (app/fonts/, AppNav, DotGrid, tests/) กัน fresh clone fail ที่ npm install (คำขอผู้ใช้ 2026-09-24)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-09-24 11:46:02 +07:00
parent e254b935ce
commit c5c6b65bd2
42 changed files with 1429 additions and 745 deletions

View file

@ -54,7 +54,7 @@ import { formatThaiDate } from "@/utils/date-format";
* ของ PDFViewer.tsx (ห้าม import ข้ามไฟล์: PDFViewer → sidebar → PDFViewer เป็น
* circular import — แก้ต้องแก้ทั้งสองที่พร้อมกัน) */
const FOCUS_RING_CLS =
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 focus-visible:z-10";
"focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary focus-visible:z-10";
/** ใส่/ล้างโน้ตคำอธิบายใน data ของ annotation (ทุกชนิดยกเว้น COMMENT — message ทำหน้าที่นี้แล้ว) */
function withNote(data: Exclude<AnnotationData, CommentData>, note: string): AnnotationData {
@ -139,7 +139,7 @@ function ViewSwitch<T extends string>({
onChange: (v: T) => void;
}) {
return (
<div className="flex rounded-md bg-slate-100 p-0.5">
<div className="flex rounded-md bg-ink/[0.05] p-0.5">
{options.map((o) => (
<button
key={o.key}
@ -147,8 +147,8 @@ function ViewSwitch<T extends string>({
aria-pressed={value === o.key}
className={`flex-1 rounded px-2 py-1 text-[11px] font-medium transition ${FOCUS_RING_CLS} ${
value === o.key
? "bg-white text-slate-900 shadow-sm"
: "text-slate-500 hover:text-slate-600"
? "bg-white text-ink ring-1 ring-line"
: "text-ink-soft hover:text-ink"
}`}
onClick={() => onChange(o.key)}
>
@ -178,7 +178,7 @@ function MenuItem({
type="button"
role="menuitem"
className={`flex w-full items-start gap-2 rounded px-2 py-1.5 text-left text-xs transition ${FOCUS_RING_CLS} ${
danger ? "text-red-600 hover:bg-red-50" : "text-slate-600 hover:bg-slate-100"
danger ? "text-error-ink hover:bg-error/10" : "text-ink-soft hover:bg-ink/[0.05]"
}`}
onClick={onClick}
>
@ -204,17 +204,17 @@ function PageGroupHeader({
<button
type="button"
aria-label={collapsed ? `กางหน้า ${pageIndex + 1}` : `หุบหน้า ${pageIndex + 1}`}
className={`flex w-full items-center gap-1.5 rounded px-1 py-1 text-left hover:bg-slate-100 ${FOCUS_RING_CLS}`}
className={`flex w-full items-center gap-1.5 rounded px-1 py-1 text-left hover:bg-ink/[0.05] ${FOCUS_RING_CLS}`}
onClick={onToggle}
>
{collapsed ? (
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-slate-500" />
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-ink-soft" />
) : (
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-slate-500" />
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-ink-soft" />
)}
<span className="text-xs font-medium text-slate-700">
<span className="text-xs font-medium text-ink">
หน้า {pageIndex + 1}
<span className="ml-1 font-normal text-slate-500">({count})</span>
<span className="ml-1 font-normal text-ink-soft">({count})</span>
</span>
</button>
);
@ -774,7 +774,7 @@ export default function AnnotationSidebar({
if (e.key === "Escape") setEditBookmark(null);
}}
placeholder="ชื่อหัวข้อ"
className="w-full rounded border border-slate-300 px-1.5 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-slate-400"
className="w-full rounded-md border border-line px-1.5 py-1 text-xs focus:border-primary focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary"
/>
<textarea
rows={2}
@ -787,16 +787,16 @@ export default function AnnotationSidebar({
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) saveBookmarkEdit(b);
}}
placeholder="โน้ต (ไม่บังคับ)"
className="mt-1 w-full resize-none rounded border border-slate-300 p-1.5 text-[11px] focus:outline-none focus:ring-1 focus:ring-slate-400"
className="mt-1 w-full resize-none rounded-md border border-line p-1.5 text-[11px] focus:border-primary focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary"
/>
<p className="mt-1 text-[11px] text-slate-500">วางที่</p>
<p className="mt-1 text-[11px] text-ink-soft">วางที่</p>
<Select
value={eb.parentId ?? ""}
onChange={(e) =>
setEditBookmark({ ...eb, parentId: e.target.value || null })
}
onKeyDown={(e) => e.stopPropagation()}
className="mt-0.5 w-full rounded border border-slate-300 bg-white px-1.5 py-1 text-[11px] focus:outline-none focus:ring-1 focus:ring-slate-400"
className="mt-0.5 w-full rounded-md border border-line bg-white px-1.5 py-1 text-[11px] focus:border-primary focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary"
>
<option value="">ระดับบนสุด (หมุดหัวข้อใหญ่)</option>
{movableTo.map((h) => (
@ -808,14 +808,14 @@ export default function AnnotationSidebar({
<div className="mt-1.5 flex justify-end gap-1.5 text-[11px]">
<button
type="button"
className={`rounded px-2 py-0.5 text-slate-500 hover:bg-slate-100 ${FOCUS_RING_CLS}`}
className={`rounded-md px-2 py-0.5 text-ink-soft hover:bg-ink/[0.05] ${FOCUS_RING_CLS}`}
onClick={() => setEditBookmark(null)}
>
ยกเลิก
</button>
<button
type="button"
className={`rounded bg-primary px-2 py-0.5 font-medium text-white ${FOCUS_RING_CLS}`}
className={`rounded-md bg-primary px-2 py-0.5 font-medium text-white ${FOCUS_RING_CLS}`}
onClick={() => saveBookmarkEdit(b)}
>
บันทึก
@ -823,7 +823,7 @@ export default function AnnotationSidebar({
</div>
</div>
) : (
<div className={`group flex items-center gap-1.5 rounded py-1 hover:bg-slate-100 ${pad}`}>
<div className={`group flex items-center gap-1.5 rounded py-1 hover:bg-ink/[0.05] ${pad}`}>
{hasChildren ? (
// bm-toggle: คอลัมน์นี้เป็น "ตาราง" ของต้นไม้ ต้องกว้าง 14px เท่า spacer
// ของแถวลูกเสมอ — บนจอสัมผัส rule coarse ที่ขยายปุ่มเป็น 32px จะดันไอคอน
@ -832,7 +832,7 @@ export default function AnnotationSidebar({
<button
type="button"
aria-label={collapsed ? `กางหัวข้อ ${b.title}` : `หุบหัวข้อ ${b.title}`}
className={`shrink-0 bm-toggle text-slate-500 hover:text-slate-600 ${FOCUS_RING_CLS}`}
className={`shrink-0 bm-toggle text-ink-soft hover:text-ink ${FOCUS_RING_CLS}`}
onClick={() => toggleCollapsed(b.id)}
>
{collapsed ? (
@ -850,26 +850,25 @@ export default function AnnotationSidebar({
className={`flex min-w-0 flex-1 items-start gap-1.5 text-left ${FOCUS_RING_CLS}`}
onClick={() => onGoToBookmark(b)}
>
<Bookmark
className={`h-3.5 w-3.5 shrink-0 ${
depth === 1 ? "text-amber-500" : "text-amber-300"
}`}
/>
{/* ไอคอน bookmark สีเดียวทุกระดับ (warning-ink ผ่าน 5:1 บนขาว — เดิมแยก
root/ลูกด้วย amber-500/300 แต่ตัว 300 จาง 1.6:1 แทบมองไม่เห็น;
ลำดับชั้นของต้นไม้สื่อด้วย font-semibold + indent อยู่แล้ว) */}
<Bookmark className="h-3.5 w-3.5 shrink-0 text-warning-ink" />
<span className="min-w-0">
<span
className={`block truncate text-[13px] leading-snug ${
depth === 1 ? "font-semibold text-slate-700" : ""
depth === 1 ? "font-semibold text-ink" : ""
}`}
>
{b.title}
</span>
{/* แสดงผู้บันทึกด้วย — เหมือนแถวผลการค้นหา (N6) และแถวหมายเหตุ
(แถวเดิมมีแค่ "หน้า N" ไม่รู้ว่าใครเป็นคนคั่นไว้) */}
<span className="block text-xs text-slate-500">
<span className="block text-xs text-ink-soft">
หน้า {b.pageIndex + 1} · {b.userName || "ไม่ทราบชื่อ"}
</span>
{b.note && (
<span className="mt-0.5 block line-clamp-2 whitespace-pre-wrap break-words text-[11px] text-slate-500">
<span className="mt-0.5 block line-clamp-2 whitespace-pre-wrap break-words text-[11px] text-ink-soft">
{b.note}
</span>
)}
@ -877,7 +876,7 @@ export default function AnnotationSidebar({
</button>
{/* จำนวนหมุดย่อย — badge ขวาสุด (UX audit: แยกจากชื่อ ไม่ปนตอนอ่านไว) */}
{hasChildren && (
<span className="shrink-0 self-center rounded-full bg-slate-100 px-1.5 text-[10px] tabular-nums leading-4 text-slate-500">
<span className="shrink-0 self-center rounded-full bg-ink/[0.05] px-1.5 text-[10px] tabular-nums leading-4 text-ink-soft">
{children.length}
</span>
)}
@ -888,7 +887,7 @@ export default function AnnotationSidebar({
aria-label="จัดการหัวข้อนี้"
aria-haspopup="menu"
aria-expanded={menuFor?.kind === "bookmark" && menuFor.id === b.id}
className={`shrink-0 text-slate-500 hover:text-slate-600 group-hover:text-slate-600 ${FOCUS_RING_CLS}`}
className={`shrink-0 text-ink-soft hover:text-ink group-hover:text-ink-soft ${FOCUS_RING_CLS}`}
onClick={(e) => {
if (menuFor?.kind === "bookmark" && menuFor.id === b.id) closeRowMenu();
else openRowMenu(e.currentTarget, { kind: "bookmark", id: b.id });
@ -902,7 +901,7 @@ export default function AnnotationSidebar({
{hasChildren && !collapsed && (
// เส้น guide แนวตั้งนำสายตากลับหา parent แบบ file tree (indent ~17px/ระดับ
// ≈ ของเดิม 16px — spacer/chevron w-3.5 คงเดิมจึงตรงแนวกันทุกระดับ)
<ul className="ml-3 space-y-0.5 border-l border-slate-200 pl-1">
<ul className="ml-3 space-y-0.5 border-l border-line pl-1">
{children.map((c) => renderBookmarkNode(c, depth + 1))}
</ul>
)}
@ -920,7 +919,7 @@ export default function AnnotationSidebar({
className={`group rounded px-2 py-1.5 ${
a.id === selectedId
? "bg-primary/10 ring-1 ring-inset ring-primary/30"
: "hover:bg-slate-100"
: "hover:bg-ink/[0.05]"
} ${a.resolved ? "opacity-60" : ""}`}
>
<div className="flex items-center gap-2">
@ -938,7 +937,7 @@ export default function AnnotationSidebar({
{/* UX audit: อักษรไทยมีสระ/วรรณยุกต์ซ้อนหลายชั้น — ข้อความหลักต้อง 13px
จึงอ่านได้สบาย (บรรทัดรอง/โน้ตคง 11px) */}
<span className="block truncate text-[13px] leading-snug">{annotationPreviewText(a)}</span>
<span className="block text-xs text-slate-500">
<span className="block text-xs text-ink-soft">
หน้า {a.pageIndex + 1} · {a.userName}
{a.type === "COMMENT" && (replyCounts[a.id] ?? 0) > 0
? ` · ตอบ ${replyCounts[a.id]}`
@ -957,10 +956,10 @@ export default function AnnotationSidebar({
title={copyFailedId === a.id ? "คัดลอกไม่ได้ (เบราว์เซอร์บล็อกคลิปบอร์ด)" : "จัดการหมายเหตุนี้"}
className={`shrink-0 ${FOCUS_RING_CLS} ${
copiedId === a.id
? "text-emerald-500"
? "text-success-ink"
: copyFailedId === a.id
? "text-red-500"
: "text-slate-500 hover:text-slate-600 group-hover:text-slate-600"
? "text-error-ink"
: "text-ink-soft hover:text-ink group-hover:text-ink-soft"
}`}
onClick={(e) => {
if (menuFor?.kind === "annotation" && menuFor.id === a.id) closeRowMenu();
@ -989,19 +988,19 @@ export default function AnnotationSidebar({
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) saveNote(a);
}}
placeholder="โน้ต เช่น เหตุผลที่ทำรายการนี้…"
className="w-full resize-none rounded border border-slate-300 p-1.5 text-[11px] focus:outline-none focus:ring-1 focus:ring-slate-400"
className="w-full resize-none rounded-md border border-line p-1.5 text-[11px] focus:border-primary focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary"
/>
<div className="mt-1 flex justify-end gap-1.5 text-[11px]">
<button
type="button"
className={`rounded px-2 py-0.5 text-slate-500 hover:bg-slate-100 ${FOCUS_RING_CLS}`}
className={`rounded-md px-2 py-0.5 text-ink-soft hover:bg-ink/[0.05] ${FOCUS_RING_CLS}`}
onClick={() => setNoteFor(null)}
>
ยกเลิก
</button>
<button
type="button"
className={`rounded bg-primary px-2 py-0.5 font-medium text-white ${FOCUS_RING_CLS}`}
className={`rounded-md bg-primary px-2 py-0.5 font-medium text-white ${FOCUS_RING_CLS}`}
onClick={() => saveNote(a)}
>
บันทึก
@ -1010,8 +1009,8 @@ export default function AnnotationSidebar({
</div>
) : (
note && (
<p className="mt-0.5 flex items-start gap-1 pl-6 text-[11px] text-slate-500">
<StickyNote className="mt-0.5 h-3 w-3 shrink-0 text-amber-400" />
<p className="mt-0.5 flex items-start gap-1 pl-6 text-[11px] text-ink-soft">
<StickyNote className="mt-0.5 h-3 w-3 shrink-0 text-warning-ink" />
<span className="line-clamp-2 whitespace-pre-wrap break-words">{note}</span>
</p>
)
@ -1023,10 +1022,10 @@ export default function AnnotationSidebar({
return (
<div className="flex h-full flex-col">
{/* F4: กรองแสดง annotation ตามผู้เขียน (viewer + sidebar พร้อมกัน) */}
<div className="shrink-0 border-b border-slate-200 p-2">
<div className="shrink-0 border-b border-line p-2">
<Select
aria-label="เลือกเจ้าของหมายเหตุที่แสดง"
className="w-full rounded-md border border-slate-300 bg-white px-2 py-1.5 text-xs text-slate-600 focus:outline-none focus:ring-2 focus:ring-slate-400"
className="w-full rounded-md border border-line bg-white px-2 py-1.5 text-xs text-ink-soft focus:border-primary focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary"
value={userFilter}
onChange={(e) => onUserFilterChange(e.target.value)}
>
@ -1041,7 +1040,7 @@ export default function AnnotationSidebar({
</div>
{/* แท็บหลัก — tablist/tab semantics ให้ screen reader ประกาศว่าเป็นแท็บ */}
<div role="tablist" aria-label="รายการในแผงข้าง" className="flex shrink-0 border-b border-slate-200">
<div role="tablist" aria-label="รายการในแผงข้าง" className="flex shrink-0 border-b border-line">
{(
[
["bookmarks", `Bookmark (${visibleBookmarks.length})`],
@ -1055,8 +1054,8 @@ export default function AnnotationSidebar({
aria-selected={tab === key}
className={`flex-1 px-2 py-2 text-xs font-medium tabular-nums transition ${FOCUS_RING_CLS} ${
tab === key
? "border-b-2 border-primary text-slate-900"
: "text-slate-500 hover:text-slate-600"
? "border-b-2 border-primary text-ink"
: "text-ink-soft hover:text-ink"
}`}
onClick={() => setTab(key)}
>
@ -1068,25 +1067,25 @@ export default function AnnotationSidebar({
{/* N6: ค้นหา bookmark พร้อมเงื่อนไข — เฉพาะแท็บที่คั่นหน้า
(แยกจากช่อง F14 ที่ค้นหมายเหตุ ซึ่งอยู่เหนือแท็บ) */}
{tab === "bookmarks" && (
<div className="shrink-0 space-y-1.5 border-b border-slate-200 p-2">
<div className="shrink-0 space-y-1.5 border-b border-line p-2">
{/* แถวคำค้น — โครงเดียวกับ F14 ของแท็บหมายเหตุ: input เต็มกว้าง + ปุ่มเดียว
(เดิมปุ่ม "เงื่อนไข"+badge แย่งแถวเดียวกันจน input เหลือพื้นที่ข้อความ
~80px ≈ 6–7 ตัวอักษรไทย พิมพ์คำค้นจริงไม่พอ) */}
<form className="flex gap-1" onSubmit={(e) => void runBookmarkSearch(e)}>
<div className="relative min-w-0 flex-1">
<Search className="pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-slate-500" />
<Search className="pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-ink-soft" />
<input
value={bmQuery}
onChange={(e) => setBmQuery(e.target.value)}
onKeyDown={(e) => e.stopPropagation()} // ไม่ให้ปุ่มในช่องค้นหาชนคีย์ลัดของ viewer
placeholder="ชื่อ หรือ โน้ต…"
className="w-full rounded-md border border-slate-300 py-1.5 pl-7 pr-2 text-xs focus:outline-none focus:ring-2 focus:ring-slate-400"
className="w-full rounded-md border border-line py-1.5 pl-7 pr-2 text-xs focus:border-primary focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary"
/>
</div>
<button
type="submit"
disabled={activeConditionCount === 0 || bmSearching}
className={`shrink-0 rounded-md border border-slate-300 px-2 py-1.5 text-xs text-slate-600 hover:bg-slate-100 disabled:opacity-40 ${FOCUS_RING_CLS}`}
className={`shrink-0 rounded-md border border-line px-2 py-1.5 text-xs text-ink-soft hover:bg-ink/[0.05] disabled:opacity-40 ${FOCUS_RING_CLS}`}
>
{bmSearching ? "…" : "หา"}
</button>
@ -1099,8 +1098,8 @@ export default function AnnotationSidebar({
type="button"
aria-expanded={bmShowConditions}
onClick={() => setBmShowConditions((v) => !v)}
className={`flex min-w-0 flex-1 items-center gap-1.5 rounded px-1.5 py-1 text-[11px] hover:bg-slate-100 ${FOCUS_RING_CLS} ${
hasBmConditionValues ? "text-primary" : "text-slate-500"
className={`flex min-w-0 flex-1 items-center gap-1.5 rounded px-1.5 py-1 text-[11px] hover:bg-ink/[0.05] ${FOCUS_RING_CLS} ${
hasBmConditionValues ? "text-primary" : "text-ink-soft"
}`}
>
<SlidersHorizontal className="h-3 w-3 shrink-0" />
@ -1119,7 +1118,7 @@ export default function AnnotationSidebar({
href={withBasePath(`/api/bookmarks/export?fileId=${fileId}`)}
title="ส่งออกทุกที่คั่นหน้าของไฟล์นี้เป็น JSON"
aria-label="ส่งออกทุกที่คั่นหน้าของไฟล์นี้เป็น JSON"
className={`coarse-36 shrink-0 rounded-md p-1.5 text-slate-500 hover:bg-slate-100 hover:text-slate-700 ${FOCUS_RING_CLS}`}
className={`coarse-36 shrink-0 rounded-md p-1.5 text-ink-soft hover:bg-ink/[0.05] hover:text-ink ${FOCUS_RING_CLS}`}
>
<Braces className="h-3.5 w-3.5" />
</a>
@ -1128,7 +1127,7 @@ export default function AnnotationSidebar({
// จังหวะเดียวทุกแถว: label บนบรรทัด + คู่ input แบ่งความกว้าง grid 1fr–1fr
// (เดิม label inline 2 แถว + label บน 1 แถว อ่านไม่เป็นระเบียบ ขอบ input
// ไม่ตรงแนวกัน และคู่ date ในแถว inline ล้น 288px จนต้องหนีขึ้นบรรทัดบน)
<div className="space-y-1.5 text-[11px] text-slate-600">
<div className="space-y-1.5 text-[11px] text-ink-soft">
<div>
<span className="block pb-0.5">หน้า</span>
<div className="grid grid-cols-[1fr_auto_1fr] items-center gap-1">
@ -1143,9 +1142,9 @@ export default function AnnotationSidebar({
}
onKeyDown={(e) => e.stopPropagation()}
placeholder="จาก"
className="w-full min-w-0 rounded border border-slate-300 px-1.5 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-slate-400"
className="w-full min-w-0 rounded-md border border-line px-1.5 py-1 text-xs focus:border-primary focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary"
/>
<span className="text-slate-400">–</span>
<span className="text-ink-faint">–</span>
<input
type="number"
min={1}
@ -1157,7 +1156,7 @@ export default function AnnotationSidebar({
}
onKeyDown={(e) => e.stopPropagation()}
placeholder="ถึง"
className="w-full min-w-0 rounded border border-slate-300 px-1.5 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-slate-400"
className="w-full min-w-0 rounded-md border border-line px-1.5 py-1 text-xs focus:border-primary focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary"
/>
</div>
</div>
@ -1170,7 +1169,7 @@ export default function AnnotationSidebar({
setBmConditions({ ...bmConditions, userId: e.target.value })
}
onKeyDown={(e) => e.stopPropagation()}
className="w-full rounded border border-slate-300 bg-white px-1.5 py-1 text-[11px] focus:outline-none focus:ring-1 focus:ring-slate-400"
className="w-full rounded-md border border-line bg-white px-1.5 py-1 text-[11px] focus:border-primary focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary"
>
<option value="">ทุกคน</option>
{bookmarkAuthors.map((u) => (
@ -1182,7 +1181,11 @@ export default function AnnotationSidebar({
</div>
<div>
<span className="block pb-0.5">บันทึกวันที่</span>
<div className="grid grid-cols-[1fr_auto_1fr] items-center gap-1">
{/* เรียง 2 แถวเต็มกว้าง (ไม่ใช่คู่แนวนอน + "–" คั่น): คอลัมน์คู่แคบ
~122px ของ sidebar ใส่ค่า "23 ก.ย. 2026" ที่ 16px (rule จอสัมผัส
กัน iOS zoom-on-focus) + icon ปฏิทินไม่พอ ปีโดนตัด (code review
2026-09-23) — aria-label กำกับช่วงเริ่ม/สุดท้ายอยู่แล้ว */}
<div className="grid gap-1">
<input
type="date"
aria-label="วันที่เริ่ม"
@ -1191,9 +1194,8 @@ export default function AnnotationSidebar({
setBmConditions({ ...bmConditions, dateFrom: e.target.value })
}
onKeyDown={(e) => e.stopPropagation()}
className="w-full min-w-0 rounded border border-slate-300 px-1.5 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-slate-400"
className="w-full min-w-0 rounded-md border border-line px-1.5 py-1 text-xs focus:border-primary focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary"
/>
<span className="text-slate-400">–</span>
<input
type="date"
aria-label="วันที่สุดท้าย"
@ -1202,14 +1204,14 @@ export default function AnnotationSidebar({
setBmConditions({ ...bmConditions, dateTo: e.target.value })
}
onKeyDown={(e) => e.stopPropagation()}
className="w-full min-w-0 rounded border border-slate-300 px-1.5 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-slate-400"
className="w-full min-w-0 rounded-md border border-line px-1.5 py-1 text-xs focus:border-primary focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary"
/>
</div>
</div>
<div className="flex justify-end">
<button
type="button"
className={`rounded px-1.5 py-1 text-slate-500 hover:bg-slate-100 ${FOCUS_RING_CLS}`}
className={`rounded-md px-1.5 py-1 text-ink-soft hover:bg-ink/[0.05] ${FOCUS_RING_CLS}`}
onClick={() => {
// UX audit: ล้างให้สิ้นเชิง — คำค้น + เงื่อนไข + ผลที่แสดง พร้อมกัน
setBmConditions(EMPTY_BM_CONDITIONS);
@ -1225,12 +1227,12 @@ export default function AnnotationSidebar({
{/* error กับ hint เป็นคำเตือนคนละเรื่อง (ฟอร์มผิด vs ผลเป็นของเงื่อนไขเก่า)
— มี error แสดงเดี่ยว ไม่ซ้อนสองข้อความพร้อมกัน */}
{bmSearchError ? (
<p className="px-1 text-[11px] text-red-600">{bmSearchError}</p>
<p className="px-1 text-[11px] text-error-ink">{bmSearchError}</p>
) : (
bmResults !== null &&
bmSearchedSig !== null &&
bmSearchedSig !== JSON.stringify([bmQuery.trim(), bmConditions]) && (
<p className="px-1 text-[11px] text-amber-600">
<p className="px-1 text-[11px] text-warning-ink">
เงื่อนไขเปลี่ยนแล้ว กด &quot;หา&quot; เพื่อค้นใหม่
</p>
)
@ -1243,23 +1245,23 @@ export default function AnnotationSidebar({
เนื้อหาใน list ด้านล่าง คลิกแถว = กระโดดไปตำแหน่ง + เลือก */}
{tab === "annotations" && (
<form
className="flex shrink-0 gap-1 border-b border-slate-200 p-2"
className="flex shrink-0 gap-1 border-b border-line p-2"
onSubmit={(e) => void runSearch(e)}
>
<div className="relative min-w-0 flex-1">
<Search className="pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-slate-500" />
<Search className="pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-ink-soft" />
<input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.stopPropagation()} // ไม่ให้ปุ่มในช่องค้นหาชนคีย์ลัดของ viewer
placeholder="ค้นหาในหมายเหตุ…"
className="w-full rounded-md border border-slate-300 py-1.5 pl-7 pr-2 text-xs focus:outline-none focus:ring-2 focus:ring-slate-400"
className="w-full rounded-md border border-line py-1.5 pl-7 pr-2 text-xs focus:border-primary focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary"
/>
</div>
<button
type="submit"
disabled={!searchQuery.trim() || searching}
className={`shrink-0 rounded-md border border-slate-300 px-2 py-1.5 text-xs text-slate-600 hover:bg-slate-100 disabled:opacity-40 ${FOCUS_RING_CLS}`}
className={`shrink-0 rounded-md border border-line px-2 py-1.5 text-xs text-ink-soft hover:bg-ink/[0.05] disabled:opacity-40 ${FOCUS_RING_CLS}`}
>
{searching ? "…" : "หา"}
</button>
@ -1271,14 +1273,14 @@ export default function AnnotationSidebar({
bmResults !== null ? (
<div>
<div className="flex items-center justify-between px-1 pb-1">
<p className="text-[11px] font-semibold tabular-nums text-slate-500">
<p className="text-[11px] font-semibold tabular-nums text-ink-soft">
ผลการค้นหา ({bmResults.length})
</p>
<button
type="button"
aria-label="ปิดผลการค้นหา"
title="กลับไปรายการที่คั่นหน้า"
className={`text-slate-500 hover:text-slate-600 ${FOCUS_RING_CLS}`}
className={`text-ink-soft hover:text-ink ${FOCUS_RING_CLS}`}
onClick={() => setBmResults(null)}
>
<X className="h-3.5 w-3.5" />
@ -1288,14 +1290,14 @@ export default function AnnotationSidebar({
แชร์จอให้คนอื่นดู (แก้ฟอร์มหลังค้นบรรทัดนี้ไม่เปลี่ยน จนกด "หา" ใหม่) */}
{bmSearchedSummary && (
<p
className="truncate px-1 pb-1 text-[11px] text-slate-500"
className="truncate px-1 pb-1 text-[11px] text-ink-soft"
title={bmSearchedSummary}
>
ค้นด้วย: {bmSearchedSummary}
</p>
)}
{bmResults.length === 0 ? (
<p className="px-1 pb-1 text-xs text-slate-500">ไม่พบ</p>
<p className="px-1 pb-1 text-xs text-ink-soft">ไม่พบ</p>
) : (
<ul className="space-y-0.5">
{bmResults.map((b) => {
@ -1304,22 +1306,22 @@ export default function AnnotationSidebar({
<li key={b.id}>
<button
type="button"
className={`flex w-full items-start gap-1.5 rounded px-1 py-1 text-left hover:bg-slate-100 ${FOCUS_RING_CLS}`}
className={`flex w-full items-start gap-1.5 rounded px-1 py-1 text-left hover:bg-ink/[0.05] ${FOCUS_RING_CLS}`}
onClick={() => onGoToBookmark(b)}
>
<Bookmark className="mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-500" />
<Bookmark className="mt-0.5 h-3.5 w-3.5 shrink-0 text-warning-ink" />
<span className="min-w-0">
<span className="block truncate text-[13px] leading-snug">{b.title}</span>
<span className="block text-xs text-slate-500">
<span className="block text-xs text-ink-soft">
หน้า {b.pageIndex + 1} · {b.userName || "ไม่ทราบชื่อ"}
</span>
{parent && (
<span className="block truncate text-[11px] text-slate-500">
<span className="block truncate text-[11px] text-ink-soft">
ใน: {parent.title}
</span>
)}
{b.note && (
<span className="mt-0.5 block line-clamp-2 whitespace-pre-wrap break-words text-[11px] text-slate-500">
<span className="mt-0.5 block line-clamp-2 whitespace-pre-wrap break-words text-[11px] text-ink-soft">
{b.note}
</span>
)}
@ -1332,11 +1334,11 @@ export default function AnnotationSidebar({
)}
</div>
) : bookmarks.length === 0 ? (
<p className="p-4 text-center text-xs text-slate-500">
<p className="p-4 text-center text-xs text-ink-soft">
ยังไม่มีที่คั่นหน้า — กดปุ่มคั่นหน้าบนแถบเครื่องมือเพื่อเพิ่ม
</p>
) : visibleBookmarks.length === 0 ? (
<p className="p-4 text-center text-xs text-slate-500">
<p className="p-4 text-center text-xs text-ink-soft">
ไม่มีที่คั่นหน้าของเจ้าของหมายเหตุที่เลือก
</p>
) : (
@ -1373,27 +1375,27 @@ export default function AnnotationSidebar({
<li key={b.id}>
<button
type="button"
className="flex w-full items-start gap-1.5 rounded py-1 pl-5 pr-1 text-left hover:bg-slate-100"
className={`flex w-full items-start gap-1.5 rounded py-1 pl-5 pr-1 text-left hover:bg-ink/[0.05] ${FOCUS_RING_CLS}`}
onClick={() => onGoToBookmark(b)}
>
<Bookmark className="mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-500" />
<Bookmark className="mt-0.5 h-3.5 w-3.5 shrink-0 text-warning-ink" />
<span className="min-w-0 flex-1">
<span className="block truncate text-[13px] leading-snug font-medium text-slate-700">
<span className="block truncate text-[13px] leading-snug font-medium text-ink">
{b.title}
</span>
{/* ผู้บันทึกเหมือนแถวผลการค้นหา — ไม่ใส่ "หน้า N"
เพราะหัวกลุ่มของมุมมองนี้บอกหน้าอยู่แล้ว */}
<span className="block text-xs text-slate-500">
<span className="block text-xs text-ink-soft">
{b.userName || "ไม่ทราบชื่อ"}
</span>
{b.note && (
<span className="mt-0.5 block line-clamp-2 whitespace-pre-wrap break-words text-[11px] text-slate-500">
<span className="mt-0.5 block line-clamp-2 whitespace-pre-wrap break-words text-[11px] text-ink-soft">
{b.note}
</span>
)}
</span>
{childCount > 0 && (
<span className="ml-auto shrink-0 self-center rounded-full bg-slate-100 px-1.5 text-[10px] tabular-nums leading-4 text-slate-500">
<span className="ml-auto shrink-0 self-center rounded-full bg-ink/[0.05] px-1.5 text-[10px] tabular-nums leading-4 text-ink-soft">
{childCount}
</span>
)}
@ -1420,29 +1422,29 @@ export default function AnnotationSidebar({
) : visibleSearchResults !== null ? (
<div>
<div className="flex items-center justify-between px-1 pb-1">
<p className="text-[11px] font-semibold tabular-nums text-slate-500">
<p className="text-[11px] font-semibold tabular-nums text-ink-soft">
ผลการค้นหา ({visibleSearchResults.length})
</p>
<button
type="button"
aria-label="ปิดผลการค้นหา"
title="กลับไปรายการหมายเหตุ"
className={`text-slate-500 hover:text-slate-600 ${FOCUS_RING_CLS}`}
className={`text-ink-soft hover:text-ink ${FOCUS_RING_CLS}`}
onClick={() => setSearchResults(null)}
>
<X className="h-3.5 w-3.5" />
</button>
</div>
{visibleSearchResults.length === 0 ? (
<p className="px-1 pb-1 text-xs text-slate-500">ไม่พบ</p>
<p className="px-1 pb-1 text-xs text-ink-soft">ไม่พบ</p>
) : (
// reuse แถวเดิม — li มี data-ann-id ให้ effect scrollIntoView หาเจอ
<ul className="space-y-1">{visibleSearchResults.map(renderAnnotationRow)}</ul>
)}
</div>
) : annotations.length === 0 ? (
<p className="p-4 text-center text-xs text-slate-500">
ยังไม่มีหมายเหตุ — คลุมข้อความ / วาดเส้น / คลิกวางคอมเมนต์บนหน้าเอกสาร
<p className="p-4 text-center text-xs text-ink-soft">
ยังไม่มีหมายเหตุ — คลุมข้อความ / วาดเส้น / คอมเมนต์บนหน้าเอกสาร
<span className="mt-1 block text-[11px]">
คีย์ลัด: H มาร์ก · D วาด · C comment · B คั่นหน้า · Ctrl+Z ย้อน
</span>
@ -1481,7 +1483,7 @@ export default function AnnotationSidebar({
<>
{pendingItems.length > 0 && (
<section>
<p className="px-2 pb-1 pt-2 text-[11px] font-semibold text-slate-500">
<p className="px-2 pb-1 pt-2 text-[11px] font-semibold text-ink-soft">
ค้างดำเนินการ
</p>
<ul className="space-y-1">{pendingItems.map(renderAnnotationRow)}</ul>
@ -1489,7 +1491,7 @@ export default function AnnotationSidebar({
)}
{resolvedItems.length > 0 && (
<section>
<p className="px-2 pb-1 pt-3 text-[11px] font-semibold text-emerald-500">
<p className="px-2 pb-1 pt-3 text-[11px] font-semibold text-success-ink">
จัดการแล้ว
</p>
<ul className="space-y-1">{resolvedItems.map(renderAnnotationRow)}</ul>
@ -1511,35 +1513,35 @@ export default function AnnotationSidebar({
{/* F19/A7: dialog ประวัติ — รายการ version (เวลา + ชนิดการเปลี่ยนแปลง) + ปุ่มกู้คืน */}
{historyFor && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/40 p-4"
className="fixed inset-0 z-50 flex items-center justify-center bg-ink/40 p-4"
onClick={() => setHistoryFor(null)}
>
<div
role="dialog"
aria-modal="true"
aria-label="ประวัติการเปลี่ยนแปลง"
className="w-96 max-w-full rounded-lg border border-slate-200 bg-white p-4 shadow-xl"
className="w-96 max-w-full rounded-lg border border-line bg-white p-4 shadow-xl"
onClick={(e) => e.stopPropagation()}
>
<p className="text-sm font-semibold">ประวัติการเปลี่ยนแปลง</p>
<p className="mt-0.5 truncate text-[11px] text-slate-500">
<p className="mt-0.5 truncate text-[11px] text-ink-soft">
{annotationPreviewText(historyFor)}
</p>
{historyError && (
<p className="mt-2 rounded bg-red-50 px-2 py-1 text-[11px] text-red-600">
<p className="mt-2 rounded bg-error/10 px-2 py-1 text-[11px] text-error-ink">
{historyError}
</p>
)}
{versions === null && !historyError && (
<p className="mt-3 text-xs text-slate-500">กำลังโหลด…</p>
<p className="mt-3 text-xs text-ink-soft">กำลังโหลด…</p>
)}
{versions !== null && versions.length === 0 && (
<p className="mt-3 text-xs text-slate-500">
<p className="mt-3 text-xs text-ink-soft">
ยังไม่มีบันทึกการเปลี่ยนแปลง (เกิดขึ้นเมื่อแก้ไขหรือลบรายการนี้)
</p>
)}
{versions !== null && versions.length > 0 && (
<ul className="mt-3 max-h-[60vh] divide-y divide-slate-100 overflow-y-auto">
<ul className="mt-3 max-h-[60vh] divide-y divide-line/60 overflow-y-auto">
{versions.map((v, i) => {
// สิ่งที่เปลี่ยนของรอบนี้ = snapshot ก่อนแก้ (v.data) เทียบ state
// ถัดไป: snapshot ของรอบแก้ที่ใหม่กว่าถัดไป (i-1) หรือค่าปัจจุบัน
@ -1559,11 +1561,11 @@ export default function AnnotationSidebar({
<span className="block text-xs font-medium">
{v.kind === "delete" ? "ถูกลบ" : "ถูกแก้ไข"}
</span>
<span className="block text-[11px] text-slate-500" suppressHydrationWarning>
<span className="block text-[11px] text-ink-soft" suppressHydrationWarning>
{formatThaiDate(v.createdAt, true)} · โดย {v.userName}
</span>
{changes.map((c) => (
<span key={c} className="mt-0.5 block text-[11px] text-slate-600">
<span key={c} className="mt-0.5 block text-[11px] text-ink-soft">
{c}
</span>
))}
@ -1571,7 +1573,7 @@ export default function AnnotationSidebar({
<button
type="button"
disabled={restoringId !== null}
className={`mt-0.5 shrink-0 rounded border border-slate-300 px-2 py-1 text-[11px] text-slate-600 hover:bg-slate-100 disabled:opacity-40 ${FOCUS_RING_CLS}`}
className={`mt-0.5 shrink-0 rounded-md border border-line px-2 py-1 text-[11px] text-ink-soft hover:bg-ink/[0.05] disabled:opacity-40 ${FOCUS_RING_CLS}`}
onClick={() => void handleRestore(v.id)}
>
{restoringId === v.id ? "กำลังกู้คืน…" : "กู้คืน"}
@ -1585,7 +1587,7 @@ export default function AnnotationSidebar({
<button
type="button"
autoFocus
className={`rounded px-3 py-1.5 text-slate-500 hover:bg-slate-100 ${FOCUS_RING_CLS}`}
className={`rounded-md px-3 py-1.5 text-ink-soft hover:bg-ink/[0.05] ${FOCUS_RING_CLS}`}
onClick={() => setHistoryFor(null)}
>
ปิด
@ -1605,7 +1607,7 @@ export default function AnnotationSidebar({
data-tool-menu=""
role="menu"
aria-label={menuFor.kind === "annotation" ? "จัดการหมายเหตุ" : "จัดการหัวข้อ"}
className="w-60 rounded-lg border border-slate-200 bg-white p-1.5 shadow-lg"
className="w-60 rounded-lg border border-line bg-white p-1.5 shadow-lg"
style={{
position: "fixed",
left: menuPos.left,
@ -1619,20 +1621,20 @@ export default function AnnotationSidebar({
>
{menuConfirmDelete ? (
<div className="p-1">
<p className="px-1 text-xs text-slate-700">
<p className="px-1 text-xs text-ink">
ลบ{menuFor.kind === "annotation" ? "รายการ" : "หัวข้อ"}นี้?
</p>
<div className="mt-1.5 flex justify-end gap-1.5">
<button
type="button"
className={`rounded px-2 py-1 text-xs text-slate-500 hover:bg-slate-100 ${FOCUS_RING_CLS}`}
className={`rounded-md px-2 py-1 text-xs text-ink-soft hover:bg-ink/[0.05] ${FOCUS_RING_CLS}`}
onClick={closeRowMenu}
>
ยกเลิก
</button>
<button
type="button"
className={`rounded bg-red-600 px-2 py-1 text-xs font-medium text-white hover:bg-red-700 ${FOCUS_RING_CLS}`}
className={`rounded-md border border-error px-2 py-1 text-xs font-medium text-error-ink hover:bg-error/10 ${FOCUS_RING_CLS}`}
onClick={() => {
// อ่าน kind/id ก่อน close — setMenuFor(null) แล้วเงื่อนไขเดิมหาย
if (menuFor.kind === "annotation") onDeleteAnnotation(menuFor.id);
@ -1678,7 +1680,7 @@ export default function AnnotationSidebar({
)}
{menuAnn.userId === userId && (
<>
<div className="my-1 border-t border-slate-100" />
<div className="my-1 border-t border-line/60" />
<MenuItem
danger
Icon={Trash2}
@ -1713,7 +1715,7 @@ export default function AnnotationSidebar({
)}
{menuBm.userId === userId && (
<>
<div className="my-1 border-t border-slate-100" />
<div className="my-1 border-t border-line/60" />
<MenuItem
danger
Icon={Trash2}