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
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