toolbar: แก้ quick wins ตาม usability audit 2026-09-22 — ปิดเมนูหลังเลือกแถวเดี่ยว + แบนเนอร์ hint ยางลบ/tool วางของ + ปุ่ม % ซูมรีเซ็ต fit-width + ชื่อ tool "เลือกข้อความ" เป็น "เลือก"
- เมนู tool 4 ตัว (รูปแบบมาร์ก/ชนิดปากกา/ชนิดรูปทรง/preset ตรายาง): เลือกแถวเดี่ยวแล้ว ปิดเมนูทันทีด้วย setState ตรง ๆ โดยไม่ผ่าน markMenuClosed (guard กลืน tap 300ms จะทำให้แตะหน้าครั้งถัดไปเงียบ — เดิมวางตรายาง 1 ชิ้นต้องแตะ 4 ครั้ง เหลือ 3 ครั้ง ที่ตอบสนองทุกครั้ง) ส่วนแถวปรับค่า สี/ความหนา/ความทึบ/ขนาด ยังคงเมนูเปิด ให้ปรับต่อหลายค่าติดกันตามเจตนาเดิม - แบนเนอร์ใต้ header (reuse pattern M3/M6/H2): tool คอมเมนต์/กล่องข้อความ/ตรายาง แสดง "โหมด…: คลิก (แตะ) ตำแหน่งบนหน้าเพื่อวาง" ตลอดตอน active (ทำหน้าที่ mode indicator แก้อาการเลือก tool แล้วจอนิ่ง — เดิม hint อยู่แค่ empty state ของ sidebar ที่หายทันทีที่มีรายการ) + ยางลบบอกขอบเขตว่าลบได้เฉพาะเส้น/รูปทรง/ตรายางของตัวเอง (ไม่แตะตรรกะ eraseAt ตามสเปก F6 เดิม) + label ปุ่มยางลบเปลี่ยนตาม - cursor เป็น crosshair ตอนใช้ tool วางของ 3 ตัวบน desktop (จอสัมผัสไม่มี cursor) - ตัวเลข % ซูมกลายเป็นปุ่มรีเซ็ตกลับ fit-width — extract สูตรจาก effect ตอนโหลด เป็น fitWidthScale() ใช้ร่วมกัน คิดจากความกว้าง main ปัจจุบัน (sidebar/จออาจเปลี่ยน) - ชื่อ tool select "เลือกข้อความ" → "เลือก" (ไม่ชน "มาร์กข้อความ" — title/aria-label/ ตัวเลื่อนปุ่ม active อ้างจาก TOOLS อัตโนมัติ) + sync comment ที่อ้างชื่อเก่า 4 จุด - contrast: text-slate-400 → slate-500 ที่ข้อความข้อมูล 4 จุด (เวลา reply, เวลา comment popup, "วางที่" ของ popup bookmark, "กำลังโหลดเอกสาร…") - ปุ่มสี ColorSwatches ใช้ชื่อสีไทยใน aria-label/title ผ่าน colorName() ที่ export เพิ่มจาก utils/annotation-display.ts (แทน hex "#ef4444" ที่ screen reader อ่านไม่รู้เรื่อง) ยังคงเดิมตามคำสั่งผู้ใช้: ปุ่ม Bookmark/Export PDF คงป้ายอังกฤษ, แท็บ sidebar, InkLayer/AnnotationSelectionLayer verify: tsc ผ่าน + vitest 139/139 + Playwright 24 checks ผ่าน (flow ตรายาง 4 แตะ→3 แตะ, แถวปรับค่าเมนูยังเปิด, แบนเนอร์/cursor ครบทุก tool, ซูม 100%→150%→รีเซ็ตกลับ 100%, aria-label "เลือกสี แดง") Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
f7cd4b8312
commit
357f3e972f
3 changed files with 90 additions and 25 deletions
|
|
@ -364,18 +364,28 @@ function selectionTextWithin(kept: DOMRect[], range: Range): string {
|
||||||
* ผู้ใช้ศาลไม่คุ้นไอคอนล้วน โดยเฉพาะจอสัมผัสที่ hover อ่าน title ไม่ได้ — แทน span
|
* ผู้ใช้ศาลไม่คุ้นไอคอนล้วน โดยเฉพาะจอสัมผัสที่ hover อ่าน title ไม่ได้ — แทน span
|
||||||
* ชื่อ tool-active ของ H3 ที่ถอดออก) */
|
* ชื่อ tool-active ของ H3 ที่ถอดออก) */
|
||||||
const TOOLS: { key: Tool; label: string; text: string; Icon: LucideIcon }[] = [
|
const TOOLS: { key: Tool; label: string; text: string; Icon: LucideIcon }[] = [
|
||||||
{ key: "select", label: "เลือกข้อความ", text: "เลือกข้อความ", Icon: MousePointer2 },
|
{ key: "select", label: "เลือก", text: "เลือก", Icon: MousePointer2 },
|
||||||
{ key: "pan", label: "เลื่อนหน้า (pan)", text: "เลื่อนหน้า", Icon: Hand },
|
{ key: "pan", label: "เลื่อนหน้า (pan)", text: "เลื่อนหน้า", Icon: Hand },
|
||||||
{ key: "highlight", label: "มาร์กข้อความ (H)", text: "มาร์กข้อความ", Icon: Highlighter },
|
{ key: "highlight", label: "มาร์กข้อความ (H)", text: "มาร์กข้อความ", Icon: Highlighter },
|
||||||
{ key: "draw", label: "วาดเส้น (D)", text: "วาดเส้น", Icon: Pencil },
|
{ key: "draw", label: "วาดเส้น (D)", text: "วาดเส้น", Icon: Pencil },
|
||||||
{ key: "comment", label: "วางคอมเมนต์ (C)", text: "วางคอมเมนต์", Icon: MessageSquarePlus },
|
{ key: "comment", label: "วางคอมเมนต์ (C)", text: "วางคอมเมนต์", Icon: MessageSquarePlus },
|
||||||
{ key: "shape", label: "รูปทรง", text: "รูปทรง", Icon: Shapes },
|
{ key: "shape", label: "รูปทรง", text: "รูปทรง", Icon: Shapes },
|
||||||
{ key: "erase", label: "ยางลบ (ลบด้วยการลาก)", text: "ยางลบ", Icon: Eraser },
|
{ key: "erase", label: "ยางลบ (เส้น/รูปทรง/ตรายางของตัวเอง)", text: "ยางลบ", Icon: Eraser },
|
||||||
{ key: "redact", label: "ปิดพราง (redaction)", text: "ปิดพราง", Icon: EyeOff },
|
{ key: "redact", label: "ปิดพราง (redaction)", text: "ปิดพราง", Icon: EyeOff },
|
||||||
{ key: "textbox", label: "กล่องข้อความ", text: "กล่องข้อความ", Icon: Type },
|
{ key: "textbox", label: "กล่องข้อความ", text: "กล่องข้อความ", Icon: Type },
|
||||||
{ key: "stamp", label: "ตรายาง (stamp)", text: "ตรายาง", Icon: Stamp },
|
{ key: "stamp", label: "ตรายาง (stamp)", text: "ตรายาง", Icon: Stamp },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** แบนเนอร์แนะนำวิธีใช้ tool ที่วางของด้วยคลิกบนหน้า (UX audit 2026-09-22): เดิมเลือก tool
|
||||||
|
* แล้วจอนิ่ง ไม่มีอะไรบอกขั้นต่อไป (hint เดิมอยู่แค่ empty state ของ sidebar ที่หายทันที
|
||||||
|
* ที่มีรายการ) — แสดงตลอดตอน tool active ให้ทำหน้าที่ mode indicator ไปพร้อมกัน
|
||||||
|
* (เงื่อนไข tool เดี่ยว ๆ จึงไม่มีวันซ้อนกับแบนเนอร์ M3/M6/H2 ข้างบน) */
|
||||||
|
const PLACE_TOOL_HINTS: Partial<Record<Tool, string>> = {
|
||||||
|
comment: "โหมดวางคอมเมนต์: คลิก (แตะ) ตำแหน่งบนหน้าเพื่อวางกล่องคอมเมนต์",
|
||||||
|
textbox: "โหมดกล่องข้อความ: คลิก (แตะ) ตำแหน่งบนหน้าเพื่อวางกล่อง แล้วพิมพ์ข้อความ",
|
||||||
|
stamp: "โหมดตรายาง: คลิก (แตะ) ตำแหน่งบนหน้าเพื่อประทับตรายาง",
|
||||||
|
};
|
||||||
|
|
||||||
/** UX review 2026-09-14 M4: focus ring ตอน keyboard focus — ทั้งโปรเจกต์ไม่เคยมี focus-visible
|
/** UX review 2026-09-14 M4: focus ring ตอน keyboard focus — ทั้งโปรเจกต์ไม่เคยมี focus-visible
|
||||||
* ปุ่ม toolbar จึงกลายเป็นกด Tab แล้วไม่รู้ focus อยู่ปุ่มไหน
|
* ปุ่ม toolbar จึงกลายเป็นกด Tab แล้วไม่รู้ focus อยู่ปุ่มไหน
|
||||||
* ring เต็มสี + ring-offset คั่นขาว 1px ให้เห็นชัดแม้บนปุ่ม active ที่พื้น primary เอง
|
* ring เต็มสี + ring-offset คั่นขาว 1px ให้เห็นชัดแม้บนปุ่ม active ที่พื้น primary เอง
|
||||||
|
|
@ -383,6 +393,16 @@ const TOOLS: { key: Tool; label: string; text: string; Icon: LucideIcon }[] = [
|
||||||
const FOCUS_RING_CLS =
|
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-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 focus-visible:z-10";
|
||||||
|
|
||||||
|
/** สเกลที่ทำให้หน้ากระดาษพอดีความกว้างของ main (ตัด padding px-4 ซ้าย-ขวา = 32px — สูตร
|
||||||
|
* เดิมของ effect fit-width ตอนโหลด ย้ายมาเป็น helper ใช้ร่วมกับปุ่ม % ที่ header ด้วย
|
||||||
|
* (UX audit 2026-09-22: ผู้ใช้ซูมดูรายละเอียดแล้วไม่มีทางกลับมาดูทั้งหน้า) — คิดจากความ
|
||||||
|
* กว้าง main ปัจจุบันทุกครั้งเพราะ sidebar/จออาจเปลี่ยนไปแล้ว; ยังไม่รู้ขนาดหน้า = null */
|
||||||
|
function fitWidthScale(mainW: number | undefined, pageW: number | undefined): number | null {
|
||||||
|
const avail = (mainW ?? 0) - 32;
|
||||||
|
if (!avail || !pageW) return null;
|
||||||
|
return pageW > avail ? clamp(Math.floor((avail / pageW) * 100) / 100, 0.5, 1) : 1;
|
||||||
|
}
|
||||||
|
|
||||||
/** ชนิดที่สร้างเสร็จแล้วเลือกรายการใหม่ให้อัตโนมัติ (กรอบ selection + sidebar ไฮไลต์)
|
/** ชนิดที่สร้างเสร็จแล้วเลือกรายการใหม่ให้อัตโนมัติ (กรอบ selection + sidebar ไฮไลต์)
|
||||||
* ทุกชนิดยกเว้น COMMENT — เปิด composer พิมพ์ก่อนบันทึก และคลิก pin = เลือกอยู่แล้ว */
|
* ทุกชนิดยกเว้น COMMENT — เปิด composer พิมพ์ก่อนบันทึก และคลิก pin = เลือกอยู่แล้ว */
|
||||||
const AUTO_SELECT_TYPES = new Set<AnnotationType>([
|
const AUTO_SELECT_TYPES = new Set<AnnotationType>([
|
||||||
|
|
@ -506,7 +526,7 @@ function ReplyBox({
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="text-[11px] font-medium text-slate-600">
|
<p className="text-[11px] font-medium text-slate-600">
|
||||||
{r.userName}
|
{r.userName}
|
||||||
<span className="ml-1 font-normal text-slate-400" suppressHydrationWarning>
|
<span className="ml-1 font-normal text-slate-500" suppressHydrationWarning>
|
||||||
{formatThaiDate(r.createdAt, true)}
|
{formatThaiDate(r.createdAt, true)}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -628,7 +648,7 @@ function PdfPage({
|
||||||
/** live preview มาร์กระหว่างลากคลุมด้วย tool highlight (null = ไม่มี) */
|
/** live preview มาร์กระหว่างลากคลุมด้วย tool highlight (null = ไม่มี) */
|
||||||
liveMark: { pageIndex: number; rects: Rect[] } | null;
|
liveMark: { pageIndex: number; rects: Rect[] } | null;
|
||||||
liveMarkStyle: { color: string; style: HighlightStyle; opacity?: number };
|
liveMarkStyle: { color: string; style: HighlightStyle; opacity?: number };
|
||||||
/** selection สีฟ้าสะอาด ๆ ของ tool เลือกข้อความ — กรองแนวเส้นทางลากเหมือน liveMark
|
/** selection สีฟ้าสะอาด ๆ ของ tool เลือก — กรองแนวเส้นทางลากเหมือน liveMark
|
||||||
* แต่ไม่สร้างไฮไลต์ (ซ่อนสี selection ของเบราว์เซอร์ วาดเองแทน) */
|
* แต่ไม่สร้างไฮไลต์ (ซ่อนสี selection ของเบราว์เซอร์ วาดเองแทน) */
|
||||||
selectPreview: { pageIndex: number; rects: Rect[] } | null;
|
selectPreview: { pageIndex: number; rects: Rect[] } | null;
|
||||||
/** ความกว้างหน้าแรกที่ scale 1 — แปลง px↔% ของ fontSize ในแผง selection */
|
/** ความกว้างหน้าแรกที่ scale 1 — แปลง px↔% ของ fontSize ในแผง selection */
|
||||||
|
|
@ -892,7 +912,7 @@ function PdfPage({
|
||||||
liveMarkStyle.opacity,
|
liveMarkStyle.opacity,
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
{/* selection สีฟ้าของ tool เลือกข้อความ — กรองแนวเส้นทางลากแล้ว วาดเองแทน
|
{/* selection สีฟ้าของ tool เลือก — กรองแนวเส้นทางลากแล้ว วาดเองแทน
|
||||||
สี selection ของเบราว์เซอร์ (ไม่สร้างไฮไลต์) */}
|
สี selection ของเบราว์เซอร์ (ไม่สร้างไฮไลต์) */}
|
||||||
{selectPreview &&
|
{selectPreview &&
|
||||||
selectPreview.pageIndex === pageNumber - 1 &&
|
selectPreview.pageIndex === pageNumber - 1 &&
|
||||||
|
|
@ -1084,7 +1104,7 @@ function PdfPage({
|
||||||
{a.data.message}
|
{a.data.message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p className="mt-1 text-[11px] text-slate-400" suppressHydrationWarning>
|
<p className="mt-1 text-[11px] text-slate-500" suppressHydrationWarning>
|
||||||
{formatThaiDate(a.createdAt, true)}
|
{formatThaiDate(a.createdAt, true)}
|
||||||
</p>
|
</p>
|
||||||
{/* F10/A5: toggle จัดการแล้ว — ใครก็ได้ (D6) */}
|
{/* F10/A5: toggle จัดการแล้ว — ใครก็ได้ (D6) */}
|
||||||
|
|
@ -1211,7 +1231,7 @@ export default function PDFViewer({
|
||||||
const [currentPage, setCurrentPage] = useState(0);
|
const [currentPage, setCurrentPage] = useState(0);
|
||||||
// tool highlight — live preview มาร์กตามรูปแบบ/สีที่ตั้งไว้ ระหว่างลากคลุมข้อความ
|
// tool highlight — live preview มาร์กตามรูปแบบ/สีที่ตั้งไว้ ระหว่างลากคลุมข้อความ
|
||||||
const [liveMark, setLiveMark] = useState<{ pageIndex: number; rects: Rect[] } | null>(null);
|
const [liveMark, setLiveMark] = useState<{ pageIndex: number; rects: Rect[] } | null>(null);
|
||||||
// tool เลือกข้อความ — selection สีฟ้าสะอาด ๆ (กรองแนวเส้นทางลากแล้ว ไม่สร้างไฮไลต์)
|
// tool เลือก — selection สีฟ้าสะอาด ๆ (กรองแนวเส้นทางลากแล้ว ไม่สร้างไฮไลต์)
|
||||||
const [selectPreview, setSelectPreview] = useState<{ pageIndex: number; rects: Rect[] } | null>(null);
|
const [selectPreview, setSelectPreview] = useState<{ pageIndex: number; rects: Rect[] } | null>(null);
|
||||||
const [composer, setComposer] = useState<Composer | null>(null);
|
const [composer, setComposer] = useState<Composer | null>(null);
|
||||||
const [draft, setDraft] = useState("");
|
const [draft, setDraft] = useState("");
|
||||||
|
|
@ -1689,11 +1709,8 @@ export default function PDFViewer({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pageSizes.length === 0 || fitWidthDoneRef.current) return;
|
if (pageSizes.length === 0 || fitWidthDoneRef.current) return;
|
||||||
fitWidthDoneRef.current = true;
|
fitWidthDoneRef.current = true;
|
||||||
const avail = (mainRef.current?.clientWidth ?? 0) - 32;
|
const s = fitWidthScale(mainRef.current?.clientWidth, pageSizes[0].width);
|
||||||
const pageW = pageSizes[0].width;
|
if (s !== null && s !== 1) setScale(s);
|
||||||
if (avail > 0 && pageW > avail) {
|
|
||||||
setScale(clamp(Math.floor((avail / pageW) * 100) / 100, 0.5, 1));
|
|
||||||
}
|
|
||||||
}, [pageSizes]);
|
}, [pageSizes]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -2641,7 +2658,7 @@ export default function PDFViewer({
|
||||||
pageEl: HTMLElement,
|
pageEl: HTMLElement,
|
||||||
) {
|
) {
|
||||||
// ปล่อยเมาส์จากการลากคลุมข้อความ (comment/textbox/stamp ก็คลุมได้) ไม่ถือเป็นคลิก —
|
// ปล่อยเมาส์จากการลากคลุมข้อความ (comment/textbox/stamp ก็คลุมได้) ไม่ถือเป็นคลิก —
|
||||||
// selection ยังค้างตอน click ยิง ให้ผ่านไปเฉย ๆ เหมือน tool เลือกข้อความ ไม่เปิด
|
// selection ยังค้างตอน click ยิง ให้ผ่านไปเฉย ๆ เหมือน tool เลือก ไม่เปิด
|
||||||
// composer/วางตรายาง (คลิกล้วน ๆ selection ถูกยุบตั้งแต่ mousedown จึงไม่โดน guard นี้)
|
// composer/วางตรายาง (คลิกล้วน ๆ selection ถูกยุบตั้งแต่ mousedown จึงไม่โดน guard นี้)
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
if (selection && !selection.isCollapsed) return;
|
if (selection && !selection.isCollapsed) return;
|
||||||
|
|
@ -2998,9 +3015,20 @@ export default function PDFViewer({
|
||||||
>
|
>
|
||||||
<ZoomOut className="h-4 w-4" />
|
<ZoomOut className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
<span className="w-12 text-center text-xs tabular-nums">
|
{/* UX audit 2026-09-22: ตัวเลข % เป็นปุ่มกลับไป fit-width — PDF viewer ที่
|
||||||
|
ผู้ใช้เคยใช้ทุกตัวมี affordance รีเซ็ตซูม คิดจากความกว้าง main ปัจจุบัน */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="ซูมพอดีความกว้างหน้า"
|
||||||
|
title="ซูมพอดีความกว้างหน้า"
|
||||||
|
className={`w-12 py-2 text-center text-xs tabular-nums text-slate-500 hover:bg-slate-100 ${FOCUS_RING_CLS}`}
|
||||||
|
onClick={() => {
|
||||||
|
const s = fitWidthScale(mainRef.current?.clientWidth, pageSizes[0]?.width);
|
||||||
|
if (s !== null) setScale(s);
|
||||||
|
}}
|
||||||
|
>
|
||||||
{Math.round(scale * 100)}%
|
{Math.round(scale * 100)}%
|
||||||
</span>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="ซูมเข้า"
|
aria-label="ซูมเข้า"
|
||||||
|
|
@ -3107,7 +3135,12 @@ export default function PDFViewer({
|
||||||
? "bg-slate-100 text-slate-900"
|
? "bg-slate-100 text-slate-900"
|
||||||
: "text-slate-600 hover:bg-slate-100"
|
: "text-slate-600 hover:bg-slate-100"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setHighlightStyle(sKey)}
|
onClick={() => {
|
||||||
|
setHighlightStyle(sKey);
|
||||||
|
// แถวเดี่ยวเลือกจบ = ปิดเมนูทันที แต่ห้ามผ่าน markMenuClosed
|
||||||
|
// (guard กลืน tap 300ms — แตะหน้าครั้งถัดไปต้องใช้ได้ทันที)
|
||||||
|
setHighlightMenuOpen(false);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<SIcon className="h-3.5 w-3.5 shrink-0" />
|
<SIcon className="h-3.5 w-3.5 shrink-0" />
|
||||||
<span className="flex-1">{sLabel}</span>
|
<span className="flex-1">{sLabel}</span>
|
||||||
|
|
@ -3191,10 +3224,13 @@ export default function PDFViewer({
|
||||||
}`}
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDrawPen(p);
|
setDrawPen(p);
|
||||||
// เลือก pen = รีเซ็ตค่ากลับเป็น preset ของ pen นั้น (เมนูเปิดค้างให้ปรับต่อ)
|
// เลือก pen = รีเซ็ตค่ากลับเป็น preset ของ pen นั้น
|
||||||
setDrawColor(DRAW_PENS[p].color);
|
setDrawColor(DRAW_PENS[p].color);
|
||||||
setDrawWidthPct(DRAW_PENS[p].strokeWidthPct);
|
setDrawWidthPct(DRAW_PENS[p].strokeWidthPct);
|
||||||
setDrawOpacityPct(DRAW_PENS[p].opacity * 100);
|
setDrawOpacityPct(DRAW_PENS[p].opacity * 100);
|
||||||
|
// แถวเดี่ยวเลือกจบ = ปิดเมนูทันที แต่ห้ามผ่าน markMenuClosed
|
||||||
|
// (guard กลืน tap 300ms — แถวปรับค่าสี/ความหนา/ความทึบยังเปิดค้างให้ปรับต่อ)
|
||||||
|
setDrawMenuOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PenIcon className="h-3.5 w-3.5 shrink-0" />
|
<PenIcon className="h-3.5 w-3.5 shrink-0" />
|
||||||
|
|
@ -3290,7 +3326,12 @@ export default function PDFViewer({
|
||||||
? "bg-slate-100 text-slate-900"
|
? "bg-slate-100 text-slate-900"
|
||||||
: "text-slate-600 hover:bg-slate-100"
|
: "text-slate-600 hover:bg-slate-100"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setShapeKind(kind)}
|
onClick={() => {
|
||||||
|
setShapeKind(kind);
|
||||||
|
// แถวเดี่ยวเลือกจบ = ปิดเมนูทันที แต่ห้ามผ่าน markMenuClosed
|
||||||
|
// (guard กลืน tap 300ms — แถวปรับค่าสี/ความหนายังเปิดค้างให้ปรับต่อ)
|
||||||
|
setShapeMenuOpen(false);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<KindIcon className="h-3.5 w-3.5 shrink-0" />
|
<KindIcon className="h-3.5 w-3.5 shrink-0" />
|
||||||
<span className="flex-1">{kindLabel}</span>
|
<span className="flex-1">{kindLabel}</span>
|
||||||
|
|
@ -3370,7 +3411,12 @@ export default function PDFViewer({
|
||||||
? "bg-slate-100 text-slate-900"
|
? "bg-slate-100 text-slate-900"
|
||||||
: "text-slate-600 hover:bg-slate-100"
|
: "text-slate-600 hover:bg-slate-100"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setStampPreset(preset)}
|
onClick={() => {
|
||||||
|
setStampPreset(preset);
|
||||||
|
// แถวเดี่ยวเลือกจบ = ปิดเมนูทันที แต่ห้ามผ่าน markMenuClosed
|
||||||
|
// (guard กลืน tap 300ms — แถวปรับค่าสี/ขนาดยังเปิดค้างให้ปรับต่อ)
|
||||||
|
setStampMenuOpen(false);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex-1">{preset}</span>
|
<span className="flex-1">{preset}</span>
|
||||||
{stampPreset === preset && <Check className="h-3.5 w-3.5 shrink-0" />}
|
{stampPreset === preset && <Check className="h-3.5 w-3.5 shrink-0" />}
|
||||||
|
|
@ -3652,7 +3698,7 @@ export default function PDFViewer({
|
||||||
placeholder="โน้ต (ไม่บังคับ)"
|
placeholder="โน้ต (ไม่บังคับ)"
|
||||||
className="mt-1.5 w-full resize-none rounded-md border border-slate-300 px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-slate-400"
|
className="mt-1.5 w-full resize-none rounded-md border border-slate-300 px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-slate-400"
|
||||||
/>
|
/>
|
||||||
<p className="mt-1.5 text-[11px] text-slate-400">วางที่</p>
|
<p className="mt-1.5 text-[11px] text-slate-500">วางที่</p>
|
||||||
<Select
|
<Select
|
||||||
value={bookmarkComposer.parentId ?? ""}
|
value={bookmarkComposer.parentId ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
|
|
@ -3735,7 +3781,9 @@ export default function PDFViewer({
|
||||||
: "grab"
|
: "grab"
|
||||||
: tool === "erase"
|
: tool === "erase"
|
||||||
? ERASER_CURSOR
|
? ERASER_CURSOR
|
||||||
: undefined,
|
: tool === "comment" || tool === "textbox" || tool === "stamp"
|
||||||
|
? "crosshair" // tool วางของ — บอกว่าคลิกวางได้ (desktop อย่างเดียว จอสัมผัสไม่มี cursor)
|
||||||
|
: undefined,
|
||||||
}}
|
}}
|
||||||
onPointerDown={(e) => {
|
onPointerDown={(e) => {
|
||||||
if (tool !== "pan" || e.button !== 0) return;
|
if (tool !== "pan" || e.button !== 0) return;
|
||||||
|
|
@ -3785,6 +3833,20 @@ export default function PDFViewer({
|
||||||
หรือสลับเป็นเครื่องมืออื่นเพื่อปัดเลื่อนตามปกติ
|
หรือสลับเป็นเครื่องมืออื่นเพื่อปัดเลื่อนตามปกติ
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{PLACE_TOOL_HINTS[tool] && (
|
||||||
|
<p className="mb-3 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-xs leading-relaxed text-slate-600">
|
||||||
|
{PLACE_TOOL_HINTS[tool]}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{/* UX audit 2026-09-22: ยางลบลบได้เฉพาะเส้น/รูปทรง/ตรายางของตัวเอง (สเปก F6 เดิม
|
||||||
|
ไม่ขยายตรรกะ) — เดิมลากผ่านไฮไลต์/รายการของผู้อื่นแล้วเงียบเปล่า ผู้ใช้สรุป
|
||||||
|
ว่าแอปไม่ทำงาน จึงบอกขอบเขตตรง ๆ ตอนใช้ tool */}
|
||||||
|
{tool === "erase" && (
|
||||||
|
<p className="mb-3 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-xs leading-relaxed text-slate-600">
|
||||||
|
ยางลบลบได้เฉพาะเส้น รูปทรง และตรายางของตัวเองเท่านั้น — ไฮไลต์และรายการอื่น ๆ
|
||||||
|
(รวมของผู้อื่นทุกชนิด) ให้ใช้เครื่องมือ "เลือก" คลิกรายการแล้วกดปุ่มลบ
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{pageSizes.length === 0 ? (
|
{pageSizes.length === 0 ? (
|
||||||
loadError ? (
|
loadError ? (
|
||||||
<div className="flex h-[70vh] items-center justify-center px-6">
|
<div className="flex h-[70vh] items-center justify-center px-6">
|
||||||
|
|
@ -3793,7 +3855,7 @@ export default function PDFViewer({
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-[70vh] items-center justify-center text-sm text-slate-400">
|
<div className="flex h-[70vh] items-center justify-center text-sm text-slate-500">
|
||||||
กำลังโหลดเอกสาร…
|
กำลังโหลดเอกสาร…
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import { Check, ChevronDown, ChevronUp, Highlighter, Strikethrough, Underline }
|
||||||
|
|
||||||
import type { HighlightStyle } from "@/db/schema";
|
import type { HighlightStyle } from "@/db/schema";
|
||||||
|
|
||||||
|
import { colorName } from "@/utils/annotation-display";
|
||||||
|
|
||||||
// ตัวควบคุมที่ใช้ร่วมระหว่าง dropdown บนปุ่ม tool (PDFViewer) กับแผงเครื่องมือ
|
// ตัวควบคุมที่ใช้ร่วมระหว่าง dropdown บนปุ่ม tool (PDFViewer) กับแผงเครื่องมือ
|
||||||
// บนกรอบ selection (AnnotationSelectionLayer) — หน้าตา/พฤติกรรมเดียวกันทั้งสองที่
|
// บนกรอบ selection (AnnotationSelectionLayer) — หน้าตา/พฤติกรรมเดียวกันทั้งสองที่
|
||||||
|
|
||||||
|
|
@ -53,8 +55,8 @@ export function ColorSwatches({ value, onChange }: { value: string; onChange: (c
|
||||||
<button
|
<button
|
||||||
key={c}
|
key={c}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={`เลือกสี ${c}`}
|
aria-label={`เลือกสี ${colorName(c)}`}
|
||||||
title={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-slate-300 transition ${FOCUS_RING_CLS} ${
|
||||||
value === c ? "ring-2 ring-primary ring-offset-1" : "hover:scale-110"
|
value === c ? "ring-2 ring-primary ring-offset-1" : "hover:scale-110"
|
||||||
}`}
|
}`}
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,8 @@ const COLOR_NAMES: Record<string, string> = {
|
||||||
"#b91c1c": "แดงเข้ม",
|
"#b91c1c": "แดงเข้ม",
|
||||||
};
|
};
|
||||||
|
|
||||||
function colorName(c: string): string {
|
/** export ให้ ToolControls ใช้ทำ aria-label/title ของปุ่มสีเป็นชื่อไทยด้วย (UX audit 2026-09-22) */
|
||||||
|
export function colorName(c: string): string {
|
||||||
return COLOR_NAMES[c.toLowerCase()] ?? c;
|
return COLOR_NAMES[c.toLowerCase()] ?? c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue