eCourt-note/components/pdf
Your Name 8571ef9aaf
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 45s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 3s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 0s
toolbar: ปุ่ม tool dropdown แสดงค่าที่เลือกเฉพาะตอน active + ฟื้น effect เลื่อนปุ่ม active กลับกึ่งกลางแถบ (เสียเงียบมาตั้งแต่ 2026-09-23) — (1) ปัญหา: คำขอผู้ใช้ 2026-09-23 เดิมให้ปุ่ม dropdown ที่มีค่าย่อย (มาร์กข้อความ/วาดเส้น/รูปทรง/ตรายาง) แสดง "ชื่อตัวเลือกล่าสุด" แทนชื่อ tool เสมอเพื่อเห็นค่าปัจจุบันโดยไม่ต้องเปิดเมนู — แต่ทำให้ชื่อ tool หายไปจาก toolbar ทั้งหมด: ปุ่ม "วาดเส้น" แสดง "ปากกา" ตั้งแต่ยังไม่ได้เลือกอะไรเลย (default drawPen=pencil) และค้างอยู่อย่างนั้นแม้สลับไป tool อื่น (ผู้ใช้รายงาน 2026-09-29: "label ของ tool ทำไมไม่เปลี่ยนทั้งที่ยังไม่ได้เลือน เช่น วาด แต่แสดง ปากกาเลย") — ขัดเหตุผลเดิมของการใส่ข้อความบนปุ่ม (2026-09-21: ผู้ใช้ศาลไม่คุ้น icon ล้วน) เพราะมองคำ "ปากกา" แล้วไม่รู้ว่านี่คือปุ่มวาดเส้น (2) แก้ตามคำขอผู้ใช้ 2026-09-29 (เลือกจาก 3 ทางเลือกที่เสนอ): tool ไม่ active → แสดง text ชื่อ tool + Icon ของ tool, title/aria-label = label เต็มเหมือนปุ่มเดี่ยวอื่น; tool active → แสดงค่าที่เลือกไว้ (ค่าที่จะใช้จริงเมื่อวาด) + icon ของค่า (draw/highlight เปลี่ยน icon ตามชนิดปากกา/รูปแบบมาร์ก, shape/stamp ใช้ tool icon เหมือนเดิม), title = "label — ค่า" ตาม pattern เดิม — กดปุ่ม dropdown เรียก switchTool เสมออยู่แล้ว (เคลียร์ popup) ปุ่มจึงสลับไปแสดงค่าทันทีที่เปิดเมนู สอดคล้องกับ "กำลังจะใช้ค่านี้" — ปุ่ม active กว้าง/แคบตามความยาวค่า ("ปากกา" สั้นกว่า "วาดเส้น") แถบรองรับอยู่แล้ว (whitespace-nowrap + max-lg:overflow-x-auto) (3) แก้ bug แฝงที่เจอระหว่างแก้: effect เลื่อนปุ่ม tool active กลับกึ่งกลางแถบบนจอ < lg (UX review A2 2026-09-21) ใช้ querySelector(button[title="..."]) exact-match ซึ่งไม่เจอปุ่ม dropdown ทั้ง 4 มาตั้งแต่ 2026-09-23 เมื่อ title ถูกเติม " — ค่าย่อย" ต่อท้าย — ฟีเจอร์เลื่อนปุ่มกลับจึงเสียเงียบ ๆ กับ 4 ปุ่มนี้ตลอดมา — เพิ่ม data-tool={key} ที่ปุ่ม tool ทุกตัว (ครบ 10/10 ค่าของ union Tool ไม่ซ้ำ) แล้ว query ด้วย button[data-tool="..."] แทน title (ตัด TOOLS.find ออกจาก effect — TOOLS ยังถูกใช้ที่ visibleTools จึงไม่ orphan) (4) แก้ตาม code review 2026-09-29: aria-pressed ของปุ่ม dropdown 4 ตัวใช้ตัวแปร active ใหม่ (const active = tool === key ประกาศครั้งเดียวใน map ใช้ร่วมกับ activeCls) แทน tool === "…" ซ้ำ (ค่าเท่ากันเป๊ะ branch บอง key) + sync rules/pdf-viewer.md ประโยค "ปุ่ม tool แสดง label กำหนดเอง" ให้ระบุเงื่อนไขเฉพาะตอน tool active ตรงโค้ดปัจจุบัน — ผ่าน code review (ไม่มี bug ระดับต้องแก้: active/activeCls equivalent ทุก branch, nested ternary stamp parse ถูก, ตำแหน่งเมนูไม่เพี้ยนเพราะ openToolMenu อ่าน getBoundingClientRect ก่อน React commit setState ที่ batch ไว้ + เมนูยึดขอบซ้ายปุ่ม, effect A2 กลับมาทำงานพร้อม guard เดิมครบ anyMenuOpen/selfScrollUntilRef) + tsc + vitest 193/193 (คำขอผู้ใช้ 2026-09-29)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-29 10:51:50 +07:00
..
AnnotationSelectionLayer.tsx stamp-editor: ตรายางพิมพ์เอง+จัดบรรทัดแบบถอยหลัง+มุมเอียง, editor กล่องข้อความ contentEditable + แก้ตาม code review 2026-09-29 ครบ 5 ข้อ — (1) STAMP คำพิมพ์เอง (D7): dropdown ของ tool stamp เป็น 2 หน้า — preset + แถว "กำหนดเอง…" (stampCustomMode reset ทุกครั้งที่เปิดเมนู) เปิด popup แก้ข้อความ/สี/ขนาด พร้อม preview ตรายาง live ตามสัดส่วนจริง (ความกว้างหน้าแรก × scale%) ใช้สูตร/ฟอนต์/measure ตัวเดียวกับ render จริง, คำพิมพ์ live ไป stampPreset เดียวกับ preset, Enter/ปุ่ม "ใช้ตรายางนี้" = ปิดเมนูแบบเลือกค่าจบ, ปุ่ม tool แสดง "กำหนดเอง" แทนคำเต็ม + แก้คำตรายางที่วางแล้วผ่านปุ่ม "แก้ไขข้อความ" ในแผง selection (StampEditor textarea ใน TextboxStampLayer ใช้ editingId/commitEdit ร่วมกับ TEXTBOX, จำกัด STAMP_MAX_LABEL_CHARS=50 ที่ UI — validator ยังยอม MAX_TEXT_CHARS ตามเจตนา) (2) จัดบรรทัดแบบถอยหลัง (คำขอผู้ใช้รอบสอง): สูตรย้ายรวมที่ utils/geometry.ts — stampLayout: คำสั้น 1 บรรทัดฟอนต์เต็ม, คำยาวพอดี ๆ ย่อฟอนต์ให้อยู่บรรทัดเดียวก่อน (stampFitFontSize เชิงเส้น) จนถึง floor 10% ของความกว้างกรอบ ("ตรวจสอบแล้ว" ย่อ ~25% = บรรทัดเดียว), ยาวเกิน floor กลับไปฟอนต์ใหญ่แล้ว wrapStampLabel ≤3 บรรทัด (×0.92/รอบ เกินบรรทัดยอมแลกกับฟอนต์อ่านออก), stampBoxHeight ให้ 1 บรรทัด = 0.4w เป๊ะเหมือนเดิม — ทุกมิติเป็นสัดส่วนของ w จำนวนบรรทัดจึงไม่ขึ้นกับ scale — constants + ฟังก์ชัน re-export จาก utils/annotation-display.ts ให้ import เดิมทุกจุด + measureStampText (canvas bold system-ui คืน 0 ตอน SSR) เป็น measure ของ render/preview/hit-test/eraser ทั้งฝั่ง client (annotationBBox รับ optional measure — ให้ความสูงกรอบตรง render จริง แก้ขอบล่างกรอบเกินตรายาง) ส่วนฝั่ง server (validator/export sticky note/describeAnnotationChange) ใช้ stampLayoutEstimate ความกว้างต่ออักษร 0.58 (3) มุมเอียง StampData.rotation? (−180..180 หมุนรอบกลางกรอบ): ช่อง "มุมเอียง" ในแผง selection (patch stampRotation, 0° = ถอด field) + จุดจับหมุนวงกลมเหนือขอบบน (mode rotate — atan2 delta รอบกลางกรอบ + snap ทวีคูณ 15° ≤4°), กรอบ selection+handles หมุนด้วย CSS transform ที่ origin เป็นพิกัดใน element ผ่าน rotOn() (ใส่พิกัด page แล้ว div แต่ละตัวหมุนคนละจุดจนกรอบไม่ตรงตัวตรายาง — bug 2026-09-28), annotationBBox คืน AABB ของกรอบหมุน (stampRect/rotatedRectAABB/rotatePoint — anchor handle scale เป็นมุมหลังหมุนบนจอ และ k คำนวณใน frame ของกรอบ), clampStampIntoPage เลื่อน position กลับเข้าหน้าตอนตั้งมุม (validator ตรวจ AABB ไม่ติดลบ ไม่ใช่ position ตรง ๆ — position มุมก่อนหมุนติดลบได้เมื่อกรอบจริงยังในหน้า), StampEditor แสดงตรง 0° พิมพ์ง่าย ปิดแล้วกลับไปเอียงค่าเดิม, export ใช้ pdf-lib rotate: degrees(−θ) + เลื่อน anchor ไปมุมล่างซ้ายหลังหมุน (pdf-lib หมุนรอบ anchor ไม่ใช่ center) + per-line baseline หมุนรอบกลางกรอบตรง CSS rotate ทุกองศา (4) TextboxEditor เป็น contentEditable WYSIWYG แทน textarea (ตัวหนาเฉพาะส่วน Ctrl/Cmd+B + Enter = insertLineBreak ไม่แตก div, paste เป็น plain text): utils/textbox-editor.ts ใหม่ — segmentsToHtml (escape & < > ก่อนใส่ dangerouslySetInnerHTML + <b> + <br>) และ serializeEditor (DOM → text รวม + segments แยกตัวหนา: ขึ้นบรรทัดจากทั้ง <br> และ <div>/<p> ที่ browser สร้าง — push \n ก่อนเข้า block ไม่ใช่หลังจบ block ไม่งั้น "abc<div>def</div>" ของ Chrome กลายเป็น "abcdef", dedupe <br>+block ติดกัน, trimEnd ตัดจาก segments ให้ตรง text กัน <br> ปิดท้ายของ browser ทิ้ง \n ค้างใน DB) — แยกเป็น pure DOM util ให้ unit test กับ jsdom ได้ (tests/utils/textbox-editor.test.ts ใหม่ 16 เคส ตาม pattern useCustomSelection-dom) (5) คิว update ต่อ id ใน PDFViewer (updateChainRef + pendingDataBaseRef): update ทุกตัวของ id เดียวกันเข้าคิวเดียวกัน base คำนวณตอนถึงตาคิวจาก data ล่าสุดที่คิวก่อนหน้าบันทึกไว้ — จอสัมผัส blur-commit ข้อความแล้วแตะสี/ขนาดต่อทันที คอมมิต full object จาก base เก่าจะทับค่าที่เพิ่งเปลี่ยน (code review 2026-09-28 รอบสอง) — ตัวควบคุมเล็กของแผง/editor commit เป็น AnnotationPatch ผสานผ่าน applyAnnotationPatch (color/strokeWidth/highlightStyle/highlightOpacity/stampScale/stampRotation/textbox/textboxText/label) (6) describeAnnotationChange: STAMP แสดง "มุมเอียง" และข้าม geometry เมื่อ label/มุมเปลี่ยน (geometry ต่างเป็นผลพลองของการจัดวาง แบบเดียวกับ TEXTBOX ที่ข้ามปรับขนาดเมื่อข้อความเปลี่ยน) (7) แก้ตาม code review 2026-09-29 ครบ: (a) patch stampScale/label ของตรายางเอียงเรียก clampStampIntoPage เหมือน stampRotation — เดิมขยาย/แก้ข้อความที่ชิดขอบแล้ว AABB ยื่นติดลบ update โดน validator ปฏิเสธเด้งกลับทั้งที่บนจอกรอบยังอยู่ในหน้า (b) validator STAMP ผ่อน tolerance ให้ AABB ติดลบได้ ≤2% ของหน้า กลืนความคลาด measure จริงของ browser กับ stampLayoutEstimate ฝั่ง server (ฟอนต์ต่อเครื่องทำให้จำนวนบรรทัด/AABB คลาด — ค่าที่บนจอพอดีเป๊ะต้องไม่ถูกปฏิเสธ) (c) applyUpdate: applyAnnotation เข้า try เดียวกับ action ให้ resolve() ใน finally ครอบทั้ง callback — เดิม dispatcher โยน sync แล้ว promise ไม่ settle ตลอดกาลจนคิวของ id นั้นตายถาวร + enqueueUpdate เปลี่ยน then(cleanup, cleanup) กัน unhandled rejection (d) catch ของ applyUpdate ล้าง pendingDataBaseRef — เดิม action กลางคิวล้มแล้ว update ถัดไปผสาน base ที่ไม่เคยติด DB จน commit การเปลี่ยนที่ล้มไปด้วย และ undo กู้สถานะที่ไม่มีอยู่จริง (e) ถอด as StampData cast 2 จุดใน AnnotationSelectionLayer — ย้าย stampData ที่ narrow ด้วย "label" in ขึ้นใช้แทนให้ type guard ทำงาน (f) export sticky note ของ STAMP ส่ง measure เดียวกับตอนวาดกรอบ (thaiFont.widthOfTextAtSize) ให้มุมเกาะตรงกรอบจริงไม่ใช่ estimate (8) InkLayer eraser ของ stamp ส่ง measureStampText ให้ bbox ตรง render จริง (9) docs sync ครบ: PLAN-FEAT.md D7/F18 + CLAUDE.md + rules/coordinates.md (patch เปลี่ยนขนาดกรอบเอียงต้อง clamp + tolerance validator 2%) + rules/pdf-viewer.md — ผ่าน code review (ไม่มี bug ระดับต้องแก้) + tsc + vitest 193/193 (คำขอผู้ใช้ 2026-09-28/29) 2026-09-29 09:56:00 +07:00
InkLayer.tsx stamp-editor: ตรายางพิมพ์เอง+จัดบรรทัดแบบถอยหลัง+มุมเอียง, editor กล่องข้อความ contentEditable + แก้ตาม code review 2026-09-29 ครบ 5 ข้อ — (1) STAMP คำพิมพ์เอง (D7): dropdown ของ tool stamp เป็น 2 หน้า — preset + แถว "กำหนดเอง…" (stampCustomMode reset ทุกครั้งที่เปิดเมนู) เปิด popup แก้ข้อความ/สี/ขนาด พร้อม preview ตรายาง live ตามสัดส่วนจริง (ความกว้างหน้าแรก × scale%) ใช้สูตร/ฟอนต์/measure ตัวเดียวกับ render จริง, คำพิมพ์ live ไป stampPreset เดียวกับ preset, Enter/ปุ่ม "ใช้ตรายางนี้" = ปิดเมนูแบบเลือกค่าจบ, ปุ่ม tool แสดง "กำหนดเอง" แทนคำเต็ม + แก้คำตรายางที่วางแล้วผ่านปุ่ม "แก้ไขข้อความ" ในแผง selection (StampEditor textarea ใน TextboxStampLayer ใช้ editingId/commitEdit ร่วมกับ TEXTBOX, จำกัด STAMP_MAX_LABEL_CHARS=50 ที่ UI — validator ยังยอม MAX_TEXT_CHARS ตามเจตนา) (2) จัดบรรทัดแบบถอยหลัง (คำขอผู้ใช้รอบสอง): สูตรย้ายรวมที่ utils/geometry.ts — stampLayout: คำสั้น 1 บรรทัดฟอนต์เต็ม, คำยาวพอดี ๆ ย่อฟอนต์ให้อยู่บรรทัดเดียวก่อน (stampFitFontSize เชิงเส้น) จนถึง floor 10% ของความกว้างกรอบ ("ตรวจสอบแล้ว" ย่อ ~25% = บรรทัดเดียว), ยาวเกิน floor กลับไปฟอนต์ใหญ่แล้ว wrapStampLabel ≤3 บรรทัด (×0.92/รอบ เกินบรรทัดยอมแลกกับฟอนต์อ่านออก), stampBoxHeight ให้ 1 บรรทัด = 0.4w เป๊ะเหมือนเดิม — ทุกมิติเป็นสัดส่วนของ w จำนวนบรรทัดจึงไม่ขึ้นกับ scale — constants + ฟังก์ชัน re-export จาก utils/annotation-display.ts ให้ import เดิมทุกจุด + measureStampText (canvas bold system-ui คืน 0 ตอน SSR) เป็น measure ของ render/preview/hit-test/eraser ทั้งฝั่ง client (annotationBBox รับ optional measure — ให้ความสูงกรอบตรง render จริง แก้ขอบล่างกรอบเกินตรายาง) ส่วนฝั่ง server (validator/export sticky note/describeAnnotationChange) ใช้ stampLayoutEstimate ความกว้างต่ออักษร 0.58 (3) มุมเอียง StampData.rotation? (−180..180 หมุนรอบกลางกรอบ): ช่อง "มุมเอียง" ในแผง selection (patch stampRotation, 0° = ถอด field) + จุดจับหมุนวงกลมเหนือขอบบน (mode rotate — atan2 delta รอบกลางกรอบ + snap ทวีคูณ 15° ≤4°), กรอบ selection+handles หมุนด้วย CSS transform ที่ origin เป็นพิกัดใน element ผ่าน rotOn() (ใส่พิกัด page แล้ว div แต่ละตัวหมุนคนละจุดจนกรอบไม่ตรงตัวตรายาง — bug 2026-09-28), annotationBBox คืน AABB ของกรอบหมุน (stampRect/rotatedRectAABB/rotatePoint — anchor handle scale เป็นมุมหลังหมุนบนจอ และ k คำนวณใน frame ของกรอบ), clampStampIntoPage เลื่อน position กลับเข้าหน้าตอนตั้งมุม (validator ตรวจ AABB ไม่ติดลบ ไม่ใช่ position ตรง ๆ — position มุมก่อนหมุนติดลบได้เมื่อกรอบจริงยังในหน้า), StampEditor แสดงตรง 0° พิมพ์ง่าย ปิดแล้วกลับไปเอียงค่าเดิม, export ใช้ pdf-lib rotate: degrees(−θ) + เลื่อน anchor ไปมุมล่างซ้ายหลังหมุน (pdf-lib หมุนรอบ anchor ไม่ใช่ center) + per-line baseline หมุนรอบกลางกรอบตรง CSS rotate ทุกองศา (4) TextboxEditor เป็น contentEditable WYSIWYG แทน textarea (ตัวหนาเฉพาะส่วน Ctrl/Cmd+B + Enter = insertLineBreak ไม่แตก div, paste เป็น plain text): utils/textbox-editor.ts ใหม่ — segmentsToHtml (escape & < > ก่อนใส่ dangerouslySetInnerHTML + <b> + <br>) และ serializeEditor (DOM → text รวม + segments แยกตัวหนา: ขึ้นบรรทัดจากทั้ง <br> และ <div>/<p> ที่ browser สร้าง — push \n ก่อนเข้า block ไม่ใช่หลังจบ block ไม่งั้น "abc<div>def</div>" ของ Chrome กลายเป็น "abcdef", dedupe <br>+block ติดกัน, trimEnd ตัดจาก segments ให้ตรง text กัน <br> ปิดท้ายของ browser ทิ้ง \n ค้างใน DB) — แยกเป็น pure DOM util ให้ unit test กับ jsdom ได้ (tests/utils/textbox-editor.test.ts ใหม่ 16 เคส ตาม pattern useCustomSelection-dom) (5) คิว update ต่อ id ใน PDFViewer (updateChainRef + pendingDataBaseRef): update ทุกตัวของ id เดียวกันเข้าคิวเดียวกัน base คำนวณตอนถึงตาคิวจาก data ล่าสุดที่คิวก่อนหน้าบันทึกไว้ — จอสัมผัส blur-commit ข้อความแล้วแตะสี/ขนาดต่อทันที คอมมิต full object จาก base เก่าจะทับค่าที่เพิ่งเปลี่ยน (code review 2026-09-28 รอบสอง) — ตัวควบคุมเล็กของแผง/editor commit เป็น AnnotationPatch ผสานผ่าน applyAnnotationPatch (color/strokeWidth/highlightStyle/highlightOpacity/stampScale/stampRotation/textbox/textboxText/label) (6) describeAnnotationChange: STAMP แสดง "มุมเอียง" และข้าม geometry เมื่อ label/มุมเปลี่ยน (geometry ต่างเป็นผลพลองของการจัดวาง แบบเดียวกับ TEXTBOX ที่ข้ามปรับขนาดเมื่อข้อความเปลี่ยน) (7) แก้ตาม code review 2026-09-29 ครบ: (a) patch stampScale/label ของตรายางเอียงเรียก clampStampIntoPage เหมือน stampRotation — เดิมขยาย/แก้ข้อความที่ชิดขอบแล้ว AABB ยื่นติดลบ update โดน validator ปฏิเสธเด้งกลับทั้งที่บนจอกรอบยังอยู่ในหน้า (b) validator STAMP ผ่อน tolerance ให้ AABB ติดลบได้ ≤2% ของหน้า กลืนความคลาด measure จริงของ browser กับ stampLayoutEstimate ฝั่ง server (ฟอนต์ต่อเครื่องทำให้จำนวนบรรทัด/AABB คลาด — ค่าที่บนจอพอดีเป๊ะต้องไม่ถูกปฏิเสธ) (c) applyUpdate: applyAnnotation เข้า try เดียวกับ action ให้ resolve() ใน finally ครอบทั้ง callback — เดิม dispatcher โยน sync แล้ว promise ไม่ settle ตลอดกาลจนคิวของ id นั้นตายถาวร + enqueueUpdate เปลี่ยน then(cleanup, cleanup) กัน unhandled rejection (d) catch ของ applyUpdate ล้าง pendingDataBaseRef — เดิม action กลางคิวล้มแล้ว update ถัดไปผสาน base ที่ไม่เคยติด DB จน commit การเปลี่ยนที่ล้มไปด้วย และ undo กู้สถานะที่ไม่มีอยู่จริง (e) ถอด as StampData cast 2 จุดใน AnnotationSelectionLayer — ย้าย stampData ที่ narrow ด้วย "label" in ขึ้นใช้แทนให้ type guard ทำงาน (f) export sticky note ของ STAMP ส่ง measure เดียวกับตอนวาดกรอบ (thaiFont.widthOfTextAtSize) ให้มุมเกาะตรงกรอบจริงไม่ใช่ estimate (8) InkLayer eraser ของ stamp ส่ง measureStampText ให้ bbox ตรง render จริง (9) docs sync ครบ: PLAN-FEAT.md D7/F18 + CLAUDE.md + rules/coordinates.md (patch เปลี่ยนขนาดกรอบเอียงต้อง clamp + tolerance validator 2%) + rules/pdf-viewer.md — ผ่าน code review (ไม่มี bug ระดับต้องแก้) + tsc + vitest 193/193 (คำขอผู้ใช้ 2026-09-28/29) 2026-09-29 09:56:00 +07:00
PDFViewer.tsx toolbar: ปุ่ม tool dropdown แสดงค่าที่เลือกเฉพาะตอน active + ฟื้น effect เลื่อนปุ่ม active กลับกึ่งกลางแถบ (เสียเงียบมาตั้งแต่ 2026-09-23) — (1) ปัญหา: คำขอผู้ใช้ 2026-09-23 เดิมให้ปุ่ม dropdown ที่มีค่าย่อย (มาร์กข้อความ/วาดเส้น/รูปทรง/ตรายาง) แสดง "ชื่อตัวเลือกล่าสุด" แทนชื่อ tool เสมอเพื่อเห็นค่าปัจจุบันโดยไม่ต้องเปิดเมนู — แต่ทำให้ชื่อ tool หายไปจาก toolbar ทั้งหมด: ปุ่ม "วาดเส้น" แสดง "ปากกา" ตั้งแต่ยังไม่ได้เลือกอะไรเลย (default drawPen=pencil) และค้างอยู่อย่างนั้นแม้สลับไป tool อื่น (ผู้ใช้รายงาน 2026-09-29: "label ของ tool ทำไมไม่เปลี่ยนทั้งที่ยังไม่ได้เลือน เช่น วาด แต่แสดง ปากกาเลย") — ขัดเหตุผลเดิมของการใส่ข้อความบนปุ่ม (2026-09-21: ผู้ใช้ศาลไม่คุ้น icon ล้วน) เพราะมองคำ "ปากกา" แล้วไม่รู้ว่านี่คือปุ่มวาดเส้น (2) แก้ตามคำขอผู้ใช้ 2026-09-29 (เลือกจาก 3 ทางเลือกที่เสนอ): tool ไม่ active → แสดง text ชื่อ tool + Icon ของ tool, title/aria-label = label เต็มเหมือนปุ่มเดี่ยวอื่น; tool active → แสดงค่าที่เลือกไว้ (ค่าที่จะใช้จริงเมื่อวาด) + icon ของค่า (draw/highlight เปลี่ยน icon ตามชนิดปากกา/รูปแบบมาร์ก, shape/stamp ใช้ tool icon เหมือนเดิม), title = "label — ค่า" ตาม pattern เดิม — กดปุ่ม dropdown เรียก switchTool เสมออยู่แล้ว (เคลียร์ popup) ปุ่มจึงสลับไปแสดงค่าทันทีที่เปิดเมนู สอดคล้องกับ "กำลังจะใช้ค่านี้" — ปุ่ม active กว้าง/แคบตามความยาวค่า ("ปากกา" สั้นกว่า "วาดเส้น") แถบรองรับอยู่แล้ว (whitespace-nowrap + max-lg:overflow-x-auto) (3) แก้ bug แฝงที่เจอระหว่างแก้: effect เลื่อนปุ่ม tool active กลับกึ่งกลางแถบบนจอ < lg (UX review A2 2026-09-21) ใช้ querySelector(button[title="..."]) exact-match ซึ่งไม่เจอปุ่ม dropdown ทั้ง 4 มาตั้งแต่ 2026-09-23 เมื่อ title ถูกเติม " — ค่าย่อย" ต่อท้าย — ฟีเจอร์เลื่อนปุ่มกลับจึงเสียเงียบ ๆ กับ 4 ปุ่มนี้ตลอดมา — เพิ่ม data-tool={key} ที่ปุ่ม tool ทุกตัว (ครบ 10/10 ค่าของ union Tool ไม่ซ้ำ) แล้ว query ด้วย button[data-tool="..."] แทน title (ตัด TOOLS.find ออกจาก effect — TOOLS ยังถูกใช้ที่ visibleTools จึงไม่ orphan) (4) แก้ตาม code review 2026-09-29: aria-pressed ของปุ่ม dropdown 4 ตัวใช้ตัวแปร active ใหม่ (const active = tool === key ประกาศครั้งเดียวใน map ใช้ร่วมกับ activeCls) แทน tool === "…" ซ้ำ (ค่าเท่ากันเป๊ะ branch บอง key) + sync rules/pdf-viewer.md ประโยค "ปุ่ม tool แสดง label กำหนดเอง" ให้ระบุเงื่อนไขเฉพาะตอน tool active ตรงโค้ดปัจจุบัน — ผ่าน code review (ไม่มี bug ระดับต้องแก้: active/activeCls equivalent ทุก branch, nested ternary stamp parse ถูก, ตำแหน่งเมนูไม่เพี้ยนเพราะ openToolMenu อ่าน getBoundingClientRect ก่อน React commit setState ที่ batch ไว้ + เมนูยึดขอบซ้ายปุ่ม, effect A2 กลับมาทำงานพร้อม guard เดิมครบ anyMenuOpen/selfScrollUntilRef) + tsc + vitest 193/193 (คำขอผู้ใช้ 2026-09-29) 2026-09-29 10:51:50 +07:00
TextboxStampLayer.tsx stamp-editor: ตรายางพิมพ์เอง+จัดบรรทัดแบบถอยหลัง+มุมเอียง, editor กล่องข้อความ contentEditable + แก้ตาม code review 2026-09-29 ครบ 5 ข้อ — (1) STAMP คำพิมพ์เอง (D7): dropdown ของ tool stamp เป็น 2 หน้า — preset + แถว "กำหนดเอง…" (stampCustomMode reset ทุกครั้งที่เปิดเมนู) เปิด popup แก้ข้อความ/สี/ขนาด พร้อม preview ตรายาง live ตามสัดส่วนจริง (ความกว้างหน้าแรก × scale%) ใช้สูตร/ฟอนต์/measure ตัวเดียวกับ render จริง, คำพิมพ์ live ไป stampPreset เดียวกับ preset, Enter/ปุ่ม "ใช้ตรายางนี้" = ปิดเมนูแบบเลือกค่าจบ, ปุ่ม tool แสดง "กำหนดเอง" แทนคำเต็ม + แก้คำตรายางที่วางแล้วผ่านปุ่ม "แก้ไขข้อความ" ในแผง selection (StampEditor textarea ใน TextboxStampLayer ใช้ editingId/commitEdit ร่วมกับ TEXTBOX, จำกัด STAMP_MAX_LABEL_CHARS=50 ที่ UI — validator ยังยอม MAX_TEXT_CHARS ตามเจตนา) (2) จัดบรรทัดแบบถอยหลัง (คำขอผู้ใช้รอบสอง): สูตรย้ายรวมที่ utils/geometry.ts — stampLayout: คำสั้น 1 บรรทัดฟอนต์เต็ม, คำยาวพอดี ๆ ย่อฟอนต์ให้อยู่บรรทัดเดียวก่อน (stampFitFontSize เชิงเส้น) จนถึง floor 10% ของความกว้างกรอบ ("ตรวจสอบแล้ว" ย่อ ~25% = บรรทัดเดียว), ยาวเกิน floor กลับไปฟอนต์ใหญ่แล้ว wrapStampLabel ≤3 บรรทัด (×0.92/รอบ เกินบรรทัดยอมแลกกับฟอนต์อ่านออก), stampBoxHeight ให้ 1 บรรทัด = 0.4w เป๊ะเหมือนเดิม — ทุกมิติเป็นสัดส่วนของ w จำนวนบรรทัดจึงไม่ขึ้นกับ scale — constants + ฟังก์ชัน re-export จาก utils/annotation-display.ts ให้ import เดิมทุกจุด + measureStampText (canvas bold system-ui คืน 0 ตอน SSR) เป็น measure ของ render/preview/hit-test/eraser ทั้งฝั่ง client (annotationBBox รับ optional measure — ให้ความสูงกรอบตรง render จริง แก้ขอบล่างกรอบเกินตรายาง) ส่วนฝั่ง server (validator/export sticky note/describeAnnotationChange) ใช้ stampLayoutEstimate ความกว้างต่ออักษร 0.58 (3) มุมเอียง StampData.rotation? (−180..180 หมุนรอบกลางกรอบ): ช่อง "มุมเอียง" ในแผง selection (patch stampRotation, 0° = ถอด field) + จุดจับหมุนวงกลมเหนือขอบบน (mode rotate — atan2 delta รอบกลางกรอบ + snap ทวีคูณ 15° ≤4°), กรอบ selection+handles หมุนด้วย CSS transform ที่ origin เป็นพิกัดใน element ผ่าน rotOn() (ใส่พิกัด page แล้ว div แต่ละตัวหมุนคนละจุดจนกรอบไม่ตรงตัวตรายาง — bug 2026-09-28), annotationBBox คืน AABB ของกรอบหมุน (stampRect/rotatedRectAABB/rotatePoint — anchor handle scale เป็นมุมหลังหมุนบนจอ และ k คำนวณใน frame ของกรอบ), clampStampIntoPage เลื่อน position กลับเข้าหน้าตอนตั้งมุม (validator ตรวจ AABB ไม่ติดลบ ไม่ใช่ position ตรง ๆ — position มุมก่อนหมุนติดลบได้เมื่อกรอบจริงยังในหน้า), StampEditor แสดงตรง 0° พิมพ์ง่าย ปิดแล้วกลับไปเอียงค่าเดิม, export ใช้ pdf-lib rotate: degrees(−θ) + เลื่อน anchor ไปมุมล่างซ้ายหลังหมุน (pdf-lib หมุนรอบ anchor ไม่ใช่ center) + per-line baseline หมุนรอบกลางกรอบตรง CSS rotate ทุกองศา (4) TextboxEditor เป็น contentEditable WYSIWYG แทน textarea (ตัวหนาเฉพาะส่วน Ctrl/Cmd+B + Enter = insertLineBreak ไม่แตก div, paste เป็น plain text): utils/textbox-editor.ts ใหม่ — segmentsToHtml (escape & < > ก่อนใส่ dangerouslySetInnerHTML + <b> + <br>) และ serializeEditor (DOM → text รวม + segments แยกตัวหนา: ขึ้นบรรทัดจากทั้ง <br> และ <div>/<p> ที่ browser สร้าง — push \n ก่อนเข้า block ไม่ใช่หลังจบ block ไม่งั้น "abc<div>def</div>" ของ Chrome กลายเป็น "abcdef", dedupe <br>+block ติดกัน, trimEnd ตัดจาก segments ให้ตรง text กัน <br> ปิดท้ายของ browser ทิ้ง \n ค้างใน DB) — แยกเป็น pure DOM util ให้ unit test กับ jsdom ได้ (tests/utils/textbox-editor.test.ts ใหม่ 16 เคส ตาม pattern useCustomSelection-dom) (5) คิว update ต่อ id ใน PDFViewer (updateChainRef + pendingDataBaseRef): update ทุกตัวของ id เดียวกันเข้าคิวเดียวกัน base คำนวณตอนถึงตาคิวจาก data ล่าสุดที่คิวก่อนหน้าบันทึกไว้ — จอสัมผัส blur-commit ข้อความแล้วแตะสี/ขนาดต่อทันที คอมมิต full object จาก base เก่าจะทับค่าที่เพิ่งเปลี่ยน (code review 2026-09-28 รอบสอง) — ตัวควบคุมเล็กของแผง/editor commit เป็น AnnotationPatch ผสานผ่าน applyAnnotationPatch (color/strokeWidth/highlightStyle/highlightOpacity/stampScale/stampRotation/textbox/textboxText/label) (6) describeAnnotationChange: STAMP แสดง "มุมเอียง" และข้าม geometry เมื่อ label/มุมเปลี่ยน (geometry ต่างเป็นผลพลองของการจัดวาง แบบเดียวกับ TEXTBOX ที่ข้ามปรับขนาดเมื่อข้อความเปลี่ยน) (7) แก้ตาม code review 2026-09-29 ครบ: (a) patch stampScale/label ของตรายางเอียงเรียก clampStampIntoPage เหมือน stampRotation — เดิมขยาย/แก้ข้อความที่ชิดขอบแล้ว AABB ยื่นติดลบ update โดน validator ปฏิเสธเด้งกลับทั้งที่บนจอกรอบยังอยู่ในหน้า (b) validator STAMP ผ่อน tolerance ให้ AABB ติดลบได้ ≤2% ของหน้า กลืนความคลาด measure จริงของ browser กับ stampLayoutEstimate ฝั่ง server (ฟอนต์ต่อเครื่องทำให้จำนวนบรรทัด/AABB คลาด — ค่าที่บนจอพอดีเป๊ะต้องไม่ถูกปฏิเสธ) (c) applyUpdate: applyAnnotation เข้า try เดียวกับ action ให้ resolve() ใน finally ครอบทั้ง callback — เดิม dispatcher โยน sync แล้ว promise ไม่ settle ตลอดกาลจนคิวของ id นั้นตายถาวร + enqueueUpdate เปลี่ยน then(cleanup, cleanup) กัน unhandled rejection (d) catch ของ applyUpdate ล้าง pendingDataBaseRef — เดิม action กลางคิวล้มแล้ว update ถัดไปผสาน base ที่ไม่เคยติด DB จน commit การเปลี่ยนที่ล้มไปด้วย และ undo กู้สถานะที่ไม่มีอยู่จริง (e) ถอด as StampData cast 2 จุดใน AnnotationSelectionLayer — ย้าย stampData ที่ narrow ด้วย "label" in ขึ้นใช้แทนให้ type guard ทำงาน (f) export sticky note ของ STAMP ส่ง measure เดียวกับตอนวาดกรอบ (thaiFont.widthOfTextAtSize) ให้มุมเกาะตรงกรอบจริงไม่ใช่ estimate (8) InkLayer eraser ของ stamp ส่ง measureStampText ให้ bbox ตรง render จริง (9) docs sync ครบ: PLAN-FEAT.md D7/F18 + CLAUDE.md + rules/coordinates.md (patch เปลี่ยนขนาดกรอบเอียงต้อง clamp + tolerance validator 2%) + rules/pdf-viewer.md — ผ่าน code review (ไม่มี bug ระดับต้องแก้) + tsc + vitest 193/193 (คำขอผู้ใช้ 2026-09-28/29) 2026-09-29 09:56:00 +07:00
ToolControls.tsx 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) 2026-09-24 11:46:02 +07:00