toolbar: ปุ่ม tool dropdown แสดงค่าที่เลือกเฉพาะตอน active + ฟื้น effect เลื่อนปุ่ม active กลับกึ่งกลางแถบ (เสียเงียบมาตั้งแต่ 2026-09-23) — (1) ปัญหา: คำขอผู้ใช้ 2026-09-23 เดิมให้ปุ่ม dropdown ที่มีค่าย่อย (มาร์กข้อความ/วาดเส้น/รูปทรง/ตรายาง) แสดง "ชื่อตัวเลือกล่าสุด" แทนชื่อ tool เสมอเพื่อเห็นค่าปัจจุบันโดยไม่ต้องเปิดเมนู — แต่ทำให้ชื่อ tool หายไปจาก toolbar ทั้งหมด: ปุ่ม "วาดเส้น" แสดง "ปากกา" ตั้งแต่ยังไม่ได้เลือกอะไรเลย (default drawPen=pencil) และค้างอยู่อย่างนั้นแม้สลับไป tool อื่น (ผู้ใช้รายงาน 2026-09-29: "label ของ tool ทำไมไม่เปลี่ยนทั้งที่ยังไม่ได้เลือน เช่น วาด แต่แสดง ปากกาเลย") — ขัดเหตุผลเดิมของการใส่ข้อความบนปุ่ม (2026-09-21: ผู้ใช้ศาลไม่คุ้น icon ล้วน) เพราะมองคำ "ปากกา" แล้วไม่รู้ว่านี่คือปุ่มวาดเส้น (2) แก้ตามคำขอผู้ใช้ 2026-09-29 (เลือกจาก 3 ทางเลือกที่เสนอ): tool ไม่ active → แสดง text ชื่อ tool + Icon ของ tool, title/aria-label = label เต็มเหมือนปุ่มเดี่ยวอื่น; tool active → แสดงค่าที่เลือกไว้ (ค่าที่จะใช้จริงเมื่อวาด) + icon ของค่า (draw/highlight เปลี่ยน icon ตามชนิดปากกา/รูปแบบมาร์ก, shape/stamp ใช้ tool icon เหมือนเดิม), title = "label — ค่า" ตาม pattern เดิม — กดปุ่ม dropdown เรียก switchTool เสมออยู่แล้ว (เคลียร์ popup) ปุ่มจึงสลับไปแสดงค่าทันทีที่เปิดเมนู สอดคล้องกับ "กำลังจะใช้ค่านี้" — ปุ่ม active กว้าง/แคบตามความยาวค่า ("ปากกา" สั้นกว่า "วาดเส้น") แถบรองรับอยู่แล้ว (whitespace-nowrap + max-lg:overflow-x-auto) (3) แก้ bug แฝงที่เจอระหว่างแก้: effect เลื่อนปุ่ม tool active กลับกึ่งกลางแถบบนจอ < lg (UX review A2 2026-09-21) ใช้ querySelector(button[title="..."]) exact-match ซึ่งไม่เจอปุ่ม dropdown ทั้ง 4 มาตั้งแต่ 2026-09-23 เมื่อ title ถูกเติม " — ค่าย่อย" ต่อท้าย — ฟีเจอร์เลื่อนปุ่มกลับจึงเสียเงียบ ๆ กับ 4 ปุ่มนี้ตลอดมา — เพิ่ม data-tool={key} ที่ปุ่ม tool ทุกตัว (ครบ 10/10 ค่าของ union Tool ไม่ซ้ำ) แล้ว query ด้วย button[data-tool="..."] แทน title (ตัด TOOLS.find ออกจาก effect — TOOLS ยังถูกใช้ที่ visibleTools จึงไม่ orphan) (4) แก้ตาม code review 2026-09-29: aria-pressed ของปุ่ม dropdown 4 ตัวใช้ตัวแปร active ใหม่ (const active = tool === key ประกาศครั้งเดียวใน map ใช้ร่วมกับ activeCls) แทน tool === "…" ซ้ำ (ค่าเท่ากันเป๊ะ branch บอง key) + sync rules/pdf-viewer.md ประโยค "ปุ่ม tool แสดง label กำหนดเอง" ให้ระบุเงื่อนไขเฉพาะตอน tool active ตรงโค้ดปัจจุบัน — ผ่าน code review (ไม่มี bug ระดับต้องแก้: active/activeCls equivalent ทุก branch, nested ternary stamp parse ถูก, ตำแหน่งเมนูไม่เพี้ยนเพราะ openToolMenu อ่าน getBoundingClientRect ก่อน React commit setState ที่ batch ไว้ + เมนูยึดขอบซ้ายปุ่ม, effect A2 กลับมาทำงานพร้อม guard เดิมครบ anyMenuOpen/selfScrollUntilRef) + tsc + vitest 193/193 (คำขอผู้ใช้ 2026-09-29)
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 3s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 45s
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 0s

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-09-29 10:51:50 +07:00
parent 651e354cad
commit 8571ef9aaf
2 changed files with 55 additions and 32 deletions

File diff suppressed because one or more lines are too long

View file

@ -380,8 +380,10 @@ function selectionTextWithin(kept: DOMRect[], range: Range): string {
* text = คำสั้น ๆ แสดงเป็นข้อความหลังไอคอนบนปุ่ม (คำขอผู้ใช้ 2026-09-21: ผู้ใช้ศาล * text = คำสั้น ๆ แสดงเป็นข้อความหลังไอคอนบนปุ่ม (คำขอผู้ใช้ 2026-09-21: ผู้ใช้ศาล
* ไม่คุ้นไอคอนล้วน โดยเฉพาะจอสัมผัสที่ hover อ่าน title ไม่ได้ — แทน span ชื่อ * ไม่คุ้นไอคอนล้วน โดยเฉพาะจอสัมผัสที่ hover อ่าน title ไม่ได้ — แทน span ชื่อ
* tool-active ของ H3 ที่ถอดออก) — สำหรับปุ่ม dropdown ที่มีตัวเลือกย่อย (มาร์ก * tool-active ของ H3 ที่ถอดออก) — สำหรับปุ่ม dropdown ที่มีตัวเลือกย่อย (มาร์ก
* ข้อความ/วาดเส้น/รูปทรง/ตรายาง) ข้อความบนปุ่มแสดงชื่อตัวเลือกล่าสุดแทน text เพื่อ * ข้อความ/วาดเส้น/รูปทรง/ตรายาง) เคยแสดงชื่อตัวเลือกล่าสุดแทน text ตลอด (คำขอผู้ใช้
* เห็นค่าปัจจุบันโดยไม่ต้องเปิดเมนู (คำขอผู้ใช้ 2026-09-23) */ * 2026-09-23) แต่ทำให้ชื่อ tool หายไปจากแถบ ("วาดเส้น" เห็นเป็น "ปากกา" ตั้งแต่
* ยังไม่ได้เลือกอะไร) — 2026-09-29 จึงแสดงค่าล่าสุด**เฉพาะตอน tool active**
* (ค่าที่จะใช้จริงเมื่อวาด) ไม่ active กลับมาแสดง text ชื่อ tool เหมือนปุ่มอื่น */
const TOOLS: { key: Tool; label: string; text: string; Icon: LucideIcon }[] = [ const TOOLS: { key: Tool; label: string; text: string; Icon: LucideIcon }[] = [
{ key: "select", label: "เลือก", text: "เลือก", Icon: MousePointer2 }, { key: "select", label: "เลือก", text: "เลือก", Icon: MousePointer2 },
{ key: "pan", label: "เลื่อนหน้า (pan)", text: "เลื่อนหน้า", Icon: Hand }, { key: "pan", label: "เลื่อนหน้า (pan)", text: "เลื่อนหน้า", Icon: Hand },
@ -1571,9 +1573,11 @@ export default function PDFViewer({
// ธรรมดาจะพลาดจอกว้างเศษส่วน เช่น 1023.5px จาก display scaling 125%/150% // ธรรมดาจะพลาดจอกว้างเศษส่วน เช่น 1023.5px จาก display scaling 125%/150%
if (!window.matchMedia("not all and (min-width: 1024px)").matches) return; if (!window.matchMedia("not all and (min-width: 1024px)").matches) return;
const row = toolbarRowRef.current; const row = toolbarRowRef.current;
const entry = TOOLS.find((t) => t.key === tool); if (!row) return;
if (!row || !entry) return; // หาปุ่มด้วย data-tool — ห้ามยึด title ของปุ่ม (title ของปุ่ม dropdown เปลี่ยนตาม
const btn = row.querySelector<HTMLButtonElement>(`button[title="${entry.label}"]`); // ค่าที่เลือก/สถานะ active ไม่ match แบบ exact — เคยเสียเงียบ ๆ ตั้งแต่ suffix
// " — ค่าย่อย" ถูกใส่ใน title 2026-09-23)
const btn = row.querySelector<HTMLButtonElement>(`button[data-tool="${tool}"]`);
if (!btn) return; if (!btn) return;
const b = btn.getBoundingClientRect(); const b = btn.getBoundingClientRect();
const r = row.getBoundingClientRect(); const r = row.getBoundingClientRect();
@ -3374,23 +3378,31 @@ export default function PDFViewer({
// มุมปุ่มปลายแถวต้องน้อยกว่า/เท่ากับ container (rounded-md 6px) — ใส่ lg // มุมปุ่มปลายแถวต้องน้อยกว่า/เท่ากับ container (rounded-md 6px) — ใส่ lg
// 8px แล้ว active bg โป๊ะชนเส้นโค้งของ container (UX review m4 2026-09-23) // 8px แล้ว active bg โป๊ะชนเส้นโค้งของ container (UX review m4 2026-09-23)
const corner = i === 0 ? "rounded-l-md" : i === visibleTools.length - 1 ? "rounded-r-md" : ""; const corner = i === 0 ? "rounded-l-md" : i === visibleTools.length - 1 ? "rounded-r-md" : "";
// tool นี้กำลัง active ไหม — ปุ่ม dropdown ที่มีค่าย่อย (มาร์กข้อความ/
// วาดเส้น/รูปทรง/ตรายาง) ใช้ตัดสินว่าจะแสดงชื่อ tool (ไม่ active) หรือค่า
// ที่เลือกไว้ (active = ค่าที่จะใช้จริงเมื่อวาด) — คำขอผู้ใช้ 2026-09-29
const active = tool === key;
const activeCls = `${ const activeCls = `${
tool === key ? "bg-primary text-white" : "text-ink-soft hover:bg-ink/[0.05]" active ? "bg-primary text-white" : "text-ink-soft hover:bg-ink/[0.05]"
} ${FOCUS_RING_CLS}`; } ${FOCUS_RING_CLS}`;
// Highlight — dropdown เลือกรูปแบบมาร์ก (4 แบบ) + สี; คลุดข้อความแล้วสร้างทันที // Highlight — dropdown เลือกรูปแบบมาร์ก (4 แบบ) + สี; คลุดข้อความแล้วสร้างทันที
if (key === "highlight") { if (key === "highlight") {
const btnText = const btnText = active
HIGHLIGHT_STYLE_BTNS.find((s) => s.key === highlightStyle)?.label ?? text; ? (HIGHLIGHT_STYLE_BTNS.find((s) => s.key === highlightStyle)?.label ?? text)
const CurrentIcon = : text;
HIGHLIGHT_STYLE_BTNS.find((s) => s.key === highlightStyle)?.Icon ?? Icon; const CurrentIcon = active
? (HIGHLIGHT_STYLE_BTNS.find((s) => s.key === highlightStyle)?.Icon ?? Icon)
: Icon;
const btnTitle = active ? `${label} — ${btnText}` : label;
return ( return (
<div key={key} ref={highlightMenuRef} className="relative"> <div key={key} ref={highlightMenuRef} className="relative">
<button <button
type="button" type="button"
title={`${label} — ${btnText}`} data-tool={key}
aria-label={`${label} — ${btnText}`} title={btnTitle}
aria-pressed={tool === "highlight"} aria-label={btnTitle}
aria-pressed={active}
aria-haspopup="menu" aria-haspopup="menu"
aria-expanded={highlightMenuOpen} aria-expanded={highlightMenuOpen}
className={`${corner} flex items-center gap-1 whitespace-nowrap px-2 py-2 transition ${activeCls}`} className={`${corner} flex items-center gap-1 whitespace-nowrap px-2 py-2 transition ${activeCls}`}
@ -3472,15 +3484,17 @@ export default function PDFViewer({
// DRAW: ปุ่มวาดเส้น = dropdown เลือกชนิดปากกา (ปากกา/ไฮไลต์) // DRAW: ปุ่มวาดเส้น = dropdown เลือกชนิดปากกา (ปากกา/ไฮไลต์)
if (key === "draw") { if (key === "draw") {
const CurrentPenIcon = DRAW_PEN_ICONS[drawPen]; const CurrentPenIcon = active ? DRAW_PEN_ICONS[drawPen] : Icon;
const btnText = DRAW_PENS[drawPen].label; const btnText = active ? DRAW_PENS[drawPen].label : text;
const btnTitle = active ? `${label} — ${btnText}` : label;
return ( return (
<div key={key} ref={drawMenuRef} className="relative"> <div key={key} ref={drawMenuRef} className="relative">
<button <button
type="button" type="button"
title={`${label} — ${btnText}`} data-tool={key}
aria-label={`${label} — ${btnText}`} title={btnTitle}
aria-pressed={tool === "draw"} aria-label={btnTitle}
aria-pressed={active}
aria-haspopup="menu" aria-haspopup="menu"
aria-expanded={drawMenuOpen} aria-expanded={drawMenuOpen}
className={`${corner} flex items-center gap-1 whitespace-nowrap px-2 py-2 transition ${activeCls}`} className={`${corner} flex items-center gap-1 whitespace-nowrap px-2 py-2 transition ${activeCls}`}
@ -3580,15 +3594,18 @@ export default function PDFViewer({
// F5/B1: ปุ่มรูปทรง = dropdown เลือกชนิดรูปทรง // F5/B1: ปุ่มรูปทรง = dropdown เลือกชนิดรูปทรง
if (key === "shape") { if (key === "shape") {
const btnText = const btnText = active
SHAPE_KIND_BTNS.find((k) => k.key === shapeKind)?.label ?? text; ? (SHAPE_KIND_BTNS.find((k) => k.key === shapeKind)?.label ?? text)
: text;
const btnTitle = active ? `${label} — ${btnText}` : label;
return ( return (
<div key={key} ref={shapeMenuRef} className="relative"> <div key={key} ref={shapeMenuRef} className="relative">
<button <button
type="button" type="button"
title={`${label} — ${btnText}`} data-tool={key}
aria-label={`${label} — ${btnText}`} title={btnTitle}
aria-pressed={tool === "shape"} aria-label={btnTitle}
aria-pressed={active}
aria-haspopup="menu" aria-haspopup="menu"
aria-expanded={shapeMenuOpen} aria-expanded={shapeMenuOpen}
className={`${corner} flex items-center gap-1 whitespace-nowrap px-2 py-2 transition ${activeCls}`} className={`${corner} flex items-center gap-1 whitespace-nowrap px-2 py-2 transition ${activeCls}`}
@ -3663,12 +3680,15 @@ export default function PDFViewer({
); );
} }
// F18/B5: ปุ่มตรายาง = dropdown เลือก preset — เลือกคำกำหนดเองแล้วปุ่มแสดง // F18/B5: ปุ่มตรายาง = dropdown เลือก preset — ตอน active เลือกคำกำหนด
// "กำหนดเอง" แทนคำเต็ม (คำจริงเห็นในเมนู/preview ตอนเลือกอยู่แล้ว) // เองแล้วปุ่มแสดง "กำหนดเอง" แทนคำเต็ม (คำจริงเห็นในเมนู/preview ตอนเลือก)
if (key === "stamp") { if (key === "stamp") {
const btnText = STAMP_PRESETS.includes(stampPreset) const btnText = active
? STAMP_PRESETS.includes(stampPreset)
? stampPreset ? stampPreset
: "กำหนดเอง"; : "กำหนดเอง"
: text;
const btnTitle = active ? `${label} — ${btnText}` : label;
/** จบหน้ากำหนดเองแบบเลือกค่าจบ (ปุ่ม "ใช้ตรายางนี้"/Enter): ช่องว่างล้วน = /** จบหน้ากำหนดเองแบบเลือกค่าจบ (ปุ่ม "ใช้ตรายางนี้"/Enter): ช่องว่างล้วน =
* กลับ preset แรก ให้สถานะทุกจุด (ปุ่ม tool/แถวเมนู/ตอนวาง) สอดคล้องกัน — * กลับ preset แรก ให้สถานะทุกจุด (ปุ่ม tool/แถวเมนู/ตอนวาง) สอดคล้องกัน —
* ปิดเมนูด้วย setState ตรง ไม่ผ่าน markMenuClosed ให้แตะหน้าวางได้ทันที */ * ปิดเมนูด้วย setState ตรง ไม่ผ่าน markMenuClosed ให้แตะหน้าวางได้ทันที */
@ -3680,9 +3700,10 @@ export default function PDFViewer({
<div key={key} ref={stampMenuRef} className="relative"> <div key={key} ref={stampMenuRef} className="relative">
<button <button
type="button" type="button"
title={`${label} — ${btnText}`} data-tool={key}
aria-label={`${label} — ${btnText}`} title={btnTitle}
aria-pressed={tool === "stamp"} aria-label={btnTitle}
aria-pressed={active}
aria-haspopup="menu" aria-haspopup="menu"
aria-expanded={stampMenuOpen} aria-expanded={stampMenuOpen}
className={`flex items-center gap-1 whitespace-nowrap px-2 py-2 transition ${corner} ${activeCls}`} className={`flex items-center gap-1 whitespace-nowrap px-2 py-2 transition ${corner} ${activeCls}`}
@ -3907,6 +3928,7 @@ export default function PDFViewer({
<div key={key} ref={textboxMenuRef} className="relative"> <div key={key} ref={textboxMenuRef} className="relative">
<button <button
type="button" type="button"
data-tool={key}
title={label} title={label}
aria-label={label} aria-label={label}
aria-pressed={tool === "textbox"} aria-pressed={tool === "textbox"}
@ -4024,6 +4046,7 @@ export default function PDFViewer({
<button <button
key={key} key={key}
type="button" type="button"
data-tool={key}
title={label} title={label}
aria-label={label} aria-label={label}
aria-pressed={tool === key} aria-pressed={tool === key}