spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
1'use client';2import { ChevronDown } from 'lucide-react';3import { useEffect, useState, type ReactNode } from 'react';4import { t } from '@/i18n';5import { cn } from '@/lib/cn';67/**8 * A subtopic block on long chart pages (compare topic tabs, country topic pages). Collapsed blocks keep the9 * information visible through `summary` (a compact table of latest values, no charts); expanded blocks mount10 * `children` (the charts, which lazy-fetch on scroll). A `#id` hash in the URL (jump nav) opens the block.11 * Heading level 3; `id` is the anchor used by `SubtopicJumpNav`.12 */13export function CollapsibleGroup({ id, title, count, defaultOpen = false, summary, children, className }: { id: string; title: ReactNode; count: number; defaultOpen?: boolean; summary: ReactNode; children: ReactNode; className?: string }) {14 const [open, setOpen] = useState(defaultOpen);15 useEffect(() => {16 const check = () => {17 if (typeof window !== 'undefined' && window.location.hash === `#${id}`) setOpen(true);18 };19 check();20 window.addEventListener('hashchange', check);21 return () => window.removeEventListener('hashchange', check);22 }, [id]);23 return (24 <section id={id} className={cn('hairline min-w-0 scroll-mt-32 pt-3 pb-4 md:pt-4 md:pb-6', className)} aria-labelledby={`${id}-h`}>25 <h3 id={`${id}-h`} className="min-w-0">26 <button type="button" aria-expanded={open} aria-controls={`${id}-body`} onClick={() => setOpen((o) => !o)} className="group/g -mx-1 flex min-h-[44px] w-full items-center justify-between gap-3 rounded-sm px-1 text-left hover:bg-surface-2/60">27 <span className="display min-w-0 truncate text-xl text-ink md:text-2xl">{title}</span>28 <span className="flex shrink-0 items-center gap-2 text-xs text-ink-3">29 <span className="tnum">{t('group.count', { n: count })}</span>30 <span className="hidden text-accent sm:inline">{open ? t('group.collapse') : t('group.expand')}</span>31 <ChevronDown size={16} aria-hidden className={cn('text-ink-3 transition-transform group-hover/g:text-ink', open && 'rotate-180')} />32 </span>33 </button>34 </h3>35 <div id={`${id}-body`} className="mt-2 min-w-0">36 {open ? children : summary}37 </div>38 </section>39 );40}4142/** In-page jump nav for the subtopic blocks: horizontal chip scroller on phones, wrapped on desktop. */43export function SubtopicJumpNav({ items, label, className }: { items: Array<{ id: string; label: string; count: number }>; label?: string; className?: string }) {44 if (items.length < 2) return null;45 return (46 <nav aria-label={label ?? t('group.jump')} className={cn('-mx-4 px-4 sm:mx-0 sm:px-0', className)}>47 <ul className="scrollbar-none flex gap-1.5 overflow-x-auto py-1 sm:flex-wrap">48 {items.map((it) => (49 <li key={it.id} className="shrink-0">50 <a href={`#${it.id}`} className="inline-flex h-11 items-center gap-1.5 whitespace-nowrap rounded-sm border border-rule px-3 text-sm text-ink-2 hover:border-accent hover:text-accent md:h-9 md:px-2.5">51 {it.label}52 <span className="tnum text-2xs text-ink-3">{it.count}</span>53 </a>54 </li>55 ))}56 </ul>57 </nav>58 );59}60