Some checks failed
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Failing after 3s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Has been skipped
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Has been skipped
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 0s
(1) hooks/useCustomSelection.ts (ใหม่): tool highlight บนอุปกรณ์ pointer หลัก coarse ลากเลือกข้อความเองผ่าน Pointer Events ชุดเดียวคุม mouse+touch — pointerdown เก็บ anchor caret (caretRangeFromPoint → caretPositionFromPoint fallback) ยังไม่ทำอะไร → เลย threshold 8px จึง setPointerCapture + เริ่ม select จริง (จับตอนเริ่ม select ไม่ใช่ pointerdown — capture ตั้งแต่กดจะ retarget click ไป container จน tap เลือก annotation/pin ใช้ไม่ได้) → pointerup = commit ทันทีผ่าน commitHighlight เดิม (ห้ามแก้ signature — ไปทาง readTextSelection(pi, { range }) ที่เพิ่ม opts.range ให้อ่าน Range จากภายนอกแทน native selection) — ชดเชย UX ที่หายจาก native: word snap ขอบคำทั้ง anchor/focus (Intl.Segmenter แยกคำไทย dictionary → fallback regex \S+, กลางช่องว่างไม่ขยาย), finger offset อ่าน caret ที่ y−12 เพราะนิ้วบังข้อความ (เฉพาะ pointerType touch), auto-scroll rAF loop เมื่อนิ้วใกล้ขอบบน/ล่าง viewport 60px, live preview rAF throttle (getClientRects → rectsInSweptPath → % + mergeRectsByLine รูปร่างเดียวกับ liveMark ส่งเข้า PdfPage ทดแทนตอนมีค่า customSelPreview ?? liveMark)
(2) กัน "ลากแล้วบางทีไม่ commit" ตาม bug report 3 จุด + code review: การหา caret เป็น 4 ชั้นเรียงตามต้นทุน (resolveTextCaretDeep ใช้เฉพาะ pointerdown กับทั้งจุด y−12 และจุดแตะจริง): caret API + descend หา text node (childNodes[startOffset] + findFirstTextNode DFS) → retry 8 ทิศ ±2/3px → elementsFromPoint เจาะ overlay ไร้ text (SVG ของ InkLayer/มาร์ก/กรอบ) จับ span ใต้จุด → span ของหน้านั้นที่ใกล้สุดแนวตั้ง ≤40px (เกินถือว่าไม่มีข้อความจริง; cache span list ต่อ 1 gesture) — ระหว่างลากทุก rAF frame ใช้เฉพาะ 2 ชั้นถูก หาไม่เจอ (ผ่านพื้นที่ว่าง/ช่องว่างระหว่างหน้า) ให้คง focus เดิมจาก frame ก่อน (lastFocus) ไม่ re-scan / lastRef เคลียร์เฉพาะ pointerdown, finish และเมื่อหน้า anchor โดน virtualization unmount กลางลาก (anchorPageEl.isConnected — กัน commit ของเก่าเงียบ ๆ) — frame ที่ rectsInSweptPath กรองว่าง (ลากเร็ว sample ห่าง/นิ้วสั่นตอนปล่อย) ล้างแค่ preview / ลากล้ำเข้าหน้าถัดไป (range คร่อม 2 หน้า) ห้าม bail — clampToAnchorPage clamp ฝั่ง focus ไปท้ายหน้า anchor (ลากลง) หรือต้นหน้า anchor (ลากขึ้น) boundary cache ต่อ gesture (clampBounds ไม่เดิน TreeWalker ทุก frame) / จุดเส้นทางกรอง rectsInSweptPath ใช้จุด offset เดียวกับ caret (anchorPathPt = จุด probe ที่ resolve anchor + pathEnd = จุด focus หัก 12px) — ใช้พิกัดนิ้ว raw ตรง ๆ จะคลาด 12px จนบรรทัดเป้าหมายโดน lineTol ตัดทิ้ง (ลากขึ้น) หรือมาร์กเกินบรรทัด (ลากลง) เมื่อฟอนต์เล็ก
(3) ถอด path เดิมทั้งหมด: touchConfirm state, touchSelRef/touchSelTimerRef/touchHideTimerRef, debounce 400ms ใน effect selectionchange, handleTouchConfirmHighlight, ปุ่ม "ไฮไลต์ข้อความ" ลอยใต้ selection — desktop (pointer หลัก fine) ใช้ native mouse selection + mouseup commit ตามเดิมไม่เปลี่ยน (enabled = tool highlight + matchMedia coarse) / ระหว่าง enabled ให้ <main> ได้ class .pdf-custom-select (user-select/-webkit-user-select/-webkit-touch-callout: none + touch-action: none กัน browser ยึด gesture เป็น scroll → pointercancel) ถอดทันทีเมื่อเปลี่ยน tool (tool อื่น native selection + long-press copy ของ OS ตามปกติ) + restore user-select:text และ touch-action:auto ให้ input/textarea/contenteditable ใต้ main (reply ของ comment pin ยังเลือก/เลื่อนข้อความในช่องได้)
(4) ย้าย ClientPt + rectsInSweptPath จาก PDFViewer ไป utils/coordinates.ts (hook import ได้โดยไม่ circular) + unit tests rectsInSweptPath 4 เคส (ลากบรรทัดเดียว/ลากขึ้นหลายบรรทัด/บรรทัดนอกเส้นทาง/ไม่ขยับ) / gap-analysis.md แถว 5.3.18 อัปเดตเป็นกลไกใหม่ (รวมงานฉบับแก้ไขข้อความข้อ 5.3.18–5.3.25 ของเจ้าของเอกสารที่ค้างใน working tree) / sync rules/pdf-viewer.md ครบทั้งกลไก 4 ชั้น + path points + กรณีหน้า unmount
verify: tsc ผ่าน + vitest 104/104 + Playwright touch emulation 3 ชุด (รอยต่อ span คำติดกัน gap=0/ขอบซ้าย-ขวาสุดบรรทัด/ช่องว่าง line-height, ลากช้า-เร็ว flick/ล้ำหน้าถัดไปทั้งลงและขึ้น/ขยับต่ำกว่า threshold ไม่สร้าง/tap annotation ได้, จุดว่างใกล้ 20px → commit vs ไกล 46px → ไม่สร้าง) — console.warn ชุด TEMP-DEBUG คงไว้ชั่วคราวตาม protocol รอยืนยันบนมือถือจริง (Safari iOS + Chrome Android) ก่อนถอด
Co-Authored-By: Claude Code <noreply@anthropic.com>
242 lines
11 KiB
CSS
242 lines
11 KiB
CSS
@tailwind base;
|
||
@tailwind components;
|
||
@tailwind utilities;
|
||
|
||
/* ─── TextLayer ของ pdfjs v4 ─────────────────────────────────────────────────
|
||
คัดมาจาก node_modules/pdfjs-dist/web/pdf_viewer.css (ส่วนที่จำเป็น)
|
||
ใช้กับ pdfjs.TextLayer ที่ render ลง div.textLayer ใน PDFViewer */
|
||
.textLayer {
|
||
position: absolute;
|
||
text-align: initial;
|
||
inset: 0;
|
||
overflow: clip;
|
||
opacity: 1;
|
||
line-height: 1;
|
||
text-size-adjust: none;
|
||
forced-color-adjust: none;
|
||
transform-origin: 0 0;
|
||
caret-color: CanvasText;
|
||
/* ห้ามใส่ z-index ที่นี่ — บล็อกนี้อยู่หลัง @tailwind utilities ใน cascade
|
||
จะ override z-[2] ของ Tailwind จน textLayer ไปอยู่ใต้ canvas (invariant #3) */
|
||
}
|
||
|
||
.textLayer :is(span, br) {
|
||
color: transparent;
|
||
position: absolute;
|
||
white-space: pre;
|
||
cursor: text;
|
||
transform-origin: 0% 0%;
|
||
}
|
||
|
||
.textLayer > :not(.markedContent),
|
||
.textLayer .markedContent span:not(.markedContent) {
|
||
z-index: 1;
|
||
}
|
||
|
||
.textLayer span.markedContent {
|
||
top: 0;
|
||
height: 0;
|
||
}
|
||
|
||
.textLayer span[role="img"] {
|
||
user-select: none;
|
||
cursor: default;
|
||
}
|
||
|
||
.textLayer ::selection {
|
||
background: rgba(0 0 255 / 0.25);
|
||
background: color-mix(in srgb, AccentColor, transparent 75%);
|
||
}
|
||
|
||
.textLayer br::selection {
|
||
background: transparent;
|
||
}
|
||
|
||
.textLayer .endOfContent {
|
||
display: block;
|
||
position: absolute;
|
||
inset: 100% 0 0;
|
||
z-index: 0;
|
||
cursor: default;
|
||
user-select: none;
|
||
}
|
||
|
||
.textLayer.selecting .endOfContent {
|
||
top: 0;
|
||
}
|
||
|
||
/* canvas วัด font metrics ที่ pdfjs.TextLayer สร้างแล้ว append ท้าย document.body
|
||
— คัดมาจาก pdf_viewer.css ของ pdfjs-dist เช่นกัน (ตอนแรกคัดมาแค่ส่วน .textLayer
|
||
กฎนี้จึงตกหล่น ทำให้ canvas ว่าง alpha:false แสดงเป็นก้อนดำ 300×150 ที่ล่างซ้าย
|
||
ของ document และเพิ่ม scrollHeight เกินจริง 150px) */
|
||
.hiddenCanvasElement {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 0;
|
||
height: 0;
|
||
background-size: 100% 100%;
|
||
background-color: transparent;
|
||
}
|
||
|
||
/* ─── UX review 2026-09-09 ข้อ 4: ปุ่ม header เล็กกว่ามาตรฐาน touch 44×44 ─────
|
||
ขยายเฉพาะอุปกรณ์จอสัมผัส — ครอบปุ่ม toggle/page nav/zoom/tool group
|
||
(รวมปุ่ม dropdown ของ tool)/undo-redo/bookmark/export + ปุ่ม summary (Link) */
|
||
@media (pointer: coarse) {
|
||
header .touch-44 > button,
|
||
header .touch-44 > div > button,
|
||
header .touch-44 > div > div > button,
|
||
header .touch-44 > div > a {
|
||
min-height: 2.75rem;
|
||
min-width: 2.75rem;
|
||
/* min 44px ทำให้ปุ่ม icon เดี่ยวกว้างเกิน content — ต้อง center icon
|
||
ไม่งั้นชิดซ้าย+ลอยตาม baseline (ปุ่ม dropdown มี flex อยู่แล้ว ไม่กระทบ) */
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
/* UX review 2026-09-14 L2: ปุ่มใน dropdown panel (สี/ความหนา/preset) — swatch 16px
|
||
เล็กเกินนิ้ว แต่ 44px ทำ panel บวม จึงใช้ 36px / C1: เมนู dropdown + popup bookmark
|
||
ย้ายไป createPortal ที่ document.body แล้ว (ไม่มี .z-50 เหลือใน header) จึง target
|
||
ผ่าน [data-tool-menu] ตรง ๆ / ไม่ใส่ display/justify — แถวเมนูเป็น text-left อยู่แล้ว */
|
||
[data-tool-menu] button:not(.combo-step),
|
||
[data-tool-menu] label {
|
||
min-height: 2.25rem;
|
||
min-width: 2.25rem;
|
||
}
|
||
|
||
/* NumberCombo (ความหนา/ความทึบ/ขนาด): ▲▼ ซ้อนแนวตั้ง 2 ปุ่ม — ถ้าโดน min 36px
|
||
คอลัมน์จะสูง 72px และ input (items-stretch) ถูกดึงบวมตามทั้งแถว — ปุ่ม ▲▼
|
||
จึงหลุดจาก rule ด้านบน (combo-step) แล้วตั้งความสูงแถวเองที่ 36px แทน:
|
||
แตะ input = เปิด preset list (แถวละ 36px จาก rule ด้านบน) คือ touch path
|
||
หลัก ส่วน ▲▼ (ครึ่งแถว) เป็น fine-tune สำรอง — ขนาดใกล้ desktop ไม่บวม */
|
||
[data-tool-menu] .number-combo > div {
|
||
height: 2.25rem;
|
||
}
|
||
[data-tool-menu] .combo-step {
|
||
min-width: 1.25rem;
|
||
}
|
||
|
||
/* M3: ตัวควบคุมบนหน้ากระดาษเล็กกว่ามาตรฐาน touch มาก (ไม่ได้อยู่ใน header จึงหลุด
|
||
จาก rule ด้านบน) — ของที่มองเห็นคงขนาดเดิม ขยายพื้นที่รับ touch แทน:
|
||
handle/strip ของกรอบ selection เป็น div ขนาดจาก inline style (10px/8px) แก้
|
||
width/height ตรง ๆ ไม่ได้ จึงใช้ ::after แผ่รับ pointer รอบ ๆ (handle 10→hit 28px,
|
||
strip 8→hit 16px — มุม render ทีหลังใน DOM จึงชนะบริเวณซ้อนกับ strip = ต้องการ) */
|
||
.sel-handle::after {
|
||
content: "";
|
||
position: absolute;
|
||
inset: -9px;
|
||
}
|
||
.sel-strip::after {
|
||
content: "";
|
||
position: absolute;
|
||
inset: -4px;
|
||
}
|
||
|
||
/* ปุ่ม bookmark/ลบในแผง selection (20×20) + swatch สีในแผง (16×16) */
|
||
.sel-panel-btn {
|
||
min-height: 2.25rem;
|
||
min-width: 2.25rem;
|
||
}
|
||
.sel-panel .color-swatches button,
|
||
.sel-panel .color-swatches label {
|
||
min-height: 2rem;
|
||
min-width: 2rem;
|
||
}
|
||
|
||
/* M6: ปุ่ม icon ใน sidebar (16/14px ติดกัน — จิ้มพลาดไปปุ่มข้าง ๆ ง่าย โดยเฉพาะ
|
||
"ลบ" ชิด "ประวัติ") → อย่างน้อย 32px แบบไม่ต้องขยายตัว icon ที่มองเห็น */
|
||
aside[data-sidebar] button.shrink-0 {
|
||
min-height: 2rem;
|
||
min-width: 2rem;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
/* M6: ตัด double-tap zoom ของ browser เมื่อแตะปุ่มรัว ๆ (ยัง pinch ได้ตามปกติ —
|
||
handle ของกรอบ selection เป็น div + touch-none อยู่แล้ว ไม่โดน rule นี้) */
|
||
button,
|
||
label {
|
||
touch-action: manipulation;
|
||
}
|
||
|
||
/* M6: กัน long-press บน iOS เปิด magnifier/callout ขอคัดลอก — scope เฉพาะ layer
|
||
ของ TEXTBOX/STAMP เท่านั้น (textLayer ต้อง select/copy ได้ตามปกติ) */
|
||
.no-callout,
|
||
.no-callout * {
|
||
-webkit-touch-callout: none;
|
||
}
|
||
|
||
/* M6: ปุ่มเล็กใน comment composer/popover/pin (~24px) → 36px ผ่าน class แทนการ
|
||
แก้ขนาดจริง (desktop คงเดิม) */
|
||
.coarse-36 {
|
||
min-height: 2.25rem;
|
||
min-width: 2.25rem;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
/* M6: ปุ่มหน้าสรุป (30px) → 44px ตามมาตรฐาน touch */
|
||
main.summary-page a {
|
||
min-height: 2.75rem;
|
||
min-width: 2.75rem;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
}
|
||
}
|
||
|
||
/* UX review 2026-09-14 H2: แอปไม่เคยตั้ง font-family เลย ข้อความไทยเลยตกไปที่
|
||
default ของ OS แต่ละเครื่อง — วางฟอนต์ไทยนำหน้า stack (ไม่โหลด webfont
|
||
ตามที่เลือกไว้: Leelawadee UI = Windows, Noto Sans Thai = Android/Linux,
|
||
Thonburi = macOS) */
|
||
body {
|
||
font-family: "Leelawadee UI", "Noto Sans Thai", "Thonburi",
|
||
ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
|
||
"Helvetica Neue", Arial, sans-serif;
|
||
}
|
||
|
||
/* M6: touch hygiene — ตัดแถบเทากระพริบของ iOS ทุกครั้งที่แตะปุ่ม และกัน
|
||
pull-to-refresh ของ Android ตอนอยู่ ๆ สุดขอบบน (reload = ล้าง undo
|
||
history ฝั่ง client ที่ไม่เก็บใน DB) */
|
||
html {
|
||
-webkit-tap-highlight-color: transparent;
|
||
overscroll-behavior-y: contain;
|
||
}
|
||
|
||
/* custom selection ของ tool highlight บนจอสัมผัส (hooks/useCustomSelection) —
|
||
ปิด native selection/callout ของ OS และยึด gesture ลากเป็นของเราทั้งหมด
|
||
(touch-action:none กัน browser ยึดไปเป็น scroll → pointercancel)
|
||
hook ใส่/ถอด class นี้บน <main> ตาม enabled — ปิดแล้ว native selection
|
||
กลับมาทันที (tool อื่น long-press copy ของ OS ใช้ได้ตามปกติ) */
|
||
.pdf-custom-select {
|
||
user-select: none;
|
||
-webkit-user-select: none;
|
||
-webkit-touch-callout: none;
|
||
touch-action: none;
|
||
}
|
||
|
||
/* ช่องกรอกข้อมูลในหน้า (reply ของ comment pin, textbox) ที่อยู่ใต้ <main> ตอน
|
||
tool highlight — ต้องเลือก/คัดลอกข้อความในช่องและเลื่อนข้อความภายในช่อง (reply
|
||
ยาว) ได้ตามปกติ ไม่โดน touch-action:none ของ class ด้านบนลากไปด้วย (การ scroll
|
||
ในช่องจบที่ตัวช่องเองซึ่งเป็น scroll container — pointerdown ของ custom
|
||
selection ก็เริ่มบน .textLayer เท่านั้น ไม่ชนกัน) */
|
||
.pdf-custom-select input,
|
||
.pdf-custom-select textarea,
|
||
.pdf-custom-select [contenteditable="true"] {
|
||
user-select: text;
|
||
-webkit-user-select: text;
|
||
-webkit-touch-callout: default;
|
||
touch-action: auto;
|
||
}
|
||
|
||
/* M6: safe-area — defensive (ค่าเป็น 0 จนกว่าจะใส่ viewport-fit=cover ใน meta
|
||
ของ layout — ยังไม่ใส่ตามขอบเขตงานนี้ ใส่ไว้ก่อนเพื่อไม่ต้องแก้ CSS ซ้ำ) */
|
||
header {
|
||
padding-top: env(safe-area-inset-top);
|
||
}
|
||
.comment-composer {
|
||
padding-bottom: env(safe-area-inset-bottom);
|
||
}
|