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
|
|
@ -26,7 +26,7 @@ import type {
|
|||
export function AnnotationTypeIcon({
|
||||
type,
|
||||
data,
|
||||
className = "h-4 w-4 shrink-0 text-slate-500",
|
||||
className = "h-4 w-4 shrink-0 text-ink-soft",
|
||||
}: {
|
||||
type: AnnotationDTO["type"];
|
||||
data: AnnotationDTO["data"];
|
||||
|
|
|
|||
36
components/AppNav.tsx
Normal file
36
components/AppNav.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// Genesis (docs/DESIGN.md) Navigation: sticky แถบบน 56px + backdrop-blur + เส้นแบ่ง
|
||||
// ล่าง 1px — โลโก้ซ้าย / ผู้ใช้ + ออกจากระบบ ขวา (แอปหน้าเดียวต่อ context จึงไม่มี
|
||||
// ลิงก์กลางจอ) ใช้ร่วมทุกหน้าเนื้อหา (หน้ารายการ /, กลุ่ม /groups/[typeId]) —
|
||||
// แทน SessionBar เดิมที่แยกเป็นแถบของตัวเองกลางหน้า
|
||||
import Link from "next/link";
|
||||
|
||||
import LogoutButton from "./LogoutButton";
|
||||
|
||||
export default function AppNav({ userName }: { userName: string }) {
|
||||
return (
|
||||
// appnav: hook ของกฎ coarse 44px ให้ปุ่มในแถบนี้ (globals.css — อย่าถอด)
|
||||
<header className="appnav sticky top-0 z-40 h-14 border-b border-line bg-white/80 backdrop-blur">
|
||||
<div className="mx-auto flex h-full max-w-7xl items-center justify-between gap-3 px-4 sm:px-6">
|
||||
{/* ชื่อแบรนด์เป็น ink ตามกฎ "น้ำเงินเฉพาะของ interactive" — tile JN น้ำเงินคือ
|
||||
แบรนด์มาร์ก (สืบต่อจากหน้าสรุป/login ดั้งเดิม) ทั้งแถบเป็นลิงก์กลับหน้ารายการ */}
|
||||
<Link
|
||||
href="/"
|
||||
className="focus-ring flex min-w-0 items-center gap-2.5 rounded-md"
|
||||
>
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary text-xs font-bold text-white">
|
||||
JN
|
||||
</span>
|
||||
<span className="font-display truncate text-[15px] font-bold tracking-tight text-ink">
|
||||
Judge's Note
|
||||
</span>
|
||||
</Link>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<p className="hidden min-w-0 truncate text-sm text-ink-soft sm:block">
|
||||
{userName}
|
||||
</p>
|
||||
<LogoutButton />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
38
components/DotGrid.tsx
Normal file
38
components/DotGrid.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
"use client";
|
||||
|
||||
// Genesis (docs/DESIGN.md) — กริดจุดอันตรกิริยา: ลวดลายตกแต่งชิ้นเดียวที่ design
|
||||
// system อนุญาต ("the interactive dot grid is the only decorative element") ใช้เป็น
|
||||
// พื้นหลังหน้า login — จุดเทาถี่ 24px + ไฮไลต์น้ำเงินจางตามตำแหน่ง pointer
|
||||
// ⚠️ ต้องเป็น pointer-events-none เสมอ: ตัว layer เป็น absolute inset-0 ครอบพื้นที่
|
||||
// ทั้งหน้า ส่วนการ์ด login เป็น static element — ตัว positioned จะชนะ hit-test ทับ
|
||||
// การ์ดจนคลิก/focus ช่องกรอกไม่ได้ (bug report 2026-09-23) จึงฟัง pointermove ที่
|
||||
// window แทนของตัวเอง — ไฮไลต์ยังตามเมาส์ได้แม้อยู่เหนือการ์ด + อัปเดตผ่าน CSS
|
||||
// variable บน style ตรง ๆ ไม่ setState กัน re-render ทุก mousemove
|
||||
// mask ไล่จางขอบให้กลืนกับพื้น canvas แทนการตัดเส้นชัด
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export default function DotGrid() {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
el.style.setProperty("--dot-mx", `${e.clientX - rect.left}px`);
|
||||
el.style.setProperty("--dot-my", `${e.clientY - rect.top}px`);
|
||||
};
|
||||
window.addEventListener("pointermove", onPointerMove, { passive: true });
|
||||
return () => window.removeEventListener("pointermove", onPointerMove);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0 -z-10 [background-image:radial-gradient(circle,rgba(10,10,10,0.10)_1px,transparent_1px)] [background-size:24px_24px] [-webkit-mask-image:radial-gradient(72%_58%_at_50%_42%,black,transparent)] [mask-image:radial-gradient(72%_58%_at_50%_42%,black,transparent)]"
|
||||
>
|
||||
<div className="absolute inset-0 [background-image:radial-gradient(280px_circle_at_var(--dot-mx,50%)_var(--dot-my,36%),rgba(0,51,161,0.07),transparent_72%)]" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -6,14 +6,15 @@
|
|||
// ลิงก์จริงจาก e-Court (รับทั้ง path ในระบบเช่น /samples/… และ URL ของโฮสต์ที่อนุญาต
|
||||
// — ไม่มี scheme นำหน้าระบบเติม https:// ให้)
|
||||
// แถบลิงก์เต็มข้างล่างแสดง URL จริงที่ปุ่มจะพาไป (absolute — copy ไปเปิด/ส่งต่อได้) ตามที่พิมพ์สด ๆ
|
||||
// Genesis: การ์ด surface 12px radius ขอบ line ใน section "เปิดเอกสารจากลิงก์ e-Court"
|
||||
// (แยกจากส่วนเอกสาร) / ปุ่มเป็น secondary ขอบฟ้า / input ผ่าน .genesis-input
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link2 } from "lucide-react";
|
||||
|
||||
import { withBasePath } from "@/utils/base-path";
|
||||
|
||||
const inputClass =
|
||||
"rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-700 placeholder:text-slate-400 focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20";
|
||||
"genesis-input mt-1 w-full text-base transition aria-invalid:border-error";
|
||||
|
||||
export default function EcourtLinkForm() {
|
||||
const router = useRouter();
|
||||
|
|
@ -50,20 +51,12 @@ export default function EcourtLinkForm() {
|
|||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={onSubmit}
|
||||
className="mt-8 rounded-xl border border-dashed border-slate-300 bg-white/60 p-4"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link2 className="h-4 w-4 shrink-0 text-primary" />
|
||||
<p className="text-sm font-medium text-slate-700">เปิดเอกสารจากลิงก์ e-Court</p>
|
||||
</div>
|
||||
<div className="mt-3 grid gap-x-2 gap-y-3 sm:grid-cols-2">
|
||||
<form onSubmit={onSubmit} className="flex flex-col rounded-xl border border-line bg-white p-5">
|
||||
{/* ไม่มีหัวการ์ดซ้ำ — h2 "เปิดเอกสารจากลิงก์ e-Court" ของ section ในหน้ารายการ
|
||||
เป็นหัวเดียวพอแล้ว (responsive audit 2026-09-24: เดิมซ้ำสองชั้นติดกัน) */}
|
||||
<div className="grid gap-x-4 gap-y-3 sm:grid-cols-2">
|
||||
<div className="sm:col-span-2">
|
||||
<label
|
||||
htmlFor="ecourt-file-url"
|
||||
className="block text-xs font-medium text-slate-600"
|
||||
>
|
||||
<label htmlFor="ecourt-file-url" className="block text-sm font-medium text-ink">
|
||||
ลิงก์ไฟล์
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -74,14 +67,11 @@ export default function EcourtLinkForm() {
|
|||
value={fileUrl}
|
||||
onChange={(e) => setFileUrl(e.target.value)}
|
||||
placeholder="เช่น /samples/ชื่อไฟล์.pdf หรือ https://…"
|
||||
className={`${inputClass} mt-1 w-full`}
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="ecourt-file-id"
|
||||
className="block text-xs font-medium text-slate-600"
|
||||
>
|
||||
<label htmlFor="ecourt-file-id" className="block text-sm font-medium text-ink">
|
||||
รหัสเอกสาร fileId
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -90,14 +80,11 @@ export default function EcourtLinkForm() {
|
|||
name="fileId"
|
||||
value={fileId}
|
||||
onChange={(e) => setFileId(e.target.value)}
|
||||
className={`${inputClass} mt-1 w-full`}
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="ecourt-type-id"
|
||||
className="block text-xs font-medium text-slate-600"
|
||||
>
|
||||
<label htmlFor="ecourt-type-id" className="block text-sm font-medium text-ink">
|
||||
กลุ่ม typeId
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -107,25 +94,25 @@ export default function EcourtLinkForm() {
|
|||
value={typeId}
|
||||
onChange={(e) => setTypeId(e.target.value)}
|
||||
placeholder="เช่น 1"
|
||||
className={`${inputClass} mt-1 w-full`}
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="mt-3 w-full rounded-md bg-primary px-4 py-2.5 text-sm font-medium text-white transition hover:opacity-90"
|
||||
className="focus-ring mt-4 w-full rounded-md border border-primary/30 px-4 py-2 text-sm font-medium text-primary transition hover:-translate-y-px hover:border-primary hover:bg-primary/5"
|
||||
>
|
||||
เปิดเอกสาร
|
||||
</button>
|
||||
<p className="mt-2 text-xs text-slate-500">
|
||||
<p className="mt-3 text-xs leading-relaxed text-ink-soft">
|
||||
กดแล้วระบบลงทะเบียนเอกสาร (ถ้ายังไม่มี) แล้วพาเข้าหน้าอ่าน/มาร์กเอกสารทันที —
|
||||
ไฟล์รีโมตรับเฉพาะ https ของโฮสต์ที่อนุญาต
|
||||
</p>
|
||||
<p className="mt-2 break-all rounded-md bg-slate-50 px-3 py-2 text-xs text-slate-500">
|
||||
<span className="font-medium text-slate-600">ลิงก์เต็ม: </span>
|
||||
<p className="mt-3 break-all rounded-lg bg-ink/[0.04] px-3 py-2.5 text-xs text-ink-soft">
|
||||
<span className="font-medium text-ink">ลิงก์เต็ม: </span>
|
||||
{fullLink || "…"}
|
||||
</p>
|
||||
{error && <p className="mt-1 text-xs text-red-600">{error}</p>}
|
||||
{error && <p className="mt-2 text-xs text-error-ink">{error}</p>}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
// ฟอร์ม login (mock e-Court) — เรียก server action login แล้ว router.replace(next ?? "/")
|
||||
// (pattern ตาม UploadFileForm: useTransition + error state + ข้อความ fallback ตอน transport fail)
|
||||
// next = path วนกลับหลัง login (N8 D5 — ผ่าน sanitizeNextPath จากหน้า login มาแล้ว)
|
||||
// Genesis (docs/DESIGN.md): การ์ด surface 12px radius ขอบ line ไม่มีเงาระดับ static /
|
||||
// input ผ่าน .genesis-input / ปุ่ม primary hover ขึ้น 1px + glow ฟ้า (shadow-btn-glow)
|
||||
import { useRef, useState, useTransition } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
|
|
@ -11,11 +13,12 @@ import Select from "@/components/Select";
|
|||
import { MOCK_USERS } from "@/utils/user";
|
||||
|
||||
// text input ของแอป — text-base (16px) เสมอ: กัน iOS auto-zoom ตอน focus บน iPad
|
||||
// (ทับ text-sm ของ .genesis-input — utility อยู่หลัง @layer components ชนะเสมอ)
|
||||
// error ตาม spec Inputs: ขอบเปลี่ยนเป็นแดง (ring ยังเป็นฟ้าตาม focus ring หลัก)
|
||||
const INPUT_CLASS =
|
||||
"mt-1 block w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-base text-slate-900 placeholder:text-slate-400 transition focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/30 disabled:cursor-not-allowed disabled:bg-slate-50 aria-invalid:border-red-600 aria-invalid:focus:ring-red-600/30";
|
||||
"genesis-input mt-1 block w-full text-base transition aria-invalid:border-error";
|
||||
// dropdown ผู้ใช้ — เหมือน INPUT_CLASS แต่ไม่มี mt-1 (Select ครอบ div.relative: มาร์จินที่ wrapper ข้างนอกแทน)
|
||||
const SELECT_CLASS =
|
||||
"block w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-base text-slate-900 transition focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/30 disabled:cursor-not-allowed disabled:bg-slate-50";
|
||||
const SELECT_CLASS = "genesis-input block w-full text-base transition";
|
||||
|
||||
export default function LoginForm({ next }: { next: string | null }) {
|
||||
const router = useRouter();
|
||||
|
|
@ -48,23 +51,29 @@ export default function LoginForm({ next }: { next: string | null }) {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-sm rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
|
||||
{/* brand lockup เดียวกับ header หน้ารายการ (ลดขนาดตัวอักษรให้พอการ์ดแคบ) */}
|
||||
<div className="w-full max-w-sm rounded-xl border border-line bg-white p-8">
|
||||
{/* brand lockup เดียวกับ nav หน้ารายการ — tile JN น้ำเงินคือแบรนด์มาร์กของระบบ
|
||||
(หน้าสรุปแถบน้ำเงิน 2026-09-17 สืบต่อจาก tile นี้) ชื่อแบรนด์เป็น ink ตามกฎ
|
||||
"blue เฉพาะของ interactive" ของ Genesis */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-primary text-base font-bold text-white shadow-sm">
|
||||
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-lg bg-primary text-base font-bold text-white">
|
||||
JN
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-lg font-bold leading-tight text-primary">Judge's Note</p>
|
||||
<p className="text-xs text-slate-500">ระบบ PDF Annotation สำหรับ e-Court</p>
|
||||
<div className="min-w-0">
|
||||
<p className="font-display text-lg font-bold leading-tight text-ink">
|
||||
Judge's Note
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs text-ink-soft">
|
||||
ระบบ PDF Annotation สำหรับ e-Court
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 className="mt-6 text-lg font-semibold text-slate-900">เข้าสู่ระบบ</h1>
|
||||
<h1 className="font-display mt-8 text-subhead text-ink">เข้าสู่ระบบ</h1>
|
||||
|
||||
<form onSubmit={onSubmit} className="mt-4">
|
||||
<form onSubmit={onSubmit} className="mt-5">
|
||||
<div>
|
||||
<label htmlFor="username" className="text-sm font-semibold text-slate-700">
|
||||
<label htmlFor="username" className="block text-sm font-medium text-ink">
|
||||
ชื่อผู้ใช้
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
|
|
@ -92,7 +101,7 @@ export default function LoginForm({ next }: { next: string | null }) {
|
|||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<label htmlFor="password" className="text-sm font-semibold text-slate-700">
|
||||
<label htmlFor="password" className="block text-sm font-medium text-ink">
|
||||
รหัสผ่าน
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -111,19 +120,20 @@ export default function LoginForm({ next }: { next: string | null }) {
|
|||
<button
|
||||
type="submit"
|
||||
disabled={pending}
|
||||
className="mt-6 w-full rounded-md bg-primary px-4 py-2.5 text-sm font-medium text-white transition hover:opacity-90 disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1"
|
||||
className="focus-ring mt-6 w-full rounded-md bg-primary px-4 py-2.5 text-sm font-medium text-white transition hover:-translate-y-px hover:bg-primary-hover hover:shadow-btn-glow disabled:pointer-events-none disabled:opacity-50"
|
||||
>
|
||||
{pending ? "กำลังตรวจสอบ…" : "เข้าสู่ระบบ"}
|
||||
</button>
|
||||
|
||||
{/* รหัสผ่านทดสอบ — รายชื่อเห็นใน dropdown แล้ว เหลือบอกแค่รหัสผ่าน (mock ไม่มีระบบสมัคร) */}
|
||||
<p className="mt-5 rounded-md bg-slate-50 px-3 py-2 text-xs text-slate-500">
|
||||
{/* รหัสผ่านทดสอบ — รายชื่อเห็นใน dropdown แล้ว เหลือบอกแค่รหัสผ่าน (mock ไม่มีระบบสมัคร)
|
||||
กล่อง metadata 8px radius ตาม Genesis (พื้นเทาโปร่งจาง ไม่ใช่การเน้น) */}
|
||||
<p className="mt-5 rounded-lg bg-ink/[0.04] px-3 py-2.5 text-xs leading-relaxed text-ink-soft">
|
||||
รหัสผ่านทดสอบ:{" "}
|
||||
<span className="font-semibold text-slate-700">Demo1234</span>
|
||||
<span className="font-semibold text-ink">Demo1234</span>
|
||||
</p>
|
||||
|
||||
{error && (
|
||||
<p role="alert" className="mt-3 text-xs text-red-600">
|
||||
<p role="alert" className="mt-3 text-xs text-error-ink">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export default function LogoutButton() {
|
|||
type="button"
|
||||
onClick={onLogout}
|
||||
disabled={pending}
|
||||
className="flex shrink-0 items-center gap-1.5 rounded-md border border-slate-300 px-3 py-2 text-xs font-medium text-slate-600 transition hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 disabled:opacity-50"
|
||||
className="focus-ring flex h-8 shrink-0 items-center gap-1.5 rounded-md border border-line px-3 text-xs font-medium text-ink-soft transition hover:-translate-y-px hover:border-ink-faint hover:text-ink disabled:pointer-events-none disabled:opacity-50"
|
||||
>
|
||||
<LogOut className="h-3.5 w-3.5" aria-hidden />
|
||||
{pending ? "กำลังออก…" : "ออกจากระบบ"}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export default function Select({
|
|||
return (
|
||||
<div className="relative">
|
||||
<select {...props} className={`appearance-none pr-7 ${className}`} />
|
||||
<ChevronDown className="pointer-events-none absolute right-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-slate-500" />
|
||||
<ChevronDown className="pointer-events-none absolute right-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-ink-faint" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
// แถบแสดงผู้ใช้ที่เข้าสู่ระบบบนหน้ารายการ (แทน UserPicker เดิม) — server component
|
||||
// รับค่าจาก cookie ผ่าน props จากหน้า (middleware การันตีว่ามี session ก่อนถึงหน้านี้)
|
||||
import LogoutButton from "./LogoutButton";
|
||||
|
||||
export default function SessionBar({ userName }: { userName: string }) {
|
||||
return (
|
||||
<div className="mt-6 flex flex-wrap items-center justify-between gap-x-3 gap-y-2 rounded-xl border border-slate-200 bg-white px-4 py-3 shadow-sm">
|
||||
<p className="min-w-0 text-sm text-slate-600">
|
||||
เข้าสู่ระบบเป็น{" "}
|
||||
<span className="font-semibold text-slate-900">{userName}</span>
|
||||
</p>
|
||||
<LogoutButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,7 +2,10 @@
|
|||
|
||||
// N4 — ฟอร์มอัปโหลดไฟล์เอกสารแบบง่ายบนหน้ารายการ (PDF/รูป รับพร้อมกัน)
|
||||
// เสร็จแล้วรายการโผล่เองจาก revalidatePath("/") ของ action
|
||||
// Genesis: หน้าตาเป็นการ์ด surface 12px radius ขอบ line (ใต้หัวข้อของ section
|
||||
// "เอกสาร" — ไฟล์ที่อัปโหลดเข้ารายการนั้น) + ปุ่ม primary เดียวของ section
|
||||
import { useState, useTransition } from "react";
|
||||
import { Upload } from "lucide-react";
|
||||
|
||||
import { uploadFile } from "@/app/actions/files";
|
||||
|
||||
|
|
@ -43,29 +46,43 @@ export default function UploadFileForm() {
|
|||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={onSubmit}
|
||||
className="mt-3 flex flex-wrap items-center gap-2 rounded-xl border border-dashed border-slate-300 bg-white/60 p-4"
|
||||
>
|
||||
<form onSubmit={onSubmit} className="flex flex-col rounded-xl border border-line bg-white p-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<Upload aria-hidden className="h-4 w-4 shrink-0 text-ink-faint" />
|
||||
<p className="text-sm font-semibold text-ink">อัปโหลดไฟล์</p>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
name="file"
|
||||
accept={ACCEPT}
|
||||
required
|
||||
disabled={pending}
|
||||
className="block w-full max-w-xs text-sm text-slate-600 file:mr-3 file:cursor-pointer file:rounded-md file:border-0 file:bg-primary/10 file:px-4 file:py-2 file:text-sm file:font-medium file:text-primary"
|
||||
className="mt-4 block w-full text-sm text-ink-soft file:mr-3 file:cursor-pointer file:rounded-md file:border-0 file:bg-ink/[0.05] file:px-4 file:py-2 file:text-sm file:font-medium file:text-ink"
|
||||
/>
|
||||
{/* กลุ่ม typeId ทางเลือก (N9) — ช่องเดียวกับลิงก์ e-Court, ไม่กรอก = "ไม่มีกลุ่ม";
|
||||
uncontrolled ส่งไปกับ FormData เหมือนช่องไฟล์ (normalize ที่ action) */}
|
||||
<label htmlFor="upload-type-id" className="mt-3 block text-sm font-medium text-ink">
|
||||
กลุ่ม typeId
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="upload-type-id"
|
||||
name="typeId"
|
||||
disabled={pending}
|
||||
placeholder="ไม่กรอก = ไม่มีกลุ่ม"
|
||||
className="genesis-input mt-1 w-full text-base"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={pending}
|
||||
className="ml-auto shrink-0 rounded-md bg-primary px-4 py-2.5 text-sm font-medium text-white transition hover:opacity-90 disabled:opacity-50"
|
||||
className="focus-ring mt-4 w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-white transition hover:-translate-y-px hover:bg-primary-hover hover:shadow-btn-glow disabled:pointer-events-none disabled:opacity-50"
|
||||
>
|
||||
{pending ? "กำลังอัปโหลด…" : "อัปโหลด"}
|
||||
</button>
|
||||
<p className="w-full text-xs text-slate-500">
|
||||
<p className="mt-3 text-xs leading-relaxed text-ink-soft">
|
||||
รองรับ PDF, JPG, PNG, TIFF — ไม่เกิน 100MB ต่อไฟล์
|
||||
</p>
|
||||
{error && <p className="w-full text-xs text-red-600">{error}</p>}
|
||||
{error && <p className="mt-2 text-xs text-error-ink">{error}</p>}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -194,7 +194,7 @@ function TextboxEditor({
|
|||
e.clipboardData.getData("text/plain"),
|
||||
);
|
||||
}}
|
||||
className="min-h-[1.5em] w-full whitespace-pre-wrap break-words rounded border border-dashed border-slate-500/70 bg-transparent p-0 caret-current focus:outline-none"
|
||||
className="min-h-[1.5em] w-full whitespace-pre-wrap break-words rounded border border-dashed border-ink-faint/70 bg-transparent p-0 caret-current focus:outline-none"
|
||||
style={{
|
||||
fontFamily: TEXTBOX_FONT_STACK,
|
||||
fontSize: toPx(fontSizePct, size.width),
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { colorName } from "@/utils/annotation-display";
|
|||
* PDFViewer/AnnotationSidebar (ประกาศแยกที่นี่เพราะ PDFViewer 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";
|
||||
|
||||
/** สี preset ให้เลือก (ทุก tool ที่ปรับสีได้: draw/shape/highlight/textbox/stamp)
|
||||
* สีอื่นนอกรายการเลือกได้ผ่านปุ่มสีรุ้ง (input type="color") ท้ายแถว */
|
||||
|
|
@ -38,7 +38,7 @@ export function PanelRow({ label, children }: { label?: string; children: ReactN
|
|||
return (
|
||||
<div className="panel-row flex items-start gap-2 px-1 py-1">
|
||||
{label ? (
|
||||
<span className="w-11 shrink-0 pt-1 text-[11px] font-medium text-slate-500">{label}</span>
|
||||
<span className="w-11 shrink-0 pt-1 text-[11px] font-medium text-ink-soft">{label}</span>
|
||||
) : null}
|
||||
{children}
|
||||
</div>
|
||||
|
|
@ -57,7 +57,7 @@ export function ColorSwatches({ value, onChange }: { value: string; onChange: (c
|
|||
type="button"
|
||||
aria-label={`เลือกสี ${colorName(c)}`}
|
||||
title={colorName(c)}
|
||||
className={`aspect-square max-w-8 w-full justify-self-center rounded-full border border-slate-300 transition ${FOCUS_RING_CLS} ${
|
||||
className={`aspect-square max-w-8 w-full justify-self-center rounded-full border border-line transition ${FOCUS_RING_CLS} ${
|
||||
value === c ? "ring-2 ring-primary ring-offset-1" : "hover:scale-110"
|
||||
}`}
|
||||
style={{ backgroundColor: c }}
|
||||
|
|
@ -66,7 +66,7 @@ export function ColorSwatches({ value, onChange }: { value: string; onChange: (c
|
|||
))}
|
||||
<label
|
||||
title="เลือกสีอื่น"
|
||||
className={`relative aspect-square max-w-8 w-full cursor-pointer justify-self-center rounded-full border border-slate-300 transition has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-primary has-[:focus-visible]:ring-offset-1 ${
|
||||
className={`relative aspect-square max-w-8 w-full cursor-pointer justify-self-center rounded-full border border-line transition has-[:focus-visible]:outline has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-primary ${
|
||||
INK_COLORS.includes(value) ? "hover:scale-110" : "ring-2 ring-primary ring-offset-1"
|
||||
}`}
|
||||
style={{
|
||||
|
|
@ -179,7 +179,7 @@ export function NumberCombo({
|
|||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-stretch overflow-hidden rounded-md border border-slate-300 bg-white">
|
||||
<div className="flex items-stretch overflow-hidden rounded-md border border-line bg-white">
|
||||
<input
|
||||
type="number"
|
||||
value={draft}
|
||||
|
|
@ -196,16 +196,16 @@ export function NumberCombo({
|
|||
if (e.key !== "Escape") e.stopPropagation(); // พิมพ์ในช่องต้องไม่ชนคีย์ลัด F3 (Esc ปล่อยไหลไปปิดเมนู)
|
||||
if (e.key === "Enter") commitDraft(); // ยืนยันครั้งเดียวตรงนี้ ไม่ต้องรอ blur
|
||||
}}
|
||||
className="min-w-0 flex-1 border-0 px-1.5 py-1.5 text-[11px] text-slate-700 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-slate-400 [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||
className="min-w-0 flex-1 border-0 px-1.5 py-1.5 text-[11px] text-ink focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||
/>
|
||||
<div className="flex flex-col border-l border-slate-300">
|
||||
<div className="flex flex-col border-l border-line">
|
||||
{/* combo-step: ปุ่ม ▲▼ ซ้อนแนวตั้ง 2 ชั้น — ต้องหลุดจาก rule touch 36px
|
||||
ของ dropdown (globals.css) ไม่งั้นคอลัมน์สูง 72px ดึงทั้งแถวบวมตาม */}
|
||||
<button
|
||||
type="button"
|
||||
aria-label="เพิ่มค่า"
|
||||
onMouseDown={(e) => e.preventDefault()} // ไม่แย่ง focus จาก input (dropdown ไม่กระพริบ)
|
||||
className={`combo-step flex flex-1 items-center justify-center rounded-sm px-1.5 text-slate-500 hover:bg-slate-100 ${FOCUS_RING_CLS}`}
|
||||
className={`combo-step flex flex-1 items-center justify-center rounded-sm px-1.5 text-ink-soft hover:bg-ink/[0.05] ${FOCUS_RING_CLS}`}
|
||||
onClick={() => stepValue(1)}
|
||||
>
|
||||
<ChevronUp className="h-3 w-3" />
|
||||
|
|
@ -214,7 +214,7 @@ export function NumberCombo({
|
|||
type="button"
|
||||
aria-label="ลดค่า"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
className={`combo-step flex flex-1 items-center justify-center rounded-sm border-t border-slate-300 px-1.5 text-slate-500 hover:bg-slate-100 ${FOCUS_RING_CLS}`}
|
||||
className={`combo-step flex flex-1 items-center justify-center rounded-sm border-t border-line px-1.5 text-ink-soft hover:bg-ink/[0.05] ${FOCUS_RING_CLS}`}
|
||||
onClick={() => stepValue(-1)}
|
||||
>
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
|
|
@ -227,7 +227,7 @@ export function NumberCombo({
|
|||
// L2 (UX audit 2026-09-16): เปิด "ขึ้นบน" (bottom-full) เสมอ — แถวท้ายเมนู
|
||||
// tool (ความทึบ/ความหนา) เปิดลงจะล้นเขต scroll ของเมนู ต้องเลื่อนเมนูจึงเห็น
|
||||
// ครบ / ทุกจุดใช้งานมีแถวอื่นอยู่ข้างบนเสมอ จึงไม่หลุดขอบบนของเมนูเช่นกัน
|
||||
className="absolute bottom-full right-0 z-[60] mb-1 w-full overflow-hidden rounded-md border border-slate-200 bg-white py-0.5 shadow-lg"
|
||||
className="absolute bottom-full right-0 z-[60] mb-1 w-full overflow-hidden rounded-md border border-line bg-white py-0.5 shadow-lg"
|
||||
>
|
||||
{options.map((opt) => (
|
||||
<button
|
||||
|
|
@ -238,8 +238,8 @@ export function NumberCombo({
|
|||
onMouseDown={(e) => e.preventDefault()} // ไม่แย่ง focus จาก input ก่อนคลิก
|
||||
className={`flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs font-medium transition ${FOCUS_RING_CLS} ${
|
||||
value === opt
|
||||
? "bg-slate-100 text-slate-900"
|
||||
: "text-slate-600 hover:bg-slate-100"
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-ink-soft hover:bg-ink/[0.05]"
|
||||
}`}
|
||||
onClick={() => {
|
||||
onChange(opt);
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ import { formatThaiDate } from "@/utils/date-format";
|
|||
* ของ PDFViewer.tsx (ห้าม import ข้ามไฟล์: PDFViewer → sidebar → PDFViewer เป็น
|
||||
* 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:z-10";
|
||||
"focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary focus-visible:z-10";
|
||||
|
||||
/** ใส่/ล้างโน้ตคำอธิบายใน data ของ annotation (ทุกชนิดยกเว้น COMMENT — message ทำหน้าที่นี้แล้ว) */
|
||||
function withNote(data: Exclude<AnnotationData, CommentData>, note: string): AnnotationData {
|
||||
|
|
@ -139,7 +139,7 @@ function ViewSwitch<T extends string>({
|
|||
onChange: (v: T) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex rounded-md bg-slate-100 p-0.5">
|
||||
<div className="flex rounded-md bg-ink/[0.05] p-0.5">
|
||||
{options.map((o) => (
|
||||
<button
|
||||
key={o.key}
|
||||
|
|
@ -147,8 +147,8 @@ function ViewSwitch<T extends string>({
|
|||
aria-pressed={value === o.key}
|
||||
className={`flex-1 rounded px-2 py-1 text-[11px] font-medium transition ${FOCUS_RING_CLS} ${
|
||||
value === o.key
|
||||
? "bg-white text-slate-900 shadow-sm"
|
||||
: "text-slate-500 hover:text-slate-600"
|
||||
? "bg-white text-ink ring-1 ring-line"
|
||||
: "text-ink-soft hover:text-ink"
|
||||
}`}
|
||||
onClick={() => onChange(o.key)}
|
||||
>
|
||||
|
|
@ -178,7 +178,7 @@ function MenuItem({
|
|||
type="button"
|
||||
role="menuitem"
|
||||
className={`flex w-full items-start gap-2 rounded px-2 py-1.5 text-left text-xs transition ${FOCUS_RING_CLS} ${
|
||||
danger ? "text-red-600 hover:bg-red-50" : "text-slate-600 hover:bg-slate-100"
|
||||
danger ? "text-error-ink hover:bg-error/10" : "text-ink-soft hover:bg-ink/[0.05]"
|
||||
}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
|
|
@ -204,17 +204,17 @@ function PageGroupHeader({
|
|||
<button
|
||||
type="button"
|
||||
aria-label={collapsed ? `กางหน้า ${pageIndex + 1}` : `หุบหน้า ${pageIndex + 1}`}
|
||||
className={`flex w-full items-center gap-1.5 rounded px-1 py-1 text-left hover:bg-slate-100 ${FOCUS_RING_CLS}`}
|
||||
className={`flex w-full items-center gap-1.5 rounded px-1 py-1 text-left hover:bg-ink/[0.05] ${FOCUS_RING_CLS}`}
|
||||
onClick={onToggle}
|
||||
>
|
||||
{collapsed ? (
|
||||
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-slate-500" />
|
||||
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-ink-soft" />
|
||||
) : (
|
||||
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-slate-500" />
|
||||
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-ink-soft" />
|
||||
)}
|
||||
<span className="text-xs font-medium text-slate-700">
|
||||
<span className="text-xs font-medium text-ink">
|
||||
หน้า {pageIndex + 1}
|
||||
<span className="ml-1 font-normal text-slate-500">({count})</span>
|
||||
<span className="ml-1 font-normal text-ink-soft">({count})</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
|
|
@ -774,7 +774,7 @@ export default function AnnotationSidebar({
|
|||
if (e.key === "Escape") setEditBookmark(null);
|
||||
}}
|
||||
placeholder="ชื่อหัวข้อ"
|
||||
className="w-full rounded border border-slate-300 px-1.5 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-slate-400"
|
||||
className="w-full rounded-md border border-line px-1.5 py-1 text-xs focus:border-primary focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary"
|
||||
/>
|
||||
<textarea
|
||||
rows={2}
|
||||
|
|
@ -787,16 +787,16 @@ export default function AnnotationSidebar({
|
|||
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) saveBookmarkEdit(b);
|
||||
}}
|
||||
placeholder="โน้ต (ไม่บังคับ)"
|
||||
className="mt-1 w-full resize-none rounded border border-slate-300 p-1.5 text-[11px] focus:outline-none focus:ring-1 focus:ring-slate-400"
|
||||
className="mt-1 w-full resize-none rounded-md border border-line p-1.5 text-[11px] focus:border-primary focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary"
|
||||
/>
|
||||
<p className="mt-1 text-[11px] text-slate-500">วางที่</p>
|
||||
<p className="mt-1 text-[11px] text-ink-soft">วางที่</p>
|
||||
<Select
|
||||
value={eb.parentId ?? ""}
|
||||
onChange={(e) =>
|
||||
setEditBookmark({ ...eb, parentId: e.target.value || null })
|
||||
}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
className="mt-0.5 w-full rounded border border-slate-300 bg-white px-1.5 py-1 text-[11px] focus:outline-none focus:ring-1 focus:ring-slate-400"
|
||||
className="mt-0.5 w-full rounded-md border border-line bg-white px-1.5 py-1 text-[11px] focus:border-primary focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary"
|
||||
>
|
||||
<option value="">ระดับบนสุด (หมุดหัวข้อใหญ่)</option>
|
||||
{movableTo.map((h) => (
|
||||
|
|
@ -808,14 +808,14 @@ export default function AnnotationSidebar({
|
|||
<div className="mt-1.5 flex justify-end gap-1.5 text-[11px]">
|
||||
<button
|
||||
type="button"
|
||||
className={`rounded px-2 py-0.5 text-slate-500 hover:bg-slate-100 ${FOCUS_RING_CLS}`}
|
||||
className={`rounded-md px-2 py-0.5 text-ink-soft hover:bg-ink/[0.05] ${FOCUS_RING_CLS}`}
|
||||
onClick={() => setEditBookmark(null)}
|
||||
>
|
||||
ยกเลิก
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`rounded bg-primary px-2 py-0.5 font-medium text-white ${FOCUS_RING_CLS}`}
|
||||
className={`rounded-md bg-primary px-2 py-0.5 font-medium text-white ${FOCUS_RING_CLS}`}
|
||||
onClick={() => saveBookmarkEdit(b)}
|
||||
>
|
||||
บันทึก
|
||||
|
|
@ -823,7 +823,7 @@ export default function AnnotationSidebar({
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`group flex items-center gap-1.5 rounded py-1 hover:bg-slate-100 ${pad}`}>
|
||||
<div className={`group flex items-center gap-1.5 rounded py-1 hover:bg-ink/[0.05] ${pad}`}>
|
||||
{hasChildren ? (
|
||||
// bm-toggle: คอลัมน์นี้เป็น "ตาราง" ของต้นไม้ ต้องกว้าง 14px เท่า spacer
|
||||
// ของแถวลูกเสมอ — บนจอสัมผัส rule coarse ที่ขยายปุ่มเป็น 32px จะดันไอคอน
|
||||
|
|
@ -832,7 +832,7 @@ export default function AnnotationSidebar({
|
|||
<button
|
||||
type="button"
|
||||
aria-label={collapsed ? `กางหัวข้อ ${b.title}` : `หุบหัวข้อ ${b.title}`}
|
||||
className={`shrink-0 bm-toggle text-slate-500 hover:text-slate-600 ${FOCUS_RING_CLS}`}
|
||||
className={`shrink-0 bm-toggle text-ink-soft hover:text-ink ${FOCUS_RING_CLS}`}
|
||||
onClick={() => toggleCollapsed(b.id)}
|
||||
>
|
||||
{collapsed ? (
|
||||
|
|
@ -850,26 +850,25 @@ export default function AnnotationSidebar({
|
|||
className={`flex min-w-0 flex-1 items-start gap-1.5 text-left ${FOCUS_RING_CLS}`}
|
||||
onClick={() => onGoToBookmark(b)}
|
||||
>
|
||||
<Bookmark
|
||||
className={`h-3.5 w-3.5 shrink-0 ${
|
||||
depth === 1 ? "text-amber-500" : "text-amber-300"
|
||||
}`}
|
||||
/>
|
||||
{/* ไอคอน bookmark สีเดียวทุกระดับ (warning-ink ผ่าน 5:1 บนขาว — เดิมแยก
|
||||
root/ลูกด้วย amber-500/300 แต่ตัว 300 จาง 1.6:1 แทบมองไม่เห็น;
|
||||
ลำดับชั้นของต้นไม้สื่อด้วย font-semibold + indent อยู่แล้ว) */}
|
||||
<Bookmark className="h-3.5 w-3.5 shrink-0 text-warning-ink" />
|
||||
<span className="min-w-0">
|
||||
<span
|
||||
className={`block truncate text-[13px] leading-snug ${
|
||||
depth === 1 ? "font-semibold text-slate-700" : ""
|
||||
depth === 1 ? "font-semibold text-ink" : ""
|
||||
}`}
|
||||
>
|
||||
{b.title}
|
||||
</span>
|
||||
{/* แสดงผู้บันทึกด้วย — เหมือนแถวผลการค้นหา (N6) และแถวหมายเหตุ
|
||||
(แถวเดิมมีแค่ "หน้า N" ไม่รู้ว่าใครเป็นคนคั่นไว้) */}
|
||||
<span className="block text-xs text-slate-500">
|
||||
<span className="block text-xs text-ink-soft">
|
||||
หน้า {b.pageIndex + 1} · {b.userName || "ไม่ทราบชื่อ"}
|
||||
</span>
|
||||
{b.note && (
|
||||
<span className="mt-0.5 block line-clamp-2 whitespace-pre-wrap break-words text-[11px] text-slate-500">
|
||||
<span className="mt-0.5 block line-clamp-2 whitespace-pre-wrap break-words text-[11px] text-ink-soft">
|
||||
{b.note}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -877,7 +876,7 @@ export default function AnnotationSidebar({
|
|||
</button>
|
||||
{/* จำนวนหมุดย่อย — badge ขวาสุด (UX audit: แยกจากชื่อ ไม่ปนตอนอ่านไว) */}
|
||||
{hasChildren && (
|
||||
<span className="shrink-0 self-center rounded-full bg-slate-100 px-1.5 text-[10px] tabular-nums leading-4 text-slate-500">
|
||||
<span className="shrink-0 self-center rounded-full bg-ink/[0.05] px-1.5 text-[10px] tabular-nums leading-4 text-ink-soft">
|
||||
{children.length}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -888,7 +887,7 @@ export default function AnnotationSidebar({
|
|||
aria-label="จัดการหัวข้อนี้"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={menuFor?.kind === "bookmark" && menuFor.id === b.id}
|
||||
className={`shrink-0 text-slate-500 hover:text-slate-600 group-hover:text-slate-600 ${FOCUS_RING_CLS}`}
|
||||
className={`shrink-0 text-ink-soft hover:text-ink group-hover:text-ink-soft ${FOCUS_RING_CLS}`}
|
||||
onClick={(e) => {
|
||||
if (menuFor?.kind === "bookmark" && menuFor.id === b.id) closeRowMenu();
|
||||
else openRowMenu(e.currentTarget, { kind: "bookmark", id: b.id });
|
||||
|
|
@ -902,7 +901,7 @@ export default function AnnotationSidebar({
|
|||
{hasChildren && !collapsed && (
|
||||
// เส้น guide แนวตั้งนำสายตากลับหา parent แบบ file tree (indent ~17px/ระดับ
|
||||
// ≈ ของเดิม 16px — spacer/chevron w-3.5 คงเดิมจึงตรงแนวกันทุกระดับ)
|
||||
<ul className="ml-3 space-y-0.5 border-l border-slate-200 pl-1">
|
||||
<ul className="ml-3 space-y-0.5 border-l border-line pl-1">
|
||||
{children.map((c) => renderBookmarkNode(c, depth + 1))}
|
||||
</ul>
|
||||
)}
|
||||
|
|
@ -920,7 +919,7 @@ export default function AnnotationSidebar({
|
|||
className={`group rounded px-2 py-1.5 ${
|
||||
a.id === selectedId
|
||||
? "bg-primary/10 ring-1 ring-inset ring-primary/30"
|
||||
: "hover:bg-slate-100"
|
||||
: "hover:bg-ink/[0.05]"
|
||||
} ${a.resolved ? "opacity-60" : ""}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -938,7 +937,7 @@ export default function AnnotationSidebar({
|
|||
{/* UX audit: อักษรไทยมีสระ/วรรณยุกต์ซ้อนหลายชั้น — ข้อความหลักต้อง 13px
|
||||
จึงอ่านได้สบาย (บรรทัดรอง/โน้ตคง 11px) */}
|
||||
<span className="block truncate text-[13px] leading-snug">{annotationPreviewText(a)}</span>
|
||||
<span className="block text-xs text-slate-500">
|
||||
<span className="block text-xs text-ink-soft">
|
||||
หน้า {a.pageIndex + 1} · {a.userName}
|
||||
{a.type === "COMMENT" && (replyCounts[a.id] ?? 0) > 0
|
||||
? ` · ตอบ ${replyCounts[a.id]}`
|
||||
|
|
@ -957,10 +956,10 @@ export default function AnnotationSidebar({
|
|||
title={copyFailedId === a.id ? "คัดลอกไม่ได้ (เบราว์เซอร์บล็อกคลิปบอร์ด)" : "จัดการหมายเหตุนี้"}
|
||||
className={`shrink-0 ${FOCUS_RING_CLS} ${
|
||||
copiedId === a.id
|
||||
? "text-emerald-500"
|
||||
? "text-success-ink"
|
||||
: copyFailedId === a.id
|
||||
? "text-red-500"
|
||||
: "text-slate-500 hover:text-slate-600 group-hover:text-slate-600"
|
||||
? "text-error-ink"
|
||||
: "text-ink-soft hover:text-ink group-hover:text-ink-soft"
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
if (menuFor?.kind === "annotation" && menuFor.id === a.id) closeRowMenu();
|
||||
|
|
@ -989,19 +988,19 @@ export default function AnnotationSidebar({
|
|||
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) saveNote(a);
|
||||
}}
|
||||
placeholder="โน้ต เช่น เหตุผลที่ทำรายการนี้…"
|
||||
className="w-full resize-none rounded border border-slate-300 p-1.5 text-[11px] focus:outline-none focus:ring-1 focus:ring-slate-400"
|
||||
className="w-full resize-none rounded-md border border-line p-1.5 text-[11px] focus:border-primary focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary"
|
||||
/>
|
||||
<div className="mt-1 flex justify-end gap-1.5 text-[11px]">
|
||||
<button
|
||||
type="button"
|
||||
className={`rounded px-2 py-0.5 text-slate-500 hover:bg-slate-100 ${FOCUS_RING_CLS}`}
|
||||
className={`rounded-md px-2 py-0.5 text-ink-soft hover:bg-ink/[0.05] ${FOCUS_RING_CLS}`}
|
||||
onClick={() => setNoteFor(null)}
|
||||
>
|
||||
ยกเลิก
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`rounded bg-primary px-2 py-0.5 font-medium text-white ${FOCUS_RING_CLS}`}
|
||||
className={`rounded-md bg-primary px-2 py-0.5 font-medium text-white ${FOCUS_RING_CLS}`}
|
||||
onClick={() => saveNote(a)}
|
||||
>
|
||||
บันทึก
|
||||
|
|
@ -1010,8 +1009,8 @@ export default function AnnotationSidebar({
|
|||
</div>
|
||||
) : (
|
||||
note && (
|
||||
<p className="mt-0.5 flex items-start gap-1 pl-6 text-[11px] text-slate-500">
|
||||
<StickyNote className="mt-0.5 h-3 w-3 shrink-0 text-amber-400" />
|
||||
<p className="mt-0.5 flex items-start gap-1 pl-6 text-[11px] text-ink-soft">
|
||||
<StickyNote className="mt-0.5 h-3 w-3 shrink-0 text-warning-ink" />
|
||||
<span className="line-clamp-2 whitespace-pre-wrap break-words">{note}</span>
|
||||
</p>
|
||||
)
|
||||
|
|
@ -1023,10 +1022,10 @@ export default function AnnotationSidebar({
|
|||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* F4: กรองแสดง annotation ตามผู้เขียน (viewer + sidebar พร้อมกัน) */}
|
||||
<div className="shrink-0 border-b border-slate-200 p-2">
|
||||
<div className="shrink-0 border-b border-line p-2">
|
||||
<Select
|
||||
aria-label="เลือกเจ้าของหมายเหตุที่แสดง"
|
||||
className="w-full rounded-md border border-slate-300 bg-white px-2 py-1.5 text-xs text-slate-600 focus:outline-none focus:ring-2 focus:ring-slate-400"
|
||||
className="w-full rounded-md border border-line bg-white px-2 py-1.5 text-xs text-ink-soft focus:border-primary focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary"
|
||||
value={userFilter}
|
||||
onChange={(e) => onUserFilterChange(e.target.value)}
|
||||
>
|
||||
|
|
@ -1041,7 +1040,7 @@ export default function AnnotationSidebar({
|
|||
</div>
|
||||
|
||||
{/* แท็บหลัก — tablist/tab semantics ให้ screen reader ประกาศว่าเป็นแท็บ */}
|
||||
<div role="tablist" aria-label="รายการในแผงข้าง" className="flex shrink-0 border-b border-slate-200">
|
||||
<div role="tablist" aria-label="รายการในแผงข้าง" className="flex shrink-0 border-b border-line">
|
||||
{(
|
||||
[
|
||||
["bookmarks", `Bookmark (${visibleBookmarks.length})`],
|
||||
|
|
@ -1055,8 +1054,8 @@ export default function AnnotationSidebar({
|
|||
aria-selected={tab === key}
|
||||
className={`flex-1 px-2 py-2 text-xs font-medium tabular-nums transition ${FOCUS_RING_CLS} ${
|
||||
tab === key
|
||||
? "border-b-2 border-primary text-slate-900"
|
||||
: "text-slate-500 hover:text-slate-600"
|
||||
? "border-b-2 border-primary text-ink"
|
||||
: "text-ink-soft hover:text-ink"
|
||||
}`}
|
||||
onClick={() => setTab(key)}
|
||||
>
|
||||
|
|
@ -1068,25 +1067,25 @@ export default function AnnotationSidebar({
|
|||
{/* N6: ค้นหา bookmark พร้อมเงื่อนไข — เฉพาะแท็บที่คั่นหน้า
|
||||
(แยกจากช่อง F14 ที่ค้นหมายเหตุ ซึ่งอยู่เหนือแท็บ) */}
|
||||
{tab === "bookmarks" && (
|
||||
<div className="shrink-0 space-y-1.5 border-b border-slate-200 p-2">
|
||||
<div className="shrink-0 space-y-1.5 border-b border-line p-2">
|
||||
{/* แถวคำค้น — โครงเดียวกับ F14 ของแท็บหมายเหตุ: input เต็มกว้าง + ปุ่มเดียว
|
||||
(เดิมปุ่ม "เงื่อนไข"+badge แย่งแถวเดียวกันจน input เหลือพื้นที่ข้อความ
|
||||
~80px ≈ 6–7 ตัวอักษรไทย พิมพ์คำค้นจริงไม่พอ) */}
|
||||
<form className="flex gap-1" onSubmit={(e) => void runBookmarkSearch(e)}>
|
||||
<div className="relative min-w-0 flex-1">
|
||||
<Search className="pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-slate-500" />
|
||||
<Search className="pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-ink-soft" />
|
||||
<input
|
||||
value={bmQuery}
|
||||
onChange={(e) => setBmQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.stopPropagation()} // ไม่ให้ปุ่มในช่องค้นหาชนคีย์ลัดของ viewer
|
||||
placeholder="ชื่อ หรือ โน้ต…"
|
||||
className="w-full rounded-md border border-slate-300 py-1.5 pl-7 pr-2 text-xs focus:outline-none focus:ring-2 focus:ring-slate-400"
|
||||
className="w-full rounded-md border border-line py-1.5 pl-7 pr-2 text-xs focus:border-primary focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={activeConditionCount === 0 || bmSearching}
|
||||
className={`shrink-0 rounded-md border border-slate-300 px-2 py-1.5 text-xs text-slate-600 hover:bg-slate-100 disabled:opacity-40 ${FOCUS_RING_CLS}`}
|
||||
className={`shrink-0 rounded-md border border-line px-2 py-1.5 text-xs text-ink-soft hover:bg-ink/[0.05] disabled:opacity-40 ${FOCUS_RING_CLS}`}
|
||||
>
|
||||
{bmSearching ? "…" : "หา"}
|
||||
</button>
|
||||
|
|
@ -1099,8 +1098,8 @@ export default function AnnotationSidebar({
|
|||
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"
|
||||
className={`flex min-w-0 flex-1 items-center gap-1.5 rounded px-1.5 py-1 text-[11px] hover:bg-ink/[0.05] ${FOCUS_RING_CLS} ${
|
||||
hasBmConditionValues ? "text-primary" : "text-ink-soft"
|
||||
}`}
|
||||
>
|
||||
<SlidersHorizontal className="h-3 w-3 shrink-0" />
|
||||
|
|
@ -1119,7 +1118,7 @@ export default function AnnotationSidebar({
|
|||
href={withBasePath(`/api/bookmarks/export?fileId=${fileId}`)}
|
||||
title="ส่งออกทุกที่คั่นหน้าของไฟล์นี้เป็น JSON"
|
||||
aria-label="ส่งออกทุกที่คั่นหน้าของไฟล์นี้เป็น JSON"
|
||||
className={`coarse-36 shrink-0 rounded-md p-1.5 text-slate-500 hover:bg-slate-100 hover:text-slate-700 ${FOCUS_RING_CLS}`}
|
||||
className={`coarse-36 shrink-0 rounded-md p-1.5 text-ink-soft hover:bg-ink/[0.05] hover:text-ink ${FOCUS_RING_CLS}`}
|
||||
>
|
||||
<Braces className="h-3.5 w-3.5" />
|
||||
</a>
|
||||
|
|
@ -1128,7 +1127,7 @@ export default function AnnotationSidebar({
|
|||
// จังหวะเดียวทุกแถว: label บนบรรทัด + คู่ input แบ่งความกว้าง grid 1fr–1fr
|
||||
// (เดิม label inline 2 แถว + label บน 1 แถว อ่านไม่เป็นระเบียบ ขอบ input
|
||||
// ไม่ตรงแนวกัน และคู่ date ในแถว inline ล้น 288px จนต้องหนีขึ้นบรรทัดบน)
|
||||
<div className="space-y-1.5 text-[11px] text-slate-600">
|
||||
<div className="space-y-1.5 text-[11px] text-ink-soft">
|
||||
<div>
|
||||
<span className="block pb-0.5">หน้า</span>
|
||||
<div className="grid grid-cols-[1fr_auto_1fr] items-center gap-1">
|
||||
|
|
@ -1143,9 +1142,9 @@ export default function AnnotationSidebar({
|
|||
}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
placeholder="จาก"
|
||||
className="w-full min-w-0 rounded border border-slate-300 px-1.5 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-slate-400"
|
||||
className="w-full min-w-0 rounded-md border border-line px-1.5 py-1 text-xs focus:border-primary focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary"
|
||||
/>
|
||||
<span className="text-slate-400">–</span>
|
||||
<span className="text-ink-faint">–</span>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
|
|
@ -1157,7 +1156,7 @@ export default function AnnotationSidebar({
|
|||
}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
placeholder="ถึง"
|
||||
className="w-full min-w-0 rounded border border-slate-300 px-1.5 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-slate-400"
|
||||
className="w-full min-w-0 rounded-md border border-line px-1.5 py-1 text-xs focus:border-primary focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1170,7 +1169,7 @@ export default function AnnotationSidebar({
|
|||
setBmConditions({ ...bmConditions, userId: e.target.value })
|
||||
}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
className="w-full rounded border border-slate-300 bg-white px-1.5 py-1 text-[11px] focus:outline-none focus:ring-1 focus:ring-slate-400"
|
||||
className="w-full rounded-md border border-line bg-white px-1.5 py-1 text-[11px] focus:border-primary focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary"
|
||||
>
|
||||
<option value="">ทุกคน</option>
|
||||
{bookmarkAuthors.map((u) => (
|
||||
|
|
@ -1182,7 +1181,11 @@ export default function AnnotationSidebar({
|
|||
</div>
|
||||
<div>
|
||||
<span className="block pb-0.5">บันทึกวันที่</span>
|
||||
<div className="grid grid-cols-[1fr_auto_1fr] items-center gap-1">
|
||||
{/* เรียง 2 แถวเต็มกว้าง (ไม่ใช่คู่แนวนอน + "–" คั่น): คอลัมน์คู่แคบ
|
||||
~122px ของ sidebar ใส่ค่า "23 ก.ย. 2026" ที่ 16px (rule จอสัมผัส
|
||||
กัน iOS zoom-on-focus) + icon ปฏิทินไม่พอ ปีโดนตัด (code review
|
||||
2026-09-23) — aria-label กำกับช่วงเริ่ม/สุดท้ายอยู่แล้ว */}
|
||||
<div className="grid gap-1">
|
||||
<input
|
||||
type="date"
|
||||
aria-label="วันที่เริ่ม"
|
||||
|
|
@ -1191,9 +1194,8 @@ export default function AnnotationSidebar({
|
|||
setBmConditions({ ...bmConditions, dateFrom: e.target.value })
|
||||
}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
className="w-full min-w-0 rounded border border-slate-300 px-1.5 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-slate-400"
|
||||
className="w-full min-w-0 rounded-md border border-line px-1.5 py-1 text-xs focus:border-primary focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary"
|
||||
/>
|
||||
<span className="text-slate-400">–</span>
|
||||
<input
|
||||
type="date"
|
||||
aria-label="วันที่สุดท้าย"
|
||||
|
|
@ -1202,14 +1204,14 @@ export default function AnnotationSidebar({
|
|||
setBmConditions({ ...bmConditions, dateTo: e.target.value })
|
||||
}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
className="w-full min-w-0 rounded border border-slate-300 px-1.5 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-slate-400"
|
||||
className="w-full min-w-0 rounded-md border border-line px-1.5 py-1 text-xs focus:border-primary focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className={`rounded px-1.5 py-1 text-slate-500 hover:bg-slate-100 ${FOCUS_RING_CLS}`}
|
||||
className={`rounded-md px-1.5 py-1 text-ink-soft hover:bg-ink/[0.05] ${FOCUS_RING_CLS}`}
|
||||
onClick={() => {
|
||||
// UX audit: ล้างให้สิ้นเชิง — คำค้น + เงื่อนไข + ผลที่แสดง พร้อมกัน
|
||||
setBmConditions(EMPTY_BM_CONDITIONS);
|
||||
|
|
@ -1225,12 +1227,12 @@ export default function AnnotationSidebar({
|
|||
{/* error กับ hint เป็นคำเตือนคนละเรื่อง (ฟอร์มผิด vs ผลเป็นของเงื่อนไขเก่า)
|
||||
— มี error แสดงเดี่ยว ไม่ซ้อนสองข้อความพร้อมกัน */}
|
||||
{bmSearchError ? (
|
||||
<p className="px-1 text-[11px] text-red-600">{bmSearchError}</p>
|
||||
<p className="px-1 text-[11px] text-error-ink">{bmSearchError}</p>
|
||||
) : (
|
||||
bmResults !== null &&
|
||||
bmSearchedSig !== null &&
|
||||
bmSearchedSig !== JSON.stringify([bmQuery.trim(), bmConditions]) && (
|
||||
<p className="px-1 text-[11px] text-amber-600">
|
||||
<p className="px-1 text-[11px] text-warning-ink">
|
||||
เงื่อนไขเปลี่ยนแล้ว กด "หา" เพื่อค้นใหม่
|
||||
</p>
|
||||
)
|
||||
|
|
@ -1243,23 +1245,23 @@ export default function AnnotationSidebar({
|
|||
เนื้อหาใน list ด้านล่าง คลิกแถว = กระโดดไปตำแหน่ง + เลือก */}
|
||||
{tab === "annotations" && (
|
||||
<form
|
||||
className="flex shrink-0 gap-1 border-b border-slate-200 p-2"
|
||||
className="flex shrink-0 gap-1 border-b border-line p-2"
|
||||
onSubmit={(e) => void runSearch(e)}
|
||||
>
|
||||
<div className="relative min-w-0 flex-1">
|
||||
<Search className="pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-slate-500" />
|
||||
<Search className="pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-ink-soft" />
|
||||
<input
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.stopPropagation()} // ไม่ให้ปุ่มในช่องค้นหาชนคีย์ลัดของ viewer
|
||||
placeholder="ค้นหาในหมายเหตุ…"
|
||||
className="w-full rounded-md border border-slate-300 py-1.5 pl-7 pr-2 text-xs focus:outline-none focus:ring-2 focus:ring-slate-400"
|
||||
className="w-full rounded-md border border-line py-1.5 pl-7 pr-2 text-xs focus:border-primary focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!searchQuery.trim() || searching}
|
||||
className={`shrink-0 rounded-md border border-slate-300 px-2 py-1.5 text-xs text-slate-600 hover:bg-slate-100 disabled:opacity-40 ${FOCUS_RING_CLS}`}
|
||||
className={`shrink-0 rounded-md border border-line px-2 py-1.5 text-xs text-ink-soft hover:bg-ink/[0.05] disabled:opacity-40 ${FOCUS_RING_CLS}`}
|
||||
>
|
||||
{searching ? "…" : "หา"}
|
||||
</button>
|
||||
|
|
@ -1271,14 +1273,14 @@ export default function AnnotationSidebar({
|
|||
bmResults !== null ? (
|
||||
<div>
|
||||
<div className="flex items-center justify-between px-1 pb-1">
|
||||
<p className="text-[11px] font-semibold tabular-nums text-slate-500">
|
||||
<p className="text-[11px] font-semibold tabular-nums text-ink-soft">
|
||||
ผลการค้นหา ({bmResults.length})
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="ปิดผลการค้นหา"
|
||||
title="กลับไปรายการที่คั่นหน้า"
|
||||
className={`text-slate-500 hover:text-slate-600 ${FOCUS_RING_CLS}`}
|
||||
className={`text-ink-soft hover:text-ink ${FOCUS_RING_CLS}`}
|
||||
onClick={() => setBmResults(null)}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
|
|
@ -1288,14 +1290,14 @@ export default function AnnotationSidebar({
|
|||
แชร์จอให้คนอื่นดู (แก้ฟอร์มหลังค้นบรรทัดนี้ไม่เปลี่ยน จนกด "หา" ใหม่) */}
|
||||
{bmSearchedSummary && (
|
||||
<p
|
||||
className="truncate px-1 pb-1 text-[11px] text-slate-500"
|
||||
className="truncate px-1 pb-1 text-[11px] text-ink-soft"
|
||||
title={bmSearchedSummary}
|
||||
>
|
||||
ค้นด้วย: {bmSearchedSummary}
|
||||
</p>
|
||||
)}
|
||||
{bmResults.length === 0 ? (
|
||||
<p className="px-1 pb-1 text-xs text-slate-500">ไม่พบ</p>
|
||||
<p className="px-1 pb-1 text-xs text-ink-soft">ไม่พบ</p>
|
||||
) : (
|
||||
<ul className="space-y-0.5">
|
||||
{bmResults.map((b) => {
|
||||
|
|
@ -1304,22 +1306,22 @@ export default function AnnotationSidebar({
|
|||
<li key={b.id}>
|
||||
<button
|
||||
type="button"
|
||||
className={`flex w-full items-start gap-1.5 rounded px-1 py-1 text-left hover:bg-slate-100 ${FOCUS_RING_CLS}`}
|
||||
className={`flex w-full items-start gap-1.5 rounded px-1 py-1 text-left hover:bg-ink/[0.05] ${FOCUS_RING_CLS}`}
|
||||
onClick={() => onGoToBookmark(b)}
|
||||
>
|
||||
<Bookmark className="mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-500" />
|
||||
<Bookmark className="mt-0.5 h-3.5 w-3.5 shrink-0 text-warning-ink" />
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-[13px] leading-snug">{b.title}</span>
|
||||
<span className="block text-xs text-slate-500">
|
||||
<span className="block text-xs text-ink-soft">
|
||||
หน้า {b.pageIndex + 1} · {b.userName || "ไม่ทราบชื่อ"}
|
||||
</span>
|
||||
{parent && (
|
||||
<span className="block truncate text-[11px] text-slate-500">
|
||||
<span className="block truncate text-[11px] text-ink-soft">
|
||||
ใน: {parent.title}
|
||||
</span>
|
||||
)}
|
||||
{b.note && (
|
||||
<span className="mt-0.5 block line-clamp-2 whitespace-pre-wrap break-words text-[11px] text-slate-500">
|
||||
<span className="mt-0.5 block line-clamp-2 whitespace-pre-wrap break-words text-[11px] text-ink-soft">
|
||||
{b.note}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -1332,11 +1334,11 @@ export default function AnnotationSidebar({
|
|||
)}
|
||||
</div>
|
||||
) : bookmarks.length === 0 ? (
|
||||
<p className="p-4 text-center text-xs text-slate-500">
|
||||
<p className="p-4 text-center text-xs text-ink-soft">
|
||||
ยังไม่มีที่คั่นหน้า — กดปุ่มคั่นหน้าบนแถบเครื่องมือเพื่อเพิ่ม
|
||||
</p>
|
||||
) : visibleBookmarks.length === 0 ? (
|
||||
<p className="p-4 text-center text-xs text-slate-500">
|
||||
<p className="p-4 text-center text-xs text-ink-soft">
|
||||
ไม่มีที่คั่นหน้าของเจ้าของหมายเหตุที่เลือก
|
||||
</p>
|
||||
) : (
|
||||
|
|
@ -1373,27 +1375,27 @@ export default function AnnotationSidebar({
|
|||
<li key={b.id}>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-start gap-1.5 rounded py-1 pl-5 pr-1 text-left hover:bg-slate-100"
|
||||
className={`flex w-full items-start gap-1.5 rounded py-1 pl-5 pr-1 text-left hover:bg-ink/[0.05] ${FOCUS_RING_CLS}`}
|
||||
onClick={() => onGoToBookmark(b)}
|
||||
>
|
||||
<Bookmark className="mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-500" />
|
||||
<Bookmark className="mt-0.5 h-3.5 w-3.5 shrink-0 text-warning-ink" />
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-[13px] leading-snug font-medium text-slate-700">
|
||||
<span className="block truncate text-[13px] leading-snug font-medium text-ink">
|
||||
{b.title}
|
||||
</span>
|
||||
{/* ผู้บันทึกเหมือนแถวผลการค้นหา — ไม่ใส่ "หน้า N"
|
||||
เพราะหัวกลุ่มของมุมมองนี้บอกหน้าอยู่แล้ว */}
|
||||
<span className="block text-xs text-slate-500">
|
||||
<span className="block text-xs text-ink-soft">
|
||||
{b.userName || "ไม่ทราบชื่อ"}
|
||||
</span>
|
||||
{b.note && (
|
||||
<span className="mt-0.5 block line-clamp-2 whitespace-pre-wrap break-words text-[11px] text-slate-500">
|
||||
<span className="mt-0.5 block line-clamp-2 whitespace-pre-wrap break-words text-[11px] text-ink-soft">
|
||||
{b.note}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{childCount > 0 && (
|
||||
<span className="ml-auto shrink-0 self-center rounded-full bg-slate-100 px-1.5 text-[10px] tabular-nums leading-4 text-slate-500">
|
||||
<span className="ml-auto shrink-0 self-center rounded-full bg-ink/[0.05] px-1.5 text-[10px] tabular-nums leading-4 text-ink-soft">
|
||||
{childCount}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -1420,29 +1422,29 @@ export default function AnnotationSidebar({
|
|||
) : visibleSearchResults !== null ? (
|
||||
<div>
|
||||
<div className="flex items-center justify-between px-1 pb-1">
|
||||
<p className="text-[11px] font-semibold tabular-nums text-slate-500">
|
||||
<p className="text-[11px] font-semibold tabular-nums text-ink-soft">
|
||||
ผลการค้นหา ({visibleSearchResults.length})
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="ปิดผลการค้นหา"
|
||||
title="กลับไปรายการหมายเหตุ"
|
||||
className={`text-slate-500 hover:text-slate-600 ${FOCUS_RING_CLS}`}
|
||||
className={`text-ink-soft hover:text-ink ${FOCUS_RING_CLS}`}
|
||||
onClick={() => setSearchResults(null)}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
{visibleSearchResults.length === 0 ? (
|
||||
<p className="px-1 pb-1 text-xs text-slate-500">ไม่พบ</p>
|
||||
<p className="px-1 pb-1 text-xs text-ink-soft">ไม่พบ</p>
|
||||
) : (
|
||||
// reuse แถวเดิม — li มี data-ann-id ให้ effect scrollIntoView หาเจอ
|
||||
<ul className="space-y-1">{visibleSearchResults.map(renderAnnotationRow)}</ul>
|
||||
)}
|
||||
</div>
|
||||
) : annotations.length === 0 ? (
|
||||
<p className="p-4 text-center text-xs text-slate-500">
|
||||
ยังไม่มีหมายเหตุ — คลุมข้อความ / วาดเส้น / คลิกวางคอมเมนต์บนหน้าเอกสาร
|
||||
<p className="p-4 text-center text-xs text-ink-soft">
|
||||
ยังไม่มีหมายเหตุ — คลุมข้อความ / วาดเส้น / คอมเมนต์บนหน้าเอกสาร
|
||||
<span className="mt-1 block text-[11px]">
|
||||
คีย์ลัด: H มาร์ก · D วาด · C comment · B คั่นหน้า · Ctrl+Z ย้อน
|
||||
</span>
|
||||
|
|
@ -1481,7 +1483,7 @@ export default function AnnotationSidebar({
|
|||
<>
|
||||
{pendingItems.length > 0 && (
|
||||
<section>
|
||||
<p className="px-2 pb-1 pt-2 text-[11px] font-semibold text-slate-500">
|
||||
<p className="px-2 pb-1 pt-2 text-[11px] font-semibold text-ink-soft">
|
||||
ค้างดำเนินการ
|
||||
</p>
|
||||
<ul className="space-y-1">{pendingItems.map(renderAnnotationRow)}</ul>
|
||||
|
|
@ -1489,7 +1491,7 @@ export default function AnnotationSidebar({
|
|||
)}
|
||||
{resolvedItems.length > 0 && (
|
||||
<section>
|
||||
<p className="px-2 pb-1 pt-3 text-[11px] font-semibold text-emerald-500">
|
||||
<p className="px-2 pb-1 pt-3 text-[11px] font-semibold text-success-ink">
|
||||
จัดการแล้ว
|
||||
</p>
|
||||
<ul className="space-y-1">{resolvedItems.map(renderAnnotationRow)}</ul>
|
||||
|
|
@ -1511,35 +1513,35 @@ export default function AnnotationSidebar({
|
|||
{/* F19/A7: dialog ประวัติ — รายการ version (เวลา + ชนิดการเปลี่ยนแปลง) + ปุ่มกู้คืน */}
|
||||
{historyFor && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/40 p-4"
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-ink/40 p-4"
|
||||
onClick={() => setHistoryFor(null)}
|
||||
>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="ประวัติการเปลี่ยนแปลง"
|
||||
className="w-96 max-w-full rounded-lg border border-slate-200 bg-white p-4 shadow-xl"
|
||||
className="w-96 max-w-full rounded-lg border border-line bg-white p-4 shadow-xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<p className="text-sm font-semibold">ประวัติการเปลี่ยนแปลง</p>
|
||||
<p className="mt-0.5 truncate text-[11px] text-slate-500">
|
||||
<p className="mt-0.5 truncate text-[11px] text-ink-soft">
|
||||
{annotationPreviewText(historyFor)}
|
||||
</p>
|
||||
{historyError && (
|
||||
<p className="mt-2 rounded bg-red-50 px-2 py-1 text-[11px] text-red-600">
|
||||
<p className="mt-2 rounded bg-error/10 px-2 py-1 text-[11px] text-error-ink">
|
||||
{historyError}
|
||||
</p>
|
||||
)}
|
||||
{versions === null && !historyError && (
|
||||
<p className="mt-3 text-xs text-slate-500">กำลังโหลด…</p>
|
||||
<p className="mt-3 text-xs text-ink-soft">กำลังโหลด…</p>
|
||||
)}
|
||||
{versions !== null && versions.length === 0 && (
|
||||
<p className="mt-3 text-xs text-slate-500">
|
||||
<p className="mt-3 text-xs text-ink-soft">
|
||||
ยังไม่มีบันทึกการเปลี่ยนแปลง (เกิดขึ้นเมื่อแก้ไขหรือลบรายการนี้)
|
||||
</p>
|
||||
)}
|
||||
{versions !== null && versions.length > 0 && (
|
||||
<ul className="mt-3 max-h-[60vh] divide-y divide-slate-100 overflow-y-auto">
|
||||
<ul className="mt-3 max-h-[60vh] divide-y divide-line/60 overflow-y-auto">
|
||||
{versions.map((v, i) => {
|
||||
// สิ่งที่เปลี่ยนของรอบนี้ = snapshot ก่อนแก้ (v.data) เทียบ state
|
||||
// ถัดไป: snapshot ของรอบแก้ที่ใหม่กว่าถัดไป (i-1) หรือค่าปัจจุบัน
|
||||
|
|
@ -1559,11 +1561,11 @@ export default function AnnotationSidebar({
|
|||
<span className="block text-xs font-medium">
|
||||
{v.kind === "delete" ? "ถูกลบ" : "ถูกแก้ไข"}
|
||||
</span>
|
||||
<span className="block text-[11px] text-slate-500" suppressHydrationWarning>
|
||||
<span className="block text-[11px] text-ink-soft" suppressHydrationWarning>
|
||||
{formatThaiDate(v.createdAt, true)} · โดย {v.userName}
|
||||
</span>
|
||||
{changes.map((c) => (
|
||||
<span key={c} className="mt-0.5 block text-[11px] text-slate-600">
|
||||
<span key={c} className="mt-0.5 block text-[11px] text-ink-soft">
|
||||
{c}
|
||||
</span>
|
||||
))}
|
||||
|
|
@ -1571,7 +1573,7 @@ export default function AnnotationSidebar({
|
|||
<button
|
||||
type="button"
|
||||
disabled={restoringId !== null}
|
||||
className={`mt-0.5 shrink-0 rounded border border-slate-300 px-2 py-1 text-[11px] text-slate-600 hover:bg-slate-100 disabled:opacity-40 ${FOCUS_RING_CLS}`}
|
||||
className={`mt-0.5 shrink-0 rounded-md border border-line px-2 py-1 text-[11px] text-ink-soft hover:bg-ink/[0.05] disabled:opacity-40 ${FOCUS_RING_CLS}`}
|
||||
onClick={() => void handleRestore(v.id)}
|
||||
>
|
||||
{restoringId === v.id ? "กำลังกู้คืน…" : "กู้คืน"}
|
||||
|
|
@ -1585,7 +1587,7 @@ export default function AnnotationSidebar({
|
|||
<button
|
||||
type="button"
|
||||
autoFocus
|
||||
className={`rounded px-3 py-1.5 text-slate-500 hover:bg-slate-100 ${FOCUS_RING_CLS}`}
|
||||
className={`rounded-md px-3 py-1.5 text-ink-soft hover:bg-ink/[0.05] ${FOCUS_RING_CLS}`}
|
||||
onClick={() => setHistoryFor(null)}
|
||||
>
|
||||
ปิด
|
||||
|
|
@ -1605,7 +1607,7 @@ export default function AnnotationSidebar({
|
|||
data-tool-menu=""
|
||||
role="menu"
|
||||
aria-label={menuFor.kind === "annotation" ? "จัดการหมายเหตุ" : "จัดการหัวข้อ"}
|
||||
className="w-60 rounded-lg border border-slate-200 bg-white p-1.5 shadow-lg"
|
||||
className="w-60 rounded-lg border border-line bg-white p-1.5 shadow-lg"
|
||||
style={{
|
||||
position: "fixed",
|
||||
left: menuPos.left,
|
||||
|
|
@ -1619,20 +1621,20 @@ export default function AnnotationSidebar({
|
|||
>
|
||||
{menuConfirmDelete ? (
|
||||
<div className="p-1">
|
||||
<p className="px-1 text-xs text-slate-700">
|
||||
<p className="px-1 text-xs text-ink">
|
||||
ลบ{menuFor.kind === "annotation" ? "รายการ" : "หัวข้อ"}นี้?
|
||||
</p>
|
||||
<div className="mt-1.5 flex justify-end gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
className={`rounded px-2 py-1 text-xs text-slate-500 hover:bg-slate-100 ${FOCUS_RING_CLS}`}
|
||||
className={`rounded-md px-2 py-1 text-xs text-ink-soft hover:bg-ink/[0.05] ${FOCUS_RING_CLS}`}
|
||||
onClick={closeRowMenu}
|
||||
>
|
||||
ยกเลิก
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`rounded bg-red-600 px-2 py-1 text-xs font-medium text-white hover:bg-red-700 ${FOCUS_RING_CLS}`}
|
||||
className={`rounded-md border border-error px-2 py-1 text-xs font-medium text-error-ink hover:bg-error/10 ${FOCUS_RING_CLS}`}
|
||||
onClick={() => {
|
||||
// อ่าน kind/id ก่อน close — setMenuFor(null) แล้วเงื่อนไขเดิมหาย
|
||||
if (menuFor.kind === "annotation") onDeleteAnnotation(menuFor.id);
|
||||
|
|
@ -1678,7 +1680,7 @@ export default function AnnotationSidebar({
|
|||
)}
|
||||
{menuAnn.userId === userId && (
|
||||
<>
|
||||
<div className="my-1 border-t border-slate-100" />
|
||||
<div className="my-1 border-t border-line/60" />
|
||||
<MenuItem
|
||||
danger
|
||||
Icon={Trash2}
|
||||
|
|
@ -1713,7 +1715,7 @@ export default function AnnotationSidebar({
|
|||
)}
|
||||
{menuBm.userId === userId && (
|
||||
<>
|
||||
<div className="my-1 border-t border-slate-100" />
|
||||
<div className="my-1 border-t border-line/60" />
|
||||
<MenuItem
|
||||
danger
|
||||
Icon={Trash2}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import { Bookmark, ChevronDown, ChevronRight, PenLine } from "lucide-react";
|
|||
* ครบ ผู้ใช้พับเฉพาะกลุ่มที่ไม่สนใจ) — รายการ (children) ส่งมาจาก server component
|
||||
* ได้ตรง ๆ และเยื้องเข้า (pl-5) ให้หัวข้อกับรายการไม่ชิดเส้นซ้ายเดียวกัน */
|
||||
const HEADS = {
|
||||
bookmark: { label: "Bookmark", Icon: Bookmark, iconCls: "text-amber-500" },
|
||||
annotation: { label: "Annotation", Icon: PenLine, iconCls: "text-slate-400" },
|
||||
bookmark: { label: "Bookmark", Icon: Bookmark, iconCls: "text-warning-ink" },
|
||||
annotation: { label: "Annotation", Icon: PenLine, iconCls: "text-ink-faint" },
|
||||
} as const;
|
||||
|
||||
export default function CollapsibleBlock({
|
||||
|
|
@ -35,17 +35,17 @@ export default function CollapsibleBlock({
|
|||
aria-expanded={open}
|
||||
aria-controls={contentId}
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="flex w-full items-center gap-1.5 rounded-md py-1.5 text-left text-xs font-semibold text-slate-500 hover:text-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1"
|
||||
className="focus-ring flex w-full items-center gap-1.5 rounded-md py-1.5 text-left text-xs font-semibold text-ink-soft hover:text-ink"
|
||||
>
|
||||
{open ? (
|
||||
<ChevronDown aria-hidden className="h-3.5 w-3.5 shrink-0 text-slate-400" />
|
||||
<ChevronDown aria-hidden className="h-3.5 w-3.5 shrink-0 text-ink-faint" />
|
||||
) : (
|
||||
<ChevronRight aria-hidden className="h-3.5 w-3.5 shrink-0 text-slate-400" />
|
||||
<ChevronRight aria-hidden className="h-3.5 w-3.5 shrink-0 text-ink-faint" />
|
||||
)}
|
||||
<Icon aria-hidden className={`h-3.5 w-3.5 shrink-0 ${iconCls}`} />
|
||||
{label}
|
||||
<span className="font-normal tabular-nums">({count})</span>
|
||||
<span aria-hidden className="ml-1 h-px min-w-4 flex-1 bg-slate-100" />
|
||||
<span aria-hidden className="ml-1 h-px min-w-4 flex-1 bg-line" />
|
||||
</button>
|
||||
</h3>
|
||||
{open && (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue