spb/company-atlas
Public
Python 66.3%
TypeScript 22.7%
JavaScript 8.6%
HTML 1.4%
CSS 0.7%
1import Link from 'next/link';2import type { ReactNode } from 'react';3import { cn } from '@/lib/cn';4import { fmtInt } from '@/lib/format';56export type BarRow = { key: string; label: ReactNode; value: number; href?: string; color?: string; hint?: ReactNode };78/** Horizontal bars (server-renderable): label · bar · value. Sorted by the caller. */9export function Bars({ rows, className, format = fmtInt, max: maxOverride, dense = false }: { rows: BarRow[]; className?: string; format?: (v: number) => string; max?: number; dense?: boolean }) {10 const max = maxOverride ?? Math.max(1, ...rows.map((r) => Math.abs(r.value)));11 if (!rows.length) return null;12 return (13 <ul className={cn('space-y-1', className)}>14 {rows.map((r) => {15 const pct = Math.min(100, (Math.abs(r.value) / max) * 100);16 const inner = (17 <>18 <span className={cn('min-w-0 truncate', dense ? 'text-xs' : 'text-[13px]')}>{r.label}</span>19 <span className="meter">20 <span style={{ width: `${pct}%`, background: r.color ?? (r.value < 0 ? 'var(--danger)' : 'var(--accent)') }} />21 </span>22 <span className={cn('tnum text-right text-ink', dense ? 'text-xs' : 'text-[13px]')}>{format(r.value)}</span>23 {r.hint !== undefined && <span className="col-span-3 text-[11px] text-ink-3">{r.hint}</span>}24 </>25 );26 const cls = cn('grid grid-cols-[minmax(0,1fr)_minmax(4rem,2fr)_auto] items-center gap-x-3', dense ? 'py-0.5' : 'py-1');27 return (28 <li key={r.key}>29 {r.href ? (30 <Link href={r.href} className={cn(cls, 'text-ink-2 hover:text-accent')}>31 {inner}32 </Link>33 ) : (34 <div className={cn(cls, 'text-ink-2')}>{inner}</div>35 )}36 </li>37 );38 })}39 </ul>40 );41}4243/** Tiny vertical column strip (e.g. events per day) — SVG, server-renderable. */44export function Columns({ values, width = 120, height = 28, className, color = 'var(--accent)', ariaLabel }: { values: number[]; width?: number; height?: number; className?: string; color?: string; ariaLabel?: string }) {45 const max = Math.max(1, ...values);46 const n = values.length || 1;47 const gap = 1;48 const bw = Math.max(1, (width - gap * (n - 1)) / n);49 return (50 <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} className={cn('block shrink-0', className)} role={ariaLabel ? 'img' : undefined} aria-label={ariaLabel} aria-hidden={ariaLabel ? undefined : true}>51 {values.map((v, i) => {52 const h = Math.max(v > 0 ? 1.5 : 0, (v / max) * (height - 1));53 return <rect key={i} x={i * (bw + gap)} y={height - h} width={bw} height={h} fill={color} opacity={v > 0 ? 0.85 : 0.2} rx={0.5} />;54 })}55 </svg>56 );57}58