N7 รายงานรายการ bookmark เป็น PDF (requirement 5.3.15): route /api/bookmarks/export สร้างเอกสารใหม่ล้วนด้วย PDFDocument.create (ไม่โหลดไฟล์ต้นฉบับเลย — invariant #1) ต้นไม้ indent 3 ระดับเรียงตาม N5 + หัวรายงาน + แบ่งหน้าต่อบรรทัด + footer หน้า n จาก N + cycle guard (seen ร่วมทุก root เติมโหนดนอก root เป็นระดับ 1) + 0 bookmark = หน้าเดียวมีข้อความแจ้ง + ปุ่ม Download ท้ายแถว toggle เงื่อนไขค้นหาในแท็บ "ที่คั่นหน้า" (ลิงก์ธรรมดา pattern F16) + export F8: outline bookmark เรียงตามลำดับหน้าเหมือน sidebar (N5: pageIndex→yOffset→createdAt แทน createdAt เดี่ยว) + ถอด drawNoteBox กลับเป็น sticky note annotation อย่างเดียวตามสเปก §F8 ไม่วาดข้อความ flatten ทับหน้ากระดาษ (กล่องเหลืองแบนทับเนื้อหาเอกสาร — ผู้ใช้ตัดสินใจ; tooltip ตัดไทยใน Chrome เป็น bug ฝั่ง PDFium) + แผง selection ของ COMMENT/REDACTION ที่ไม่มี row สไตล์หุบเป็น w-fit ปุ่ม bookmark/ลบติดกันคั่นเส้นแนวตั้ง
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 49s
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

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-09-17 10:09:57 +07:00
parent bc8ba83deb
commit 460cd9c600
7 changed files with 379 additions and 28 deletions

View file

@ -333,6 +333,10 @@ export default function AnnotationSelectionLayer({
// และซ่อนมุม (scale ไม่มีความหมายกับกล่องกว้างคงที่) — ย้ายกล่องใช้ขอบบน/ล่าง
const isFixedTextbox = textboxData?.width !== undefined;
// แผงมี row ตัวควบคุมสไตล์หรือเหลือแค่ปุ่ม bookmark+ลบ (COMMENT/REDACTION) —
// กรณีหลังหุบเป็น w-fit ไม่คั่นเส้น และปุ่มติดกันแทน justify-between
const hasStyleControls = !!(highlightData || strokeData || stampData || textboxData);
// มุมทั้ง 4 — ตำแหน่งวางเป็น px, anchor มุมตรงข้ามเป็น % ตรง ๆ จาก boxPct
// (พิกัด % คำนวณผ่าน utils/coordinates เท่านั้น — invariant #2)
const corners: { key: string; x: number; y: number; cursor: string; anchor: Point }[] = [
@ -498,11 +502,12 @@ export default function AnnotationSelectionLayer({
(สี/ความหนา/ขนาด/รูปแบบตามชนิด) + ปุ่มลบท้ายแผง — แสดงทุกชนิดรวม HIGHLIGHT
(ไม่มี handle ย้าย/ขยาย แต่เปลี่ยนสี/รูปแบบ/ลบได้) — เฉพาะรายการของตัวเอง
clamp แนวนอน: รายการชิดขวาหน้า (จอแคบ fit-width) ต้องไม่ทำแผง w-60 ล้นขอบขวา
จนปุ่มลบ/ตัวควบคุมหลุดจอ (เจอตอน verify จอ 390px — ปุ่มลบครึ่งเดียวจิ้มได้ยาก) */}
จนปุ่มลบ/ตัวควบคุมหลุดจอ (เจอตอน verify จอ 390px — ปุ่มลบครึ่งเดียวจิ้มได้ยาก)
กรณี w-fit ใช้ 240 เป็น upper bound ต่อ (แคบกว่า w-60 เสมอ — clamp หลวมเกินไม่มีผลร้าย) */}
{editable && !disabled && (
<div
ref={panelRef}
className="absolute w-60 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-slate-200 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,
@ -676,8 +681,8 @@ export default function AnnotationSelectionLayer({
</PanelRow>
</>
)}
<div className="my-1 border-t border-slate-100" />
<div className="flex items-center justify-between">
{hasStyleControls && <div className="my-1 border-t border-slate-100" />}
<div className={hasStyleControls ? "flex items-center justify-between" : "flex items-center"}>
{/* N2: bookmark ตำแหน่งของรายการนี้ — เปิด popup ตั้งชื่อ/โน้ต/เลือกหัวข้อต่อ */}
<button
type="button"
@ -688,6 +693,10 @@ export default function AnnotationSelectionLayer({
>
<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" />
)}
<button
type="button"
aria-label="ลบรายการนี้"

View file

@ -11,6 +11,7 @@ import {
Circle,
Clock,
Copy,
Download,
EyeOff,
Highlighter,
MessageSquare,
@ -1094,24 +1095,36 @@ export default function AnnotationSidebar({
{/* แถวสรุปเงื่อนไข — toggle แผงแบบเต็มความกว้าง (เป้าใหญ่จิ้มง่ายบนจอสัมผัส)
แสดงเงื่อนไขที่ตั้งอยู่ "เป็นข้อความ" สดตามฟอร์ม ตลอดแม้แผงหุบ/ปิดผลแล้ว —
แทน badge ตัวเลขที่เห็นแค่จำนวน / ไม่มีเงื่อนไข = บอกขอบเขตที่ตั้งได้เลย */}
<button
type="button"
aria-expanded={bmShowConditions}
onClick={() => setBmShowConditions((v) => !v)}
className={`flex w-full items-center gap-1.5 rounded px-1.5 py-1 text-[11px] hover:bg-slate-100 ${FOCUS_RING_CLS} ${
hasBmConditionValues ? "text-primary" : "text-slate-500"
}`}
>
<SlidersHorizontal className="h-3 w-3 shrink-0" />
<span className="min-w-0 flex-1 truncate text-left">
{hasBmConditionValues ? bmLiveSummary : "หน้า · ผู้บันทึก · วันที่"}
</span>
{bmShowConditions ? (
<ChevronDown className="h-3 w-3 shrink-0" />
) : (
<ChevronRight className="h-3 w-3 shrink-0" />
)}
</button>
<div className="flex items-center gap-1">
<button
type="button"
aria-expanded={bmShowConditions}
onClick={() => setBmShowConditions((v) => !v)}
className={`flex min-w-0 flex-1 items-center gap-1.5 rounded px-1.5 py-1 text-[11px] hover:bg-slate-100 ${FOCUS_RING_CLS} ${
hasBmConditionValues ? "text-primary" : "text-slate-500"
}`}
>
<SlidersHorizontal className="h-3 w-3 shrink-0" />
<span className="min-w-0 flex-1 truncate text-left">
{hasBmConditionValues ? bmLiveSummary : "หน้า · ผู้บันทึก · วันที่"}
</span>
{bmShowConditions ? (
<ChevronDown className="h-3 w-3 shrink-0" />
) : (
<ChevronRight className="h-3 w-3 shrink-0" />
)}
</button>
{/* N7: ส่งออกรายการ bookmark เป็น PDF — ลิงก์ธรรมดา pattern Export CSV (F16)
ส่งออกทุกรายการของไฟล์เสมอ ไม่ผูกกับเงื่อนไขค้นหาที่ตั้งไว้ */}
<a
href={`/api/bookmarks/export?fileId=${fileId}`}
title="ส่งออกทุกที่คั่นหน้าของไฟล์นี้เป็น PDF"
aria-label="ส่งออกทุกที่คั่นหน้าของไฟล์นี้เป็น PDF"
className={`coarse-36 shrink-0 rounded-md p-1.5 text-slate-500 hover:bg-slate-100 hover:text-slate-700 ${FOCUS_RING_CLS}`}
>
<Download className="h-3.5 w-3.5" />
</a>
</div>
{bmShowConditions && (
// จังหวะเดียวทุกแถว: label บนบรรทัด + คู่ input แบ่งความกว้าง grid 1fr–1fr
// (เดิม label inline 2 แถว + label บน 1 แถว อ่านไม่เป็นระเบียบ ขอบ input
@ -1456,7 +1469,7 @@ export default function AnnotationSidebar({
onToggle={() => togglePageExpanded(pi)}
/>
{!collapsed && (
<ul className="space-y-1">{items.map(renderAnnotationRow)}</ul>
<ul className="ml-4 mr-1 space-y-1">{items.map(renderAnnotationRow)}</ul>
)}
</section>
);