From e0c78d68fe5e497e482c6d7670cd45e6a22390c4 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 17 Sep 2026 17:02:31 +0700 Subject: [PATCH] =?UTF-8?q?bookmark:=20=E0=B8=94=E0=B8=B1=E0=B8=81?= =?UTF-8?q?=E0=B8=84=E0=B8=A7=E0=B8=B2=E0=B8=A1=E0=B8=A2=E0=B8=B2=E0=B8=A7?= =?UTF-8?q?=E0=B8=8A=E0=B8=B7=E0=B9=88=E0=B8=AD/=E0=B9=82=E0=B8=99?= =?UTF-8?q?=E0=B9=89=E0=B8=95=E0=B8=97=E0=B8=B5=E0=B9=88=E0=B8=8A=E0=B9=88?= =?UTF-8?q?=E0=B8=AD=E0=B8=87=E0=B8=81=E0=B8=A3=E0=B8=AD=E0=B8=81=20+=20F1?= =?UTF-8?q?9=20=E0=B8=AA=E0=B8=A3=E0=B8=B8=E0=B8=9B=E0=B8=AA=E0=B8=B4?= =?UTF-8?q?=E0=B9=88=E0=B8=87=E0=B8=97=E0=B8=B5=E0=B9=88=E0=B9=80=E0=B8=9B?= =?UTF-8?q?=E0=B8=A5=E0=B8=B5=E0=B9=88=E0=B8=A2=E0=B8=99=E0=B9=83=E0=B8=99?= =?UTF-8?q?=20dialog=20=E0=B8=9B=E0=B8=A3=E0=B8=B0=E0=B8=A7=E0=B8=B1?= =?UTF-8?q?=E0=B8=95=E0=B8=B4=20+=20formatThaiDate=20=E2=80=94=20(1)=20boo?= =?UTF-8?q?kmark=20limits:=20utils/bookmark-limits.ts=20=E0=B9=83=E0=B8=AB?= =?UTF-8?q?=E0=B8=A1=E0=B9=88=E0=B9=80=E0=B8=9B=E0=B9=87=E0=B8=99=20source?= =?UTF-8?q?=20of=20truth=20=E0=B9=80=E0=B8=94=E0=B8=B5=E0=B8=A2=E0=B8=A7?= =?UTF-8?q?=20(=E0=B8=8A=E0=B8=B7=E0=B9=88=E0=B8=AD=20120/=E0=B9=82?= =?UTF-8?q?=E0=B8=99=E0=B9=89=E0=B8=95=20200=20=E0=B8=95=E0=B8=B1=E0=B8=A7?= =?UTF-8?q?=E0=B8=AD=E0=B8=B1=E0=B8=81=E0=B8=A9=E0=B8=A3)=20server=20actio?= =?UTF-8?q?n=20import=20=E0=B8=A3=E0=B9=88=E0=B8=A7=E0=B8=A1=E0=B9=81?= =?UTF-8?q?=E0=B8=97=E0=B8=99=20const=20=E0=B8=97=E0=B9=89=E0=B8=AD?= =?UTF-8?q?=E0=B8=87=E0=B8=96=E0=B8=B4=E0=B9=88=E0=B8=99=20+=20=E0=B9=83?= =?UTF-8?q?=E0=B8=AA=E0=B9=88=20maxLength=20=E0=B8=97=E0=B8=B5=E0=B9=88=20?= =?UTF-8?q?popup=20=E0=B8=9A=E0=B8=B1=E0=B8=99=E0=B8=97=E0=B8=B6=E0=B8=81?= =?UTF-8?q?=20bookmark=20(PDFViewer)=20=E0=B9=81=E0=B8=A5=E0=B8=B0?= =?UTF-8?q?=E0=B8=9F=E0=B8=AD=E0=B8=A3=E0=B9=8C=E0=B8=A1=E0=B9=81=E0=B8=81?= =?UTF-8?q?=E0=B9=89=E0=B9=84=E0=B8=82=20=E2=9C=8F=20=E0=B9=83=E0=B8=99=20?= =?UTF-8?q?sidebar=20=E0=B8=94=E0=B8=B1=E0=B8=81=E0=B8=97=E0=B8=B1?= =?UTF-8?q?=E0=B9=89=E0=B8=87=E0=B8=9E=E0=B8=B4=E0=B8=A1=E0=B8=9E=E0=B9=8C?= =?UTF-8?q?/paste=20=E0=B8=95=E0=B8=B1=E0=B9=89=E0=B8=87=E0=B9=81=E0=B8=95?= =?UTF-8?q?=E0=B9=88=E0=B9=83=E0=B8=99=E0=B8=8A=E0=B9=88=E0=B8=AD=E0=B8=87?= =?UTF-8?q?=20=E0=B9=84=E0=B8=A1=E0=B9=88=E0=B8=A3=E0=B8=AD=20error=20?= =?UTF-8?q?=E0=B8=95=E0=B8=AD=E0=B8=99=E0=B8=81=E0=B8=94=E0=B8=9A=E0=B8=B1?= =?UTF-8?q?=E0=B8=99=E0=B8=97=E0=B8=B6=E0=B8=81=20+=20=E0=B9=81=E0=B8=81?= =?UTF-8?q?=E0=B9=89=20off-by-one=20=E0=B8=82=E0=B8=AD=E0=B8=87=20normaliz?= =?UTF-8?q?eBookmarkTitle=20(=E0=B9=80=E0=B8=94=E0=B8=B4=E0=B8=A1=20slice?= =?UTF-8?q?=20120=20=E0=B9=81=E0=B8=A5=E0=B9=89=E0=B8=A7=E0=B8=95=E0=B9=88?= =?UTF-8?q?=E0=B8=AD=20=E2=80=A6=20=3D=20121=20=E0=B8=95=E0=B8=B1=E0=B8=A7?= =?UTF-8?q?=20=E0=B8=97=E0=B8=B3=E0=B9=83=E0=B8=AB=E0=B9=89=E0=B8=84?= =?UTF-8?q?=E0=B8=A5=E0=B8=B8=E0=B8=A1=E0=B8=82=E0=B9=89=E0=B8=AD=E0=B8=84?= =?UTF-8?q?=E0=B8=A7=E0=B8=B2=E0=B8=A1=E0=B8=A2=E0=B8=B2=E0=B8=A7=E0=B9=81?= =?UTF-8?q?=E0=B8=A5=E0=B9=89=E0=B8=A7=E0=B8=81=E0=B8=94=20B=20=E0=B8=9A?= =?UTF-8?q?=E0=B8=B1=E0=B8=99=E0=B8=97=E0=B8=B6=E0=B8=81=E0=B9=80=E0=B8=A5?= =?UTF-8?q?=E0=B8=A2=E0=B9=82=E0=B8=94=E0=B8=99=20limit=20=E0=B8=82?= =?UTF-8?q?=E0=B8=AD=E0=B8=87=20server=20=E0=B8=95=E0=B8=B5=E0=B8=81?= =?UTF-8?q?=E0=B8=A5=E0=B8=B1=E0=B8=9A=E0=B8=97=E0=B8=B1=E0=B9=89=E0=B8=87?= =?UTF-8?q?=E0=B8=97=E0=B8=B5=E0=B9=88=E0=B9=84=E0=B8=A1=E0=B9=88=E0=B9=84?= =?UTF-8?q?=E0=B8=94=E0=B9=89=E0=B8=9E=E0=B8=B4=E0=B8=A1=E0=B8=9E=E0=B9=8C?= =?UTF-8?q?=E0=B9=80=E0=B8=AD=E0=B8=87=20=E2=80=94=20=E0=B8=95=E0=B8=AD?= =?UTF-8?q?=E0=B8=99=E0=B8=99=E0=B8=B5=E0=B9=89=20slice=20119=20+=20?= =?UTF-8?q?=E2=80=A6=20=3D=20120=20=E0=B8=9E=E0=B8=AD=E0=B8=94=E0=B8=B5?= =?UTF-8?q?=E0=B8=95=E0=B8=B2=E0=B8=A1=E0=B8=AA=E0=B9=80=E0=B8=9B=E0=B8=81?= =?UTF-8?q?=20=C2=A77=20N2=20=E0=B8=97=E0=B8=B5=E0=B9=88=E0=B9=80=E0=B8=82?= =?UTF-8?q?=E0=B8=B5=E0=B8=A2=E0=B8=99=E0=B9=84=E0=B8=A7=E0=B9=89=20?= =?UTF-8?q?=E2=89=A4=20120=20=E0=B8=AD=E0=B8=A2=E0=B8=B9=E0=B9=88=E0=B9=81?= =?UTF-8?q?=E0=B8=A5=E0=B9=89=E0=B8=A7)=20+=20PLAN-FEAT.md=20=C2=A77=20N2?= =?UTF-8?q?=20=E0=B8=9A=E0=B8=B1=E0=B8=99=E0=B8=97=E0=B8=B6=E0=B8=81?= =?UTF-8?q?=E0=B9=80=E0=B8=9E=E0=B8=B4=E0=B9=88=E0=B8=A1=20=E2=80=94=20(2)?= =?UTF-8?q?=20=E0=B8=87=E0=B8=B2=E0=B8=99=E0=B8=84=E0=B8=B9=E0=B9=88?= =?UTF-8?q?=E0=B8=82=E0=B8=99=E0=B8=B2=E0=B8=94=E0=B8=88=E0=B8=B2=E0=B8=81?= =?UTF-8?q?=E0=B8=AD=E0=B8=B5=E0=B8=81=20session=20(=E0=B9=81=E0=B8=8A?= =?UTF-8?q?=E0=B8=A3=E0=B9=8C=E0=B9=84=E0=B8=9F=E0=B8=A5=E0=B9=8C=20PDFVie?= =?UTF-8?q?wer/sidebar=20=E0=B9=80=E0=B8=A5=E0=B8=A2=E0=B8=A3=E0=B8=A7?= =?UTF-8?q?=E0=B8=A1=20commit):=20dialog=20=E0=B8=9B=E0=B8=A3=E0=B8=B0?= =?UTF-8?q?=E0=B8=A7=E0=B8=B1=E0=B8=95=E0=B8=B4=20F19=20=E0=B9=81=E0=B8=AA?= =?UTF-8?q?=E0=B8=94=E0=B8=87=E0=B8=AA=E0=B8=A3=E0=B8=B8=E0=B8=9B=E0=B8=AA?= =?UTF-8?q?=E0=B8=B4=E0=B9=88=E0=B8=87=E0=B8=97=E0=B8=B5=E0=B9=88=E0=B9=80?= =?UTF-8?q?=E0=B8=9B=E0=B8=A5=E0=B8=B5=E0=B9=88=E0=B8=A2=E0=B8=99=E0=B8=95?= =?UTF-8?q?=E0=B9=88=E0=B8=AD=E0=B8=A3=E0=B8=AD=E0=B8=9A=E0=B8=9C=E0=B9=88?= =?UTF-8?q?=E0=B8=B2=E0=B8=99=20describeAnnotationChange=20=E0=B9=83?= =?UTF-8?q?=E0=B8=AB=E0=B8=A1=E0=B9=88=E0=B9=83=E0=B8=99=20utils/annotatio?= =?UTF-8?q?n-display.ts=20(=E0=B9=80=E0=B8=97=E0=B8=B5=E0=B8=A2=E0=B8=9A?= =?UTF-8?q?=20snapshot=20=E0=B8=81=E0=B9=88=E0=B8=AD=E0=B8=99=E0=B9=81?= =?UTF-8?q?=E0=B8=81=E0=B9=89=20v.data=20=E0=B8=81=E0=B8=B1=E0=B8=9A=20sta?= =?UTF-8?q?te=20=E0=B8=96=E0=B8=B1=E0=B8=94=E0=B9=84=E0=B8=9B=20=E2=80=94?= =?UTF-8?q?=20snapshot=20=E0=B8=A3=E0=B8=AD=E0=B8=9A=E0=B9=83=E0=B8=AB?= =?UTF-8?q?=E0=B8=A1=E0=B9=88=E0=B8=81=E0=B8=A7=E0=B9=88=E0=B8=B2=20?= =?UTF-8?q?=E0=B8=AB=E0=B8=A3=E0=B8=B7=E0=B8=AD=E0=B8=84=E0=B9=88=E0=B8=B2?= =?UTF-8?q?=E0=B8=9B=E0=B8=B1=E0=B8=88=E0=B8=88=E0=B8=B8=E0=B8=9A=E0=B8=B1?= =?UTF-8?q?=E0=B8=99=E0=B8=88=E0=B8=B2=E0=B8=81=20realtime=20lookup=20?= =?UTF-8?q?=E0=B8=AA=E0=B8=94=20historyCurrent=20=E2=80=94=20=E0=B8=AA?= =?UTF-8?q?=E0=B8=A3=E0=B8=B8=E0=B8=9B=E0=B9=80=E0=B8=9B=E0=B9=87=E0=B8=99?= =?UTF-8?q?=E0=B8=AB=E0=B8=99=E0=B9=88=E0=B8=A7=E0=B8=A2=E0=B9=80=E0=B8=94?= =?UTF-8?q?=E0=B8=B5=E0=B8=A2=E0=B8=A7=E0=B8=81=E0=B8=B1=E0=B8=9A=E0=B8=95?= =?UTF-8?q?=E0=B8=B1=E0=B8=A7=E0=B8=9B=E0=B8=A3=E0=B8=B1=E0=B8=9A=20UI:=20?= =?UTF-8?q?=E0=B8=84=E0=B8=A7=E0=B8=B2=E0=B8=A1=E0=B8=AB=E0=B8=99=E0=B8=B2?= =?UTF-8?q?=20=C3=9710=20=E0=B8=84=E0=B8=A7=E0=B8=B2=E0=B8=A1=E0=B8=97?= =?UTF-8?q?=E0=B8=B6=E0=B8=9A=20%=20fontSize=20px=20=E0=B8=8A=E0=B8=B7?= =?UTF-8?q?=E0=B9=88=E0=B8=AD=E0=B8=AA=E0=B8=B5=20preset=20=E0=B8=A2?= =?UTF-8?q?=E0=B9=89=E0=B8=B2=E0=B8=A2/=E0=B8=9B=E0=B8=A3=E0=B8=B1?= =?UTF-8?q?=E0=B8=9A=E0=B8=82=E0=B8=99=E0=B8=B2=E0=B8=94=E0=B8=88=E0=B8=B2?= =?UTF-8?q?=E0=B8=81=20bbox=20=E0=B8=9C=E0=B8=A5=E0=B8=95=E0=B9=88?= =?UTF-8?q?=E0=B8=B2=E0=B8=87=E0=B9=80=E0=B8=81=E0=B8=B4=E0=B8=99=20GEOM?= =?UTF-8?q?=5FEPS=5FPCT=200.5%=20=E0=B8=82=E0=B9=89=E0=B8=AD=E0=B8=84?= =?UTF-8?q?=E0=B8=A7=E0=B8=B2=E0=B8=A1/=E0=B9=82=E0=B8=99=E0=B9=89?= =?UTF-8?q?=E0=B8=95/label)=20+=20dialog=20=E0=B8=82=E0=B8=A2=E0=B8=B2?= =?UTF-8?q?=E0=B8=A2=20w-96=20max-h-[60vh]=20=E0=B9=81=E0=B8=96=E0=B8=A7?= =?UTF-8?q?=E0=B9=80=E0=B8=9B=E0=B8=A5=E0=B8=B5=E0=B9=88=E0=B8=A2=E0=B8=99?= =?UTF-8?q?=20items-start=20+=20utils/date-format.ts=20=E0=B9=83=E0=B8=AB?= =?UTF-8?q?=E0=B8=A1=E0=B9=88=20formatThaiDate=20=E0=B8=A7=E0=B8=B1?= =?UTF-8?q?=E0=B8=99=E0=B8=97=E0=B8=B5=E0=B9=88=E0=B9=84=E0=B8=97=E0=B8=A2?= =?UTF-8?q?=E0=B9=80=E0=B8=95=E0=B9=87=E0=B8=A1=20"18=20=E0=B8=81=E0=B8=B1?= =?UTF-8?q?=E0=B8=99=E0=B8=A2=E0=B8=B2=E0=B8=A2=E0=B8=99=20=E0=B8=9E.?= =?UTF-8?q?=E0=B8=A8.=202569"=20(+=E0=B9=80=E0=B8=A7=E0=B8=A5=E0=B8=B2)=20?= =?UTF-8?q?=E0=B9=81=E0=B8=97=E0=B8=99=20toLocaleString("th-TH")=20?= =?UTF-8?q?=E0=B8=97=E0=B8=B8=E0=B8=81=E0=B8=88=E0=B8=B8=E0=B8=94=E0=B9=81?= =?UTF-8?q?=E0=B8=AA=E0=B8=94=E0=B8=87=E0=B8=A7=E0=B8=B1=E0=B8=99=E0=B8=97?= =?UTF-8?q?=E0=B8=B5=E0=B9=88=E0=B9=83=E0=B8=99=20UI=20(=E0=B8=AB=E0=B8=99?= =?UTF-8?q?=E0=B9=89=E0=B8=B2=E0=B8=A3=E0=B8=B2=E0=B8=A2=E0=B8=81=E0=B8=B2?= =?UTF-8?q?=E0=B8=A3=E0=B9=84=E0=B8=9F=E0=B8=A5=E0=B9=8C=20sidebar=20?= =?UTF-8?q?=E0=B8=9B=E0=B8=A3=E0=B8=B0=E0=B8=A7=E0=B8=B1=E0=B8=95=E0=B8=B4?= =?UTF-8?q?/=E0=B8=A3=E0=B8=B2=E0=B8=A2=E0=B8=81=E0=B8=B2=E0=B8=A3=20times?= =?UTF-8?q?tamps=20=E0=B9=83=E0=B8=99=20PDFViewer)=20=E0=B8=81=E0=B8=B1?= =?UTF-8?q?=E0=B8=99=20server/client=20locale=20=E0=B9=84=E0=B8=A1?= =?UTF-8?q?=E0=B9=88=E0=B8=95=E0=B8=A3=E0=B8=87=E0=B8=81=E0=B8=B1=E0=B8=99?= =?UTF-8?q?=20+=20tests=20=E0=B9=83=E0=B8=AB=E0=B8=A1=E0=B9=88/=E0=B8=82?= =?UTF-8?q?=E0=B8=A2=E0=B8=B2=E0=B8=A2=20(date-format=20+=20describeAnnota?= =?UTF-8?q?tionChange)=20=E2=80=94=20CLAUDE.md=20sync=20=E0=B9=82=E0=B8=84?= =?UTF-8?q?=E0=B8=A3=E0=B8=87=E0=B8=AA=E0=B8=A3=E0=B9=89=E0=B8=B2=E0=B8=87?= =?UTF-8?q?=20utils=20=E2=80=94=20verify:=20tsc=20strict=20=E0=B8=9C?= =?UTF-8?q?=E0=B9=88=E0=B8=B2=E0=B8=99=20+=20vitest=20139/139=20=E0=B8=9C?= =?UTF-8?q?=E0=B9=88=E0=B8=B2=E0=B8=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- CLAUDE.md | 3 +- app/actions/annotations.ts | 6 +- app/page.tsx | 3 +- components/pdf/PDFViewer.tsx | 23 ++- components/sidebar/AnnotationSidebar.tsx | 80 +++++--- docs/PLAN-FEAT.md | 4 +- tests/utils/annotation-display.test.ts | 100 +++++++++- tests/utils/date-format.test.ts | 26 +++ utils/annotation-display.ts | 221 +++++++++++++++++++++++ utils/bookmark-limits.ts | 7 + utils/date-format.ts | 34 ++++ 11 files changed, 469 insertions(+), 38 deletions(-) create mode 100644 tests/utils/date-format.test.ts create mode 100644 utils/bookmark-limits.ts create mode 100644 utils/date-format.ts diff --git a/CLAUDE.md b/CLAUDE.md index 41b5d9f..1681511 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,7 +41,8 @@ lib/server-pdf.ts ← path guard อ่าน PDF จาก public/ (export lib/session.ts ← getSessionUser — identity จาก cookie oacCase (mock login e-Court) utils/coordinates.ts ← pixel⇄% + getPointerPercent + clamp + mergeRectsByLine utils/geometry.ts ← bbox/translate/scale/hitTestPath ของ annotation (pure, หน่วย %) -utils/annotation-display.ts ← label/ข้อความย่อต่อชนิด + ค่า default TEXTBOX/STAMP (sidebar/สรุป/CSV ใช้ร่วม) +utils/annotation-display.ts ← label/ข้อความย่อต่อชนิด + ค่า default TEXTBOX/STAMP + describeAnnotationChange สรุปสิ่งที่เปลี่ยนของ version history F19 (sidebar/สรุป/CSV ใช้ร่วม) +utils/date-format.ts ← formatThaiDate วันที่ไทยแบบเต็ม "18 กันยายน พ.ศ. 2569" (+เวลา "… 16:33:00 น.") — ทุกจุดที่แสดงวันที่ใน UI ใช้ร่วม (เว้น export CSV ที่ lock Asia/Bangkok) utils/pdf-export.ts ← แปลงพิกัดจอ⇄PDF (/Rotate) + wrapText + hexToRgb (export route) utils/user.ts ← parse/build ค่า cookie oacCase → OacUser {userId: u_userid, userName: u_userlogin} (pure module — middleware ใช้ร่วมได้) utils/base-path.ts ← withBasePath() เติม basePath /docedit/note ให้ URL ฝั่ง browser ที่ Next ไม่เติมให้เอง (ค่าจาก next.config.mjs ผ่าน env — invariant #12) diff --git a/app/actions/annotations.ts b/app/actions/annotations.ts index 2217211..6aea7b9 100644 --- a/app/actions/annotations.ts +++ b/app/actions/annotations.ts @@ -30,6 +30,10 @@ import { type StampData, type TextboxData, } from "@/db/schema"; +import { + MAX_BOOKMARK_NOTE_CHARS, + MAX_BOOKMARK_TITLE_CHARS, +} from "@/utils/bookmark-limits"; // ─── Input types (client ใช้ตอนเรียก action) ──────────────────────────────── @@ -88,8 +92,6 @@ const SHAPE_KINDS: readonly string[] = ["rect", "ellipse", "line", "arrow"]; // ตรง ๆ ด้วยข้อความขนาดหลาย MB จน JSONB บวมและ SSE event ใหญ่ผิดปกติ const MAX_USER_NAME_CHARS = 100; const MAX_TEXT_CHARS = 5_000; // message/text/note/label ของ annotation + reply -const MAX_BOOKMARK_TITLE_CHARS = 120; // ให้ตรงค่าที่ UI ตัดไว้ -const MAX_BOOKMARK_NOTE_CHARS = 200; const MAX_QUERY_CHARS = 200; // คำค้นหา (F14) function fieldError(field: string, expect: string): never { diff --git a/app/page.tsx b/app/page.tsx index 7ed5772..de03430 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -8,6 +8,7 @@ import { files } from "@/db/schema"; import SessionBar from "@/components/SessionBar"; import UploadFileForm from "@/components/UploadFileForm"; import { imageFormatFromUrl } from "@/utils/file-kind"; +import { formatThaiDate } from "@/utils/date-format"; import { getSessionUser } from "@/lib/session"; export const dynamic = "force-dynamic"; @@ -95,7 +96,7 @@ export default async function HomePage() { {kindBadge(f.pdfUrl)} - {new Date(f.createdAt).toLocaleString("th-TH")} + {formatThaiDate(f.createdAt, true)} {/* ปุ่มเข้า annotate แทนลิงก์กางเต็มการ์ด (M4) ตาม feedback — เป้าจิ้ม 36px (H4) */} diff --git a/components/pdf/PDFViewer.tsx b/components/pdf/PDFViewer.tsx index 7a6a30b..6b5c939 100644 --- a/components/pdf/PDFViewer.tsx +++ b/components/pdf/PDFViewer.tsx @@ -114,8 +114,13 @@ import { type Size, } from "@/utils/coordinates"; import type { OacUser } from "@/utils/user"; +import { + MAX_BOOKMARK_NOTE_CHARS, + MAX_BOOKMARK_TITLE_CHARS, +} from "@/utils/bookmark-limits"; import { fileKindFromUrl, imageFormatFromUrl } from "@/utils/file-kind"; import { withBasePath } from "@/utils/base-path"; +import { formatThaiDate } from "@/utils/date-format"; import { decodeTiffPages } from "@/utils/tiff-client"; import { TEXTBOX_DEFAULT_WIDTH_PCT, @@ -196,10 +201,13 @@ const HIGHLIGHT_COLORS = ["#fde047", "#86efac", "#93c5fd", "#f9a8d4", "#fdba74"] /** ชื่อ bookmark ที่มาจากข้อความ (คลุม/ข้อความรายการ) — ล้างขยะของ textLayer ไทย * (sanitizePdfText ครอบด้วยข้อความจาก DB เก่าที่ยังไม่ผ่านการล้าง) ยุบ whitespace - * และตัดยาวไม่เกิน 120 ตัวอักษร */ + * และตัดยาวไม่เกิน MAX_BOOKMARK_TITLE_CHARS ตัวอักษร (รวม … ที่ต่อท้าย — + * เดิม slice 120 แล้วต่อ … กลายเป็น 121 ตัว โดน limit ของ server ตีกลับตอนบันทึก) */ function normalizeBookmarkTitle(s: string): string { const t = sanitizePdfText(s).replace(/\s+/g, " ").trim(); - return t.length > 120 ? `${t.slice(0, 120)}…` : t; + return t.length > MAX_BOOKMARK_TITLE_CHARS + ? `${t.slice(0, MAX_BOOKMARK_TITLE_CHARS - 1)}…` + : t; } /** ความสูง sticky header ของ viewer — ใช้เว้นระยะเวลาเลื่อนหน้าไม่ให้เป้าหมายถูก header บัง */ @@ -492,12 +500,7 @@ function ReplyBox({

{r.userName} - {new Date(r.createdAt).toLocaleString("th-TH", { - hour: "2-digit", - minute: "2-digit", - day: "2-digit", - month: "short", - })} + {formatThaiDate(r.createdAt, true)}

{r.message}

@@ -1075,7 +1078,7 @@ function PdfPage({

)}

- {new Date(a.createdAt).toLocaleString("th-TH")} + {formatThaiDate(a.createdAt, true)}

{/* F10/A5: toggle จัดการแล้ว — ใครก็ได้ (D6) */}