Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 118 additions & 111 deletions apps/web/src/pages/browse/[title].astro
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export async function getStaticPaths() {
const { titleNum, entries } = Astro.props;
const titleName = TITLE_NAMES[titleNum] ?? `Title ${titleNum}`;

// Load per-section change index (generated by generate-diffs.ts)
// Load per-section change index
let changedSections: Record<string, string[]> = {};
try {
const { readFile } = await import('node:fs/promises');
Expand All @@ -39,13 +39,12 @@ try {
// No change data available yet
}

/** Check if a section was changed in any release point */
function sectionChangedIn(section: string): string[] {
const key = `title-${titleNum}/${section}`;
return changedSections[key] ?? [];
}

// Group entries by chapter, sort sections within each chapter
// Group entries by chapter, sort sections within each
const byChapter = new Map<number, typeof entries>();
for (const entry of entries) {
const ch = entry.data.chapter;
Expand All @@ -55,7 +54,6 @@ for (const entry of entries) {
byChapter.get(ch)!.push(entry);
}

// Sort chapters numerically, sort sections within each
const sortedChapters = [...byChapter.entries()].sort((a, b) => a[0] - b[0]);
for (const [, chapterEntries] of sortedChapters) {
chapterEntries.sort((a, b) => {
Expand All @@ -67,9 +65,55 @@ for (const [, chapterEntries] of sortedChapters) {
}

const base = import.meta.env.BASE_URL;

// Determine which chapters to auto-expand (small titles expand all)
const autoExpand = sortedChapters.length <= 10;

// Pre-compute ALL template data in frontmatter to avoid const-in-map esbuild issues
interface SectionView {
id: string;
uscSection: string;
displayTitle: string;
isInactive: boolean;
statusLabel: string | null;
lastChange: string | null;
lastChangeLabel: string | null;
}

interface ChapterView {
chapterNum: number;
activeCount: number;
inactiveCount: number;
chapterHint: string | undefined;
sections: SectionView[];
}

const chapters: ChapterView[] = sortedChapters.map(([chapterNum, chapterEntries]) => {
const activeCount = chapterEntries.filter(e => (e.data.status ?? 'active') === 'active').length;
const inactiveCount = chapterEntries.length - activeCount;
const firstActive = chapterEntries.find(e => (e.data.status ?? 'active') === 'active');
const chapterHint = firstActive
? firstActive.data.title.replace(/^Section \S+ - /, '').split(/[;,—]/)[0]?.trim().slice(0, 50)
: undefined;

const sections: SectionView[] = chapterEntries.map(entry => {
const sTitle = entry.data.title;
const status = entry.data.status ?? 'active';
const isInactive = status !== 'active' || sTitle.includes('Repealed') || sTitle.includes('Reserved') || sTitle.includes('Omitted') || sTitle.includes('Transferred') || sTitle.includes('Renumbered');
const statusLabel = status !== 'active' ? status.charAt(0).toUpperCase() + status.slice(1) : (
sTitle.includes('Repealed') ? 'Repealed' :
sTitle.includes('Reserved') ? 'Reserved' :
sTitle.includes('Omitted') ? 'Omitted' :
sTitle.includes('Renumbered') ? 'Renumbered' :
sTitle.includes('Transferred') ? 'Transferred' : null
);
const changes = sectionChangedIn(`section-${entry.data.usc_section}`);
const lastChange = changes.length > 0 ? changes[changes.length - 1] ?? null : null;
const lastChangeLabel = lastChange ? lastChange.replace('pl-', 'PL ').replace('-', '-') : null;
const displayTitle = sTitle.replace(/^Section \S+ - /, '');
return { id: entry.id, uscSection: entry.data.usc_section, displayTitle, isInactive, statusLabel, lastChange, lastChangeLabel };
});

return { chapterNum, activeCount, inactiveCount, chapterHint, sections };
});
---

<BaseLayout
Expand All @@ -92,14 +136,12 @@ const autoExpand = sortedChapters.length <= 10;
Click a chapter to expand its sections, or click a section to view its full text and change history.
</p>

<!-- In-title search filter -->
{entries.length > 20 && (
<div class="not-prose mt-4">
<TitleFilter client:idle placeholder={`Filter ${entries.length} sections...`} />
</div>
)}

<!-- Expand/Collapse all toggle -->
{sortedChapters.length > 5 && (
<div class="not-prose mt-3 mb-2 flex gap-2 font-sans text-xs">
<button
Expand All @@ -115,115 +157,80 @@ const autoExpand = sortedChapters.length <= 10;
</div>
)}

<!-- Expandable TOC -->
<div id="title-toc" class="not-prose mt-4 space-y-1">
{sortedChapters.map(([chapterNum, chapterEntries]) => {
const activeCount = chapterEntries.filter(e => {
const s = e.data.status ?? 'active';
return s === 'active';
}).length;
const inactiveCount = chapterEntries.length - activeCount;
// Derive chapter topic from first active section's title
const firstActive = chapterEntries.find(e => (e.data.status ?? 'active') === 'active');
const chapterHint = firstActive
? firstActive.data.title.replace(/^Section \S+ - /, '').split(/[;,—]/)[0]?.trim().slice(0, 50)
: undefined;

return (
<details
id={`chapter-${chapterNum}`}
class="group rounded border border-gray-200 dark:border-gray-800"
open={autoExpand}
>
<summary class="flex cursor-pointer items-center gap-3 px-4 py-2.5 font-sans text-sm transition-colors hover:bg-gray-50 dark:hover:bg-gray-900 select-none">
<svg class="h-3 w-3 shrink-0 text-gray-400 transition-transform group-open:rotate-90" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
</svg>
<span class="font-mono text-xs text-slate dark:text-gray-500 w-10 shrink-0 text-right">
Ch. {chapterNum}
</span>
<span class="flex items-baseline gap-2 min-w-0">
{chapters.map(ch => (
<details
id={`chapter-${ch.chapterNum}`}
class="group rounded border border-gray-200 dark:border-gray-800"
open={autoExpand}
>
<summary class="flex cursor-pointer items-center gap-3 px-4 py-2.5 font-sans text-sm transition-colors hover:bg-gray-50 dark:hover:bg-gray-900 select-none">
<svg class="h-3 w-3 shrink-0 text-gray-400 transition-transform group-open:rotate-90" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
</svg>
<span class="font-mono text-xs text-slate dark:text-gray-500 w-10 shrink-0 text-right">
Ch. {ch.chapterNum}
</span>
<span class="flex items-baseline gap-2 min-w-0">
<a
href={`${base}browse/title-${titleNum}/chapter-${ch.chapterNum}/`}
class="shrink-0 font-medium text-navy hover:underline dark:text-gray-200"
>
Chapter {ch.chapterNum}
</a>
{ch.chapterHint && (
<span class="truncate text-xs text-gray-400 dark:text-gray-600 hidden sm:inline">{ch.chapterHint}</span>
)}
</span>
<span class="ml-auto flex items-center gap-2 text-xs text-gray-400">
{ch.inactiveCount > 0 && (
<span class="text-gray-300 dark:text-gray-600">{ch.inactiveCount} inactive</span>
)}
<span>{ch.activeCount} section{ch.activeCount !== 1 ? 's' : ''}</span>
</span>
</summary>

<ul class="divide-y divide-gray-100 border-t border-gray-200 dark:divide-gray-800 dark:border-gray-800">
{ch.sections.map(s => (
<li>
<a
href={`${base}browse/title-${titleNum}/chapter-${chapterNum}/`}
class="shrink-0 font-medium text-navy hover:underline dark:text-gray-200"
onclick="event.stopPropagation()"
href={`${base}statute/${s.id}/`}
class:list={[
'flex items-center gap-3 px-4 py-1.5 pl-12 text-xs transition-colors hover:bg-gray-50 dark:hover:bg-gray-900',
s.isInactive && 'opacity-50',
]}
>
Chapter {chapterNum}
{s.lastChange ? (
<span class="h-1.5 w-1.5 shrink-0 rounded-full bg-teal" title={`Changed in ${s.lastChangeLabel}`} aria-label="Recently changed"></span>
) : (
<span class="h-1.5 w-1.5 shrink-0"></span>
)}
<span class="w-14 shrink-0 text-right font-mono text-[11px] text-slate dark:text-gray-500">
&sect; {s.uscSection}
</span>
<span
class:list={['truncate text-gray-700 dark:text-gray-300', s.isInactive && 'italic']}
title={s.displayTitle}
>
{s.displayTitle}
</span>
{s.statusLabel ? (
<span class="ml-auto shrink-0 rounded bg-gray-100 px-1 py-0.5 text-[9px] font-medium text-gray-500 dark:bg-gray-800 dark:text-gray-500">
{s.statusLabel}
</span>
) : s.lastChangeLabel ? (
<span class="ml-auto shrink-0 text-[9px] text-teal dark:text-teal-bright">
{s.lastChangeLabel}
</span>
) : null}
</a>
{chapterHint && (
<span class="truncate text-xs text-gray-400 dark:text-gray-600 hidden sm:inline">{chapterHint}</span>
)}
</span>
<span class="ml-auto flex items-center gap-2 text-xs text-gray-400">
{inactiveCount > 0 && (
<span class="text-gray-300 dark:text-gray-600">{inactiveCount} inactive</span>
)}
<span>{activeCount} section{activeCount !== 1 ? 's' : ''}</span>
</span>
</summary>

<ul class="divide-y divide-gray-100 border-t border-gray-200 dark:divide-gray-800 dark:border-gray-800">
{chapterEntries.map((entry) => {
const sTitle = entry.data.title;
const status = entry.data.status ?? 'active';
const isInactive = status !== 'active' || sTitle.includes('Repealed') || sTitle.includes('Reserved') || sTitle.includes('Omitted') || sTitle.includes('Transferred') || sTitle.includes('Renumbered');
const statusLabel = status !== 'active' ? status.charAt(0).toUpperCase() + status.slice(1) : (
sTitle.includes('Repealed') ? 'Repealed' :
sTitle.includes('Reserved') ? 'Reserved' :
sTitle.includes('Omitted') ? 'Omitted' :
sTitle.includes('Renumbered') ? 'Renumbered' :
sTitle.includes('Transferred') ? 'Transferred' : null
);
const changes = sectionChangedIn(`section-${entry.data.usc_section}`);
const lastChange = changes.length > 0 ? changes[changes.length - 1] : null;

return (
<li>
<a
href={`${base}statute/${entry.id}/`}
class:list={[
'flex items-center gap-3 px-4 py-1.5 pl-12 text-xs transition-colors hover:bg-gray-50 dark:hover:bg-gray-900',
isInactive && 'opacity-50',
]}
>
{lastChange ? (
<span class="h-1.5 w-1.5 shrink-0 rounded-full bg-teal" title={`Changed in ${lastChange.replace('pl-', 'PL ').replace('-', '-')}`} aria-label="Recently changed"></span>
) : (
<span class="h-1.5 w-1.5 shrink-0"></span>
)}
<span class="w-14 shrink-0 text-right font-mono text-[11px] text-slate dark:text-gray-500">
&sect; {entry.data.usc_section}
</span>
{@const displayTitle = sTitle.replace(/^Section \S+ - /, '')}
<span
class:list={[
'truncate text-gray-700 dark:text-gray-300',
isInactive && 'italic',
]}
title={displayTitle}
>
{displayTitle}
</span>
{statusLabel ? (
<span class="ml-auto shrink-0 rounded bg-gray-100 px-1 py-0.5 text-[9px] font-medium text-gray-500 dark:bg-gray-800 dark:text-gray-500">
{statusLabel}
</span>
) : lastChange ? (
<span class="ml-auto shrink-0 text-[9px] text-teal dark:text-teal-bright">
{lastChange.replace('pl-', 'PL ').replace('-', '-')}
</span>
) : null}
</a>
</li>
);
})}
</ul>
</details>
);
})}
</li>
))}
</ul>
</details>
))}
</div>

<!-- URL hash support: auto-expand chapter from #chapter-N and scroll to it -->
<script is:inline>
(function () {
function openFromHash() {
Expand Down
Loading