summary: หน้าสรุป design "แฟ้มสำนวน" + แก้ตาม design review 2026-09-17 — การ์ดรายหน้า 1 หน้า 1 การ์ด + สารบัญมือถือ + ghost buttons
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 4s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 46s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 4s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 1s
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 4s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 46s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 4s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 1s
งานหลักจาก design review (H/M/L ทั้งหมดยกเว้น L5 ที่จบเอง — หัวข้อ Explanation ถูกถอดไปก่อนหน้านี้ตามคำขอผู้ใช้ พร้อม icon ℹ + เส้น):
(1) H1 กฎ coarse 44px ครอบ button ด้วย (main.summary-page button min-height 2.75rem) — ปุ่มหุบ/กาง CollapsibleBlock เดิมหลุดกฎเพราะเป็น <button> ไม่ใช่ <a> สูงจริง ~29px
(2) H2 สารบัญมือถือ (< lg) — <details> การ์ด "สารบัญและผู้ร่วมทำ" จุดไข่ปลา + ผู้ร่วมทำภาษาเดียวกับ desktop zero JS (ได้ keyboard + screen reader ฟรี) แทนที่เดิมมือถือไม่มีทางกระโดดหน้าเลย
(3) H3 typography ขยายเพื่อผู้ใช้สูงวัย + อักษรไทย — เนื้อหาแถวรายการ text-[13px]→text-sm, โน้ต/badge text-[11px]→text-xs, บรรทัดสรุปหน้า→text-sm, "หน้า" text-[10px]→text-[11px], แถบ action→text-sm (CollapsibleBlock รวม)
(4) M1 aria-label anchor สารบัญ "หน้า N, X รายการ" กัน screen reader อ่านเลขติดกัน / M2 sticky aside max-h overflow (เอกสารหลายสิบหน้าครึ่งล่างเคยกดไม่ได้ตลอดกาล) / M3 ปุ่ม "เปิดหน้านี้" แสดงตลอดไม่ซ่อนบนมือถือ / M4 "ส่งออก CSV"→"ส่งออกสรุป (CSV)" คู่กับ "ส่งออก Bookmark (JSON)" (คง terminology อังกฤษตาม b1bc65e) ถอด title tooltip ที่จอสัมผัสใช้ไม่ได้ / M5 generateMetadata ชื่อแท็บ browser "สรุปหมายเหตุ — ชื่อคดี | Judge's Note"
(5) L2 CollapsibleBlock เพิ่ม useId + aria-controls/id ครบ disclosure pattern / L3 empty state CTA ใช้ RING_CLS ร่วม / L4 @media print (globals.css): header แปลงกรอบเส้นน้ำเงิน #0033a1 กัน text ขาวบนกระดาษขาว + break-inside ไม่ผ่าการ์ดกลางหน้ากระดาษ + ซ่อนแถบ action/สารบัญมือถือ / L1 แก้ comment หัวไฟล์ให้ตรงความจริง (มี client component เดียวคือ CollapsibleBlock)
ปรับตามคำขอผู้ใช้ระหว่างทาง (2026-09-17): เนื้อหาเปลี่ยนจากแผ่นกระดาษแผ่นเดียวคั่นเส้นเป็นการ์ดรายหน้า 1 หน้า 1 การ์ด (flex flex-col gap-4) / สารบัญ desktop เป็นการ์ด sticky / action bar เปลี่ยนลิงก์ข้อความเปล่าเป็น ghost button ขาวมีขอบ ภาษาเดียวกับการ์ด
งานคู่ขนานจากอีก session ในไฟล์ชุดนี้: แยก TypeIcon ออกจาก sidebar เป็น components/AnnotationTypeIcon.tsx (ไม่มี "use client") ใช้ร่วม sidebar + หน้าสรุป + ถอดหัวข้อคำอธิบายออกจากหน้าสรุป
CLAUDE.md sync โครงสร้าง/พฤติกรรมหน้าสรุปล่าสุด
verify: tsc strict ผ่าน + vitest 139/139 ผ่าน
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
b1bc65e65d
commit
f9114b3f00
6 changed files with 570 additions and 114 deletions
58
components/AnnotationTypeIcon.tsx
Normal file
58
components/AnnotationTypeIcon.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import {
|
||||
ArrowRight,
|
||||
Circle,
|
||||
EyeOff,
|
||||
Highlighter,
|
||||
MessageSquare,
|
||||
Minus,
|
||||
Pencil,
|
||||
Square,
|
||||
Stamp,
|
||||
Strikethrough,
|
||||
Type,
|
||||
Underline,
|
||||
} from "lucide-react";
|
||||
|
||||
import type {
|
||||
AnnotationDTO,
|
||||
DrawData,
|
||||
HighlightData,
|
||||
ShapeData,
|
||||
} from "@/db/schema";
|
||||
|
||||
/** ไอคอนตามชนิด annotation — presentational ล้วน **ไม่มี "use client"** ให้หน้าสรุป
|
||||
* (server component, F15) ใช้ร่วมกับ sidebar (client) ได้: lucide icon เป็น SVG
|
||||
* component ล้วน render ฝั่ง server ได้ (ย้ายมาจาก AnnotationSidebar.tsx) */
|
||||
export function AnnotationTypeIcon({
|
||||
type,
|
||||
data,
|
||||
className = "h-4 w-4 shrink-0 text-slate-500",
|
||||
}: {
|
||||
type: AnnotationDTO["type"];
|
||||
data: AnnotationDTO["data"];
|
||||
className?: string;
|
||||
}) {
|
||||
if (type === "HIGHLIGHT") {
|
||||
const style = (data as HighlightData).style ?? "highlight";
|
||||
if (style === "underline") return <Underline className={className} />;
|
||||
if (style === "strikethrough") return <Strikethrough className={className} />;
|
||||
return <Highlighter className={className} />; // highlight / squiggle
|
||||
}
|
||||
if (type === "DRAW")
|
||||
return (data as DrawData).pen === "highlighter" ? (
|
||||
<Highlighter className={className} />
|
||||
) : (
|
||||
<Pencil className={className} />
|
||||
);
|
||||
if (type === "REDACTION") return <EyeOff className={className} />;
|
||||
if (type === "TEXTBOX") return <Type className={className} />;
|
||||
if (type === "STAMP") return <Stamp className={className} />;
|
||||
if (type === "SHAPE") {
|
||||
const shape = (data as ShapeData).shape;
|
||||
if (shape === "rect") return <Square className={className} />;
|
||||
if (shape === "ellipse") return <Circle className={className} />;
|
||||
if (shape === "line") return <Minus className={className} />;
|
||||
return <ArrowRight className={className} />;
|
||||
}
|
||||
return <MessageSquare className={className} />;
|
||||
}
|
||||
|
|
@ -3,35 +3,25 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState, type FormEvent } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import {
|
||||
ArrowRight,
|
||||
Bookmark,
|
||||
Braces,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Circle,
|
||||
Clock,
|
||||
Copy,
|
||||
EyeOff,
|
||||
Highlighter,
|
||||
MessageSquare,
|
||||
Minus,
|
||||
MoreHorizontal,
|
||||
Pencil,
|
||||
Plus,
|
||||
Search,
|
||||
SlidersHorizontal,
|
||||
Square,
|
||||
Stamp,
|
||||
StickyNote,
|
||||
Trash2,
|
||||
Type,
|
||||
Underline,
|
||||
Strikethrough,
|
||||
X,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import { AnnotationTypeIcon } from "../AnnotationTypeIcon";
|
||||
import Select from "../Select";
|
||||
|
||||
import {
|
||||
|
|
@ -46,10 +36,7 @@ import type {
|
|||
AnnotationDTO,
|
||||
BookmarkDTO,
|
||||
CommentData,
|
||||
DrawData,
|
||||
HighlightData,
|
||||
RedactionData,
|
||||
ShapeData,
|
||||
} from "@/db/schema";
|
||||
import {
|
||||
annotationNote,
|
||||
|
|
@ -69,33 +56,6 @@ import { formatThaiDate } from "@/utils/date-format";
|
|||
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";
|
||||
|
||||
function TypeIcon({ type, data }: { type: AnnotationDTO["type"]; data: AnnotationDTO["data"] }) {
|
||||
const cls = "h-4 w-4 shrink-0 text-slate-500";
|
||||
if (type === "HIGHLIGHT") {
|
||||
const style = (data as HighlightData).style ?? "highlight";
|
||||
if (style === "underline") return <Underline className={cls} />;
|
||||
if (style === "strikethrough") return <Strikethrough className={cls} />;
|
||||
return <Highlighter className={cls} />; // highlight / squiggle
|
||||
}
|
||||
if (type === "DRAW")
|
||||
return (data as DrawData).pen === "highlighter" ? (
|
||||
<Highlighter className={cls} />
|
||||
) : (
|
||||
<Pencil className={cls} />
|
||||
);
|
||||
if (type === "REDACTION") return <EyeOff className={cls} />;
|
||||
if (type === "TEXTBOX") return <Type className={cls} />;
|
||||
if (type === "STAMP") return <Stamp className={cls} />;
|
||||
if (type === "SHAPE") {
|
||||
const shape = (data as ShapeData).shape;
|
||||
if (shape === "rect") return <Square className={cls} />;
|
||||
if (shape === "ellipse") return <Circle className={cls} />;
|
||||
if (shape === "line") return <Minus className={cls} />;
|
||||
return <ArrowRight className={cls} />;
|
||||
}
|
||||
return <MessageSquare className={cls} />;
|
||||
}
|
||||
|
||||
/** ใส่/ล้างโน้ตคำอธิบายใน data ของ annotation (ทุกชนิดยกเว้น COMMENT — message ทำหน้าที่นี้แล้ว) */
|
||||
function withNote(data: Exclude<AnnotationData, CommentData>, note: string): AnnotationData {
|
||||
return note ? { ...data, note } : { ...data, note: undefined };
|
||||
|
|
@ -952,7 +912,7 @@ export default function AnnotationSidebar({
|
|||
onGoToAnnotation(a);
|
||||
}}
|
||||
>
|
||||
<TypeIcon type={a.type} data={a.data} />
|
||||
<AnnotationTypeIcon type={a.type} data={a.data} />
|
||||
<span className="min-w-0">
|
||||
{/* UX audit: อักษรไทยมีสระ/วรรณยุกต์ซ้อนหลายชั้น — ข้อความหลักต้อง 13px
|
||||
จึงอ่านได้สบาย (บรรทัดรอง/โน้ตคง 11px) */}
|
||||
|
|
|
|||
58
components/summary/CollapsibleBlock.tsx
Normal file
58
components/summary/CollapsibleBlock.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
"use client";
|
||||
|
||||
import { useId, useState, type ReactNode } from "react";
|
||||
import { Bookmark, ChevronDown, ChevronRight, PenLine } from "lucide-react";
|
||||
|
||||
/** หุบ/กางกลุ่มรายการ (Bookmark/Annotation) ในหน้าสรุป — client component ตัวเดียว
|
||||
* ของหน้า: เก็บแค่ state หุบ/กางต่อบล็อก เริ่มกางไว้เสมอ (เปิดหน้าสรุปครั้งแรกเห็น
|
||||
* ครบ ผู้ใช้พับเฉพาะกลุ่มที่ไม่สนใจ) — รายการ (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" },
|
||||
} as const;
|
||||
|
||||
export default function CollapsibleBlock({
|
||||
kind,
|
||||
count,
|
||||
children,
|
||||
}: {
|
||||
kind: keyof typeof HEADS;
|
||||
count: number;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [open, setOpen] = useState(true);
|
||||
// design review 2026-09-17 L2: aria-controls ชี้ id ของก้อนเนื้อหา (useId
|
||||
// กันชนกันระหว่างหลายหน้าเอกสารที่ render บล็อกนี้ซ้ำ)
|
||||
const contentId = useId();
|
||||
const { label, Icon, iconCls } = HEADS[kind];
|
||||
return (
|
||||
<div>
|
||||
{/* h3 ครอบ button = disclosure pattern มาตรฐาน (heading level คงเดิม) */}
|
||||
<h3>
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
{open ? (
|
||||
<ChevronDown 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-slate-400" />
|
||||
)}
|
||||
<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" />
|
||||
</button>
|
||||
</h3>
|
||||
{open && (
|
||||
<div id={contentId} className="pl-5">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue