File size: 8,562 Bytes
d91cbff 1714902 d91cbff 1714902 d91cbff cd7e224 d91cbff 1714902 d91cbff 1714902 d91cbff cd7e224 d91cbff 1714902 d91cbff 1714902 d91cbff 1714902 cd7e224 1714902 d91cbff 77dd060 1714902 d91cbff 1714902 77dd060 cd7e224 d91cbff | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | 'use client';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { JURISDICTION_COLORS, JURISDICTION_LABELS } from '@/lib/constants';
import type { SectionContent } from '@/lib/types';
interface Props {
content: SectionContent | null;
isLoading: boolean;
onClose: () => void;
onNodeNavigate: (sectionId: string, jurisdiction: string) => void;
onChatAboutSection: (sectionId: string, title: string, docName: string, jurisdiction: string) => void;
}
export function SectionDrawer({
content,
isLoading,
onClose,
onNodeNavigate,
onChatAboutSection,
}: Props) {
const isOpen = isLoading || content !== null;
const color = content ? JURISDICTION_COLORS[content.jurisdiction] ?? '#888' : '#888';
return (
<div
className={`absolute z-20 flex min-h-0 flex-col overflow-hidden bg-[#141414]/95 shadow-[0_22px_70px_rgba(0,0,0,0.4)] backdrop-blur transition-[opacity,transform] duration-300 ease-out
max-lg:inset-x-0 max-lg:bottom-0 max-lg:h-[70%] max-lg:rounded-t-[24px] max-lg:border-t max-lg:border-white/10
lg:inset-x-3 lg:bottom-3 lg:max-h-[42%] lg:border lg:border-white/[0.07]
${isOpen ? 'pointer-events-auto translate-y-0 opacity-100' : 'pointer-events-none translate-y-full lg:translate-y-3 opacity-0'}
`}
aria-hidden={!isOpen}
>
<div className="my-3 h-1.5 w-12 shrink-0 self-center rounded-full bg-zinc-800 lg:hidden" />
{content ? (
<header className="flex shrink-0 items-start justify-between gap-3 border-b border-white/[0.06] px-4 py-3">
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<span className="font-mono text-[10px] uppercase tracking-[0.22em] text-white/30">Selected statute</span>
<span
className="px-1.5 py-0.5 font-mono text-[9px] uppercase tracking-[0.14em]"
style={{ backgroundColor: `${color}24`, color }}
>
{JURISDICTION_LABELS[content.jurisdiction] ?? content.jurisdiction}
</span>
{content.effective_date ? (
<span className="font-mono text-[9px] uppercase tracking-[0.14em] text-white/25">
Effective {content.effective_date}
</span>
) : null}
</div>
<p className="mt-1 truncate text-sm font-semibold text-white/90">
Sec {content.section_id} / {content.title}
</p>
<p className="mt-0.5 truncate text-[11px] text-white/30">{content.doc_name}</p>
</div>
<div className="flex shrink-0 items-center gap-3">
{content.source_url ? (
<a
href={content.source_url}
target="_blank"
rel="noopener noreferrer"
className="inline-block font-mono text-[9px] uppercase tracking-[0.18em] text-white/30 transition-[color,transform] duration-150 ease-out hover:text-[#4f98a3] active:scale-[0.97]"
>
View PDF
</a>
) : null}
<button
onClick={onClose}
className="text-white/30 transition-[color,transform] duration-150 ease-out hover:text-white/70 active:scale-[0.97]"
aria-label="Close section drawer"
type="button"
>
x
</button>
</div>
</header>
) : null}
{isLoading ? (
<div className="flex flex-1 items-center justify-center">
<div className="flex items-center gap-2 font-mono text-[10px] uppercase tracking-[0.24em] text-white/30">
{[0, 1, 2].map(i => (
<span
key={i}
className="h-1.5 w-1.5 animate-pulse rounded-full bg-white/20"
style={{ animationDelay: `${i * 150}ms` }}
/>
))}
Loading section
</div>
</div>
) : null}
{!isLoading && content ? (
<>
<div className="ledger-scroll min-h-0 flex-1 overflow-y-auto px-4 py-3 text-[14px] leading-7 text-white/75">
{content.chunks.map((chunk, index) => (
<article key={chunk.chunk_id} className={index > 0 ? 'mt-6 border-t border-white/[0.05] pt-6' : ''}>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
p: ({ children }) => <p className="mb-4 last:mb-0">{children}</p>,
ul: ({ children }) => <ul className="mb-4 list-disc space-y-2 pl-5 last:mb-0">{children}</ul>,
ol: ({ children }) => <ol className="mb-4 list-decimal space-y-2 pl-5 last:mb-0">{children}</ol>,
li: ({ children }) => <li className="pl-1">{children}</li>,
strong: ({ children }) => <strong className="font-semibold text-white/90">{children}</strong>,
table: ({ children }) => (
<div className="ledger-scroll mb-4 overflow-x-auto last:mb-0">
<table className="min-w-full border-collapse text-left text-[13px] leading-6">
{children}
</table>
</div>
),
thead: ({ children }) => <thead className="border-b border-white/15 text-white/85">{children}</thead>,
tbody: ({ children }) => <tbody className="divide-y divide-white/10">{children}</tbody>,
th: ({ children }) => <th className="px-3 py-2 font-semibold">{children}</th>,
td: ({ children }) => <td className="px-3 py-2 align-top text-white/65">{children}</td>,
code: ({ children, className }) => (
<code className={`rounded bg-white/10 px-1.5 py-0.5 font-mono text-[0.92em] text-white/80 ${className ?? ''}`}>
{children}
</code>
),
}}
>
{chunk.text}
</ReactMarkdown>
</article>
))}
</div>
<footer className="flex shrink-0 flex-col gap-4 border-t border-white/[0.06] px-4 py-3 max-lg:pb-24 lg:flex-row lg:items-center lg:gap-3 lg:py-2.5">
<div className="flex min-w-0 flex-1 items-center gap-3">
<span className="shrink-0 font-mono text-[9px] uppercase tracking-[0.14em] text-white/25 lg:hidden">Related</span>
<div className="ledger-scroll flex min-w-0 flex-1 gap-1.5 overflow-x-auto">
{content.connected_sections.slice(0, 10).map((section, index) => (
<button
key={`${section.section_id}-${index}`}
onClick={() => onNodeNavigate(section.section_id, section.jurisdiction)}
className="shrink-0 border border-white/[0.08] bg-white/[0.02] px-2 py-1 font-mono text-[9px] uppercase tracking-[0.14em] text-white/40 transition-[background-color,border-color,color,transform] duration-150 ease-out hover:border-white/20 hover:bg-white/[0.05] hover:text-white/75 active:scale-[0.97]"
type="button"
style={{
borderColor: section.edge_type.startsWith('DERIVED') ? 'rgba(232,175,52,0.28)' : undefined,
}}
>
Sec {section.section_id}
</button>
))}
</div>
</div>
<button
onClick={() =>
onChatAboutSection(content.section_id, content.title, content.doc_name, content.jurisdiction)
}
className="flex w-full items-center justify-between border border-[#4f98a3]/35 bg-[#4f98a3]/10 px-4 py-3 text-[13px] font-medium text-[#9ed4dc] transition-[background-color,border-color,transform] duration-150 ease-out hover:border-[#4f98a3]/70 hover:bg-[#4f98a3]/16 active:scale-[0.97] lg:w-auto lg:px-3 lg:py-1.5 lg:text-[11px]"
type="button"
>
<span>Chat about this section</span>
<svg suppressHydrationWarning xmlns="http://www.w3.org/2000/svg" height="18px" viewBox="0 -960 960 960" width="18px" fill="#e3e3e3">
<path d="M630-444H192v-72h438L429-717l51-51 288 288-288 288-51-51 201-201Z" />
</svg>
</button>
</footer>
</>
) : null}
</div>
);
}
|