@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 ที่มองเห็น */ /* UX audit 2026-09-16: ขยายเป็นปุ่มทุกตัวใน sidebar — รวมปุ่มข้อความเล็ก (ยกเลิก/บันทึก/ล้างทั้งหมด/ViewSwitch/แท็บ) ที่เดิมหลุดจาก rule; แยกเป็น 2 rule เพราะ specificity ของ selector นี้ชนะ Tailwind class เดี่ยว — ใส่ display/ align-items ใน rule กว้างจะขโมย items-start ของปุ่มแถวรายการ (ไอคอนลอยกลาง ข้อความ 2 บรรทัด) ตัวจัด center จึงคงไว้เฉพาะ button.shrink-0 (ไอคอนเดี่ยว) ส่วนแถวรายการเป็นปุ่มเต็มความกว้างสูงพออยู่แล้ว ไม่กระทบ */ aside[data-sidebar] button { min-height: 2rem; } 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 นี้บน
ตาม 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) ที่อยู่ใต้
ตอน 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); }