groups+summary: redesign หน้า /groups/[typeId] + หน้าสรุปเป็น explorer 2 คอลัมน์ (ตัวกรองซ้าย + list เดียว) — หน้าสรุปใช้มิติแรก "หน้า" ตามแบบหน้ากลุ่มที่ใช้ "ไฟล์" — (1) หน้า /groups/[typeId]: ถอด navbar (เหลือ back link — AppNav เหลือใช้ที่หน้า / อย่างเดียว) + ถอดหมายเหตุ "แสดงเฉพาะเอกสารที่เคยเปิดในระบบนี้", query ทั้งหมดฝั่ง server ส่ง GroupFileSummary[]+GroupListItem[] ให้ GroupExplorer กรองใน memory — ตัวกรอง tree menu 2 ระดับสไตล์ Quasar docs 4 กลุ่ม (ไฟล์/ชนิดรายการ/ชนิดของ Annotation/ผู้บันทึก — ชนิดรายการเป็น checkbox คู่ ไม่เลือก = แสดงทั้งหมด, ติ๊กชนิด → เลือก Annotation อัตโนมัติ, เลิกเลือก Annotation → ล้างชนิดค้าง) + GroupList แถว passive จุดคลิกเดียวคือปุ่ม "View" (2) แยกแผงตัวกรองเป็น component กลาง components/explorer/FacetFilterPanel.tsx — มิติแรกส่งเข้าทาง prop firstGroup (groups = ไฟล์ / สรุป = หน้า พร้อม optionsScroll ให้ scroll ภายในกลุ่ม max-h-64 กันหลายสิบหน้าดันกลุ่มอื่นออกจอ) + TYPE_SHORT/TYPE_ORDER รวมจาก 2 ที่ที่เคย define ซ้ำมาอยู่ utils/annotation-display.ts จุดเดียว (3) หน้าสรุป /annotate/[id]/summary: server ส่ง ExplorerItem[] (หน้า → bookmark ก่อน annotation → ในหน้าเดียวกัน bookmark เรียง yOffset ตาม N5/annotation ตาม createdAt) ให้ SummaryExplorer + SummaryList การ์ดรายหน้าหัวการ์ด sticky (เลขหน้า + บรรทัดสรุปหน้าคำนวณจากรายการที่เห็นจริงหลังกรอง + ปุ่มเปิดหน้านี้) + แถว passive ปุ่ม View deep link ?ann=/?page= เหมือนหน้ากลุ่ม — ถอดรายการตามหน้าจุดไข่ปลา + การ์ด folio + CollapsibleBlock ออก (anchor #page-N ของ section คงไว้ให้ลิงก์เก่ายังใช้ได้) (4) ตอนพิมพ์แสดงฉบับเต็มเสมอ — ตัวกรองที่ตั้งค้างต้องไม่ตัดรายการออกเงียบ ๆ บนกระดาษ (ตอนกรองอยู่ render ชุดบนจอ print:hidden + ชุดเต็ม hidden print:block, prop anchorIds กัน duplicate id) + กฎ print ใหม่ใน globals.css (section>ul>li break-inside:avoid-page + .page-head position:static) (5) ผ่าน code review 2026-09-24 (tsc/build/vitest 144 tests): คืน N5 yOffset ให้ bookmark ต่อหน้าที่ sort createdAt ทับไปโดยไม่ตั้งใจ + จดรับความเสี่ยง localeCompare "th" รันทั้ง SSR/hydration ไว้ที่ comment พร้อมทางแก้ + docs sync ครบ (CLAUDE.md โครงสร้างไฟล์ + PLAN-FEAT ถอดหมายเหตุ + PLAN-DOCSET อ้าง FacetFilterPanel) (คำขอผู้ใช้ 2026-09-24)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-09-24 17:34:23 +07:00
parent c5c6b65bd2
commit df502088b7
14 changed files with 1522 additions and 616 deletions

View file

@ -2,19 +2,10 @@ import type { Metadata } from "next";
import Link from "next/link";
import { asc, eq } from "drizzle-orm";
import { notFound } from "next/navigation";
import {
ArrowLeft,
Bookmark,
Braces,
ChevronDown,
ChevronRight,
ClipboardList,
FileDown,
StickyNote,
} from "lucide-react";
import { ArrowLeft, Braces, ClipboardList, FileDown } from "lucide-react";
import { AnnotationTypeIcon } from "@/components/AnnotationTypeIcon";
import CollapsibleBlock from "@/components/summary/CollapsibleBlock";
import type { ExplorerItem } from "@/components/explorer/FacetFilterPanel";
import { SummaryExplorer } from "@/components/summary/SummaryExplorer";
import { db } from "@/db";
import {
annotations,
@ -22,68 +13,21 @@ import {
files,
toAnnotationDTO,
toBookmarkDTO,
type AnnotationDTO,
type AnnotationType,
type BookmarkDTO,
} from "@/db/schema";
import {
annotationNote,
annotationPreviewText,
annotationTypeLabel,
} from "@/utils/annotation-display";
import { withBasePath } from "@/utils/base-path";
// หน้าสรุปหมายเหตุ (F15/D2) — server component รวม annotation (ทุกผู้ใช้) + bookmark
// จัดกลุ่มตามหน้า แต่ละแถวกระโดดเป็น deep link `/annotate/<id>?ann=` / `?page=` (F12)
// — design "แฟ้มสำนวน" 2026-09-17: แถบหัวกระดาษน้ำเงิน (สืบต่อจาก tile JN ของแบรนด์) +
// รายการตามหน้าจุดไข่ปลา + การ์ดรายหน้า 1 หน้า 1 การ์ด (คำขอผู้ใช้ 2026-09-17 แทนแผ่น
// กระดาษแผ่นเดียวคั่นเส้น) แต่ละการ์ดมีขอบ folio เลขหน้าใหญ่; รายการตามหน้าเป็น anchor
// link (#page-N) ล้วน — desktop เป็น sticky aside แบบการ์ด ส่วนมือถือ (< lg) เป็น
// <details> แบบการ์ด ไม่มี JS / client component เดียวของหน้าคือ CollapsibleBlock
// (หุบ/กางกลุ่ม Bookmark/Annotation)
// แต่ละแถวกระโดดเป็น deep link `/annotate/<id>?ann=` / `?page=` (F12)
// — design "แฟ้มสำนวน" 2026-09-17 (แถบหัวน้ำเงิน + การ์ดรายหน้าขอบ folio +
// รายการตามหน้าจุดไข่ปลา) → redesign 2026-09-24 ตามคำขอผู้ใช้เป็น explorer
// 2 คอลัมน์แบบหน้ากลุ่ม N9: ตัวกรองฝั่งซ้าย (หน้า/ชนิดรายการ/ชนิดของ
// Annotation/ผู้บันทึก) + การ์ดรายหน้าหัว sticky — แถบหัวน้ำเงิน + action bar +
// empty state คงเดิม; query ทั้งหมดทำที่นี่แล้วส่ง `ExplorerItem[]` ให้
// SummaryExplorer กรองใน memory ฝั่ง client (read-only snapshot ไม่มี SSE)
const UUID_RE =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
/** ชื่อย่อต่อชนิดสำหรับบรรทัดสรุปของแต่ละหน้า (ชื่อเต็มอยู่ที่ annotationTypeLabel) */
const TYPE_SHORT: Record<AnnotationType, string> = {
HIGHLIGHT: "ไฮไลต์",
DRAW: "เส้นวาด",
COMMENT: "คอมเมนต์",
SHAPE: "รูปทรง",
REDACTION: "ปิดพราง",
TEXTBOX: "กล่องข้อความ",
STAMP: "ตรายาง",
};
/** ลำดับชนิดในบรรทัดสรุป — ตามลำดับ enum ให้ deterministic ไม่ขึ้นกับลำดับสร้างรายการ */
const TYPE_ORDER = Object.keys(TYPE_SHORT) as AnnotationType[];
type PageBucket = {
anns: AnnotationDTO[];
bms: BookmarkDTO[];
typeCounts: Partial<Record<AnnotationType, number>>;
resolvedCount: number;
participants: Set<string>;
};
/** บรรทัดสรุปของหน้า: "ไฮไลต์ 1 · คอมเมนต์ 1 (จัดการแล้ว 1) · โดย ผู้พิพากษา ก., เลขานุการ ข."
* (หน้าที่มีแต่ที่คั่นหน้า = "โดย …" อย่างเดียว) */
function pageSummaryLine(b: PageBucket): string {
const parts: string[] = [];
for (const t of TYPE_ORDER) {
const n = b.typeCounts[t];
if (!n) continue;
if (t === "COMMENT" && b.resolvedCount > 0)
parts.push(`คอมเมนต์ ${n} (จัดการแล้ว ${b.resolvedCount})`);
else parts.push(`${TYPE_SHORT[t]} ${n}`);
}
const who = Array.from(b.participants).sort((x, y) => x.localeCompare(y, "th"));
const shown = who.slice(0, 3).join(", ");
const whoLabel = who.length > 3 ? `${shown} และอีก ${who.length - 3} คน` : shown;
return parts.length > 0 ? `${parts.join(" · ")} · โดย ${whoLabel}` : `โดย ${whoLabel}`;
}
/** focus ring ร่วมของทุกลิงก์ในหน้า — Genesis .focus-ring (outline น้ำเงินทึบ 2px
* + offset ตาม spec Elevation); จอสัมผัสได้ 44px อัตโนมัติจากกฎ
* `main.summary-page a`/`button` ใน globals.css (ห้ามถอด class summary-page ของ <main>) */
@ -140,49 +84,46 @@ export default async function SummaryPage({
const bmDtos = bmRows.map(toBookmarkDTO);
const bookmarkById = new Map(bmDtos.map((b) => [b.id, b] as const));
// จัดกลุ่มตามหน้าแบบ union — หน้าปรากฏเมื่อมี annotation หรือ bookmark ≥ 1 รายการ
const pageMap = new Map<number, PageBucket>();
const bucketOf = (pageIndex: number): PageBucket => {
let b = pageMap.get(pageIndex);
if (!b) {
b = { anns: [], bms: [], typeCounts: {}, resolvedCount: 0, participants: new Set() };
pageMap.set(pageIndex, b);
}
return b;
};
for (const a of dtos) {
const b = bucketOf(a.pageIndex);
b.anns.push(a);
b.typeCounts[a.type] = (b.typeCounts[a.type] ?? 0) + 1;
if (a.type === "COMMENT" && a.resolved) b.resolvedCount += 1;
b.participants.add(a.userName);
}
for (const b of bmDtos) {
const bucket = bucketOf(b.pageIndex);
bucket.bms.push(b);
bucket.participants.add(b.userName || "ไม่ทราบชื่อ");
}
const pages = Array.from(pageMap.entries()).sort((x, y) => x[0] - y[0]);
// list เดียวรวมทั้งไฟล์ไล่ตามลำดับอ่าน: หน้า → หน้าเดียวกันให้ที่คั่นหน้าก่อน
// หมายเหตุ → ในหน้าเดียวกัน bookmark เรียงตามตำแหน่งบนหน้า (N5 เหมือน query/
// sidebar — code review 2026-09-24 คืนจาก createdAt ที่เคยแทนที่โดยไม่ตั้งใจ)
// ส่วน annotation ตาม createdAt (sort ครั้งเดียวบน server — ฝั่ง client กรอง
// เป็น pure predicate ไม่ sort ซ้ำ)
const items: ExplorerItem[] = [
...bmDtos.map((dto) => ({
kind: "bm" as const,
pageIndex: dto.pageIndex,
dto,
parentTitle: dto.parentId
? bookmarkById.get(dto.parentId)?.title ?? null
: null,
})),
...dtos.map((dto) => ({ kind: "ann" as const, pageIndex: dto.pageIndex, dto })),
].sort(
(a, x) =>
a.pageIndex - x.pageIndex ||
(a.kind === x.kind ? 0 : a.kind === "bm" ? -1 : 1) ||
(a.kind === "bm" && x.kind === "bm"
? a.dto.yOffset - x.dto.yOffset
: new Date(a.dto.createdAt).getTime() -
new Date(x.dto.createdAt).getTime()),
);
// ภาพรวมระดับไฟล์สำหรับรายการตามหน้า desktop (sticky aside) + มือถือ (<details>)
const allParticipants = Array.from(
new Set([
...dtos.map((a) => a.userName),
...bmDtos.map((b) => b.userName || "ไม่ทราบชื่อ"),
]),
).sort((x, y) => x.localeCompare(y, "th"));
// จำนวนหน้าที่มีรายการ ≥1 สำหรับสถิติแถบหัว
const pageCount = new Set(items.map((it) => it.pageIndex)).size;
return (
<main className="summary-page mx-auto max-w-3xl px-4 py-6 lg:max-w-6xl lg:px-8">
{/* หัวกระดาษสำนวน — แถบน้ำเงินสืบต่อจาก tile JN ของแบรนด์ (หน้ารายการ/login):
ชื่อรายงาน + ชื่อแฟ้ม + สถิติในที่เดียว แล้วปิดท้ายด้วยเส้นคั่นบาง */}
ชื่อรายงาน + ชื่อแฟ้ม + สถิติในที่เดียว แล้วปิดท้ายด้วยเส้นคั่นบาง
(ตอนพิมพ์เป็นกรอบเส้นน้ำเงินตามกฎ @media print ของ summary-page) */}
<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 && (
{items.length > 0 && (
<p className="mt-2 text-xs tabular-nums text-white/70">
Annotation {dtos.length} รายการ · Bookmark {bmDtos.length} รายการ ·{" "}
{pages.length} หน้า
{pageCount} หน้า
</p>
)}
</header>
@ -219,7 +160,9 @@ export default async function SummaryPage({
</div>
</div>
{pages.length === 0 ? (
{items.length === 0 ? (
/* ไฟล์ว่าง = ไม่ mount explorer เลย — แผงกรองของข้อมูลว่างไม่มีความหมาย
(minimize choices) ทางเข้าเอกสารคือปุ่ม "เปิดหน้าอ่าน" */
<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">
@ -236,235 +179,7 @@ export default async function SummaryPage({
</Link>
</div>
) : (
<div className="mt-5 lg:grid lg:grid-cols-[220px_minmax(0,1fr)] lg:items-start lg:gap-8">
{/* รายการตามหน้ามือถือ (จอ < lg) — design review 2026-09-17 H2: คืน navigation ที่หาย
ไปบนมือถือ (aside ด้านล่างแสดงเฉพาะ lg) ใช้ <details> ได้หุบ/กาง + keyboard
โดยไม่มี JS เพิ่ม — จุดไข่ปลา + ผู้ร่วมทำ ภาษาเดียวกับ desktop (จอ lg กับ
ตอนพิมพ์ซ่อน — display:none ไม่กินช่องของกริด) */}
<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-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-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-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-ink-faint"
/>
<span className="leading-5 tabular-nums text-ink-soft">
{b.anns.length + b.bms.length}
</span>
</a>
))}
</nav>
<p className="border-t border-line/70 px-4 py-3 text-xs leading-relaxed text-ink-soft">
ผู้ร่วมทำ: {allParticipants.join(" · ")}
</p>
</details>
{/* รายการตามหน้า (จอ ≥ lg) — จุดไข่ปลาแบบสารบัญของเอกสารราชการ anchor ล้วนไม่มี JS
เป็นการ์ดชุดเดียวกับ <details> มือถือและการ์ดรายหน้า (แสดงเฉพาะหน้าที่มี
รายการ ≥ 1 ตามหลัก minimize choices; max-h กันเอกสารหลายสิบหน้าสูงเกินจอ
— ครึ่งล่างของ sticky กดไม่ได้ตลอดกาล) */}
<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-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-ink-faint"
/>
<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-line pt-4 text-sm font-semibold text-ink">
ผู้ร่วมทำ
</h2>
<p className="mt-1.5 px-1 text-xs leading-relaxed text-ink-soft">
{allParticipants.join(" · ")}
</p>
</aside>
{/* การ์ดรายหน้า — 1 หน้า 1 การ์ด เรียงลงมาแยกด้วยช่องว่าง (คำขอผู้ใช้
2026-09-17 แทนแผ่นกระดาษแผ่นเดียวคั่นเส้น); แต่ละการ์ดมีขอบ folio:
เลขหน้าใหญ่ + เส้นดิ่ง แบบเลขหน้าบนขอบกระดาษ (mt-5 lg:mt-0 — ระยะจาก
รายการตามหน้า <details> บนมือถือ; จอ lg กริดจัดให้) */}
<div className="mt-5 flex flex-col gap-4 lg:mt-0">
{pages.map(([pi, b]) => {
return (
<section
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-line bg-white sm:grid-cols-[3.75rem_1fr]"
>
{/* ขอบ folio — "หน้า" เล็ก + เลขใหญ่ชิดเส้นดิ่ง */}
<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-ink-soft">
หน้า
</span>
{/* เลขหน้าเป็น 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>
</div>
{/* เนื้อหาของหน้า — บรรทัดสรุปหน้า (ไม่มีหัวข้อคำอธิบาย) /
Bookmark / Annotation (หุบ-กางได้, รายการเยื้องจากหัวข้อ) */}
<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-ink-soft">
{pageSummaryLine(b)}
</p>
</div>
<Link
href={`/annotate/${id}?page=${pi + 1}`}
className={`inline-flex shrink-0 items-center gap-0.5 text-xs text-primary hover:underline ${RING_CLS}`}
>
เปิดหน้านี้
<ChevronRight aria-hidden className="h-3 w-3" />
</Link>
</div>
{/* ที่คั่นหน้าของหน้านี้ทั้งหมด (root + sub) เรียงตาม yOffset */}
{b.bms.length > 0 && (
<div className="mt-2">
<CollapsibleBlock kind="bookmark" count={b.bms.length}>
<ul className="divide-y divide-line/70">
{b.bms.map((bm) => {
const parent = bm.parentId
? bookmarkById.get(bm.parentId)
: undefined;
return (
<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-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">
{bm.title}
</span>
<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-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">
{bm.note}
</span>
</span>
)}
</span>
<ChevronRight
aria-hidden
className="mt-1.5 h-4 w-4 shrink-0 text-ink-faint group-hover:text-ink"
/>
</Link>
</li>
);
})}
</ul>
</CollapsibleBlock>
</div>
)}
{/* หมายเหตุของหน้านี้ — แถวคลิกได้ทั้งแถว (เป้าใหญ่บนจอสัมผัส) */}
{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-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-ink/[0.03] ${RING_CLS}`}
>
<AnnotationTypeIcon
type={a.type}
data={a.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(a)}
{a.type === "COMMENT" && a.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 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-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(a)}
</span>
</span>
)}
</span>
<ChevronRight
aria-hidden
className="mt-1.5 h-4 w-4 shrink-0 text-ink-faint group-hover:text-ink"
/>
</Link>
</li>
))}
</ul>
</CollapsibleBlock>
</div>
)}
</div>
</section>
);
})}
</div>
</div>
<SummaryExplorer fileId={id} items={items} />
)}
</main>
);