SPB Git forge

spb/countryatlas

Public
20commits 1branches 0releases
268.3 MBsize
maindefault branch
12 days agolast push
TypeScript 57% Python 38.6% JavaScript 3.6% CSS 0.6%
3.2 KB · 60 lines tsx
Raw Blame History
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