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
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:
parent
e713bcc5e3
commit
f7b5a8488d
2 changed files with 87 additions and 34 deletions
|
|
@ -51,7 +51,7 @@ const TYPE_SHORT: Record<AnnotationType, string> = {
|
|||
DRAW: "เส้นวาด",
|
||||
COMMENT: "คอมเมนต์",
|
||||
SHAPE: "รูปทรง",
|
||||
REDACTION: "ปิดพาง",
|
||||
REDACTION: "ปิดพราง",
|
||||
TEXTBOX: "กล่องข้อความ",
|
||||
STAMP: "ตรายาง",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -356,17 +356,21 @@ function selectionTextWithin(kept: DOMRect[], range: Range): string {
|
|||
return sanitizePdfText(out).slice(0, 5000) || sanitizePdfText(raw.slice(0, 5000));
|
||||
}
|
||||
|
||||
const TOOLS: { key: Tool; label: string; Icon: LucideIcon }[] = [
|
||||
{ key: "select", label: "เลือกข้อความ", Icon: MousePointer2 },
|
||||
{ key: "pan", label: "เลื่อนหน้า (pan)", Icon: Hand },
|
||||
{ key: "highlight", label: "มาร์กข้อความ (H)", Icon: Highlighter },
|
||||
{ key: "draw", label: "วาดเส้น (D)", Icon: Pencil },
|
||||
{ key: "comment", label: "วาง comment (C)", Icon: MessageSquarePlus },
|
||||
{ key: "shape", label: "รูปทรง", Icon: Shapes },
|
||||
{ key: "erase", label: "ลบด้วยการลาก (eraser)", Icon: Eraser },
|
||||
{ key: "redact", label: "ปิดพราง (redaction)", Icon: EyeOff },
|
||||
{ key: "textbox", label: "กล่องข้อความ", Icon: Type },
|
||||
{ key: "stamp", label: "ตรายาง (stamp)", Icon: Stamp },
|
||||
/** label = ชื่อเต็มสำหรับ title/aria-label (พร้อมคีย์ลัด/คำอังกฤษประกอบ) —
|
||||
* text = คำสั้น ๆ แสดงเป็นข้อความหลังไอคอนบนปุ่มทุก tool (คำขอผู้ใช้ 2026-09-21:
|
||||
* ผู้ใช้ศาลไม่คุ้นไอคอนล้วน โดยเฉพาะจอสัมผัสที่ hover อ่าน title ไม่ได้ — แทน span
|
||||
* ชื่อ tool-active ของ H3 ที่ถอดออก) */
|
||||
const TOOLS: { key: Tool; label: string; text: string; Icon: LucideIcon }[] = [
|
||||
{ key: "select", label: "เลือกข้อความ", text: "เลือกข้อความ", Icon: MousePointer2 },
|
||||
{ key: "pan", label: "เลื่อนหน้า (pan)", text: "เลื่อนหน้า", Icon: Hand },
|
||||
{ key: "highlight", label: "มาร์กข้อความ (H)", text: "มาร์กข้อความ", Icon: Highlighter },
|
||||
{ key: "draw", label: "วาดเส้น (D)", text: "วาดเส้น", Icon: Pencil },
|
||||
{ key: "comment", label: "วางคอมเมนต์ (C)", text: "วางคอมเมนต์", Icon: MessageSquarePlus },
|
||||
{ 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
|
||||
|
|
@ -1361,6 +1365,12 @@ export default function PDFViewer({
|
|||
textboxMenuOpen ||
|
||||
!!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 จะเพี้ยน
|
||||
// จากปุ่มทันทีที่แถบเลื่อน (header sticky → เลื่อนหน้าเอกสารไม่กระทบ ปุ่มไม่ขยับเทียบ viewport)
|
||||
// Major 1 (code review): resize ของมือถือจาก URL bar/คีย์บอร์ดเปลี่ยนแค่ความสูงและเมนูยัง
|
||||
|
|
@ -1379,14 +1389,52 @@ export default function PDFViewer({
|
|||
const onResize = () => {
|
||||
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);
|
||||
return () => {
|
||||
toolbarRowRef.current?.removeEventListener("scroll", close);
|
||||
toolbarRowRef.current?.removeEventListener("scroll", onScroll);
|
||||
window.removeEventListener("resize", onResize);
|
||||
};
|
||||
}, [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 ตอน
|
||||
// tool highlight ว่าไฟล์นี้คลุมข้อความไม่ได้ / เขียนค่าเดิมคืน object เดิมให้ callback
|
||||
// identity คงที่ (useCallback []) กัน effect render ของทุกหน้ารันซ้ำเพราะ prop เปลี่ยน
|
||||
|
|
@ -2948,13 +2996,16 @@ export default function PDFViewer({
|
|||
</div>
|
||||
|
||||
<div className="flex flex-1 items-center justify-end gap-2">
|
||||
{/* F15/D2: หน้าสรุปหมายเหตุทั้งไฟล์ */}
|
||||
{/* F15/D2: หน้าสรุปหมายเหตุทั้งไฟล์ — label แสดงตั้งแต่ sm ขึ้นไปเท่านั้น
|
||||
(จอแคบกว่านั้นแถว title ใส่ไม่พอ ให้เหลือ icon เดี่ยวเหมือนชื่อไฟล์ที่ hidden sm:block) */}
|
||||
<Link
|
||||
href={`/annotate/${file.id}/summary`}
|
||||
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" />
|
||||
<span className="hidden text-xs font-medium sm:inline">หน้าสรุป</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -2977,7 +3028,7 @@ export default function PDFViewer({
|
|||
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">
|
||||
{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 activeCls = `${
|
||||
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-haspopup="menu"
|
||||
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) => {
|
||||
openToolMenu(e.currentTarget);
|
||||
// เรียก switchTool เสมอแม้เป็น tool เดิม — เคลียร์ popup บนหน้าที่ค้างเปิด
|
||||
|
|
@ -3008,6 +3059,7 @@ export default function PDFViewer({
|
|||
}}
|
||||
>
|
||||
<CurrentIcon className="h-4 w-4" />
|
||||
<span className="text-xs font-medium">{text}</span>
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
</button>
|
||||
{highlightMenuOpen &&
|
||||
|
|
@ -3079,7 +3131,7 @@ export default function PDFViewer({
|
|||
aria-pressed={tool === "draw"}
|
||||
aria-haspopup="menu"
|
||||
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) => {
|
||||
openToolMenu(e.currentTarget);
|
||||
// switchTool เสมอแม้เป็น tool เดิม — เคลียร์ popup บนหน้า (จำ intent ก่อน)
|
||||
|
|
@ -3089,6 +3141,7 @@ export default function PDFViewer({
|
|||
}}
|
||||
>
|
||||
<CurrentPenIcon className="h-4 w-4" />
|
||||
<span className="text-xs font-medium">{text}</span>
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
</button>
|
||||
{drawMenuOpen &&
|
||||
|
|
@ -3181,7 +3234,7 @@ export default function PDFViewer({
|
|||
aria-pressed={tool === "shape"}
|
||||
aria-haspopup="menu"
|
||||
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) => {
|
||||
openToolMenu(e.currentTarget);
|
||||
// switchTool เสมอแม้เป็น tool เดิม — เคลียร์ popup บนหน้า (จำ intent ก่อน)
|
||||
|
|
@ -3191,6 +3244,7 @@ export default function PDFViewer({
|
|||
}}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
<span className="text-xs font-medium">{text}</span>
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
</button>
|
||||
{shapeMenuOpen &&
|
||||
|
|
@ -3258,7 +3312,7 @@ export default function PDFViewer({
|
|||
aria-pressed={tool === "stamp"}
|
||||
aria-haspopup="menu"
|
||||
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) => {
|
||||
openToolMenu(e.currentTarget);
|
||||
// switchTool เสมอแม้เป็น tool เดิม — เคลียร์ popup บนหน้า (จำ intent ก่อน)
|
||||
|
|
@ -3268,6 +3322,7 @@ export default function PDFViewer({
|
|||
}}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
<span className="text-xs font-medium">{text}</span>
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
</button>
|
||||
{stampMenuOpen &&
|
||||
|
|
@ -3338,7 +3393,7 @@ export default function PDFViewer({
|
|||
aria-pressed={tool === "textbox"}
|
||||
aria-haspopup="menu"
|
||||
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) => {
|
||||
openToolMenu(e.currentTarget);
|
||||
// switchTool เสมอแม้เป็น tool เดิม — เคลียร์ popup บนหน้า (จำ intent ก่อน)
|
||||
|
|
@ -3348,6 +3403,7 @@ export default function PDFViewer({
|
|||
}}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
<span className="text-xs font-medium">{text}</span>
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
</button>
|
||||
{textboxMenuOpen &&
|
||||
|
|
@ -3452,24 +3508,19 @@ export default function PDFViewer({
|
|||
title={label}
|
||||
aria-label={label}
|
||||
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)}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
<span className="text-xs font-medium">{text}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* H3 (UX audit 2026-09-16): จอสัมผัสไม่มี hover อ่าน title ไม่ได้ — แสดงชื่อ tool
|
||||
ที่กำลังใช้เป็นข้อความถัดกลุ่มปุ่ม (จอ ≥ lg มี tooltip จาก title อยู่แล้ว)
|
||||
คำเตือน 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>
|
||||
{/* (เดิมมี span แสดงชื่อ tool ที่ active เฉพาะจอสัมผัส — H3 UX audit 2026-09-16
|
||||
ถอดออก 2026-09-21 เพราะปุ่มทุก tool แสดงชื่อไทยหลังไอคอนแล้ว ส่วนคำเตือน
|
||||
redaction อยู่ที่แถบเตือนใต้ header แสดงทุกขนาดจอตามเดิม (H2)) */}
|
||||
|
||||
{/* F2: Undo/Redo — ติดกลุ่มปุ่ม tool ในแถวเดียวกัน, disabled ตาม stack
|
||||
M5: จอ < lg สั่ง order-first ให้อยู่หัวแถวเลื่อน — มองเห็นเสมอโดยไม่ต้อง
|
||||
|
|
@ -3510,7 +3561,7 @@ export default function PDFViewer({
|
|||
aria-label={`เพิ่ม bookmark หน้า ${currentPage + 1} (B)`}
|
||||
disabled={!user}
|
||||
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 ปิดอยู่ก็รู้ว่าเซฟแล้ว */}
|
||||
{bookmarkSaved ? (
|
||||
|
|
@ -3518,6 +3569,7 @@ export default function PDFViewer({
|
|||
) : (
|
||||
<BookmarkPlus className="h-4 w-4" />
|
||||
)}
|
||||
<span className="text-xs font-medium">Bookmark</span>
|
||||
</button>
|
||||
{bookmarkComposer &&
|
||||
bookmarkPos &&
|
||||
|
|
@ -3641,10 +3693,11 @@ export default function PDFViewer({
|
|||
title="Export PDF พร้อม annotation"
|
||||
aria-label="Export PDF พร้อม annotation"
|
||||
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()}
|
||||
>
|
||||
<Download className={`h-4 w-4 ${exporting ? "animate-pulse" : ""}`} />
|
||||
<span className="text-xs font-medium">Export PDF</span>
|
||||
</button>
|
||||
</div>
|
||||
{/* M5: เงาขวาบอกว่าเลื่อนต่อได้ — หายเมื่อถึงท้ายแถบ (จอ ≥ lg ไม่มีการ scroll) */}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue