จอสัมผัส (iOS/Android): แก้ครบตาม UX audit C1+M1–M6 + code review + งาน export PDF ที่ค้างใน working tree:
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 42s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 13s
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 3s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 42s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 13s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 1s
(1) C1 — เมนู dropdown 5 ตัว + popup bookmark เดิมเป็น absolute ใน scroll container ของแถบ tool ที่สลับ overflow ตอนเปิด ทำให้บนมือถือ scroll position รีเซ็ต (ปุ่มเด้งกลับซ้าย) เมนูเปิดนอก viewport และทั้ง app เอียงเลื่อนแนวนอนได้ — ย้ายทั้งหมดไป createPortal(document.body) เป็น position:fixed ตาม rect จริงของปุ่มตอนคลิก (fixedMenuPos + clamp ให้อยู่ในจอ; bookmarkPos จาก ref ปุ่ม 🔖 ใช้ร่วมทุกทางเข้า รวมคีย์ลัด B) + ถอดการสลับ overflow (คง overflow-x-auto เสมอ) + ปิดเมนูเมื่อเลื่อนแถบต่อ หรือ "ความกว้าง" จอเปลี่ยน (code review Major1: resize ที่เปลี่ยนแค่ความสูง เช่น URL bar ยุบ/คีย์บอร์ดของมือถือ ไม่ปิด — เมนู fixed ยึด sticky header อยู่กับที่อยู่แล้ว กันเมนู/popup ที่เปิดค้างปรับค่าโดนปิดกลางคัน) + outside-click ข้ามโหนดใน [data-tool-menu] (ref ของ wrapper มองไม่เห็นโหนดใน portal ไม่ข้ามแล้วคลิกในเมนู=ปิดเมนูเอง) + rule ปุ่ม 36px ใน globals.css เปลี่ยนไป target [data-tool-menu] ตรง ๆ (เดิมเจาะผ่าน .z-50 ที่ไม่มีแล้ว)
(2) M4 — tap/คลิกที่ใช้ "ปิดเมนู" (mousedown ปิดก่อน click ยิงถึง page) กลายเป็นวาง comment/textbox/stamp โดยไม่ตั้งใจ — menuClosedAtRef (markMenuClosed ทุก path ปิด: outside-click/Escape/switchTool เฉพาะเมื่อมีเมนูเปิดจริง/เลื่อนแถบ/resize) แล้ว handlePageClick เฉย 300ms แรก คลิกที่สองค่อยวาง (มาตรฐาน dropdown ใช้ทั้ง desktop+touch — pattern เดียวกับ touchHideTimerRef ของปุ่มไฮไลต์)
(3) M1+M2 ของ TEXTBOX — ทางเข้า editor เคยผูก double-click อย่างเดียว (บน iOS double-tap = zoom หน้า แก้ข้อความไม่ได้เลยตลอดชีวิตบนมือถือ) — เพิ่มปุ่ม "แก้ไขข้อความ" ในแผง selection (onStartEdit → startTextboxEdit) ใช้ได้ทั้ง touch/desktop และ default fontSize 1.2% (≈7px เล็กกว่า preset ต่ำสุด 10px ของตัวเอง) → 2.5% (≈15px อ่านออกทันที) กระทบเฉพาะกล่องใหม่
(4) M3+clamp — hit area บน pointer:coarse โดยของที่มองเห็นคงขนาดเดิม: มุมกรอบใช้ ::after (10→hit 28px), แถบขอบ (8→16px), ปุ่มแผง 20→36px, swatch 16→32px, icon sidebar 16/14→32px (hooks sel-handle/sel-strip/sel-panel/sel-panel-btn/color-swatches + aside[data-sidebar]) และ clamp แนวนอนของแผง selection (left: max(0, min(box.x, size.width-244))) กันรายการชิดขวาทำแผง w-60 ล้นขอบจอแคบจนปุ่มลบหลุดจอ (เจอตอน verify จอ 390px)
(5) M5 — undo/redo max-lg:order-first ไปหัวแถวเลื่อน: เห็นเสมอโดยไม่ต้องเลื่อนแถบ (undo คือทางแก้พลาดหลักของจอสัมผัส) จอ ≥ lg ไม่มี order ลำดับ/หน้าตาเดิมทุกประการ
(6) M6 อื่น ๆ — -webkit-tap-highlight-color: transparent + overscroll-behavior-y: contain (กัน pull-to-refresh ของ Android ล้าง undo history ฝั่ง client), button/label touch-action: manipulation (ตัด double-tap zoom), safe-area inset แบบ defensive (จะมีค่าเมื่อใส่ viewport-fit=cover ในอนาคต), no-callout กัน long-press magnifier เฉพาะ layer TEXTBOX/STAMP (textLayer ยัง select/copy ได้), ปุ่ม composer/popover/pin 24→36px (coarse-36), ปุ่มหน้าสรุป 30→44px (summary-page), ซ่อนชื่อไฟล์บนจอเล็ก, ปุ่ม X ปิด sidebar บน max-lg (เดิมปิดได้ทางเดียวคือจิ้ม backdrop เพราะ overlay z-50 กลบปุ่ม toggle ใน header), tab sidebar เป็นไทย "ที่คั่นหน้า/หมายเหตุ" ให้สอดคล้อง UI ไทยล้วน, chip แจ้ง "หน้าที่เปิดดูเป็นภาพสแกน" เมื่อ tool highlight และทุกหน้าที่ render มี 0 text spans (onTextLayerRendered รายงานต่อหน้า + pageSpanCount)
(7) งาน export PDF ที่อยู่ใน working tree ก่อนหน้า + แก้ตาม code review — COMMENT กลายเป็น sticky note จริง (/Subtype /Text), bookmark → PDF outline (/Dest คำนวณตาม /Rotate เหมือน annotation), โน้ต N1 → sticky note แนบตำแหน่ง, นิยาม yOffset เป็น "ขอบบนของสิ่งที่ดูอยู่" + goToBookmark ใช้ scrollToBelowHeader, และเพิ่ม guard roots.length===0 ใน buildOutline กัน bookmark cycle ไร้ root (DB ถูกแก้ตรง ๆ) ทำ kids[0].ref พังเป็น 500 ทั้ง export
(8) sync .claude/rules/pdf-viewer.md — portal เมนู+ห้ามสลับ overflow ของแถบ, guard 300ms, hit-area hooks ระดับ coarse, TEXTBOX default 2.5% + ปุ่มแก้ไขข้อความ
verify: tsc ผ่าน · vitest 100/100 · Playwright touch emulation จริง (iPhone 390×844 / iPad 820×1180 / desktop 1280): เมนูอยู่ใน viewport ทุกสถานะหลังเลื่อนแถบ + scrollLeft คงที่ + ไม่มี horizontal overflow, แตะในเมนูไม่ปิดเอง, เลื่อนแถบ/กว้างจอเปลี่ยน=ปิด/สูงเปลี่ยน=ค้าง, guard 300ms ทั้ง stamp/textbox/comment, ปุ่มแก้ไขข้อความเปิด editor พร้อมข้อความเดิม, fontSize ใหม่ 14.9px, hit มุม 28px/ปุ่มลบ 36px/swatch 32px, undo อยู่ในจอ + undo/redo ทำงาน, X ปิด sidebar, chip สแกนโชว์/ไฟล์ text ไม่โชว์, desktop เหมือนเดิม, แผง clamp (x=130..370 ปุ่มลบจบ 363) — annotation ทดสอบลบเฉพาะ id ที่สร้างเองทั้งหมด DB กลับสะอาด
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
b3b884c736
commit
df0392ea45
17 changed files with 1085 additions and 215 deletions
|
|
@ -6,7 +6,7 @@ import {
|
|||
useState,
|
||||
type PointerEvent as ReactPointerEvent,
|
||||
} from "react";
|
||||
import { AlignCenter, AlignLeft, AlignRight, BookmarkPlus, Trash2 } from "lucide-react";
|
||||
import { AlignCenter, AlignLeft, AlignRight, BookmarkPlus, Pencil, Trash2 } from "lucide-react";
|
||||
|
||||
import type {
|
||||
AnnotationData,
|
||||
|
|
@ -83,6 +83,7 @@ export default function AnnotationSelectionLayer({
|
|||
onCommit,
|
||||
onDelete,
|
||||
onBookmark,
|
||||
onStartEdit,
|
||||
}: {
|
||||
annotation: AnnotationDTO;
|
||||
/** ขนาด px ของหน้า (จาก width/height ที่คำนวณจาก scale) */
|
||||
|
|
@ -101,6 +102,9 @@ export default function AnnotationSelectionLayer({
|
|||
onDelete: (id: string) => void;
|
||||
/** N2: ปุ่ม bookmark ในแผง — เปิด popup ที่ตำแหน่ง (yOffset %) ของ annotation นี้ */
|
||||
onBookmark: (yOffsetPct: number) => void;
|
||||
/** M1: ปุ่ม "แก้ไขข้อความ" ของ TEXTBOX — เข้าโหมดแก้ไขโดยไม่ต้อง double-click
|
||||
* (จอสัมผัสไม่มี dblclick ที่เชื่อถือได้: iOS double-tap = zoom หน้า) */
|
||||
onStartEdit?: (id: string) => void;
|
||||
}) {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const dragRef = useRef<DragState | null>(null);
|
||||
|
|
@ -401,7 +405,7 @@ export default function AnnotationSelectionLayer({
|
|||
{/* ขอบกรอบ 4 ด้าน — ลาก = translate ทั้งกลุ่ม (touch-none: จอสัมผัสลาก
|
||||
ได้จริง ไม่ถูก browser ยึดเป็น scroll แล้วส่ง pointercancel) */}
|
||||
<div
|
||||
className="absolute cursor-move touch-none"
|
||||
className="absolute cursor-move touch-none sel-strip"
|
||||
style={{
|
||||
left: boxPx.x - STRIP_PX / 2,
|
||||
top: boxPx.y - STRIP_PX / 2,
|
||||
|
|
@ -415,7 +419,7 @@ export default function AnnotationSelectionLayer({
|
|||
onPointerCancel={cancelDrag}
|
||||
/>
|
||||
<div
|
||||
className="absolute cursor-move touch-none"
|
||||
className="absolute cursor-move touch-none sel-strip"
|
||||
style={{
|
||||
left: boxPx.x - STRIP_PX / 2,
|
||||
top: boxPx.y + boxPx.height - STRIP_PX / 2,
|
||||
|
|
@ -429,7 +433,7 @@ export default function AnnotationSelectionLayer({
|
|||
onPointerCancel={cancelDrag}
|
||||
/>
|
||||
<div
|
||||
className={`absolute touch-none ${isFixedTextbox ? "cursor-ew-resize" : "cursor-move"}`}
|
||||
className={`absolute touch-none sel-strip ${isFixedTextbox ? "cursor-ew-resize" : "cursor-move"}`}
|
||||
style={{
|
||||
left: boxPx.x - STRIP_PX / 2,
|
||||
top: boxPx.y,
|
||||
|
|
@ -445,7 +449,7 @@ export default function AnnotationSelectionLayer({
|
|||
onPointerCancel={cancelDrag}
|
||||
/>
|
||||
<div
|
||||
className={`absolute touch-none ${isFixedTextbox ? "cursor-ew-resize" : "cursor-move"}`}
|
||||
className={`absolute touch-none sel-strip ${isFixedTextbox ? "cursor-ew-resize" : "cursor-move"}`}
|
||||
style={{
|
||||
left: boxPx.x + boxPx.width - STRIP_PX / 2,
|
||||
top: boxPx.y,
|
||||
|
|
@ -466,7 +470,7 @@ export default function AnnotationSelectionLayer({
|
|||
{!isFixedTextbox && corners.map((c) => (
|
||||
<div
|
||||
key={c.key}
|
||||
className="absolute touch-none rounded-sm border border-primary bg-primary/50"
|
||||
className="absolute touch-none sel-handle rounded-sm border border-primary bg-primary/50"
|
||||
style={{
|
||||
left: c.x - HANDLE_PX / 2,
|
||||
top: c.y - HANDLE_PX / 2,
|
||||
|
|
@ -487,13 +491,15 @@ export default function AnnotationSelectionLayer({
|
|||
|
||||
{/* แผงเครื่องมือของรายการที่เลือก — ตัวควบคุมเดียวกับ dropdown ของ tool ผู้สร้าง
|
||||
(สี/ความหนา/ขนาด/รูปแบบตามชนิด) + ปุ่มลบท้ายแผง — แสดงทุกชนิดรวม HIGHLIGHT
|
||||
(ไม่มี handle ย้าย/ขยาย แต่เปลี่ยนสี/รูปแบบ/ลบได้) — เฉพาะรายการของตัวเอง */}
|
||||
(ไม่มี handle ย้าย/ขยาย แต่เปลี่ยนสี/รูปแบบ/ลบได้) — เฉพาะรายการของตัวเอง
|
||||
clamp แนวนอน: รายการชิดขวาหน้า (จอแคบ fit-width) ต้องไม่ทำแผง w-60 ล้นขอบขวา
|
||||
จนปุ่มลบ/ตัวควบคุมหลุดจอ (เจอตอน verify จอ 390px — ปุ่มลบครึ่งเดียวจิ้มได้ยาก) */}
|
||||
{editable && !disabled && (
|
||||
<div
|
||||
ref={panelRef}
|
||||
className="absolute w-60 rounded-lg border border-slate-200 bg-white p-1.5 shadow-lg"
|
||||
className="absolute w-60 sel-panel rounded-lg border border-slate-200 bg-white p-1.5 shadow-lg"
|
||||
style={{
|
||||
left: boxPx.x,
|
||||
left: Math.max(0, Math.min(boxPx.x, size.width - 240 - 4)),
|
||||
top: openAbove ? boxPx.y - 6 : boxPx.y + boxPx.height + 4,
|
||||
transform: openAbove ? "translateY(-100%)" : undefined,
|
||||
pointerEvents: "auto",
|
||||
|
|
@ -578,6 +584,18 @@ export default function AnnotationSelectionLayer({
|
|||
)}
|
||||
{textboxData && (
|
||||
<>
|
||||
{/* M1: ทางเข้า editor นอกจาก double-click — แผงโผล่เฉพาะของตัวเองอยู่แล้ว
|
||||
(editable) จึงไม่ต้องเช็ค userId ซ้ำ / root มี stopPropagation คลิกไม่หลุดไป deselect */}
|
||||
<PanelRow label="ข้อความ">
|
||||
<button
|
||||
type="button"
|
||||
className="flex flex-1 items-center justify-center gap-1 rounded-md border border-slate-300 px-2 py-1 text-[11px] font-medium text-slate-600 hover:bg-slate-100"
|
||||
onClick={() => onStartEdit?.(annotation.id)}
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
แก้ไขข้อความ
|
||||
</button>
|
||||
</PanelRow>
|
||||
<PanelRow label="สี">
|
||||
<ColorSwatches
|
||||
value={textboxData.color}
|
||||
|
|
@ -660,7 +678,7 @@ export default function AnnotationSelectionLayer({
|
|||
type="button"
|
||||
aria-label="บันทึก bookmark ตำแหน่งรายการนี้"
|
||||
title="Bookmark ตำแหน่งนี้"
|
||||
className="flex h-5 w-5 items-center justify-center rounded text-slate-400 hover:bg-amber-50 hover:text-amber-500"
|
||||
className="flex h-5 w-5 sel-panel-btn items-center justify-center rounded text-slate-400 hover:bg-amber-50 hover:text-amber-500"
|
||||
onClick={() => onBookmark(boxPct.y)}
|
||||
>
|
||||
<BookmarkPlus className="h-3.5 w-3.5" />
|
||||
|
|
@ -669,7 +687,7 @@ export default function AnnotationSelectionLayer({
|
|||
type="button"
|
||||
aria-label="ลบรายการนี้"
|
||||
title="ลบ"
|
||||
className="flex h-5 w-5 items-center justify-center rounded text-red-500 hover:bg-red-50"
|
||||
className="flex h-5 w-5 sel-panel-btn items-center justify-center rounded text-red-500 hover:bg-red-50"
|
||||
onClick={() => onDelete(annotation.id)}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue