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
|
|
@ -11,6 +11,7 @@ import { revalidatePath } from "next/cache";
|
|||
|
||||
import { db } from "@/db";
|
||||
import { files } from "@/db/schema";
|
||||
import { normDocumentGroup } from "@/utils/external-file";
|
||||
import { SAMPLE_FILE_EXTENSIONS } from "@/utils/file-kind";
|
||||
|
||||
const MAX_UPLOAD_BYTES = 100 * 1024 * 1024; // 100MB (ตาม bodySizeLimit ใน next.config.mjs)
|
||||
|
|
@ -94,8 +95,9 @@ export async function uploadFile(formData: FormData): Promise<UploadFileResult>
|
|||
|
||||
const title = sanitizeTitle(file.name);
|
||||
const pdfUrl = `/uploads/${diskName}`;
|
||||
const typeId = normDocumentGroup(formData.get("typeId"));
|
||||
try {
|
||||
await db.insert(files).values({ id, title, pdfUrl });
|
||||
await db.insert(files).values({ id, title, pdfUrl, typeId });
|
||||
} catch (err) {
|
||||
// DB fail หลังเขียนไฟล์ — เก็บกวาดไฟล์ก่อนโยนต่อ กันไฟล์กำพร้าไม่มี row ชี้ (ไม่มีกลไกเก็บอื่น)
|
||||
await unlink(diskPath).catch(() => undefined);
|
||||
|
|
|
|||
|
|
@ -84,11 +84,10 @@ function pageSummaryLine(b: PageBucket): string {
|
|||
return parts.length > 0 ? `${parts.join(" · ")} · โดย ${whoLabel}` : `โดย ${whoLabel}`;
|
||||
}
|
||||
|
||||
/** focus ring ร่วมของทุกลิงก์ในหน้า — จอสัมผัสได้ 44px อัตโนมัติจากกฎ
|
||||
* `main.summary-page a`/`button` ใน globals.css (ห้ามถอด class summary-page
|
||||
* ของ <main>) */
|
||||
const RING_CLS =
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1";
|
||||
/** focus ring ร่วมของทุกลิงก์ในหน้า — Genesis .focus-ring (outline น้ำเงินทึบ 2px
|
||||
* + offset ตาม spec Elevation); จอสัมผัสได้ 44px อัตโนมัติจากกฎ
|
||||
* `main.summary-page a`/`button` ใน globals.css (ห้ามถอด class summary-page ของ <main>) */
|
||||
const RING_CLS = "focus-ring";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
|
|
@ -177,7 +176,7 @@ export default async function SummaryPage({
|
|||
<main className="summary-page mx-auto max-w-3xl px-4 py-6 lg:max-w-6xl lg:px-8">
|
||||
{/* หัวกระดาษสำนวน — แถบน้ำเงินสืบต่อจาก tile JN ของแบรนด์ (หน้ารายการ/login):
|
||||
ชื่อรายงาน + ชื่อแฟ้ม + สถิติในที่เดียว แล้วปิดท้ายด้วยเส้นคั่นบาง */}
|
||||
<header className="rounded-xl bg-primary px-5 py-4 text-white shadow-sm sm:px-6">
|
||||
<header className="rounded-xl bg-primary px-5 py-4 text-white sm:px-6">
|
||||
<h1 className="text-lg font-semibold leading-snug">สรุปหมายเหตุ</h1>
|
||||
<p className="mt-0.5 break-words text-sm text-white/85">{file.title}</p>
|
||||
{pages.length > 0 && (
|
||||
|
|
@ -194,7 +193,7 @@ export default async function SummaryPage({
|
|||
<div className="mt-3 flex flex-wrap items-center justify-between gap-x-2 gap-y-1 print:hidden">
|
||||
<Link
|
||||
href={`/annotate/${id}`}
|
||||
className={`inline-flex items-center gap-1.5 rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-sm text-slate-700 shadow-sm hover:border-slate-300 hover:bg-slate-50 ${RING_CLS}`}
|
||||
className={`inline-flex items-center gap-1.5 rounded-md border border-line bg-white px-3 py-1.5 text-sm text-ink hover:border-ink-faint hover:bg-ink/[0.03] ${RING_CLS}`}
|
||||
>
|
||||
<ArrowLeft aria-hidden className="h-3.5 w-3.5" />
|
||||
กลับหน้าอ่าน
|
||||
|
|
@ -203,7 +202,7 @@ export default async function SummaryPage({
|
|||
{/* F16/D3: export สรุปเป็น CSV (UTF-8 BOM) */}
|
||||
<a
|
||||
href={withBasePath(`/api/annotations/export-csv?fileId=${id}`)}
|
||||
className={`flex items-center gap-1.5 rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-sm text-slate-700 shadow-sm hover:border-slate-300 hover:bg-slate-50 ${RING_CLS}`}
|
||||
className={`flex items-center gap-1.5 rounded-md border border-line bg-white px-3 py-1.5 text-sm text-ink hover:border-ink-faint hover:bg-ink/[0.03] ${RING_CLS}`}
|
||||
>
|
||||
<FileDown aria-hidden className="h-3.5 w-3.5" />
|
||||
ส่งออกสรุป (CSV)
|
||||
|
|
@ -212,7 +211,7 @@ export default async function SummaryPage({
|
|||
"ส่งออกสรุป (CSV)" ข้างบน ไม่ต้องพึ่ง title tooltip ที่จอสัมผัสใช้ไม่ได้) */}
|
||||
<a
|
||||
href={withBasePath(`/api/bookmarks/export?fileId=${id}`)}
|
||||
className={`flex items-center gap-1.5 rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-sm text-slate-700 shadow-sm hover:border-slate-300 hover:bg-slate-50 ${RING_CLS}`}
|
||||
className={`flex items-center gap-1.5 rounded-md border border-line bg-white px-3 py-1.5 text-sm text-ink hover:border-ink-faint hover:bg-ink/[0.03] ${RING_CLS}`}
|
||||
>
|
||||
<Braces aria-hidden className="h-3.5 w-3.5" />
|
||||
ส่งออก Bookmark (JSON)
|
||||
|
|
@ -221,17 +220,17 @@ export default async function SummaryPage({
|
|||
</div>
|
||||
|
||||
{pages.length === 0 ? (
|
||||
<div className="mt-5 rounded-xl border border-dashed border-slate-300 bg-white/70 p-10 text-center">
|
||||
<ClipboardList aria-hidden className="mx-auto h-8 w-8 text-slate-300" />
|
||||
<p className="mt-3 text-sm font-medium text-slate-600">
|
||||
<div className="mt-5 rounded-xl border border-dashed border-line bg-white/70 p-10 text-center">
|
||||
<ClipboardList aria-hidden className="mx-auto h-8 w-8 text-ink-faint" />
|
||||
<p className="mt-3 text-sm font-medium text-ink-soft">
|
||||
ยังไม่มี Annotation หรือ Bookmark ในไฟล์นี้
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-slate-500">
|
||||
<p className="mt-1 text-xs text-ink-soft">
|
||||
เปิดหน้าอ่านเพื่อมาร์กข้อความ วาดเส้น ใส่คอมเมนต์ หรือคั่นหน้า
|
||||
</p>
|
||||
<Link
|
||||
href={`/annotate/${id}`}
|
||||
className={`mt-4 inline-flex rounded-md bg-primary px-4 py-2 text-sm font-medium text-white hover:opacity-90 ${RING_CLS}`}
|
||||
className={`mt-4 inline-flex 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 ${RING_CLS}`}
|
||||
>
|
||||
เปิดหน้าอ่าน
|
||||
</Link>
|
||||
|
|
@ -242,37 +241,37 @@ export default async function SummaryPage({
|
|||
ไปบนมือถือ (aside ด้านล่างแสดงเฉพาะ lg) ใช้ <details> ได้หุบ/กาง + keyboard
|
||||
โดยไม่มี JS เพิ่ม — จุดไข่ปลา + ผู้ร่วมทำ ภาษาเดียวกับ desktop (จอ lg กับ
|
||||
ตอนพิมพ์ซ่อน — display:none ไม่กินช่องของกริด) */}
|
||||
<details className="group rounded-xl border border-slate-200 bg-white shadow-sm lg:hidden print:hidden">
|
||||
<summary className="flex cursor-pointer list-none items-center justify-between rounded-md px-4 py-3 text-sm font-semibold text-slate-800 hover:text-primary [&::-webkit-details-marker]:hidden">
|
||||
<details className="group rounded-xl border border-line bg-white lg:hidden print:hidden">
|
||||
<summary className="flex cursor-pointer list-none items-center justify-between rounded-md px-4 py-3 text-sm font-semibold text-ink hover:text-primary [&::-webkit-details-marker]:hidden">
|
||||
รายการตามหน้าและผู้ร่วมทำ
|
||||
<ChevronDown
|
||||
aria-hidden
|
||||
className="h-4 w-4 shrink-0 text-slate-400 transition-transform group-open:rotate-180 motion-reduce:transition-none"
|
||||
className="h-4 w-4 shrink-0 text-ink-faint transition-transform group-open:rotate-180 motion-reduce:transition-none"
|
||||
/>
|
||||
</summary>
|
||||
<nav
|
||||
aria-label="รายการตามหน้า"
|
||||
className="flex flex-col gap-0.5 border-t border-slate-100 px-2 pb-2 pt-2"
|
||||
className="flex flex-col gap-0.5 border-t border-line/70 px-2 pb-2 pt-2"
|
||||
>
|
||||
{pages.map(([pi, b]) => (
|
||||
<a
|
||||
key={pi}
|
||||
href={`#page-${pi + 1}`}
|
||||
aria-label={`หน้า ${pi + 1}, ${b.anns.length + b.bms.length} รายการ`}
|
||||
className={`flex items-end gap-1.5 rounded-md px-2 py-2 text-sm text-slate-600 hover:text-primary ${RING_CLS}`}
|
||||
className={`flex items-end gap-1.5 rounded-md px-2 py-2 text-sm text-ink-soft hover:text-primary ${RING_CLS}`}
|
||||
>
|
||||
<span className="leading-5">หน้า {pi + 1}</span>
|
||||
<span
|
||||
aria-hidden
|
||||
className="mb-1.5 min-w-4 flex-1 border-b border-dotted border-slate-400"
|
||||
className="mb-1.5 min-w-4 flex-1 border-b border-dotted border-ink-faint"
|
||||
/>
|
||||
<span className="leading-5 tabular-nums text-slate-500">
|
||||
<span className="leading-5 tabular-nums text-ink-soft">
|
||||
{b.anns.length + b.bms.length}
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
<p className="border-t border-slate-100 px-4 py-3 text-xs leading-relaxed text-slate-600">
|
||||
<p className="border-t border-line/70 px-4 py-3 text-xs leading-relaxed text-ink-soft">
|
||||
ผู้ร่วมทำ: {allParticipants.join(" · ")}
|
||||
</p>
|
||||
</details>
|
||||
|
|
@ -281,32 +280,32 @@ export default async function SummaryPage({
|
|||
เป็นการ์ดชุดเดียวกับ <details> มือถือและการ์ดรายหน้า (แสดงเฉพาะหน้าที่มี
|
||||
รายการ ≥ 1 ตามหลัก minimize choices; max-h กันเอกสารหลายสิบหน้าสูงเกินจอ
|
||||
— ครึ่งล่างของ sticky กดไม่ได้ตลอดกาล) */}
|
||||
<aside className="hidden rounded-xl border border-slate-200 bg-white p-4 shadow-sm lg:sticky lg:top-6 lg:block lg:max-h-[calc(100vh-3rem)] lg:overflow-y-auto">
|
||||
<h2 className="text-sm font-semibold text-slate-800">รายการตามหน้า</h2>
|
||||
<aside className="hidden rounded-xl border border-line bg-white p-4 lg:sticky lg:top-6 lg:block lg:max-h-[calc(100vh-3rem)] lg:overflow-y-auto">
|
||||
<h2 className="text-sm font-semibold text-ink">รายการตามหน้า</h2>
|
||||
<nav aria-label="รายการตามหน้า" className="mt-2 flex flex-col gap-1">
|
||||
{pages.map(([pi, b]) => (
|
||||
<a
|
||||
key={pi}
|
||||
href={`#page-${pi + 1}`}
|
||||
aria-label={`หน้า ${pi + 1}, ${b.anns.length + b.bms.length} รายการ`}
|
||||
className={`flex items-end gap-1.5 rounded-md px-1 text-sm text-slate-600 hover:text-primary ${RING_CLS}`}
|
||||
className={`flex items-end gap-1.5 rounded-md px-1 text-sm text-ink-soft hover:text-primary ${RING_CLS}`}
|
||||
>
|
||||
<span className="leading-5">หน้า {pi + 1}</span>
|
||||
<span
|
||||
aria-hidden
|
||||
className="mb-1.5 min-w-4 flex-1 border-b border-dotted border-slate-400"
|
||||
className="mb-1.5 min-w-4 flex-1 border-b border-dotted border-ink-faint"
|
||||
/>
|
||||
<span className="leading-5 tabular-nums text-slate-500">
|
||||
<span className="leading-5 tabular-nums text-ink-soft">
|
||||
{b.anns.length + b.bms.length}
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<h2 className="mt-6 border-t border-slate-300/60 pt-4 text-sm font-semibold text-slate-800">
|
||||
<h2 className="mt-6 border-t border-line pt-4 text-sm font-semibold text-ink">
|
||||
ผู้ร่วมทำ
|
||||
</h2>
|
||||
<p className="mt-1.5 px-1 text-xs leading-relaxed text-slate-600">
|
||||
<p className="mt-1.5 px-1 text-xs leading-relaxed text-ink-soft">
|
||||
{allParticipants.join(" · ")}
|
||||
</p>
|
||||
</aside>
|
||||
|
|
@ -322,15 +321,18 @@ export default async function SummaryPage({
|
|||
key={pi}
|
||||
id={`page-${pi + 1}`}
|
||||
aria-labelledby={`page-${pi + 1}-title`}
|
||||
className="scroll-mt-6 grid grid-cols-[2.75rem_1fr] rounded-xl border border-slate-200 bg-white shadow-sm sm:grid-cols-[3.75rem_1fr]"
|
||||
className="scroll-mt-6 grid grid-cols-[2.75rem_1fr] rounded-xl border border-line bg-white sm:grid-cols-[3.75rem_1fr]"
|
||||
>
|
||||
{/* ขอบ folio — "หน้า" เล็ก + เลขใหญ่ชิดเส้นดิ่ง */}
|
||||
<div className="border-r border-slate-100 py-4 pl-3 pr-2 text-right sm:pl-4">
|
||||
<div className="border-r border-line/60 py-4 pl-3 pr-2 text-right sm:pl-4">
|
||||
<h2 id={`page-${pi + 1}-title`} className="tabular-nums">
|
||||
<span className="block text-[11px] leading-none text-slate-500">
|
||||
<span className="block text-[11px] leading-none text-ink-soft">
|
||||
หน้า
|
||||
</span>
|
||||
<span className="mt-1 block text-2xl font-semibold leading-none text-primary">
|
||||
{/* เลขหน้าเป็น ink ไม่ใช่ primary — Genesis "น้ำเงินเฉพาะของ
|
||||
interactive" (ตัดสินใจพร้อม exception list ใน DESIGN.md
|
||||
2026-09-23) ความเด่นมาจากขนาด 2xl semibold tabular-nums */}
|
||||
<span className="mt-1 block text-2xl font-semibold leading-none text-ink">
|
||||
{pi + 1}
|
||||
</span>
|
||||
</h2>
|
||||
|
|
@ -341,7 +343,7 @@ export default async function SummaryPage({
|
|||
<div className="min-w-0 py-3 pl-3 pr-3 sm:pl-4 sm:pr-5 sm:py-4">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm leading-relaxed text-slate-600">
|
||||
<p className="text-sm leading-relaxed text-ink-soft">
|
||||
{pageSummaryLine(b)}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -358,7 +360,7 @@ export default async function SummaryPage({
|
|||
{b.bms.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<CollapsibleBlock kind="bookmark" count={b.bms.length}>
|
||||
<ul className="divide-y divide-slate-100">
|
||||
<ul className="divide-y divide-line/70">
|
||||
{b.bms.map((bm) => {
|
||||
const parent = bm.parentId
|
||||
? bookmarkById.get(bm.parentId)
|
||||
|
|
@ -367,25 +369,25 @@ export default async function SummaryPage({
|
|||
<li key={bm.id} className="ml-4">
|
||||
<Link
|
||||
href={`/annotate/${id}?page=${pi + 1}`}
|
||||
className={`group -mx-2 flex items-start gap-2 rounded-lg px-2 py-2.5 transition hover:bg-slate-50 ${RING_CLS}`}
|
||||
className={`group -mx-2 flex items-start gap-2 rounded-lg px-2 py-2.5 transition hover:bg-ink/[0.03] ${RING_CLS}`}
|
||||
>
|
||||
<Bookmark
|
||||
aria-hidden
|
||||
className="mt-1 h-4 w-4 shrink-0 text-amber-500"
|
||||
className="mt-1 h-4 w-4 shrink-0 text-warning-ink"
|
||||
/>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block whitespace-pre-wrap break-words text-sm font-medium leading-snug text-slate-800">
|
||||
<span className="block whitespace-pre-wrap break-words text-sm font-medium leading-snug text-ink">
|
||||
{bm.title}
|
||||
</span>
|
||||
<span className="mt-0.5 block truncate text-xs text-slate-500">
|
||||
<span className="mt-0.5 block truncate text-xs text-ink-soft">
|
||||
โดย {bm.userName || "ไม่ทราบชื่อ"}
|
||||
{parent ? ` · ใน: ${parent.title}` : ""}
|
||||
</span>
|
||||
{bm.note && (
|
||||
<span className="mt-0.5 flex items-start gap-1 text-xs leading-relaxed text-slate-500">
|
||||
<span className="mt-0.5 flex items-start gap-1 text-xs leading-relaxed text-ink-soft">
|
||||
<StickyNote
|
||||
aria-hidden
|
||||
className="mt-0.5 h-3 w-3 shrink-0 text-amber-400"
|
||||
className="mt-0.5 h-3 w-3 shrink-0 text-warning-ink"
|
||||
/>
|
||||
<span className="line-clamp-2 whitespace-pre-wrap break-words">
|
||||
{bm.note}
|
||||
|
|
@ -395,7 +397,7 @@ export default async function SummaryPage({
|
|||
</span>
|
||||
<ChevronRight
|
||||
aria-hidden
|
||||
className="mt-1.5 h-4 w-4 shrink-0 text-slate-300 group-hover:text-slate-400"
|
||||
className="mt-1.5 h-4 w-4 shrink-0 text-ink-faint group-hover:text-ink"
|
||||
/>
|
||||
</Link>
|
||||
</li>
|
||||
|
|
@ -410,35 +412,35 @@ export default async function SummaryPage({
|
|||
{b.anns.length > 0 && (
|
||||
<div className={b.bms.length > 0 ? "mt-3" : "mt-2"}>
|
||||
<CollapsibleBlock kind="annotation" count={b.anns.length}>
|
||||
<ul className="divide-y divide-slate-100">
|
||||
<ul className="divide-y divide-line/70">
|
||||
{b.anns.map((a) => (
|
||||
<li key={a.id} className="ml-4">
|
||||
<Link
|
||||
href={`/annotate/${id}?ann=${a.id}`}
|
||||
className={`group -mx-2 flex items-start gap-2 rounded-lg px-2 py-2.5 transition hover:bg-slate-50 ${RING_CLS}`}
|
||||
className={`group -mx-2 flex items-start gap-2 rounded-lg px-2 py-2.5 transition hover:bg-ink/[0.03] ${RING_CLS}`}
|
||||
>
|
||||
<AnnotationTypeIcon
|
||||
type={a.type}
|
||||
data={a.data}
|
||||
className="mt-1 h-4 w-4 shrink-0 text-slate-500"
|
||||
className="mt-1 h-4 w-4 shrink-0 text-ink-soft"
|
||||
/>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block whitespace-pre-wrap break-words text-sm leading-snug text-slate-800">
|
||||
<span className="block whitespace-pre-wrap break-words text-sm leading-snug text-ink">
|
||||
{annotationPreviewText(a)}
|
||||
{a.type === "COMMENT" && a.resolved && (
|
||||
<span className="ml-1.5 whitespace-nowrap rounded border border-emerald-600/50 bg-emerald-50 px-1.5 py-0.5 text-xs text-emerald-700">
|
||||
<span className="ml-1.5 whitespace-nowrap rounded border border-success/40 bg-success/10 px-1.5 py-0.5 text-xs font-medium text-success-ink">
|
||||
จัดการแล้ว
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="mt-0.5 block text-xs text-slate-500">
|
||||
<span className="mt-0.5 block text-xs text-ink-soft">
|
||||
{annotationTypeLabel(a)} · โดย {a.userName}
|
||||
</span>
|
||||
{annotationNote(a) && (
|
||||
<span className="mt-0.5 flex items-start gap-1 text-xs leading-relaxed text-slate-500">
|
||||
<span className="mt-0.5 flex items-start gap-1 text-xs leading-relaxed text-ink-soft">
|
||||
<StickyNote
|
||||
aria-hidden
|
||||
className="mt-0.5 h-3 w-3 shrink-0 text-amber-400"
|
||||
className="mt-0.5 h-3 w-3 shrink-0 text-warning-ink"
|
||||
/>
|
||||
<span className="line-clamp-2 whitespace-pre-wrap break-words">
|
||||
{annotationNote(a)}
|
||||
|
|
@ -448,7 +450,7 @@ export default async function SummaryPage({
|
|||
</span>
|
||||
<ChevronRight
|
||||
aria-hidden
|
||||
className="mt-1.5 h-4 w-4 shrink-0 text-slate-300 group-hover:text-slate-400"
|
||||
className="mt-1.5 h-4 w-4 shrink-0 text-ink-faint group-hover:text-ink"
|
||||
/>
|
||||
</Link>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -29,13 +29,13 @@ export default async function AnnotateEntryPage({
|
|||
<main className="mx-auto max-w-2xl px-4 py-10 sm:px-8">
|
||||
<div
|
||||
role="alert"
|
||||
className="rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-700"
|
||||
className="rounded-lg border border-error/30 bg-error/10 p-4 text-sm text-error-ink"
|
||||
>
|
||||
เปิดเอกสารจากลิงก์ไม่สำเร็จ: {result.error}
|
||||
</div>
|
||||
<Link
|
||||
href="/"
|
||||
className="mt-4 inline-block rounded-md bg-primary px-4 py-2 text-sm font-medium text-white transition hover:opacity-90"
|
||||
className="focus-ring mt-4 inline-block 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"
|
||||
>
|
||||
กลับหน้ารายการ
|
||||
</Link>
|
||||
|
|
@ -46,15 +46,15 @@ export default async function AnnotateEntryPage({
|
|||
// ไม่มี fileUrl เลย — ไม่มีอะไรให้เปิด (route จริงของ viewer อยู่ที่ /annotate/[id])
|
||||
return (
|
||||
<main className="mx-auto max-w-2xl px-4 py-10 sm:px-8">
|
||||
<div className="rounded-xl border border-slate-200 bg-white p-6 text-sm text-slate-600">
|
||||
ไม่ได้ระบุลิงก์เอกสาร — ใช้ <code className="rounded bg-slate-100 px-1">
|
||||
<div className="rounded-xl border border-line bg-white p-6 text-sm text-ink-soft">
|
||||
ไม่ได้ระบุลิงก์เอกสาร — ใช้ <code className="rounded bg-ink/[0.05] px-1">
|
||||
/annotate?fileUrl=…&fileId=…&typeId=…
|
||||
</code>{" "}
|
||||
หรือเลือกไฟล์จากหน้ารายการ
|
||||
</div>
|
||||
<Link
|
||||
href="/"
|
||||
className="mt-4 inline-block rounded-md bg-primary px-4 py-2 text-sm font-medium text-white transition hover:opacity-90"
|
||||
className="focus-ring mt-4 inline-block 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"
|
||||
>
|
||||
กลับหน้ารายการ
|
||||
</Link>
|
||||
|
|
|
|||
BIN
app/fonts/GeneralSans-Variable.woff2
Normal file
BIN
app/fonts/GeneralSans-Variable.woff2
Normal file
Binary file not shown.
114
app/globals.css
114
app/globals.css
|
|
@ -2,6 +2,22 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* ─── Genesis utilities (docs/DESIGN.md 2026-09-23) ────────────────────────
|
||||
focus indicator = outline น้ำเงินทึบ 2px + offset 2px ของ design system — แยกเป็น
|
||||
class ร่วมเพื่อใช้ซ้ำทุกปุ่ม/ลิงก์; input ใช้ .genesis-input (ขอบน้ำเงิน + outline ตอน
|
||||
:focus ตาม spec Inputs) — ครอบด้วย @layer components ให้ Tailwind utilities ทับได้
|
||||
(UX review M3 2026-09-23: halo alpha 0.12 ของ spec เดิม blend บนขาวแล้ว ~1.2:1
|
||||
ต่ำกว่าเกณฑ์ 3:1 ของ WCAG 1.4.11 — alpha 0.25–0.35 ก็ยังไม่ผ่าน จึงเปลี่ยนเป็น
|
||||
outline ทึบเป็นตัวชี้วัด + offset คั่นไม่ให้จมกับพื้นปุ่ม primary) */
|
||||
@layer components {
|
||||
.focus-ring {
|
||||
@apply focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary;
|
||||
}
|
||||
.genesis-input {
|
||||
@apply rounded-md border border-line bg-white px-3.5 py-2 text-sm text-ink placeholder:text-ink-faint focus:border-primary focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-primary;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── TextLayer ของ pdfjs v4 ─────────────────────────────────────────────────
|
||||
คัดมาจาก node_modules/pdfjs-dist/web/pdf_viewer.css (ส่วนที่จำเป็น)
|
||||
ใช้กับ pdfjs.TextLayer ที่ render ลง div.textLayer ใน PDFViewer */
|
||||
|
|
@ -136,10 +152,15 @@
|
|||
inset: -4px;
|
||||
}
|
||||
|
||||
/* ปุ่ม bookmark/ลบในแผง selection (20×20) + swatch สีในแผง (16×16) */
|
||||
/* ปุ่ม bookmark/ลบในแผง selection (20×20) + swatch สีในแผง (16×16) —
|
||||
sel-panel-btn ใส่ centering แบบ .coarse-36 กันผู้ใช้ class ไม่ได้เอา flex
|
||||
มาเอง (ไอคอนลอยชิดบนในกล่อง 36px); ปุ่มที่ใช้อยู่มี flex แล้ว ผลลัพธ์ไม่เปลี่ยน */
|
||||
.sel-panel-btn {
|
||||
min-height: 2.25rem;
|
||||
min-width: 2.25rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.sel-panel .color-swatches button,
|
||||
.sel-panel .color-swatches label {
|
||||
|
|
@ -226,6 +247,59 @@
|
|||
main.summary-page button {
|
||||
min-height: 2.75rem;
|
||||
}
|
||||
|
||||
/* UX review M5 2026-09-23: ช่องกรอกทุกชนิด ≥16px บนจอสัมผัส — iOS Safari ซูมทั้ง
|
||||
หน้าทุกครั้งที่แตะโฟกัสช่องที่ตัวอักษรเล็กกว่า 16px (ทีมตั้งกติกาเดียวกันไว้
|
||||
หน้า login แล้ว — ขยายครบทุกช่องของแอปเฉพาะอุปกรณ์จอสัมผัส; !important จำเป็น
|
||||
เพราะช่องส่วนใหญ่กำหนดขนาดด้วย text-xs/text-[11px] ที่ specificity สูงกว่า)
|
||||
responsive audit 2026-09-24: ครอบ contentEditable ของ TEXTBOX editor ด้วย —
|
||||
fontSize default 2.5% ≈ 15px ทำ Safari ซูม viewport ตอนแตะเข้าโหมดแก้ไข
|
||||
(ตัวอักษรขยายเป็น 16px เฉพาะช่วงพิมพ์ ต่างจากผลลัพธ์ ~7% — trade-off มาตรฐาน
|
||||
ของ editor บน iOS เพราะ maximum-scale กัน focus-zoom ไม่ได้) */
|
||||
input,
|
||||
textarea,
|
||||
select,
|
||||
[contenteditable]:not([contenteditable="false"]) {
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* UX review M7 2026-09-23: ปุ่มหลักของหน้ารายการ/กลุ่ม (Annotate h-8, ลิงก์กลุ่ม,
|
||||
ออกจากระบบ) สูง 32px ต่ำกว่ามาตรฐาน touch 44px ที่ viewer/หน้าสรุปได้แล้ว —
|
||||
หน้าที่ผู้ใช้ iPad แตะบ่อยที่สุดก่อนเปิดเอกสารต้องได้เป้าเท่ากัน (ครอบ a/button
|
||||
ของ <main class="listing-page"> + header ของ AppNav; select/summary เป็น
|
||||
native control/มี flex อยู่แล้ว ใส่แค่ min-height)
|
||||
ครอบด้วย @layer components ให้ Tailwind utilities (flex/items-*) ทับได้ —
|
||||
กฎ unlayered ชนะทุก layer จนบังคับ align-items:center ทับ items-start ของ
|
||||
แถวลิงก์ในการ์ดกลุ่ม กลายเป็นไอคอนลอยกลางแถว 2–3 บรรทัด (code review 2026-09-23)
|
||||
+ justify-content:center คืนค่า default ของปุ่ม (text-align:center ของ UA
|
||||
หมดผลเมื่อเป็น flex) ไม่งั้นปุ่ม w-full เช่น "เปิดเอกสาร"/อัปโหลด ข้อความชิดซ้าย
|
||||
(คำขอผู้ใช้ 2026-09-23)
|
||||
responsive audit 2026-09-24: @layer ต้องครอบ @media จากด้านนอกเท่านั้น — ซ้อนกลับกัน
|
||||
(@media { @layer {…} }) แล้ว Tailwind v3 ยกเนื้อหา @layer ออกไปวางที่ components
|
||||
section โดยทิ้ง media wrapper กฎจึงหลุดไป apply บน desktop ด้วย (ยืนยันด้วยการ
|
||||
build จริง: ปุ่มหน้ารายการได้ min-height 44px + flex บนจอ desktop) */
|
||||
@layer components {
|
||||
@media (pointer: coarse) {
|
||||
main.listing-page a,
|
||||
main.listing-page button {
|
||||
min-height: 2.75rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
main.listing-page select,
|
||||
main.listing-page summary {
|
||||
min-height: 2.75rem;
|
||||
}
|
||||
header.appnav a,
|
||||
header.appnav button {
|
||||
min-height: 2.75rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Print (หน้าสรุป) — design review 2026-09-17 L4 ───────────────────────
|
||||
|
|
@ -249,16 +323,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* UX review 2026-09-14 H2 → typography audit 2026-09-16 (T1 + Option B): webfont
|
||||
Sarabun (next/font จาก layout.tsx — variable มี adjusted fallback ของ next/font
|
||||
ในตัวแล้ว) นำหน้า stack เพื่อแสดงผลเหมือนกันทุกอุปกรณ์; ที่เหลือเป็น fallback
|
||||
ตอนฟอนต์ยังไม่ถึง (display:swap)/โหลดไม่ได้ — เรียงแบบ Latin-first ตาม T1:
|
||||
Latin/ตัวเลขได้ UI font ของ OS (Segoe UI/SF) ส่วนไทยได้ Leelawadee UI
|
||||
(Windows) / Thonburi (iPad) / Noto Sans Thai (Android/Linux) */
|
||||
/* UX review 2026-09-14 H2 → typography audit 2026-09-16 (T1 + Option B) →
|
||||
Genesis 2026-09-23: DM Sans (next/font ตัวแปร Latin) นำหน้า stack ให้ตัว
|
||||
Latin/ตัวเลขได้บุคลิก body ของ design system ก่อน จากนั้น webfont Sarabun
|
||||
(next/font — มี adjusted fallback ของ next/font ในตัว) รับ glyph ไทยโดยเฉพาะ
|
||||
แสดงผลเหมือนกันทุกอุปกรณ์; ที่เหลือเป็น fallback ตอนฟอนต์ยังไม่ถึง
|
||||
(display:swap)/โหลดไม่ได้ — ไทยได้ Leelawadee UI (Windows) / Thonburi (iPad)
|
||||
/ Noto Sans Thai (Android/Linux) / heading ใช้คลาส font-display ที่นำ
|
||||
General Sans มาก่อน Sarabun ด้วยหลักการเดียวกัน */
|
||||
body {
|
||||
font-family: var(--font-sarabun, "Sarabun"), -apple-system, "Segoe UI",
|
||||
"Helvetica Neue", Arial, "Leelawadee UI", "Thonburi", "Noto Sans Thai",
|
||||
sans-serif;
|
||||
font-family: var(--font-dm-sans, "DM Sans"), var(--font-sarabun, "Sarabun"),
|
||||
-apple-system, "Segoe UI", "Helvetica Neue", Arial, "Leelawadee UI",
|
||||
"Thonburi", "Noto Sans Thai", sans-serif;
|
||||
}
|
||||
|
||||
/* M6: touch hygiene — ตัดแถบเทากระพริบของ iOS ทุกครั้งที่แตะปุ่ม และกัน
|
||||
|
|
@ -306,12 +382,13 @@ html {
|
|||
}
|
||||
|
||||
/* เส้นคั่นบางระหว่างแถวตั้งค่า (PanelRow) ที่ต่อกันในเมนู tool/แผง selection —
|
||||
ระยะ/เส้นเท่ากับแทรก <div class="my-1.5 border-t border-slate-100"> ไว้ระหว่างแถว
|
||||
ทุกจุด (แถวแรกของกลุ่มไม่มีเส้น — ไม่ซ้ำกับ divider ที่เมนูใส่ไว้เองแล้ว) */
|
||||
ระยะ/เส้นเท่ากับแทรก <div class="my-1.5 border-t border-line"> ไว้ระหว่างแถว
|
||||
ทุกจุด (แถวแรกของกลุ่มไม่มีเส้น — ไม่ซ้ำกับ divider ที่เมนูใส่ไว้เองแล้ว)
|
||||
(#E8E8EC = token line — เดิม #f1f5f9 เป็น slate-100 หลงเหลือก่อน Genesis) */
|
||||
.panel-row + .panel-row {
|
||||
margin-top: 0.375rem;
|
||||
padding-top: 0.625rem;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
border-top: 1px solid #e8e8ec;
|
||||
}
|
||||
|
||||
/* M6: safe-area — defensive (ค่าเป็น 0 จนกว่าจะใส่ viewport-fit=cover ใน meta
|
||||
|
|
@ -325,7 +402,9 @@ header {
|
|||
|
||||
/* UX audit 2026-09-16 L7: เคารพ prefers-reduced-motion — ตัด transition ของ
|
||||
sidebar, animation กระพริบของปุ่ม export และ smooth scroll ทุกจุด
|
||||
(scrollToBelowHeader ฝั่ง JS เช็ค media query เดียวกันตอนเลือก behavior) */
|
||||
(scrollToBelowHeader ฝั่ง JS เช็ค media query เดียวกันตอนเลือก behavior)
|
||||
UX review m7 2026-09-23: เพิ่ม blanket rule ครอบ transition/animation ที่เหลือ
|
||||
(hover-lift ของการ์ด, scale ของสวอตช์สี ฯลฯ) — ยก motion ทั้งหน้าให้จบทันที */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
aside[data-sidebar] {
|
||||
transition: none;
|
||||
|
|
@ -333,7 +412,12 @@ header {
|
|||
.animate-pulse {
|
||||
animation: none;
|
||||
}
|
||||
* {
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
transition-duration: 0.01ms !important;
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { asc, eq, inArray } from "drizzle-orm";
|
||||
import {
|
||||
ArrowLeft,
|
||||
|
|
@ -13,6 +13,7 @@ import {
|
|||
} from "lucide-react";
|
||||
|
||||
import { AnnotationTypeIcon } from "@/components/AnnotationTypeIcon";
|
||||
import AppNav from "@/components/AppNav";
|
||||
import { db } from "@/db";
|
||||
import {
|
||||
annotations,
|
||||
|
|
@ -30,6 +31,7 @@ import {
|
|||
} from "@/utils/annotation-display";
|
||||
import { formatThaiDate } from "@/utils/date-format";
|
||||
import { imageFormatFromUrl } from "@/utils/file-kind";
|
||||
import { getSessionUser } from "@/lib/session";
|
||||
|
||||
// N9 (PLAN-DOCSET.md D3) — หน้ารวมเอกสารของกลุ่ม (type_id จากลิงก์ e-Court)
|
||||
// server component force-dynamic — login ถูกบังคับโดย middleware.ts อยู่แล้ว
|
||||
|
|
@ -53,9 +55,9 @@ function mergedItems(b: FileBucket) {
|
|||
);
|
||||
}
|
||||
|
||||
/** focus ring ร่วมของลิงก์รายการในการ์ด (เดียวกับ RING_CLS ของหน้าสรุป) */
|
||||
const RING_CLS =
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1";
|
||||
/** focus ring ร่วมของลิงก์รายการในการ์ด — Genesis .focus-ring (outline น้ำเงินทึบ
|
||||
* 2px + offset ตาม spec Elevation) เดียวกับหน้าสรุป */
|
||||
const RING_CLS = "focus-ring";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
|
|
@ -74,6 +76,10 @@ export default async function GroupPage({
|
|||
}) {
|
||||
const { typeId } = params;
|
||||
|
||||
// AppNav ต้องการชื่อผู้ใช้ — defense ซ้อน middleware กัน render หน้าให้คนไม่มี session
|
||||
const user = getSessionUser();
|
||||
if (!user) redirect("/login");
|
||||
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(files)
|
||||
|
|
@ -118,166 +124,173 @@ export default async function GroupPage({
|
|||
const bookmarkById = new Map(bmDtos.map((b) => [b.id, b] as const));
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-2xl px-4 py-10 sm:px-8">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-1.5 text-sm text-slate-500 transition hover:text-primary"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
หน้ารายการ
|
||||
</Link>
|
||||
<>
|
||||
<AppNav userName={user.userName} />
|
||||
{/* listing-page: hook ของกฎ coarse 44px ใน globals.css (อย่าถอด) */}
|
||||
<main className="listing-page mx-auto max-w-3xl px-4 py-8 sm:px-6">
|
||||
<Link
|
||||
href="/"
|
||||
className="focus-ring inline-flex items-center gap-1.5 rounded-md text-sm text-ink-soft transition hover:text-ink"
|
||||
>
|
||||
<ArrowLeft aria-hidden className="h-4 w-4" />
|
||||
หน้ารายการ
|
||||
</Link>
|
||||
|
||||
<header className="mt-4 flex items-center gap-3">
|
||||
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-primary/10">
|
||||
<Folder className="h-5 w-5 text-primary" />
|
||||
<header className="mt-4 flex items-center gap-3">
|
||||
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-lg bg-ink/[0.04]">
|
||||
<Folder aria-hidden className="h-5 w-5 text-ink-soft" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h1 className="font-display break-all text-section-title text-ink">
|
||||
{typeId}
|
||||
</h1>
|
||||
<p className="mt-0.5 text-sm text-ink-soft">{rows.length} เอกสาร</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* หมายเหตุแสดงทุกครั้ง (แผน N9 D3): ระบบรู้จักเฉพาะเอกสารที่เคยเปิดผ่านลิงก์ e-Court
|
||||
รายการครบทั้งคดีอยู่ใน e-Court ไม่ใช่ระบบนี้ — Genesis: แผงข้อมูลกลาง 8px radius
|
||||
(น้ำเงินสงวนให้ของ interactive จึงเป็นเทาโปร่ง ไม่ใช่แบนเนอร์ฟ้า) */}
|
||||
<div className="mt-4 rounded-lg border border-line bg-ink/[0.03] px-4 py-3 text-sm text-ink-soft">
|
||||
แสดงเฉพาะเอกสารที่เคยเปิดในระบบนี้
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h1 className="break-all text-xl font-bold leading-tight text-primary">
|
||||
{typeId}
|
||||
</h1>
|
||||
<p className="text-sm text-slate-500">{rows.length} เอกสาร</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* หมายเหตุแสดงทุกครั้ง (แผน N9 D3): ระบบรู้จักเฉพาะเอกสารที่เคยเปิดผ่านลิงก์ e-Court
|
||||
รายการครบทั้งคดีอยู่ใน e-Court ไม่ใช่ระบบนี้ */}
|
||||
<div className="mt-4 rounded-xl border border-blue-200 bg-blue-50 p-4 text-sm text-blue-800">
|
||||
แสดงเฉพาะเอกสารที่เคยเปิดในระบบนี้
|
||||
</div>
|
||||
|
||||
<ul className="mt-6 space-y-3">
|
||||
{rows.map((f) => {
|
||||
const bucket = byFile.get(f.id) ?? { anns: [], bms: [] };
|
||||
const total = bucket.anns.length + bucket.bms.length;
|
||||
return (
|
||||
<li
|
||||
key={f.id}
|
||||
className="rounded-xl border border-slate-200 bg-white p-4 shadow-sm transition hover:border-primary/30 hover:shadow-md"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10">
|
||||
{imageFormatFromUrl(f.pdfUrl) ? (
|
||||
<FileImage className="h-5 w-5 text-primary" />
|
||||
) : (
|
||||
<FileText className="h-5 w-5 text-primary" />
|
||||
)}
|
||||
<ul className="mt-6 space-y-4">
|
||||
{rows.map((f) => {
|
||||
const bucket = byFile.get(f.id) ?? { anns: [], bms: [] };
|
||||
const total = bucket.anns.length + bucket.bms.length;
|
||||
return (
|
||||
<li
|
||||
key={f.id}
|
||||
className="rounded-xl border border-line bg-white p-5"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-ink/[0.04]">
|
||||
{imageFormatFromUrl(f.pdfUrl) ? (
|
||||
<FileImage aria-hidden className="h-5 w-5 text-ink-soft" />
|
||||
) : (
|
||||
<FileText aria-hidden className="h-5 w-5 text-ink-soft" />
|
||||
)}
|
||||
</div>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block break-all line-clamp-2 text-body font-medium text-ink">
|
||||
{f.title}
|
||||
</span>
|
||||
{/* วันที่/จำนวนเป็นข้อมูลที่อ่านจริง ใช้ ink-soft (ผ่าน 4.5:1) —
|
||||
ink-faint เก็บไว้ให้ placeholder/disabled เท่านั้น (UX review M2) */}
|
||||
<span className="mt-0.5 block text-caption text-ink-soft">
|
||||
นำเข้า {formatThaiDate(f.createdAt, true)} · Annotations{" "}
|
||||
{bucket.anns.length} · Bookmarks {bucket.bms.length}
|
||||
</span>
|
||||
</span>
|
||||
<Link
|
||||
href={`/annotate/${f.id}`}
|
||||
className="focus-ring inline-flex h-8 shrink-0 items-center rounded-md border border-line px-3 text-sm font-medium text-ink transition hover:-translate-y-px hover:border-ink-faint"
|
||||
>
|
||||
Annotate
|
||||
</Link>
|
||||
</div>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block break-all line-clamp-2 text-sm font-medium text-slate-900">
|
||||
{f.title}
|
||||
</span>
|
||||
<span className="mt-0.5 block text-xs text-slate-500">
|
||||
นำเข้า {formatThaiDate(f.createdAt, true)} · Annotations{" "}
|
||||
{bucket.anns.length} · Bookmarks {bucket.bms.length}
|
||||
</span>
|
||||
</span>
|
||||
<Link
|
||||
href={`/annotate/${f.id}`}
|
||||
className="shrink-0 rounded-md bg-primary px-4 py-2 text-sm font-medium text-white transition hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1"
|
||||
>
|
||||
Annotate
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{total > 0 && (
|
||||
<details className="group mt-3 border-t border-slate-100">
|
||||
<summary className="flex cursor-pointer list-none items-center justify-between rounded-md px-1 py-2 text-sm font-medium text-slate-700 transition hover:text-primary [&::-webkit-details-marker]:hidden">
|
||||
Annotations & Bookmarks ({total})
|
||||
<ChevronDown
|
||||
aria-hidden
|
||||
className="h-4 w-4 shrink-0 text-slate-400 transition-transform group-open:rotate-180 motion-reduce:transition-none"
|
||||
/>
|
||||
</summary>
|
||||
<ul className="divide-y divide-slate-100">
|
||||
{mergedItems(bucket).map((item) => {
|
||||
const parent =
|
||||
item.kind === "bm" && item.dto.parentId
|
||||
? bookmarkById.get(item.dto.parentId)
|
||||
: undefined;
|
||||
return item.kind === "bm" ? (
|
||||
<li key={item.dto.id}>
|
||||
<Link
|
||||
href={`/annotate/${f.id}?page=${item.pageIndex + 1}`}
|
||||
className={`-mx-2 flex items-start gap-2 rounded-lg px-2 py-2.5 transition hover:bg-slate-50 ${RING_CLS}`}
|
||||
>
|
||||
<Bookmark
|
||||
aria-hidden
|
||||
className="mt-1 h-4 w-4 shrink-0 text-amber-500"
|
||||
/>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block whitespace-pre-wrap break-words text-sm font-medium leading-snug text-slate-800">
|
||||
{item.dto.title}
|
||||
</span>
|
||||
<span className="mt-0.5 block truncate text-xs text-slate-500">
|
||||
หน้า {item.pageIndex + 1} ·{" "}
|
||||
{formatThaiDate(item.dto.createdAt, true)} · โดย{" "}
|
||||
{item.dto.userName || "ไม่ทราบชื่อ"}
|
||||
{parent ? ` · ใน: ${parent.title}` : ""}
|
||||
</span>
|
||||
{item.dto.note && (
|
||||
<span className="mt-0.5 flex items-start gap-1 text-xs leading-relaxed text-slate-500">
|
||||
<StickyNote
|
||||
aria-hidden
|
||||
className="mt-0.5 h-3 w-3 shrink-0 text-amber-400"
|
||||
/>
|
||||
<span className="line-clamp-2 whitespace-pre-wrap break-words">
|
||||
{item.dto.note}
|
||||
</span>
|
||||
{total > 0 && (
|
||||
<details className="group mt-3 border-t border-line/70">
|
||||
<summary className="flex cursor-pointer list-none items-center justify-between rounded-md px-1 py-2.5 text-sm font-medium text-ink-soft transition hover:text-primary [&::-webkit-details-marker]:hidden">
|
||||
Annotations & Bookmarks ({total})
|
||||
<ChevronDown
|
||||
aria-hidden
|
||||
className="h-4 w-4 shrink-0 text-ink-faint transition-transform group-open:rotate-180 motion-reduce:transition-none"
|
||||
/>
|
||||
</summary>
|
||||
<ul className="divide-y divide-line/70">
|
||||
{mergedItems(bucket).map((item) => {
|
||||
const parent =
|
||||
item.kind === "bm" && item.dto.parentId
|
||||
? bookmarkById.get(item.dto.parentId)
|
||||
: undefined;
|
||||
return item.kind === "bm" ? (
|
||||
<li key={item.dto.id}>
|
||||
<Link
|
||||
href={`/annotate/${f.id}?page=${item.pageIndex + 1}`}
|
||||
className={`-mx-2 flex items-start gap-2 rounded-lg px-2 py-2.5 transition hover:bg-ink/[0.03] ${RING_CLS}`}
|
||||
>
|
||||
<Bookmark
|
||||
aria-hidden
|
||||
className="mt-1 h-4 w-4 shrink-0 text-warning-ink"
|
||||
/>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block whitespace-pre-wrap break-words text-sm font-medium leading-snug text-ink">
|
||||
{item.dto.title}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
) : (
|
||||
<li key={item.dto.id}>
|
||||
<Link
|
||||
href={`/annotate/${f.id}?ann=${item.dto.id}`}
|
||||
className={`-mx-2 flex items-start gap-2 rounded-lg px-2 py-2.5 transition hover:bg-slate-50 ${RING_CLS}`}
|
||||
>
|
||||
<AnnotationTypeIcon
|
||||
type={item.dto.type}
|
||||
data={item.dto.data}
|
||||
className="mt-1 h-4 w-4 shrink-0 text-slate-500"
|
||||
/>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block whitespace-pre-wrap break-words text-sm leading-snug text-slate-800">
|
||||
{annotationPreviewText(item.dto)}
|
||||
{item.dto.type === "COMMENT" &&
|
||||
item.dto.resolved && (
|
||||
<span className="ml-1.5 whitespace-nowrap rounded border border-emerald-600/50 bg-emerald-50 px-1.5 py-0.5 text-xs text-emerald-700">
|
||||
จัดการแล้ว
|
||||
<span className="mt-0.5 block truncate text-xs text-ink-soft">
|
||||
หน้า {item.pageIndex + 1} ·{" "}
|
||||
{formatThaiDate(item.dto.createdAt, true)} · โดย{" "}
|
||||
{item.dto.userName || "ไม่ทราบชื่อ"}
|
||||
{parent ? ` · ใน: ${parent.title}` : ""}
|
||||
</span>
|
||||
{item.dto.note && (
|
||||
<span className="mt-0.5 flex items-start gap-1 text-xs leading-relaxed text-ink-soft">
|
||||
<StickyNote
|
||||
aria-hidden
|
||||
className="mt-0.5 h-3 w-3 shrink-0 text-warning-ink"
|
||||
/>
|
||||
<span className="line-clamp-2 whitespace-pre-wrap break-words">
|
||||
{item.dto.note}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="mt-0.5 block text-xs text-slate-500">
|
||||
หน้า {item.pageIndex + 1} ·{" "}
|
||||
{annotationTypeLabel(item.dto)} ·{" "}
|
||||
{formatThaiDate(item.dto.createdAt, true)} · โดย{" "}
|
||||
{item.dto.userName}
|
||||
</span>
|
||||
{annotationNote(item.dto) && (
|
||||
<span className="mt-0.5 flex items-start gap-1 text-xs leading-relaxed text-slate-500">
|
||||
<StickyNote
|
||||
aria-hidden
|
||||
className="mt-0.5 h-3 w-3 shrink-0 text-amber-400"
|
||||
/>
|
||||
<span className="line-clamp-2 whitespace-pre-wrap break-words">
|
||||
{annotationNote(item.dto)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
) : (
|
||||
<li key={item.dto.id}>
|
||||
<Link
|
||||
href={`/annotate/${f.id}?ann=${item.dto.id}`}
|
||||
className={`-mx-2 flex items-start gap-2 rounded-lg px-2 py-2.5 transition hover:bg-ink/[0.03] ${RING_CLS}`}
|
||||
>
|
||||
<AnnotationTypeIcon
|
||||
type={item.dto.type}
|
||||
data={item.dto.data}
|
||||
className="mt-1 h-4 w-4 shrink-0 text-ink-soft"
|
||||
/>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block whitespace-pre-wrap break-words text-sm leading-snug text-ink">
|
||||
{annotationPreviewText(item.dto)}
|
||||
{item.dto.type === "COMMENT" &&
|
||||
item.dto.resolved && (
|
||||
<span className="ml-1.5 whitespace-nowrap rounded border border-success/40 bg-success/10 px-1.5 py-0.5 text-xs font-medium text-success-ink">
|
||||
จัดการแล้ว
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</details>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</main>
|
||||
<span className="mt-0.5 block text-xs text-ink-soft">
|
||||
หน้า {item.pageIndex + 1} ·{" "}
|
||||
{annotationTypeLabel(item.dto)} ·{" "}
|
||||
{formatThaiDate(item.dto.createdAt, true)} · โดย{" "}
|
||||
{item.dto.userName}
|
||||
</span>
|
||||
{annotationNote(item.dto) && (
|
||||
<span className="mt-0.5 flex items-start gap-1 text-xs leading-relaxed text-ink-soft">
|
||||
<StickyNote
|
||||
aria-hidden
|
||||
className="mt-0.5 h-3 w-3 shrink-0 text-warning-ink"
|
||||
/>
|
||||
<span className="line-clamp-2 whitespace-pre-wrap break-words">
|
||||
{annotationNote(item.dto)}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</details>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,39 @@
|
|||
import type { Metadata } from "next";
|
||||
import { Sarabun } from "next/font/google";
|
||||
import { DM_Sans, Sarabun } from "next/font/google";
|
||||
import localFont from "next/font/local";
|
||||
|
||||
import "./globals.css";
|
||||
|
||||
/** typography audit 2026-09-16 (Option B): Sarabun — ฟอนต์ตระกูลมาตรฐานเอกสาร
|
||||
* ราชการไทย (TH Sarabun) เจ้าหน้าที่คุ้นเคยที่สุด / next/font self-host ตอน build:
|
||||
* ไฟล์ฟอนต์ serve จาก origin เดียวกับแอป ไม่พึ่ง Google ตอนรัน จึงใช้ใน intranet
|
||||
* ได้ (แต่เครื่อง build ต้องเห็นอินเทอร์เน็ต) — โหลดเฉพาะ 2 weights ตามแผน
|
||||
* ~100-160KB แลกกับการแสดงผลเหมือนกันทุกอุปกรณ์ รวมกล่อง TEXTBOX ที่ผูก
|
||||
* stack นี้ (T1-C: wrap ข้อความตรงกันข้ามอุปกรณ์จริง) */
|
||||
/* typography audit 2026-09-16 (Option B): Sarabun — ฟอนต์ตระกูลมาตรฐานเอกสาร
|
||||
* ราชการไทย (TH Sarabun) เจ้าหน้าที่คุ้นเคยที่สุด / next/font self-host ตอน build:
|
||||
* ไฟล์ฟอนต์ serve จาก origin เดียวกับแอป ไม่พึ่ง Google ตอนรัน จึงใช้ใน intranet
|
||||
* ได้ (แต่เครื่อง build ต้องเห็นอินเทอร์เน็ต)
|
||||
*
|
||||
* Genesis (docs/DESIGN.md 2026-09-23): เพิ่ม General Sans (display/heading) จาก
|
||||
* Fontshare เป็น variable woff2 ไฟล์เดียว self-host ผ่าน next/font/local + DM Sans
|
||||
* (body) จาก Google — ทั้งคู่เป็น Latin เท่านั้น ไม่มี glyph ไทย จึงวาง Sarabun
|
||||
* ต่อท้ายใน stack (globals.css / tailwind fontFamily): ตัว Latin/ตัวเลขได้บุคลิก
|
||||
* Genesis ส่วนข้อความไทยคง Sarabun เดิม — Sarabun เพิ่ม weight 700 ให้ heading
|
||||
* ไทย (fallback ของ General Sans bold) */
|
||||
const generalSans = localFont({
|
||||
src: "./fonts/GeneralSans-Variable.woff2",
|
||||
// ประกาศช่วง wght จริงของ variable font (Fontshare: 200–700) — ไม่ใส่แล้ว
|
||||
// @font-face ที่ next/font generate ไม่มี descriptor font-weight → cover แค่ 400
|
||||
// และ font-bold/700 กลายเป็น synthetic bold จาก instance 400 (UX review C1 2026-09-23)
|
||||
weight: "200 700",
|
||||
display: "swap",
|
||||
variable: "--font-general-sans",
|
||||
});
|
||||
|
||||
const dmSans = DM_Sans({
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
variable: "--font-dm-sans",
|
||||
});
|
||||
|
||||
const sarabun = Sarabun({
|
||||
subsets: ["thai", "latin"],
|
||||
weight: ["400", "600"],
|
||||
weight: ["400", "600", "700"],
|
||||
display: "swap",
|
||||
variable: "--font-sarabun",
|
||||
});
|
||||
|
|
@ -27,8 +49,11 @@ export default function RootLayout({
|
|||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="th" className={sarabun.variable}>
|
||||
<body className="bg-slate-100 text-slate-900 antialiased">{children}</body>
|
||||
<html
|
||||
lang="th"
|
||||
className={`${generalSans.variable} ${dmSans.variable} ${sarabun.variable}`}
|
||||
>
|
||||
<body className="bg-canvas font-body text-ink antialiased">{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { Metadata } from "next";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import DotGrid from "@/components/DotGrid";
|
||||
import LoginForm from "@/components/LoginForm";
|
||||
import { getSessionUser } from "@/lib/session";
|
||||
import { firstSearchParam, sanitizeNextPath } from "@/utils/external-file";
|
||||
|
|
@ -24,7 +25,10 @@ export default function LoginPage({
|
|||
if (user) redirect(next ?? "/");
|
||||
|
||||
return (
|
||||
<main className="flex min-h-dvh items-center justify-center px-4 py-10">
|
||||
// Genesis: กริดจุดอันตรกิริยา ( decorative element ชิ้นเดียวที่อนุญาต) เป็นพื้นหลัง
|
||||
// หน้า login — DotGrid อยู่ก่อนการ์ดใน DOM จึงถูกทับโดยการ์ดเอง ไม่ต้องเล่น z-index
|
||||
<main className="relative flex min-h-dvh items-center justify-center px-4 py-10">
|
||||
<DotGrid />
|
||||
<LoginForm next={next} />
|
||||
</main>
|
||||
);
|
||||
|
|
|
|||
261
app/page.tsx
261
app/page.tsx
|
|
@ -5,8 +5,8 @@ import { FileImage, FileText, Folder } from "lucide-react";
|
|||
|
||||
import { db } from "@/db";
|
||||
import { files } from "@/db/schema";
|
||||
import AppNav from "@/components/AppNav";
|
||||
import EcourtLinkForm from "@/components/EcourtLinkForm";
|
||||
import SessionBar from "@/components/SessionBar";
|
||||
import UploadFileForm from "@/components/UploadFileForm";
|
||||
import { imageFormatFromUrl } from "@/utils/file-kind";
|
||||
import { formatThaiDate } from "@/utils/date-format";
|
||||
|
|
@ -32,35 +32,42 @@ function kindBadge(pdfUrl: string): string {
|
|||
}
|
||||
}
|
||||
|
||||
/** การ์ดไฟล์ของหน้ารายการ — ใช้ร่วมทุก section (N9 จัดกลุ่มแล้วยังหน้าตาเดิม) */
|
||||
/** การ์ดไฟล์ของหน้ารายการ (Genesis kit-card: surface 12px radius ขอบ line ไม่มีเงา
|
||||
* static — hover ยก -2px + shadow-card-hover; tile ไอคอนเป็นกลาง น้ำเงินสงวนให้ของ
|
||||
* interactive ตาม Do&Don't ของ design system) ใช้ร่วมทุก section (N9) */
|
||||
function FileCard({ file }: { file: FileRow }) {
|
||||
return (
|
||||
<li className="group flex items-center gap-3 rounded-xl border border-slate-200 bg-white p-4 shadow-sm transition hover:border-primary/30 hover:shadow-md">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10">
|
||||
{imageFormatFromUrl(file.pdfUrl) ? (
|
||||
<FileImage className="h-5 w-5 text-primary" />
|
||||
) : (
|
||||
<FileText className="h-5 w-5 text-primary" />
|
||||
)}
|
||||
<li className="flex flex-col rounded-xl border border-line bg-white p-5 transition duration-200 hover:-translate-y-0.5 hover:shadow-card-hover">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-lg bg-ink/[0.04]">
|
||||
{imageFormatFromUrl(file.pdfUrl) ? (
|
||||
<FileImage aria-hidden className="h-5 w-5 text-ink-soft" />
|
||||
) : (
|
||||
<FileText aria-hidden className="h-5 w-5 text-ink-soft" />
|
||||
)}
|
||||
</div>
|
||||
<span className="rounded bg-ink/[0.05] px-1.5 py-0.5 text-[11px] font-medium text-ink-soft">
|
||||
{kindBadge(file.pdfUrl)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block break-all line-clamp-2 text-sm font-medium text-slate-900">
|
||||
{file.title}
|
||||
</span>
|
||||
<span className="mt-0.5 block text-xs text-slate-500">
|
||||
<span className="mr-1.5 rounded bg-slate-100 px-1 py-0.5 font-medium text-slate-500">
|
||||
{kindBadge(file.pdfUrl)}
|
||||
</span>
|
||||
{formatThaiDate(file.createdAt, true)}
|
||||
</span>
|
||||
<span className="mt-4 block break-all line-clamp-2 text-body font-medium text-ink">
|
||||
{file.title}
|
||||
</span>
|
||||
{/* ปุ่มเข้า annotate แทนลิงก์กางเต็มการ์ด (M4) ตาม feedback — เป้าจิ้ม 36px (H4) */}
|
||||
<Link
|
||||
href={`/annotate/${file.id}`}
|
||||
className="shrink-0 rounded-md bg-primary px-4 py-2 text-sm font-medium text-white transition hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1"
|
||||
>
|
||||
Annotate
|
||||
</Link>
|
||||
{/* วันที่เป็นข้อมูลที่ผู้ใช้อ่านจริง — ink-soft (ผ่าน 4.5:1); ink-faint เก็บไว้
|
||||
ให้ placeholder/disabled เท่านั้น (UX review M2 2026-09-23) */}
|
||||
<span className="mt-1 block text-caption text-ink-soft">
|
||||
{formatThaiDate(file.createdAt, true)}
|
||||
</span>
|
||||
{/* ปุ่มเข้า annotate แทนลิงก์กางเต็มการ์ด (M4) ตาม feedback — เป็น secondary
|
||||
(หนึ่ง primary ต่อ section ตาม Genesis; การ์ดทั้งกริดคือ passive surface) */}
|
||||
<div className="mt-5 flex flex-1 items-end justify-end">
|
||||
<Link
|
||||
href={`/annotate/${file.id}`}
|
||||
className="focus-ring inline-flex h-8 items-center rounded-md border border-line px-3 text-sm font-medium text-ink transition hover:-translate-y-px hover:border-ink-faint"
|
||||
>
|
||||
Annotate
|
||||
</Link>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
|
@ -113,103 +120,117 @@ export default async function HomePage({
|
|||
);
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-2xl px-4 py-10 sm:px-8">
|
||||
<header 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">
|
||||
JN
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold leading-tight text-primary">
|
||||
Judge's Note
|
||||
</h1>
|
||||
<p className="text-sm text-slate-500">
|
||||
ระบบ PDF Annotation สำหรับ e-Court
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
<>
|
||||
<AppNav userName={user.userName} />
|
||||
{/* listing-page: hook ของกฎ coarse 44px ใน globals.css (อย่าถอด — เหมือน
|
||||
summary-page ของหน้าสรุป) */}
|
||||
<main className="listing-page mx-auto max-w-7xl px-4 pb-16 pt-10 sm:px-6">
|
||||
{/* ชื่อระบบ Latin ล้วน → General Sans tracking แน่นตาม Genesis display type */}
|
||||
<h1 className="font-display text-4xl tracking-tight text-ink sm:text-headline">
|
||||
Judge's Note
|
||||
</h1>
|
||||
<p className="mt-2 text-body text-ink-soft">
|
||||
ระบบ PDF Annotation สำหรับ e-Court
|
||||
</p>
|
||||
|
||||
<SessionBar userName={user.userName} />
|
||||
|
||||
{registerError && (
|
||||
<div
|
||||
role="alert"
|
||||
className="mt-6 rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-700"
|
||||
>
|
||||
เปิดเอกสารจากลิงก์ไม่สำเร็จ: {registerError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* การ์ดเปิดเอกสารจากลิงก์ e-Court — บล็อกแยกของตัวเอง มีหัวเรื่องในตัวแล้ว (N8) */}
|
||||
<EcourtLinkForm />
|
||||
|
||||
<section className="mt-8">
|
||||
<h2 className="text-sm font-semibold text-slate-700">เพิ่มเอกสาร</h2>
|
||||
<UploadFileForm />
|
||||
</section>
|
||||
|
||||
<div className="mt-8 flex items-baseline justify-between">
|
||||
<h2 className="text-sm font-semibold text-slate-700">เอกสาร</h2>
|
||||
{rows.length > 0 && (
|
||||
<span className="text-xs tabular-nums text-slate-500">{rows.length} ไฟล์</span>
|
||||
{registerError && (
|
||||
<div
|
||||
role="alert"
|
||||
className="mt-6 rounded-lg border border-error/25 bg-error/5 p-4 text-sm text-error-ink"
|
||||
>
|
||||
เปิดเอกสารจากลิงก์ไม่สำเร็จ: {registerError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{rows.length === 0 ? (
|
||||
<div className="mt-3 rounded-xl border border-dashed border-slate-300 bg-white/60 p-10 text-center">
|
||||
<FileText className="mx-auto h-8 w-8 text-slate-300" />
|
||||
<p className="mt-3 text-sm font-medium text-slate-500">
|
||||
ยังไม่มีไฟล์ในระบบ
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-slate-500">
|
||||
รัน /sync-db เพื่อ seed ไฟล์ตัวอย่าง
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{sections.map(([typeId, list]) => (
|
||||
<section key={typeId} className="mt-6">
|
||||
<div className="flex items-center gap-2 border-b border-slate-200 pb-2">
|
||||
<Folder className="h-4 w-4 shrink-0 text-primary" />
|
||||
<span className="min-w-0 break-all text-sm font-semibold text-slate-700">
|
||||
{typeId}
|
||||
</span>
|
||||
<span className="shrink-0 text-xs tabular-nums text-slate-500">
|
||||
{list.length} ไฟล์
|
||||
</span>
|
||||
{/* Next Link เติม basePath ให้เอง (invariant #12) */}
|
||||
<Link
|
||||
href={`/groups/${encodeURIComponent(typeId)}`}
|
||||
className="ml-auto shrink-0 rounded text-xs font-medium text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1"
|
||||
>
|
||||
เอกสารในกลุ่ม
|
||||
</Link>
|
||||
</div>
|
||||
<ul className="mt-3 space-y-3">
|
||||
{list.map((f) => (
|
||||
<FileCard key={f.id} file={f} />
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
))}
|
||||
{ungrouped.length > 0 && (
|
||||
<section className="mt-6">
|
||||
<div className="flex items-center gap-2 border-b border-slate-200 pb-2">
|
||||
<span className="text-sm font-semibold text-slate-700">
|
||||
ไม่มีกลุ่ม
|
||||
</span>
|
||||
<span className="text-xs tabular-nums text-slate-500">
|
||||
{ungrouped.length} ไฟล์
|
||||
</span>
|
||||
</div>
|
||||
<ul className="mt-3 space-y-3">
|
||||
{ungrouped.map((f) => (
|
||||
<FileCard key={f.id} file={f} />
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
{/* การ์ดจำลองลิงก์ e-Court (N8) อยู่เหนือรายการเอกสาร — เป็นทางเข้าเปิดเอกสาร
|
||||
จากระบบ e-Court ไม่ใช่การเพิ่มไฟล์เข้ารายการ (คุมความกว้างเดียวกับ
|
||||
ฟอร์มอัปโหลด) */}
|
||||
<section className="mt-12">
|
||||
<div className="border-b border-line pb-3">
|
||||
<h2 className="font-display text-section-title text-ink">
|
||||
เปิดเอกสารจากลิงก์ e-Court
|
||||
</h2>
|
||||
</div>
|
||||
<div className="mt-5 max-w-xl">
|
||||
<EcourtLinkForm />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-12">
|
||||
<div className="flex items-baseline justify-between gap-3 border-b border-line pb-3">
|
||||
<h2 className="font-display text-section-title text-ink">เอกสาร</h2>
|
||||
{rows.length > 0 && (
|
||||
<span className="text-caption tabular-nums text-ink-soft">
|
||||
{rows.length} ไฟล์
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* อัปโหลดไฟล์ (N4) อยู่กับรายการเอกสาร — ไฟล์ที่อัปโหลดเข้ารายการนี้เอง
|
||||
(คุมความกว้างไม่ให้ฟอร์มกินเต็มความกว้างของกริดรายการ) */}
|
||||
<div className="mt-5 max-w-xl">
|
||||
<UploadFileForm />
|
||||
</div>
|
||||
|
||||
{rows.length === 0 ? (
|
||||
<div className="mt-5 rounded-xl border border-dashed border-line bg-white/60 p-12 text-center">
|
||||
<FileText aria-hidden className="mx-auto h-8 w-8 text-ink-faint" />
|
||||
<p className="mt-3 text-sm font-medium text-ink-soft">
|
||||
ยังไม่มีไฟล์ในระบบ
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-ink-soft">
|
||||
รัน /sync-db เพื่อ seed ไฟล์ตัวอย่าง
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{sections.map(([typeId, list]) => (
|
||||
<section key={typeId} className="mt-8">
|
||||
<div className="flex flex-wrap items-center gap-x-2.5 gap-y-1">
|
||||
<Folder
|
||||
aria-hidden
|
||||
className="h-4 w-4 shrink-0 text-ink-soft"
|
||||
/>
|
||||
<h3 className="font-display min-w-0 break-all text-subhead text-ink">
|
||||
{typeId}
|
||||
</h3>
|
||||
<span className="text-caption tabular-nums text-ink-soft">
|
||||
{list.length} ไฟล์
|
||||
</span>
|
||||
{/* Next Link เติม basePath ให้เอง (invariant #12) */}
|
||||
<Link
|
||||
href={`/groups/${encodeURIComponent(typeId)}`}
|
||||
className="focus-ring ml-auto shrink-0 rounded text-sm font-medium text-primary hover:underline"
|
||||
>
|
||||
เอกสารในกลุ่ม
|
||||
</Link>
|
||||
</div>
|
||||
<ul className="mt-4 grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{list.map((f) => (
|
||||
<FileCard key={f.id} file={f} />
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
))}
|
||||
{ungrouped.length > 0 && (
|
||||
<section className="mt-8">
|
||||
<div className="flex flex-wrap items-center gap-x-2.5 gap-y-1">
|
||||
<h3 className="font-display text-subhead text-ink">ไม่มีกลุ่ม</h3>
|
||||
<span className="text-caption tabular-nums text-ink-soft">
|
||||
{ungrouped.length} ไฟล์
|
||||
</span>
|
||||
</div>
|
||||
<ul className="mt-4 grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{ungrouped.map((f) => (
|
||||
<FileCard key={f.id} file={f} />
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue