genesis+worker: design system Genesis ทั้งชุด + patch pdf.worker แก้ fake-space ไทย + ปิด review design/responsive/quality รอบ 2026-09-24 — (1) Genesis: ฟอนต์ General Sans self-host + DM Sans + Sarabun, AppNav 56px backdrop-blur แทน SessionBar, DotGrid หน้า login, palette/รัศมี/เงา/focus ตาม docs/DESIGN.md ครบทุกหน้า (2) postinstall scripts/patch-pdf-worker.mjs ถอด if (scaledDim) รอบ prevTransform ให้อัปเดตทุก glyph — กัน addFakeSpaces แทรก space กลางคำไทยใน PDF ที่ justify ด้วย letter-spacing + test กัน regression (tests/scripts/ + fixture thai_letterspacing.pdf, invariant #6) (3) ฟอร์มอัปโหลด + ช่องกลุ่ม typeId ทางเลือก — normDocumentGroup ที่ utils/external-file.ts เป็น single source ร่วมกับ N8 (4) token fixes: focus ring ปุ่ม pin + แถว bookmark ตามหน้า, ปุ่ม action ใน popup/composer/dialog เป็น rounded-md ทั้งคู่, selection preview เป็น tint ของ primary แทน blue-500 ดิบ, default สี shape/highlight รวมที่ utils/annotation-display, loading animate-pulse + จดข้อยกเว้น (เงา pin, font weights ของ viewer) ลง DESIGN.md (5) responsive: Export PDF max-lg:order-first ให้เห็นบน iPad, แถวบน header แถวเดียวเลื่อนได้บนจอแคบ, re-fit ความกว้างตอน toggle sidebar เฉพาะเมื่อยังไม่ซูมเอง, pin popover flip ขึ้นบนเมื่อใกล้ท้ายหน้า, toast hint ตอนสลับ tool กลางเอกสาร, ป้าย "ชนิด · ผู้ทำ" ของรายการผู้อื่นสำหรับจอสัมผัส (6) คุณภาพ: @layer ครอบ @media กันกฎ touch รั่ยไป desktop (Tailwind v3 ยก @layer ออกจาก media ทิ้ง wrapper — ยืนยันด้วย build จริง + วัดบนจอ), กฎ M5 ครอบ contenteditable กัน Safari zoom ตอนแก้ไข TEXTBOX บน iPad, COMMENT ใช้ box มาตราฐานเดียวตาม pin px จริง (render/scroll target/move-clamp/bookmark), ตัดหัวการ์ดซ้ำกับ h2 หน้ารายการ — พร้อมไฟล์ untracked ที่ postinstall/โค้ดอ้างถึงครบชุด (app/fonts/, AppNav, DotGrid, tests/) กัน fresh clone fail ที่ npm install (คำขอผู้ใช้ 2026-09-24)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
e254b935ce
commit
c5c6b65bd2
42 changed files with 1429 additions and 745 deletions
|
|
@ -11,24 +11,35 @@ import { AlignCenter, AlignLeft, AlignRight, BookmarkPlus, Pencil, Trash2 } from
|
|||
import type {
|
||||
AnnotationData,
|
||||
AnnotationDTO,
|
||||
CommentData,
|
||||
HighlightData,
|
||||
HighlightStyle,
|
||||
TextboxData,
|
||||
} from "@/db/schema";
|
||||
import {
|
||||
COMMENT_PIN_COARSE_SIZE_PX,
|
||||
COMMENT_PIN_SIZE_PX,
|
||||
TEXTBOX_MAX_WIDTH_PCT,
|
||||
annotationBBox,
|
||||
commentPinBoxPct,
|
||||
commentPinBoxPx,
|
||||
scaleAnnotationData,
|
||||
translateAnnotationData,
|
||||
} from "@/utils/geometry";
|
||||
import { clamp, getPointerPercent, type Point, type Rect, type Size } from "@/utils/coordinates";
|
||||
import { STAMP_COLOR } from "@/utils/annotation-display";
|
||||
import {
|
||||
clamp,
|
||||
getPointerPercent,
|
||||
type Point,
|
||||
type Rect,
|
||||
type Size,
|
||||
} from "@/utils/coordinates";
|
||||
import { annotationTypeLabel, STAMP_COLOR } from "@/utils/annotation-display";
|
||||
import { ColorSwatches, HIGHLIGHT_STYLE_BTNS, NumberCombo, PanelRow } from "./ToolControls";
|
||||
|
||||
/** UX audit 2026-09-16: focus ring คีย์บอร์ด — ค่าเดียวกับ FOCUS_RING_CLS ของ
|
||||
* PDFViewer/AnnotationSidebar (ประกาศแยกที่นี่กัน circular import) */
|
||||
const FOCUS_RING_CLS =
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1";
|
||||
"focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary";
|
||||
|
||||
// กรอบ selection + handles ของ annotation ที่เลือก (PLAN-FEAT.md §3 F1)
|
||||
// - container เป็น z-4 เหมือน comment pins และ pointer-events: none **เสมอ**
|
||||
|
|
@ -42,6 +53,9 @@ const STRIP_PX = 8; // ความหนาขอบกรอบ (translate)
|
|||
const MIN_BOX_PCT = 0.5; // bbox เล็กสุด (% ของหน้า) กันกรอบหาย
|
||||
const EPS = 1e-6;
|
||||
|
||||
// COMMENT: ขนาด pin px + bbox px/% ของ pin ย้ายไป utils/geometry.ts แล้ว (ใช้ร่วมกับ
|
||||
// goToAnnotation ของ PDFViewer — single source ตาม responsive audit 2026-09-24)
|
||||
|
||||
type DragState = {
|
||||
// wleft/wright = TEXTBOX กว้างคงที่: ลากขอบซ้าย/ขวาปรับความกว้าง (ฟอนต์คงขนาด)
|
||||
mode: "move" | "scale" | "wleft" | "wright";
|
||||
|
|
@ -116,8 +130,25 @@ export default function AnnotationSelectionLayer({
|
|||
const movedRef = useRef(false);
|
||||
const latestRef = useRef<AnnotationData | null>(null);
|
||||
|
||||
const boxPx = annotationBBox(annotation, size);
|
||||
const boxPct = annotationBBox(annotation, { width: 100, height: 100 });
|
||||
// อุปกรณ์จอสัมผัส (pointer หลัก coarse) — คำนวณครั้งเดียวตอน mount เหมือน
|
||||
// coarseTouch ของ PDFViewer: .coarse-36 ขยายปุ่ม pin เป็น 36px บนพวกนี้
|
||||
const [coarseTouch] = useState(
|
||||
() =>
|
||||
typeof window !== "undefined" &&
|
||||
window.matchMedia("(pointer: coarse)").matches,
|
||||
);
|
||||
|
||||
// COMMENT ใช้กล่องครอบ pin จริง (px คงที่ตามอุปกรณ์) — ชนิดอื่นคือ bbox ตามขนาดหน้า
|
||||
// boxPct ก็ใช้มาตราฐานเดียวกัน (% จาก px จริง): move-clamp กับปุ่ม bookmark ของ pin
|
||||
// ต้องเห็น pin ตามที่ render ไม่ใช่ COMMENT_PIN_BOX_PCT ประมาณ (audit 2026-09-24)
|
||||
const isCommentPin = annotation.type === "COMMENT";
|
||||
const pinPx = coarseTouch ? COMMENT_PIN_COARSE_SIZE_PX : COMMENT_PIN_SIZE_PX;
|
||||
const boxPx = isCommentPin
|
||||
? commentPinBoxPx(annotation.data as CommentData, size, pinPx)
|
||||
: annotationBBox(annotation, size);
|
||||
const boxPct = isCommentPin
|
||||
? commentPinBoxPct(annotation.data as CommentData, size, pinPx)
|
||||
: annotationBBox(annotation, { width: 100, height: 100 });
|
||||
|
||||
// FIX-9: เอา non-null ! ออก — handler ยิงได้แค่ตอน DOM mount แล้ว
|
||||
// ref จึงไม่ควรเป็น null (คืน 0,0 เผื่อไว้เฉย ๆ ให้ type ชัด)
|
||||
|
|
@ -475,8 +506,10 @@ export default function AnnotationSelectionLayer({
|
|||
/>
|
||||
|
||||
{/* มุมทั้ง 4 — ลาก = scale รอบ anchor มุมตรงข้าม (TEXTBOX กว้างคงที่ไม่มีมุม —
|
||||
ปรับความกว้างที่ขอบซ้าย/ขวาแทน ส่วนสูงตามจำนวนบรรทัดอัตโนมัติ) */}
|
||||
{!isFixedTextbox && corners.map((c) => (
|
||||
ปรับความกว้างที่ขอบซ้าย/ขวาแทน ส่วนสูงตามจำนวนบรรทัดอัตโนมัติ; COMMENT ไม่
|
||||
มีมุมเช่นกัน — pin ไม่มี field ขนาด scale จึงไร้ความหมาย และ anchor จาก boxPct
|
||||
(% รอบจุด) กับ handle บนกล่อง pin คนละกล่องทำให้ลากแล้วเหลื่อมจากนิ้ว) */}
|
||||
{!isFixedTextbox && !isCommentPin && corners.map((c) => (
|
||||
<div
|
||||
key={c.key}
|
||||
className="absolute touch-none sel-handle rounded-sm border border-primary bg-primary/50"
|
||||
|
|
@ -498,6 +531,23 @@ export default function AnnotationSelectionLayer({
|
|||
</>
|
||||
)}
|
||||
|
||||
{/* Responsive audit 2026-09-24: รายการของผู้อื่นที่เลือกอยู่ — ป้าย "ชนิด · ผู้ทำ" ลอย
|
||||
เหนือกรอบ (จอสัมผัสไม่มี hover tooltip N1 ให้ดูว่าใครทำ เดิมต้องเปิด sidebar
|
||||
อย่างเดียว) — read-only pointer-events:none ตามกติกา container ของ layer นี้
|
||||
ไม่บัง textLayer; ชิดหัวหน้าเกินก็ห้อยใต้กรอบแทน */}
|
||||
{!editable && (
|
||||
<div
|
||||
className="absolute z-10 max-w-[200px] truncate rounded-md border border-line bg-white px-1.5 py-0.5 text-[11px] text-ink-soft shadow-lg"
|
||||
style={{
|
||||
left: Math.max(0, Math.min(boxPx.x, size.width - 204)),
|
||||
top: boxPx.y - 24 < 0 ? boxPx.y + boxPx.height + 4 : boxPx.y - 24,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
{annotationTypeLabel(annotation)} · {annotation.userName || "ไม่ทราบชื่อ"}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* แผงเครื่องมือของรายการที่เลือก — ตัวควบคุมเดียวกับ dropdown ของ tool ผู้สร้าง
|
||||
(สี/ความหนา/ขนาด/รูปแบบตามชนิด) + ปุ่มลบท้ายแผง — แสดงทุกชนิดรวม HIGHLIGHT
|
||||
(ไม่มี handle ย้าย/ขยาย แต่เปลี่ยนสี/รูปแบบ/ลบได้) — เฉพาะรายการของตัวเอง
|
||||
|
|
@ -507,7 +557,7 @@ export default function AnnotationSelectionLayer({
|
|||
{editable && !disabled && (
|
||||
<div
|
||||
ref={panelRef}
|
||||
className={`absolute ${hasStyleControls ? "w-60" : "w-fit"} sel-panel rounded-lg border border-slate-200 bg-white p-1.5 shadow-lg`}
|
||||
className={`absolute ${hasStyleControls ? "w-60" : "w-fit"} sel-panel rounded-lg border border-line bg-white p-1.5 shadow-lg`}
|
||||
style={{
|
||||
left: Math.max(0, Math.min(boxPx.x, size.width - 240 - 4)),
|
||||
top: openAbove ? boxPx.y - 6 : boxPx.y + boxPx.height + 4,
|
||||
|
|
@ -532,7 +582,7 @@ export default function AnnotationSelectionLayer({
|
|||
className={`flex h-6 w-6 items-center justify-center rounded-md transition ${FOCUS_RING_CLS} ${
|
||||
(highlightData.style ?? "highlight") === key
|
||||
? "bg-primary text-white"
|
||||
: "text-slate-500 hover:bg-slate-100"
|
||||
: "text-ink-soft hover:bg-ink/[0.05]"
|
||||
}`}
|
||||
onClick={() => commitHighlightStyle(key)}
|
||||
>
|
||||
|
|
@ -599,7 +649,7 @@ export default function AnnotationSelectionLayer({
|
|||
<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 ${FOCUS_RING_CLS}`}
|
||||
className={`flex flex-1 items-center justify-center gap-1 rounded-md border border-line px-2 py-1 text-[11px] font-medium text-ink-soft hover:bg-ink/[0.05] ${FOCUS_RING_CLS}`}
|
||||
onClick={() => onStartEdit?.(annotation.id)}
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
|
|
@ -643,7 +693,7 @@ export default function AnnotationSelectionLayer({
|
|||
className={`flex h-6 w-6 items-center justify-center rounded-md border transition ${FOCUS_RING_CLS} ${
|
||||
textboxData.bold
|
||||
? "border-primary bg-primary text-white"
|
||||
: "border-slate-300 bg-white text-slate-500 hover:bg-slate-100"
|
||||
: "border-line bg-white text-ink-soft hover:bg-ink/[0.05]"
|
||||
}`}
|
||||
onClick={() => commitTextboxPatch({ bold: !textboxData.bold })}
|
||||
>
|
||||
|
|
@ -654,7 +704,7 @@ export default function AnnotationSelectionLayer({
|
|||
</button>
|
||||
</PanelRow>
|
||||
<PanelRow label="จัดแนว">
|
||||
<div className="flex overflow-hidden rounded-md border border-slate-300">
|
||||
<div className="flex overflow-hidden rounded-md border border-line">
|
||||
{(
|
||||
[
|
||||
["left", AlignLeft, "ชิดซ้าย"],
|
||||
|
|
@ -670,7 +720,7 @@ export default function AnnotationSelectionLayer({
|
|||
className={`px-2 py-1 transition ${FOCUS_RING_CLS} ${
|
||||
(textboxData.align ?? "left") === a
|
||||
? "bg-primary text-white"
|
||||
: "text-slate-500 hover:bg-slate-100"
|
||||
: "text-ink-soft hover:bg-ink/[0.05]"
|
||||
}`}
|
||||
onClick={() => commitTextboxPatch({ align: a })}
|
||||
>
|
||||
|
|
@ -681,27 +731,29 @@ export default function AnnotationSelectionLayer({
|
|||
</PanelRow>
|
||||
</>
|
||||
)}
|
||||
{hasStyleControls && <div className="my-1 border-t border-slate-100" />}
|
||||
{hasStyleControls && <div className="my-1 border-t border-line/60" />}
|
||||
<div className={hasStyleControls ? "flex items-center justify-between" : "flex items-center"}>
|
||||
{/* N2: bookmark ตำแหน่งของรายการนี้ — เปิด popup ตั้งชื่อ/โน้ต/เลือกหัวข้อต่อ */}
|
||||
{/* hover เป็นกลางแบบปุ่มจิ๋วอื่นในแผง (เดิม copy มาจากปุ่มลบข้าง ๆ จน hover
|
||||
แดง = สัญญาณอันตรายเท็จกับการ "บันทึก" — UX review M4 2026-09-23) */}
|
||||
<button
|
||||
type="button"
|
||||
aria-label="บันทึก bookmark ตำแหน่งรายการนี้"
|
||||
title="Bookmark ตำแหน่งนี้"
|
||||
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 ${FOCUS_RING_CLS}`}
|
||||
className={`flex h-5 w-5 sel-panel-btn items-center justify-center rounded text-ink-faint hover:bg-ink/[0.05] hover:text-ink ${FOCUS_RING_CLS}`}
|
||||
onClick={() => onBookmark(boxPct.y)}
|
||||
>
|
||||
<BookmarkPlus className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{/* เส้นคั่นแนวตั้ง — เฉพาะกรณีปุ่มติดกัน (แผงไม่มี row สไตล์): แยก bookmark ออกจากปุ่มลบ */}
|
||||
{!hasStyleControls && (
|
||||
<span aria-hidden="true" className="mx-1 h-3.5 w-px bg-slate-200" />
|
||||
<span aria-hidden="true" className="mx-1 h-3.5 w-px bg-line" />
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
aria-label="ลบรายการนี้"
|
||||
title="ลบ"
|
||||
className={`flex h-5 w-5 sel-panel-btn items-center justify-center rounded text-red-500 hover:bg-red-50 ${FOCUS_RING_CLS}`}
|
||||
className={`flex h-5 w-5 sel-panel-btn items-center justify-center rounded text-error-ink hover:bg-error/10 ${FOCUS_RING_CLS}`}
|
||||
onClick={() => onDelete(annotation.id)}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue