toolbar: ใส่คำอธิบายไทยหลัง icon ทุกปุ่มของ header หน้า annotate (คำขอผู้ใช้ 2026-09-21)
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 4s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 44s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 3s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 1s

- ปุ่ม tool ทั้ง 10 แสดงชื่อสั้นหลัง icon — เพิ่ม field "text" ใน TOOLS แยกจาก "label"
  (label เต็มพร้อมคีย์ลัด H/D/C ยังใช้ทำ title/aria-label ตามเดิม): เลือกข้อความ/เลื่อนหน้า/
  มาร์กข้อความ/วาดเส้น/วางคอมเมนต์/รูปทรง/ยางลบ/ปิดพราง/กล่องข้อความ/ตรายาง — เหตุผล:
  ผู้ใช้ศาลไม่คุ้น icon ล้วน (NN Group: icon เข้าใจสากลมีน้อยมาก) โดยเฉพาะจอสัมผัส
  ที่ hover อ่าน title ไม่ได้ จึงถอด span ชื่อ tool-active เฉพาะ max-lg (H3 UX audit
  2026-09-16) ออกด้วย เพราะ label มองเห็นทุกปุ่มแล้ว (เหลือ comment จดประวัติ)
- ปุ่ม bookmark → "Bookmark" (ตามแท็บ sidebar Bookmark (N) ที่เลือกไว้ commit b1bc65e),
  ปุ่ม export → "Export PDF" (ตรง string เดิม), ลิงก์หน้าสรุป → "หน้าสรุป" แบบ
  hidden sm:inline ตาม pattern ชื่อไฟล์ (จอ < 640 เหลือ icon เดี่ยวไม่ให้แถว title ล้น)
  + เติม aria-label ที่ Link โดยตรง
- แก้ตาม UX review (ui-ux-designer): typo หน้าสรุป "ปิดพาง"→"ปิดพราง" (TYPE_SHORT),
  label ยางลบ → "ยางลบ (ลบด้วยการลาก)" ให้ accessible name มีคำที่แสดงบนปุ่ม
  (WCAG 2.5.3 Label in Name — เดิม aria คนละชื่อกับข้อความที่เห็น), "วาง comment"
  → "วางคอมเมนต์" ให้เป็นศัพท์เดียวกับ sidebar/หน้าสรุป/CSV
- A2 (UX review): effect ใหม่ — tool เปลี่ยนแล้วปุ่มของ tool นั้นหลุดจอ (จอ < lg ที่แถบ
  เป็น overflow-x-auto) ให้ smooth-scroll กลับกึ่งกลางแถบ กันเคสเลื่อนแถบไปแตะ tool
  ท้ายแถว (เช่น ตรายาง) แล้วเลื่อนกลับหา undo จนลืมว่า tool ไหน active แล้วแตะหน้า
  เอกสาร = สร้างรายการโดยไม่ตั้งใจ — guard ข้ามตอนเมนูเปิด (scroll จะ trigger listener
  ปิดเมนู C1) + selfScrollUntilRef ให้ listener ของ C1 เมิน scroll event ของแอนิเมชัน
  ตัวเอง ~600ms กันเมนูที่ผู้ใช้เพิ่งเปิดกลางแอนิเมชันโดนปิดทันที (code review) +
  matchMedia "not all and (min-width: 1024px)" เป็น complement แท้ของ min-width: 1024px
  ให้ตรง max-lg ของ Tailwind (max-width: 1023px เดิมพลาดจอเศษส่วน 1023.5px จาก
  display scaling) + เคารพ prefers-reduced-motion

verify: tsc ผ่าน + vitest 139/139 + DOM จริง 4 viewport (1440/1024/820/390: label ครบ
บรรทัดเดียวไม่ clip, undo เห็นหัวแถวมือถือ) + A2 จริงที่ 390 (เลื่อนท้ายสุด 836 → กด D →
เลื่อนกลับ 337 ปุ่มเข้าจอ, จอ 1280 ไม่มีผลข้างเคียง) — ผลข้างเคียงที่ยอมรับ: header จอ
1024–1180 สูง 178px (~23%) จากเดิม ~110px (ปุ่ม label กว้างขึ้น กรอบ tool wrap 2 แถว)
ตามการตัดสินใจของผู้ใช้ ถ้าใช้จริงแล้วหนักไป ย้อนด้วย lg:hidden xl:inline ที่ span ได้

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-09-21 12:03:16 +07:00
parent e713bcc5e3
commit f7b5a8488d
2 changed files with 87 additions and 34 deletions

View file

@ -51,7 +51,7 @@ const TYPE_SHORT: Record<AnnotationType, string> = {
DRAW: "เส้นวาด", DRAW: "เส้นวาด",
COMMENT: "คอมเมนต์", COMMENT: "คอมเมนต์",
SHAPE: "รูปทรง", SHAPE: "รูปทรง",
REDACTION: "ปิดพาง", REDACTION: "ปิดพราง",
TEXTBOX: "กล่องข้อความ", TEXTBOX: "กล่องข้อความ",
STAMP: "ตรายาง", STAMP: "ตรายาง",
}; };

View file

@ -356,17 +356,21 @@ function selectionTextWithin(kept: DOMRect[], range: Range): string {
return sanitizePdfText(out).slice(0, 5000) || sanitizePdfText(raw.slice(0, 5000)); return sanitizePdfText(out).slice(0, 5000) || sanitizePdfText(raw.slice(0, 5000));
} }
const TOOLS: { key: Tool; label: string; Icon: LucideIcon }[] = [ /** label = ชื่อเต็มสำหรับ title/aria-label (พร้อมคีย์ลัด/คำอังกฤษประกอบ) —
{ key: "select", label: "เลือกข้อความ", Icon: MousePointer2 }, * text = คำสั้น ๆ แสดงเป็นข้อความหลังไอคอนบนปุ่มทุก tool (คำขอผู้ใช้ 2026-09-21:
{ key: "pan", label: "เลื่อนหน้า (pan)", Icon: Hand }, * ผู้ใช้ศาลไม่คุ้นไอคอนล้วน โดยเฉพาะจอสัมผัสที่ hover อ่าน title ไม่ได้ — แทน span
{ key: "highlight", label: "มาร์กข้อความ (H)", Icon: Highlighter }, * ชื่อ tool-active ของ H3 ที่ถอดออก) */
{ key: "draw", label: "วาดเส้น (D)", Icon: Pencil }, const TOOLS: { key: Tool; label: string; text: string; Icon: LucideIcon }[] = [
{ key: "comment", label: "วาง comment (C)", Icon: MessageSquarePlus }, { key: "select", label: "เลือกข้อความ", text: "เลือกข้อความ", Icon: MousePointer2 },
{ key: "shape", label: "รูปทรง", Icon: Shapes }, { key: "pan", label: "เลื่อนหน้า (pan)", text: "เลื่อนหน้า", Icon: Hand },
{ key: "erase", label: "ลบด้วยการลาก (eraser)", Icon: Eraser }, { key: "highlight", label: "มาร์กข้อความ (H)", text: "มาร์กข้อความ", Icon: Highlighter },
{ key: "redact", label: "ปิดพราง (redaction)", Icon: EyeOff }, { key: "draw", label: "วาดเส้น (D)", text: "วาดเส้น", Icon: Pencil },
{ key: "textbox", label: "กล่องข้อความ", Icon: Type }, { key: "comment", label: "วางคอมเมนต์ (C)", text: "วางคอมเมนต์", Icon: MessageSquarePlus },
{ key: "stamp", label: "ตรายาง (stamp)", Icon: Stamp }, { key: "shape", label: "รูปทรง", text: "รูปทรง", Icon: Shapes },
{ key: "erase", label: "ยางลบ (ลบด้วยการลาก)", text: "ยางลบ", Icon: Eraser },
{ key: "redact", label: "ปิดพราง (redaction)", text: "ปิดพราง", Icon: EyeOff },
{ key: "textbox", label: "กล่องข้อความ", text: "กล่องข้อความ", Icon: Type },
{ key: "stamp", label: "ตรายาง (stamp)", text: "ตรายาง", Icon: Stamp },
]; ];
/** UX review 2026-09-14 M4: focus ring ตอน keyboard focus — ทั้งโปรเจกต์ไม่เคยมี focus-visible /** UX review 2026-09-14 M4: focus ring ตอน keyboard focus — ทั้งโปรเจกต์ไม่เคยมี focus-visible
@ -1361,6 +1365,12 @@ export default function PDFViewer({
textboxMenuOpen || textboxMenuOpen ||
!!bookmarkComposer; !!bookmarkComposer;
/** A2 (code review 2026-09-21): เวลาที่แอนิเมชัน smooth scroll เลื่อนปุ่ม tool active
* (effect ด้านล่าง) จะหมด — listener ปิดเมนูของ C1 เช็ค ref นี้ก่อน ไม่งั้น scroll
* event ที่เหลือจากแอนิเมชันจะปิดเมนูที่ผู้ใช้เพิ่งเปิดระหว่างนั้นทันที (และ
* markMenuClosed กลืน tap ถัดไป 300ms ด้วย) */
const selfScrollUntilRef = useRef(0);
// C1: ปิดเมนูทั้งหมดเมื่อเลื่อนแถบ tool ต่อ หรือ resize/หมุนจอ — ตำแหน่ง fixed จะเพี้ยน // C1: ปิดเมนูทั้งหมดเมื่อเลื่อนแถบ tool ต่อ หรือ resize/หมุนจอ — ตำแหน่ง fixed จะเพี้ยน
// จากปุ่มทันทีที่แถบเลื่อน (header sticky → เลื่อนหน้าเอกสารไม่กระทบ ปุ่มไม่ขยับเทียบ viewport) // จากปุ่มทันทีที่แถบเลื่อน (header sticky → เลื่อนหน้าเอกสารไม่กระทบ ปุ่มไม่ขยับเทียบ viewport)
// Major 1 (code review): resize ของมือถือจาก URL bar/คีย์บอร์ดเปลี่ยนแค่ความสูงและเมนูยัง // Major 1 (code review): resize ของมือถือจาก URL bar/คีย์บอร์ดเปลี่ยนแค่ความสูงและเมนูยัง
@ -1379,14 +1389,52 @@ export default function PDFViewer({
const onResize = () => { const onResize = () => {
if (window.innerWidth !== menuOpenWRef.current) close(); if (window.innerWidth !== menuOpenWRef.current) close();
}; };
toolbarRowRef.current?.addEventListener("scroll", close, { passive: true }); const onScroll = () => {
// A2: ข้าม scroll event ที่มาจากแอนิเมชันเลื่อนปุ่ม active ของเราเอง —
// กันเมนูที่ผู้ใช้เพิ่งเปิดกลางแอนิเมชันโดนปิดทันที
if (performance.now() < selfScrollUntilRef.current) return;
close();
};
toolbarRowRef.current?.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", onResize); window.addEventListener("resize", onResize);
return () => { return () => {
toolbarRowRef.current?.removeEventListener("scroll", close); toolbarRowRef.current?.removeEventListener("scroll", onScroll);
window.removeEventListener("resize", onResize); window.removeEventListener("resize", onResize);
}; };
}, [anyMenuOpen]); }, [anyMenuOpen]);
// UX review 2026-09-21 A2: เมื่อ tool เปลี่ยนแล้วปุ่มของ tool นั้นหลุดออกนอกจอ (แถบเลื่อน
// ของจอ < lg) ให้เลื่อนกลับเข้ามากึ่งกลางแถบ — กันกรณีเลื่อนแถบไปแตะ tool ท้ายแถว
// (เช่น ตรายาง) แล้วเลื่อนกลับหา undo จนปุ่ม active อยู่นอกจอ ผู้ใช้ลืมว่า tool ไหน
// กำลังทำงานแล้วแตะหน้าเอกสาร = สร้างรายการโดยไม่ตั้งใจ — ข้ามเมื่อมีเมนูเปิดอยู่
// เพราะ smooth scroll ของแถบจะ trigger listener ปิดเมนูด้านบนทันที (ปุ่ม dropdown
// ที่เพิ่งแตะเห็นอยู่แล้วจึงไม่ต้องเลื่อน), ทำเฉพาะจอแคบ < lg (desktop แถบไม่เลื่อน)
useEffect(() => {
if (anyMenuOpen) return;
// เงื่อนไขเป็นส่วนเติมเต็มของ min-width: 1024px (จุดเดียวกับ effect ปิด sidebar ด้านบน)
// ให้ตรงกับ max-lg ของ Tailwind (= not all and (min-width: 1024px)) — max-width: 1023px
// ธรรมดาจะพลาดจอกว้างเศษส่วน เช่น 1023.5px จาก display scaling 125%/150%
if (!window.matchMedia("not all and (min-width: 1024px)").matches) return;
const row = toolbarRowRef.current;
const entry = TOOLS.find((t) => t.key === tool);
if (!row || !entry) return;
const btn = row.querySelector<HTMLButtonElement>(`button[title="${entry.label}"]`);
if (!btn) return;
const b = btn.getBoundingClientRect();
const r = row.getBoundingClientRect();
if (b.left >= r.left - 1 && b.right <= r.right + 1) return; // เห็นอยู่แล้ว
// บอก listener ของ C1 ให้เมิน scroll event ช่วง ~600ms นี้ (แอนิเมชันยาว ~450ms)
selfScrollUntilRef.current = performance.now() + 600;
row.scrollTo({
// กึ่งกลางแถบ — ปุ่มกว้างกว่าแถว (จอเล็กมาก) ก็ชิดซ้ายขอบแถบพอ
left: row.scrollLeft + b.left - r.left - Math.max(0, (r.width - b.width) / 2),
// L7: เคารพ prefers-reduced-motion (pattern เดียวกับ scrollToBelowHeader)
behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches
? "auto"
: "smooth",
});
}, [tool, anyMenuOpen]);
// M6: จำนวน span ของ textLayer ต่อหน้า (0 = หน้าสแกนไม่มีข้อความ) — ใช้แสดง hint ตอน // M6: จำนวน span ของ textLayer ต่อหน้า (0 = หน้าสแกนไม่มีข้อความ) — ใช้แสดง hint ตอน
// tool highlight ว่าไฟล์นี้คลุมข้อความไม่ได้ / เขียนค่าเดิมคืน object เดิมให้ callback // tool highlight ว่าไฟล์นี้คลุมข้อความไม่ได้ / เขียนค่าเดิมคืน object เดิมให้ callback
// identity คงที่ (useCallback []) กัน effect render ของทุกหน้ารันซ้ำเพราะ prop เปลี่ยน // identity คงที่ (useCallback []) กัน effect render ของทุกหน้ารันซ้ำเพราะ prop เปลี่ยน
@ -2948,13 +2996,16 @@ export default function PDFViewer({
</div> </div>
<div className="flex flex-1 items-center justify-end gap-2"> <div className="flex flex-1 items-center justify-end gap-2">
{/* F15/D2: หน้าสรุปหมายเหตุทั้งไฟล์ */} {/* F15/D2: หน้าสรุปหมายเหตุทั้งไฟล์ — label แสดงตั้งแต่ sm ขึ้นไปเท่านั้น
(จอแคบกว่านั้นแถว title ใส่ไม่พอ ให้เหลือ icon เดี่ยวเหมือนชื่อไฟล์ที่ hidden sm:block) */}
<Link <Link
href={`/annotate/${file.id}/summary`} href={`/annotate/${file.id}/summary`}
title="หน้าสรุปหมายเหตุ" title="หน้าสรุปหมายเหตุ"
className={`rounded-lg border border-slate-300 px-2 py-2 text-slate-500 hover:bg-slate-100 ${FOCUS_RING_CLS}`} aria-label="หน้าสรุปหมายเหตุ"
className={`flex items-center gap-1 whitespace-nowrap rounded-lg border border-slate-300 px-2 py-2 text-slate-500 hover:bg-slate-100 ${FOCUS_RING_CLS}`}
> >
<ClipboardList className="h-4 w-4" /> <ClipboardList className="h-4 w-4" />
<span className="hidden text-xs font-medium sm:inline">หน้าสรุป</span>
</Link> </Link>
</div> </div>
</div> </div>
@ -2977,7 +3028,7 @@ export default function PDFViewer({
UX review 2026-09-09: บน desktop ปุ่ม wrap ภายในกรอบได้ / M5 2026-09-14: UX review 2026-09-09: บน desktop ปุ่ม wrap ภายในกรอบได้ / M5 2026-09-14:
จอแคบเป็นแถวเดียว ไหลตามแถบทั้งแถวที่เลื่อนแนวนอน */} จอแคบเป็นแถวเดียว ไหลตามแถบทั้งแถวที่เลื่อนแนวนอน */}
<div className="flex max-lg:shrink-0 flex-wrap gap-0.5 rounded-lg border border-slate-300 max-lg:flex-nowrap"> <div className="flex max-lg:shrink-0 flex-wrap gap-0.5 rounded-lg border border-slate-300 max-lg:flex-nowrap">
{TOOLS.map(({ key, label, Icon }, i) => { {TOOLS.map(({ key, label, text, Icon }, i) => {
const corner = i === 0 ? "rounded-l-lg" : i === TOOLS.length - 1 ? "rounded-r-lg" : ""; const corner = i === 0 ? "rounded-l-lg" : i === TOOLS.length - 1 ? "rounded-r-lg" : "";
const activeCls = `${ const activeCls = `${
tool === key ? "bg-primary text-white" : "text-slate-500 hover:bg-slate-100" tool === key ? "bg-primary text-white" : "text-slate-500 hover:bg-slate-100"
@ -2996,7 +3047,7 @@ export default function PDFViewer({
aria-pressed={tool === "highlight"} aria-pressed={tool === "highlight"}
aria-haspopup="menu" aria-haspopup="menu"
aria-expanded={highlightMenuOpen} aria-expanded={highlightMenuOpen}
className={`flex items-center gap-0.5 px-2 py-2 transition ${activeCls}`} className={`flex items-center gap-1 whitespace-nowrap px-2 py-2 transition ${activeCls}`}
onClick={(e) => { onClick={(e) => {
openToolMenu(e.currentTarget); openToolMenu(e.currentTarget);
// เรียก switchTool เสมอแม้เป็น tool เดิม — เคลียร์ popup บนหน้าที่ค้างเปิด // เรียก switchTool เสมอแม้เป็น tool เดิม — เคลียร์ popup บนหน้าที่ค้างเปิด
@ -3008,6 +3059,7 @@ export default function PDFViewer({
}} }}
> >
<CurrentIcon className="h-4 w-4" /> <CurrentIcon className="h-4 w-4" />
<span className="text-xs font-medium">{text}</span>
<ChevronDown className="h-3 w-3" /> <ChevronDown className="h-3 w-3" />
</button> </button>
{highlightMenuOpen && {highlightMenuOpen &&
@ -3079,7 +3131,7 @@ export default function PDFViewer({
aria-pressed={tool === "draw"} aria-pressed={tool === "draw"}
aria-haspopup="menu" aria-haspopup="menu"
aria-expanded={drawMenuOpen} aria-expanded={drawMenuOpen}
className={`flex items-center gap-0.5 px-2 py-2 transition ${activeCls}`} className={`flex items-center gap-1 whitespace-nowrap px-2 py-2 transition ${activeCls}`}
onClick={(e) => { onClick={(e) => {
openToolMenu(e.currentTarget); openToolMenu(e.currentTarget);
// switchTool เสมอแม้เป็น tool เดิม — เคลียร์ popup บนหน้า (จำ intent ก่อน) // switchTool เสมอแม้เป็น tool เดิม — เคลียร์ popup บนหน้า (จำ intent ก่อน)
@ -3089,6 +3141,7 @@ export default function PDFViewer({
}} }}
> >
<CurrentPenIcon className="h-4 w-4" /> <CurrentPenIcon className="h-4 w-4" />
<span className="text-xs font-medium">{text}</span>
<ChevronDown className="h-3 w-3" /> <ChevronDown className="h-3 w-3" />
</button> </button>
{drawMenuOpen && {drawMenuOpen &&
@ -3181,7 +3234,7 @@ export default function PDFViewer({
aria-pressed={tool === "shape"} aria-pressed={tool === "shape"}
aria-haspopup="menu" aria-haspopup="menu"
aria-expanded={shapeMenuOpen} aria-expanded={shapeMenuOpen}
className={`flex items-center gap-0.5 px-2 py-2 transition ${activeCls}`} className={`flex items-center gap-1 whitespace-nowrap px-2 py-2 transition ${activeCls}`}
onClick={(e) => { onClick={(e) => {
openToolMenu(e.currentTarget); openToolMenu(e.currentTarget);
// switchTool เสมอแม้เป็น tool เดิม — เคลียร์ popup บนหน้า (จำ intent ก่อน) // switchTool เสมอแม้เป็น tool เดิม — เคลียร์ popup บนหน้า (จำ intent ก่อน)
@ -3191,6 +3244,7 @@ export default function PDFViewer({
}} }}
> >
<Icon className="h-4 w-4" /> <Icon className="h-4 w-4" />
<span className="text-xs font-medium">{text}</span>
<ChevronDown className="h-3 w-3" /> <ChevronDown className="h-3 w-3" />
</button> </button>
{shapeMenuOpen && {shapeMenuOpen &&
@ -3258,7 +3312,7 @@ export default function PDFViewer({
aria-pressed={tool === "stamp"} aria-pressed={tool === "stamp"}
aria-haspopup="menu" aria-haspopup="menu"
aria-expanded={stampMenuOpen} aria-expanded={stampMenuOpen}
className={`flex items-center gap-0.5 px-2 py-2 transition ${corner} ${activeCls}`} className={`flex items-center gap-1 whitespace-nowrap px-2 py-2 transition ${corner} ${activeCls}`}
onClick={(e) => { onClick={(e) => {
openToolMenu(e.currentTarget); openToolMenu(e.currentTarget);
// switchTool เสมอแม้เป็น tool เดิม — เคลียร์ popup บนหน้า (จำ intent ก่อน) // switchTool เสมอแม้เป็น tool เดิม — เคลียร์ popup บนหน้า (จำ intent ก่อน)
@ -3268,6 +3322,7 @@ export default function PDFViewer({
}} }}
> >
<Icon className="h-4 w-4" /> <Icon className="h-4 w-4" />
<span className="text-xs font-medium">{text}</span>
<ChevronDown className="h-3 w-3" /> <ChevronDown className="h-3 w-3" />
</button> </button>
{stampMenuOpen && {stampMenuOpen &&
@ -3338,7 +3393,7 @@ export default function PDFViewer({
aria-pressed={tool === "textbox"} aria-pressed={tool === "textbox"}
aria-haspopup="menu" aria-haspopup="menu"
aria-expanded={textboxMenuOpen} aria-expanded={textboxMenuOpen}
className={`flex items-center gap-0.5 px-2 py-2 transition ${activeCls}`} className={`flex items-center gap-1 whitespace-nowrap px-2 py-2 transition ${activeCls}`}
onClick={(e) => { onClick={(e) => {
openToolMenu(e.currentTarget); openToolMenu(e.currentTarget);
// switchTool เสมอแม้เป็น tool เดิม — เคลียร์ popup บนหน้า (จำ intent ก่อน) // switchTool เสมอแม้เป็น tool เดิม — เคลียร์ popup บนหน้า (จำ intent ก่อน)
@ -3348,6 +3403,7 @@ export default function PDFViewer({
}} }}
> >
<Icon className="h-4 w-4" /> <Icon className="h-4 w-4" />
<span className="text-xs font-medium">{text}</span>
<ChevronDown className="h-3 w-3" /> <ChevronDown className="h-3 w-3" />
</button> </button>
{textboxMenuOpen && {textboxMenuOpen &&
@ -3452,24 +3508,19 @@ export default function PDFViewer({
title={label} title={label}
aria-label={label} aria-label={label}
aria-pressed={tool === key} aria-pressed={tool === key}
className={`px-2 py-2 transition ${corner} ${activeCls}`} className={`flex items-center gap-1 whitespace-nowrap px-2 py-2 transition ${corner} ${activeCls}`}
onClick={() => switchTool(key)} onClick={() => switchTool(key)}
> >
<Icon className="h-4 w-4" /> <Icon className="h-4 w-4" />
<span className="text-xs font-medium">{text}</span>
</button> </button>
); );
})} })}
</div> </div>
{/* H3 (UX audit 2026-09-16): จอสัมผัสไม่มี hover อ่าน title ไม่ได้ — แสดงชื่อ tool {/* (เดิมมี span แสดงชื่อ tool ที่ active เฉพาะจอสัมผัส — H3 UX audit 2026-09-16
ที่กำลังใช้เป็นข้อความถัดกลุ่มปุ่ม (จอ ≥ lg มี tooltip จาก title อยู่แล้ว) ถอดออก 2026-09-21 เพราะปุ่มทุก tool แสดงชื่อไทยหลังไอคอนแล้ว ส่วนคำเตือน
คำเตือน redaction ย้ายไปเป็นแถบเตือนใต้ header แสดงทุกขนาดจอแทน (H2) */} redaction อยู่ที่แถบเตือนใต้ header แสดงทุกขนาดจอตามเดิม (H2)) */}
<span
aria-hidden
className="hidden max-lg:block shrink-0 text-xs font-medium text-slate-600"
>
{TOOLS.find((t) => t.key === tool)?.label}
</span>
{/* F2: Undo/Redo — ติดกลุ่มปุ่ม tool ในแถวเดียวกัน, disabled ตาม stack {/* F2: Undo/Redo — ติดกลุ่มปุ่ม tool ในแถวเดียวกัน, disabled ตาม stack
M5: จอ < lg สั่ง order-first ให้อยู่หัวแถวเลื่อน — มองเห็นเสมอโดยไม่ต้อง M5: จอ < lg สั่ง order-first ให้อยู่หัวแถวเลื่อน — มองเห็นเสมอโดยไม่ต้อง
@ -3510,7 +3561,7 @@ export default function PDFViewer({
aria-label={`เพิ่ม bookmark หน้า ${currentPage + 1} (B)`} aria-label={`เพิ่ม bookmark หน้า ${currentPage + 1} (B)`}
disabled={!user} disabled={!user}
onClick={openBookmarkComposer} onClick={openBookmarkComposer}
className={`rounded-lg border border-slate-300 px-2 py-2 text-slate-500 hover:bg-slate-100 disabled:opacity-40 ${FOCUS_RING_CLS}`} className={`flex items-center gap-1 whitespace-nowrap rounded-lg border border-slate-300 px-2 py-2 text-slate-500 hover:bg-slate-100 disabled:opacity-40 ${FOCUS_RING_CLS}`}
> >
{/* M6: ✓ เขียว 1.5 วิหลังบันทึกสำเร็จ — sidebar ปิดอยู่ก็รู้ว่าเซฟแล้ว */} {/* M6: ✓ เขียว 1.5 วิหลังบันทึกสำเร็จ — sidebar ปิดอยู่ก็รู้ว่าเซฟแล้ว */}
{bookmarkSaved ? ( {bookmarkSaved ? (
@ -3518,6 +3569,7 @@ export default function PDFViewer({
) : ( ) : (
<BookmarkPlus className="h-4 w-4" /> <BookmarkPlus className="h-4 w-4" />
)} )}
<span className="text-xs font-medium">Bookmark</span>
</button> </button>
{bookmarkComposer && {bookmarkComposer &&
bookmarkPos && bookmarkPos &&
@ -3641,10 +3693,11 @@ export default function PDFViewer({
title="Export PDF พร้อม annotation" title="Export PDF พร้อม annotation"
aria-label="Export PDF พร้อม annotation" aria-label="Export PDF พร้อม annotation"
disabled={exporting} disabled={exporting}
className={`max-lg:shrink-0 rounded-lg border border-slate-300 px-2 py-2 text-slate-500 hover:bg-slate-100 disabled:opacity-50 ${FOCUS_RING_CLS}`} className={`flex items-center gap-1 whitespace-nowrap max-lg:shrink-0 rounded-lg border border-slate-300 px-2 py-2 text-slate-500 hover:bg-slate-100 disabled:opacity-50 ${FOCUS_RING_CLS}`}
onClick={() => void handleExportPdf()} onClick={() => void handleExportPdf()}
> >
<Download className={`h-4 w-4 ${exporting ? "animate-pulse" : ""}`} /> <Download className={`h-4 w-4 ${exporting ? "animate-pulse" : ""}`} />
<span className="text-xs font-medium">Export PDF</span>
</button> </button>
</div> </div>
{/* M5: เงาขวาบอกว่าเลื่อนต่อได้ — หายเมื่อถึงท้ายแถบ (จอ ≥ lg ไม่มีการ scroll) */} {/* M5: เงาขวาบอกว่าเลื่อนต่อได้ — หายเมื่อถึงท้ายแถบ (จอ ≥ lg ไม่มีการ scroll) */}