UX audit ทั้งแอป + typography audit 2026-09-16: toast แจ้ง error การบันทึก (H1 — เดิมรายการหายเงียบ + ReplyBox เก็บ draft ตอน fail), คำเตือน redaction แสดงทุกขนาดจอ (H2), toolbar แสดงชื่อ tool active + aria-pressed บนจอสัมผัส (H3/M9), ปุ่ม popover จิ้มง่าย 36px + focus ring เติมครบ (H4) + M1-M9 (ปิด sidebar ตอน navigate, bookmark หัวแถวจอเล็ก, hint โหมดมาร์กบนจอสัมผัส, การ์ดหน้ารายการคลิกได้ทั้งใบ, เช็ค 100MB ฝั่ง client, feedback ✓/pending ตอนบันทึก bookmark, dropdown วางที่ sort ตามหน้า+เลขหน้า, slate-500 แทน slate-400) + webfont Sarabun ผ่าน next/font self-host 400/600 (Option B — แสดงผลตรงกันทุกอุปกรณ์ รวม TEXTBOX ที่ผูก stack นี้) + NumberCombo preset เป็น chips แทน dropdown ล้นเมนู (L2) + leading-relaxed/13px ข้อความไทยที่ wrap + tabular-nums + prefers-reduced-motion (L7) + L1/L4/L5 + แถว bookmark แสดงชื่อผู้บันทึกเหมือนผลการค้นหา
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
a020e96bda
commit
21b9958600
10 changed files with 351 additions and 158 deletions
|
|
@ -126,8 +126,9 @@ function isDescendantOf(id: string, ancestorId: string, byId: Map<string, Bookma
|
|||
}
|
||||
|
||||
/** N5: เรียงตามลำดับหน้า — หน้า → ตำแหน่งบนหน้า (บน→ล่าง) → เวลาสร้าง (tiebreak ให้ deterministic)
|
||||
* sort ตอน render ฝั่ง client เท่านั้น — realtime แทรก row ต่อท้าย state เชื่อลำดับจาก server อย่างเดียวไม่ได้ */
|
||||
function byPageOrder(a: BookmarkDTO, b: BookmarkDTO): number {
|
||||
* sort ตอน render ฝั่ง client เท่านั้น — realtime แทรก row ต่อท้าย state เชื่อลำดับจาก server อย่างเดียวไม่ได้
|
||||
* (export ให้ PDFViewer ใช้กับ dropdown "วางที่" ของ popup bookmark ด้วย — M7 UX audit 2026-09-16) */
|
||||
export function byPageOrder(a: BookmarkDTO, b: BookmarkDTO): number {
|
||||
return (
|
||||
a.pageIndex - b.pageIndex ||
|
||||
a.yOffset - b.yOffset ||
|
||||
|
|
@ -747,14 +748,16 @@ export default function AnnotationSidebar({
|
|||
// (server ตรวจซ้ำอีกชั้นพร้อม lock — ตรงเกณฑ์เดียวกัน)
|
||||
// FIX-11: คำนวณเฉพาะตอนเปิดแก้หัวข้อนี้ — ไล่ subtree ทุก node ทุก render เปลืองเปล่า
|
||||
const movableTo = editing
|
||||
? bookmarks.filter(
|
||||
(t) =>
|
||||
t.id !== b.id &&
|
||||
!isDescendantOf(t.id, b.id, bookmarkById) &&
|
||||
bookmarkDepth(t, bookmarkById) +
|
||||
subtreeHeight(b, (id) => bookmarks.filter((x) => x.parentId === id)) <=
|
||||
3,
|
||||
)
|
||||
? bookmarks
|
||||
.filter(
|
||||
(t) =>
|
||||
t.id !== b.id &&
|
||||
!isDescendantOf(t.id, b.id, bookmarkById) &&
|
||||
bookmarkDepth(t, bookmarkById) +
|
||||
subtreeHeight(b, (id) => bookmarks.filter((x) => x.parentId === id)) <=
|
||||
3,
|
||||
)
|
||||
.sort(byPageOrder) // M7: เรียงตามลำดับหน้าเหมือนต้นไม้ — หาหัวข้อแม่ใน select ง่ายขึ้น
|
||||
: [];
|
||||
// พื้นหลัง hover อยู่ที่ div แถวรายการ ไม่ใช่ li — li ครอบ <ul> ลูกทั้งก้อน
|
||||
// ทำให้ :hover ของแม่ทำงานด้วยเมื่อชี้รายการลูก (ไฮไลต์ทั้ง subtree เป็นก้อนเดียว)
|
||||
|
|
@ -802,7 +805,7 @@ export default function AnnotationSidebar({
|
|||
<option value="">ระดับบนสุด (หมุดหัวข้อใหญ่)</option>
|
||||
{movableTo.map((h) => (
|
||||
<option key={h.id} value={h.id}>
|
||||
ใต้ "{h.title}"
|
||||
ใต้ "{h.title}" (หน้า {h.pageIndex + 1})
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
|
@ -855,12 +858,16 @@ export default function AnnotationSidebar({
|
|||
<span className="min-w-0">
|
||||
<span
|
||||
className={`block truncate text-[13px] leading-snug ${
|
||||
depth === 1 ? "font-medium text-slate-700" : ""
|
||||
depth === 1 ? "font-semibold text-slate-700" : ""
|
||||
}`}
|
||||
>
|
||||
{b.title}
|
||||
</span>
|
||||
<span className="block text-xs text-slate-500">หน้า {b.pageIndex + 1}</span>
|
||||
{/* แสดงผู้บันทึกด้วย — เหมือนแถวผลการค้นหา (N6) และแถวหมายเหตุ
|
||||
(แถวเดิมมีแค่ "หน้า N" ไม่รู้ว่าใครเป็นคนคั่นไว้) */}
|
||||
<span className="block text-xs text-slate-500">
|
||||
หน้า {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">
|
||||
{b.note}
|
||||
|
|
@ -870,7 +877,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] leading-4 text-slate-500">
|
||||
<span className="shrink-0 self-center rounded-full bg-slate-100 px-1.5 text-[10px] tabular-nums leading-4 text-slate-500">
|
||||
{children.length}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -1046,7 +1053,7 @@ export default function AnnotationSidebar({
|
|||
type="button"
|
||||
role="tab"
|
||||
aria-selected={tab === key}
|
||||
className={`flex-1 px-2 py-2 text-xs font-medium transition ${FOCUS_RING_CLS} ${
|
||||
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"
|
||||
|
|
@ -1252,7 +1259,7 @@ export default function AnnotationSidebar({
|
|||
bmResults !== null ? (
|
||||
<div>
|
||||
<div className="flex items-center justify-between px-1 pb-1">
|
||||
<p className="text-[11px] font-semibold text-slate-500">
|
||||
<p className="text-[11px] font-semibold tabular-nums text-slate-500">
|
||||
ผลการค้นหา ({bmResults.length})
|
||||
</p>
|
||||
<button
|
||||
|
|
@ -1358,6 +1365,11 @@ export default function AnnotationSidebar({
|
|||
<span className="block truncate text-[13px] leading-snug font-medium text-slate-700">
|
||||
{b.title}
|
||||
</span>
|
||||
{/* ผู้บันทึกเหมือนแถวผลการค้นหา — ไม่ใส่ "หน้า N"
|
||||
เพราะหัวกลุ่มของมุมมองนี้บอกหน้าอยู่แล้ว */}
|
||||
<span className="block text-xs text-slate-500">
|
||||
{b.userName || "ไม่ทราบชื่อ"}
|
||||
</span>
|
||||
{b.note && (
|
||||
<span className="mt-0.5 block line-clamp-2 whitespace-pre-wrap break-words text-[11px] text-slate-500">
|
||||
{b.note}
|
||||
|
|
@ -1365,7 +1377,7 @@ export default function AnnotationSidebar({
|
|||
)}
|
||||
</span>
|
||||
{childCount > 0 && (
|
||||
<span className="ml-auto shrink-0 self-center rounded-full bg-slate-100 px-1.5 text-[10px] leading-4 text-slate-500">
|
||||
<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">
|
||||
{childCount}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -1392,7 +1404,7 @@ export default function AnnotationSidebar({
|
|||
) : visibleSearchResults !== null ? (
|
||||
<div>
|
||||
<div className="flex items-center justify-between px-1 pb-1">
|
||||
<p className="text-[11px] font-semibold text-slate-500">
|
||||
<p className="text-[11px] font-semibold tabular-nums text-slate-500">
|
||||
ผลการค้นหา ({visibleSearchResults.length})
|
||||
</p>
|
||||
<button
|
||||
|
|
@ -1482,12 +1494,16 @@ 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">
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/40 p-4"
|
||||
onClick={() => setHistoryFor(null)}
|
||||
>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="ประวัติการเปลี่ยนแปลง"
|
||||
className="w-80 rounded-lg border border-slate-200 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">
|
||||
|
|
@ -1574,14 +1590,14 @@ export default function AnnotationSidebar({
|
|||
<div className="mt-1.5 flex justify-end gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
className={`rounded px-2 py-1 text-[11px] text-slate-500 hover:bg-slate-100 ${FOCUS_RING_CLS}`}
|
||||
className={`rounded px-2 py-1 text-xs text-slate-500 hover:bg-slate-100 ${FOCUS_RING_CLS}`}
|
||||
onClick={closeRowMenu}
|
||||
>
|
||||
ยกเลิก
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`rounded bg-red-600 px-2 py-1 text-[11px] font-medium text-white hover:bg-red-700 ${FOCUS_RING_CLS}`}
|
||||
className={`rounded bg-red-600 px-2 py-1 text-xs font-medium text-white hover:bg-red-700 ${FOCUS_RING_CLS}`}
|
||||
onClick={() => {
|
||||
// อ่าน kind/id ก่อน close — setMenuFor(null) แล้วเงื่อนไขเดิมหาย
|
||||
if (menuFor.kind === "annotation") onDeleteAnnotation(menuFor.id);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue