จอสัมผัส (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
|
|
@ -49,7 +49,7 @@ import type {
|
|||
import { annotationNote, annotationPreviewText } from "@/utils/annotation-display";
|
||||
|
||||
function TypeIcon({ type, data }: { type: AnnotationDTO["type"]; data: AnnotationDTO["data"] }) {
|
||||
const cls = "h-4 w-4 shrink-0 text-slate-400";
|
||||
const cls = "h-4 w-4 shrink-0 text-slate-500";
|
||||
if (type === "HIGHLIGHT") {
|
||||
const style = (data as HighlightData).style ?? "highlight";
|
||||
if (style === "underline") return <Underline className={cls} />;
|
||||
|
|
@ -376,7 +376,7 @@ export default function AnnotationSidebar({
|
|||
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"
|
||||
/>
|
||||
<p className="mt-1 text-[10px] text-slate-400">วางที่</p>
|
||||
<p className="mt-1 text-[11px] text-slate-500">วางที่</p>
|
||||
<Select
|
||||
value={eb.parentId ?? ""}
|
||||
onChange={(e) =>
|
||||
|
|
@ -415,7 +415,7 @@ export default function AnnotationSidebar({
|
|||
<button
|
||||
type="button"
|
||||
aria-label={collapsed ? "กางหัวข้อ" : "หุบหัวข้อ"}
|
||||
className="shrink-0 text-slate-400 hover:text-slate-600"
|
||||
className="shrink-0 text-slate-500 hover:text-slate-600"
|
||||
onClick={() => toggleCollapsed(b.id)}
|
||||
>
|
||||
{collapsed ? (
|
||||
|
|
@ -445,10 +445,10 @@ export default function AnnotationSidebar({
|
|||
>
|
||||
{b.title}
|
||||
{hasChildren && (
|
||||
<span className="ml-1 font-normal text-slate-400">({children.length})</span>
|
||||
<span className="ml-1 font-normal text-slate-500">({children.length})</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="block text-[10px] text-slate-400">หน้า {b.pageIndex + 1}</span>
|
||||
<span className="block text-[11px] text-slate-500">หน้า {b.pageIndex + 1}</span>
|
||||
{b.note && (
|
||||
<span className="mt-0.5 block line-clamp-2 whitespace-pre-wrap break-words text-[11px] text-slate-500">
|
||||
{b.note}
|
||||
|
|
@ -462,7 +462,7 @@ export default function AnnotationSidebar({
|
|||
type="button"
|
||||
aria-label="แก้ไข bookmark"
|
||||
title="แก้ชื่อ/โน้ต/ย้ายหัวข้อ"
|
||||
className="shrink-0 text-slate-300 hover:text-slate-500 group-hover:text-slate-400"
|
||||
className="shrink-0 text-slate-400 hover:text-slate-600 group-hover:text-slate-500"
|
||||
onClick={() => startBookmarkEdit(b)}
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
|
|
@ -470,7 +470,7 @@ export default function AnnotationSidebar({
|
|||
<button
|
||||
type="button"
|
||||
aria-label="ลบ bookmark"
|
||||
className="shrink-0 text-slate-300 hover:text-red-500 group-hover:text-slate-400"
|
||||
className="shrink-0 text-slate-400 hover:text-red-500 group-hover:text-slate-500"
|
||||
onClick={() => onDeleteBookmark(b.id)}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
|
|
@ -483,7 +483,7 @@ export default function AnnotationSidebar({
|
|||
type="button"
|
||||
aria-label="เพิ่มหน้าที่ดูอยู่เป็น sub ของหัวข้อนี้"
|
||||
title="เพิ่มหน้าที่ดูอยู่เป็น sub"
|
||||
className="shrink-0 text-slate-300 hover:text-emerald-600 group-hover:text-slate-400"
|
||||
className="shrink-0 text-slate-400 hover:text-emerald-600 group-hover:text-slate-500"
|
||||
onClick={() => onAddSubBookmark(b.id)}
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
|
|
@ -526,7 +526,7 @@ export default function AnnotationSidebar({
|
|||
<TypeIcon type={a.type} data={a.data} />
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-xs">{annotationPreviewText(a)}</span>
|
||||
<span className="block text-[11px] text-slate-400">
|
||||
<span className="block text-[11px] text-slate-500">
|
||||
หน้า {a.pageIndex + 1} · {a.userName}
|
||||
{a.type === "COMMENT" && (replyCounts[a.id] ?? 0) > 0
|
||||
? ` · ตอบ ${replyCounts[a.id]}`
|
||||
|
|
@ -544,7 +544,7 @@ export default function AnnotationSidebar({
|
|||
? "text-emerald-500"
|
||||
: copyFailedId === a.id
|
||||
? "text-red-500"
|
||||
: "text-slate-300 hover:text-slate-500 group-hover:text-slate-400"
|
||||
: "text-slate-400 hover:text-slate-600 group-hover:text-slate-500"
|
||||
}`}
|
||||
onClick={() => copyAnnotationLink(a)}
|
||||
>
|
||||
|
|
@ -567,7 +567,7 @@ export default function AnnotationSidebar({
|
|||
className={`shrink-0 ${
|
||||
note
|
||||
? "text-amber-400 hover:text-amber-500"
|
||||
: "text-slate-300 hover:text-slate-500 group-hover:text-slate-400"
|
||||
: "text-slate-400 hover:text-slate-600 group-hover:text-slate-500"
|
||||
}`}
|
||||
onClick={() => (editingNote ? setNoteFor(null) : startNoteEdit(a))}
|
||||
>
|
||||
|
|
@ -579,7 +579,7 @@ export default function AnnotationSidebar({
|
|||
type="button"
|
||||
aria-label="ดูประวัติการเปลี่ยนแปลง"
|
||||
title="ประวัติ"
|
||||
className="shrink-0 text-slate-300 hover:text-slate-500 group-hover:text-slate-400"
|
||||
className="shrink-0 text-slate-400 hover:text-slate-600 group-hover:text-slate-500"
|
||||
onClick={() => void openHistory(a)}
|
||||
>
|
||||
<Clock className="h-4 w-4" />
|
||||
|
|
@ -587,7 +587,7 @@ export default function AnnotationSidebar({
|
|||
<button
|
||||
type="button"
|
||||
aria-label="ลบ annotation"
|
||||
className="shrink-0 text-slate-300 hover:text-red-500 group-hover:text-slate-400"
|
||||
className="shrink-0 text-slate-400 hover:text-red-500 group-hover:text-slate-500"
|
||||
onClick={() => onDeleteAnnotation(a.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
|
|
@ -665,7 +665,7 @@ export default function AnnotationSidebar({
|
|||
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-400" />
|
||||
<Search className="pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-slate-500" />
|
||||
<input
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
|
|
@ -685,13 +685,13 @@ export default function AnnotationSidebar({
|
|||
|
||||
{searchResults !== null && (
|
||||
<div className="shrink-0 border-b border-slate-200 p-2">
|
||||
<p className="px-1 pb-1 text-[10px] font-semibold uppercase tracking-wide text-slate-400">
|
||||
<p className="px-1 pb-1 text-[11px] font-semibold text-slate-500">
|
||||
{searchResults.length > 0
|
||||
? `ผลการค้นหา (${searchResults.length})`
|
||||
: "ผลการค้นหา"}
|
||||
</p>
|
||||
{searchResults.length === 0 ? (
|
||||
<p className="px-1 pb-1 text-xs text-slate-400">ไม่พบ</p>
|
||||
<p className="px-1 pb-1 text-xs text-slate-500">ไม่พบ</p>
|
||||
) : (
|
||||
<ul className="max-h-40 space-y-1 overflow-y-auto">
|
||||
{searchResults.map((a) => (
|
||||
|
|
@ -708,7 +708,7 @@ export default function AnnotationSidebar({
|
|||
<TypeIcon type={a.type} data={a.data} />
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-xs">{annotationPreviewText(a)}</span>
|
||||
<span className="block text-[11px] text-slate-400">
|
||||
<span className="block text-[11px] text-slate-500">
|
||||
หน้า {a.pageIndex + 1} · {a.userName}
|
||||
</span>
|
||||
</span>
|
||||
|
|
@ -723,8 +723,8 @@ export default function AnnotationSidebar({
|
|||
<div className="flex shrink-0 border-b border-slate-200">
|
||||
{(
|
||||
[
|
||||
["bookmarks", `Bookmarks (${bookmarks.length})`],
|
||||
["annotations", `Annotations (${annotations.length})`],
|
||||
["bookmarks", `ที่คั่นหน้า (${bookmarks.length})`],
|
||||
["annotations", `หมายเหตุ (${annotations.length})`],
|
||||
] as const
|
||||
).map(([key, label]) => (
|
||||
<button
|
||||
|
|
@ -733,7 +733,7 @@ export default function AnnotationSidebar({
|
|||
className={`flex-1 px-2 py-2 text-xs font-medium transition ${
|
||||
tab === key
|
||||
? "border-b-2 border-primary text-slate-900"
|
||||
: "text-slate-400 hover:text-slate-600"
|
||||
: "text-slate-500 hover:text-slate-600"
|
||||
}`}
|
||||
onClick={() => setTab(key)}
|
||||
>
|
||||
|
|
@ -745,7 +745,7 @@ export default function AnnotationSidebar({
|
|||
<div ref={listRef} className="flex-1 overflow-y-auto p-2">
|
||||
{tab === "bookmarks" ? (
|
||||
bookmarks.length === 0 ? (
|
||||
<p className="p-4 text-center text-xs text-slate-400">
|
||||
<p className="p-4 text-center text-xs text-slate-500">
|
||||
ยังไม่มี bookmark — กดปุ่ม bookmark บน toolbar เพื่อเพิ่ม
|
||||
</p>
|
||||
) : (
|
||||
|
|
@ -757,14 +757,17 @@ export default function AnnotationSidebar({
|
|||
</ul>
|
||||
)
|
||||
) : annotations.length === 0 ? (
|
||||
<p className="p-4 text-center text-xs text-slate-400">
|
||||
ยังไม่มี annotation — คลุมดำ / วาดเส้น / คลิกวาง comment บนหน้าเอกสาร
|
||||
<p className="p-4 text-center text-xs text-slate-500">
|
||||
ยังไม่มี annotation — คลุมข้อความ / วาดเส้น / คลิกวาง comment บนหน้าเอกสาร
|
||||
<span className="mt-1 block text-[11px]">
|
||||
คีย์ลัด: H มาร์ก · D วาด · C comment · B คั่นหน้า · Ctrl+Z ย้อน
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
{pendingItems.length > 0 && (
|
||||
<section>
|
||||
<p className="px-2 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-wide text-slate-400">
|
||||
<p className="px-2 pb-1 pt-2 text-[11px] font-semibold text-slate-500">
|
||||
ค้างดำเนินการ
|
||||
</p>
|
||||
<ul className="space-y-1">{pendingItems.map(renderAnnotationRow)}</ul>
|
||||
|
|
@ -772,7 +775,7 @@ export default function AnnotationSidebar({
|
|||
)}
|
||||
{resolvedItems.length > 0 && (
|
||||
<section>
|
||||
<p className="px-2 pb-1 pt-3 text-[10px] font-semibold uppercase tracking-wide text-emerald-500">
|
||||
<p className="px-2 pb-1 pt-3 text-[11px] font-semibold text-emerald-500">
|
||||
จัดการแล้ว
|
||||
</p>
|
||||
<ul className="space-y-1">{resolvedItems.map(renderAnnotationRow)}</ul>
|
||||
|
|
@ -794,7 +797,7 @@ export default function AnnotationSidebar({
|
|||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/40 p-4">
|
||||
<div className="w-80 rounded-lg border border-slate-200 bg-white p-4 shadow-xl">
|
||||
<p className="text-sm font-semibold">ประวัติการเปลี่ยนแปลง</p>
|
||||
<p className="mt-0.5 truncate text-[11px] text-slate-400">
|
||||
<p className="mt-0.5 truncate text-[11px] text-slate-500">
|
||||
{annotationPreviewText(historyFor)}
|
||||
</p>
|
||||
{historyError && (
|
||||
|
|
@ -803,10 +806,10 @@ export default function AnnotationSidebar({
|
|||
</p>
|
||||
)}
|
||||
{versions === null && !historyError && (
|
||||
<p className="mt-3 text-xs text-slate-400">กำลังโหลด…</p>
|
||||
<p className="mt-3 text-xs text-slate-500">กำลังโหลด…</p>
|
||||
)}
|
||||
{versions !== null && versions.length === 0 && (
|
||||
<p className="mt-3 text-xs text-slate-400">
|
||||
<p className="mt-3 text-xs text-slate-500">
|
||||
ยังไม่มีบันทึกการเปลี่ยนแปลง (เกิดขึ้นเมื่อแก้ไขหรือลบรายการนี้)
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -818,7 +821,7 @@ export default function AnnotationSidebar({
|
|||
<span className="block text-xs font-medium">
|
||||
{v.kind === "delete" ? "ถูกลบ" : "ถูกแก้ไข"}
|
||||
</span>
|
||||
<span className="block text-[11px] text-slate-400" suppressHydrationWarning>
|
||||
<span className="block text-[11px] text-slate-500" suppressHydrationWarning>
|
||||
{new Date(v.createdAt).toLocaleString("th-TH")}
|
||||
</span>
|
||||
</span>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue