"use client";
import { useEffect, useRef, useState, type ComponentType, type ReactNode } from "react";
import { Check, ChevronDown, ChevronUp, Highlighter, Strikethrough, Underline } from "lucide-react";
import type { HighlightStyle } from "@/db/schema";
import { colorName } from "@/utils/annotation-display";
// ตัวควบคุมที่ใช้ร่วมระหว่าง dropdown บนปุ่ม tool (PDFViewer) กับแผงเครื่องมือ
// บนกรอบ selection (AnnotationSelectionLayer) — หน้าตา/พฤติกรรมเดียวกันทั้งสองที่
/** UX audit 2026-09-16: focus ring คีย์บอร์ด — ค่าเดียวกับ FOCUS_RING_CLS ของ
* PDFViewer/AnnotationSidebar (ประกาศแยกที่นี่เพราะ PDFViewer import ไฟล์นี้
* อยู่แล้ว ห้ามวนกลับ) */
const FOCUS_RING_CLS =
"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") ท้ายแถว */
export const INK_COLORS = [
"#ef4444", // แดง (default ปากกา/รูปทรง)
"#f97316", // ส้ม
"#fde047", // เหลือง (default ไฮไลต์)
"#22c55e", // เขียว
"#14b8a6", // เขียวอมฟ้า
"#3b82f6", // น้ำเงิน
"#8b5cf6", // ม่วง
"#ec4899", // ชมพู
"#b45309", // น้ำตาล
"#111827", // ดำ (default textbox)
"#6b7280", // เทา
];
/** แถวตั้งค่าในเมนู/แผงเครื่องมือ — label ซ้าย (optional: แถวสีไม่ใส่ ให้ตารางกินเต็มแถว)
* + ตัวควบคุมขวา */
export function PanelRow({ label, children }: { label?: string; children: ReactNode }) {
return (
{label ? (
{label}
) : null}
{children}
);
}
/** แถวปุ่มสีวงกลม preset + ปุ่มสีรุ้งวงกลม (color picker เลือกสีอื่นได้ไม่จำกัด)
* grid 4 ต่อแถว — 11 สี preset + สีรุ้ง = 12 วง เรียงจบพอดี 3 แถว (4+4+4)
* ช่อง ~37px พ้นมาตรฐาน touch 36px เลยไม่ต้องปลด min ใน CSS จอสัมผัส */
export function ColorSwatches({ value, onChange }: { value: string; onChange: (c: string) => void }) {
return (
{INK_COLORS.map((c) => (
);
}
/** ช่องตัวเลข combobox — กดที่ input เปิดรายการค่า preset ให้เลือก (หรือพิมพ์เองได้)
* + ปุ่ม ▲▼ ปรับค่าทีละ stepBy (clamp ใน min–max) */
export function NumberCombo({
value,
options,
min,
max,
step,
stepBy,
suffix,
onChange,
}: {
value: number;
options: number[];
min: number;
/** ขอบบนตอน clamp — ไม่ส่ง = ไม่จำกัด (เช่น ความหนาเส้นวาด) */
max?: number;
step: number;
/** ละขั้นของปุ่ม ▲▼ (default 1 — เช่น fontSize ใช้ 0.1) */
stepBy?: number;
/** ต่อท้ายค่าในแถวรายการ (เช่น "%") */
suffix?: string;
onChange: (v: number) => void;
}) {
const [open, setOpen] = useState(false);
// FIX-3: ถือ draft ระหว่างพิมพ์ — commit ครั้งเดียวตอน blur/Enter
// ไม่ยิง server action + version snapshot ต่อ keystroke; sync กลับเมื่อค่าจากภายนอกเปลี่ยน
const [draft, setDraft] = useState(String(value));
const rootRef = useRef(null);
useEffect(() => setDraft(String(value)), [value]);
// FIX-3 (ปรับ): หยุดพิมพ์แล้ว ~0.5 วิ commit เอง — ไม่ต้องรอ Enter/blur แต่ยัง
// รวมเป็น commit เดียวต่อชุดที่พิมพ์เสร็จ (ไม่ยิง server action + version
// snapshot ต่อ keystroke — สำคัญกับแผง selection ที่ commit ทัพหลังถึง DB)
useEffect(() => {
if (draft === String(value)) return; // sync กลับจาก value ไม่นับเป็นการพิมพ์
const t = setTimeout(() => {
const v = Number(draft);
if (draft.trim() === "" || !Number.isFinite(v)) return; // คั่นกลางพิมพ์/ลบหมด — ให้ blur จัดการ
const clamped = Math.min(max ?? Infinity, Math.max(min, Number(v.toFixed(2))));
if (clamped !== value) onChange(clamped); // value เปลี่ยน → effect ด้านบน sync draft กลับ
else setDraft(String(clamped)); // เกินขอบแต่ clamped แล้วเท่าค่าเดิม → แสดงค่าจริง
}, 500);
return () => clearTimeout(t);
// eslint-disable-next-line react-hooks/exhaustive-deps -- draft/value/onChange ใช้ค่าล่าสุดตอน timer ยิง
}, [draft]);
// คลิกนอกตัวควบคุม = ปิด dropdown (คลิกใน dropdown เองไม่นับ — อยู่ใน rootRef)
useEffect(() => {
if (!open) return;
const onMouseDown = (e: MouseEvent) => {
if (!rootRef.current?.contains(e.target as Node)) setOpen(false);
};
document.addEventListener("mousedown", onMouseDown);
return () => document.removeEventListener("mousedown", onMouseDown);
}, [open]);
/** ยืนยันค่าที่พิมพ์หนึ่งครั้ง — clamp เข้า min–max; พิมพ์ไม่เป็นตัวเลข = คืนค่าเดิม
* ค่าเท่าเดิมไม่ยิง onChange — focus/blur เฉย ๆ ต้องไม่เกิด server action + snapshot ขยะ */
function commitDraft() {
const v = Number(draft);
if (draft.trim() !== "" && Number.isFinite(v)) {
const clamped = Math.min(max ?? Infinity, Math.max(min, Number(v.toFixed(2))));
setDraft(String(clamped));
if (clamped !== value) onChange(clamped);
} else {
setDraft(String(value));
}
}
/** ▲▼ ปรับค่าทีละ stepBy จาก draft ล่าสุด (ค่าที่พิมพ์เองก็ต่อจากนั้นได้) — clamp ใน min–max
* ชนขอบแล้วค่าไม่เดิน (next === value) ไม่ยิง onChange เช่นเดียวกับ commitDraft */
function stepValue(dir: 1 | -1) {
const by = stepBy ?? 1;
const base = draft.trim() !== "" && Number.isFinite(Number(draft)) ? Number(draft) : value;
const next = Math.min(max ?? Infinity, Math.max(min, Number((base + dir * by).toFixed(2))));
setDraft(String(next));
if (next !== value) onChange(next);
}
return (
{
// Esc ปิดแค่ dropdown นี้ก่อน (stopPropagation ไม่ให้ปิดทั้งเมนู/กรอบ)
if (e.key === "Escape" && open) {
e.stopPropagation();
setOpen(false);
}
}}
>
{open && (
{options.map((opt) => (
))}
)}
);
}
/** ไอคอนเส้นขยุกขยิก (squiggle) — วาดเองเพราะ lucide ไม่มี */
export function SquiggleIcon({ className }: { className?: string }) {
return (
);
}
/** รายการรูปแบบการมาร์กข้อความ + ไอคอน — ใช้ใน dropdown ของ tool highlight และแผงของกรอบ selection */
export const HIGHLIGHT_STYLE_BTNS: {
key: HighlightStyle;
label: string;
Icon: ComponentType<{ className?: string }>;
}[] = [
{ key: "highlight", label: "ไฮไลต์ข้อความ", Icon: Highlighter },
{ key: "underline", label: "เส้นใต้", Icon: Underline },
{ key: "strikethrough", label: "ขีดฆ่า", Icon: Strikethrough },
{ key: "squiggle", label: "ขยุกขยิก", Icon: SquiggleIcon },
];