import { Badge } from '@/components/badge'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/tabs'; import { KindBadge } from '~/chrome/ui/badges'; import { compare, norm, slotProps, SETS, type AxisRow, type FigmaSet, type RowKind, } from '../figma/spec'; import { type Messages, useMessages } from '../i18n'; import { entries } from '../registry'; import type { Entry } from '../registry/types'; import { Link, useParam } from '../router'; import { ParityFindings } from './parity-findings'; /** * Figma parity — what CBAR's file draws against what the kit implements. * * `figma-spec.json` is a capture of every component set on the canvas, taken * with the local bridge plugin (`figma_sets --page "*"`) and reduced by * `scripts/gen-figma-spec.mjs`. It is data, not a schema: re-run the generator * after the design file moves and this page re-reads it. * * The point is that a divergence stops being something you notice by eye on a * particular component and becomes a row in a table. Everything here is * document-wide; a single component's own properties, and a live comparison * against the variant on the canvas, are on its page (`figma/figma-panel.tsx`). */ interface SetReport { figma: FigmaSet; entry?: Entry; rows: AxisRow[]; } /* * The tint lives in `chrome/ui/badges.tsx`, shared with the per-component Figma * panel; only the wording is local. That split is deliberate: this page's * `state` row says "state axis" because the axis is what is being reported, * while the panel's says "drawn in CSS" because the rendering is. Same colour, * different sentence. */ const LABEL: Record = { match: 'match', diff: 'diff', state: 'state', 'no-prop': 'noProp', extra: 'extra', }; function Values({ label, values }: { label: string; values: string[] }) { if (!values.length) return null; return (
{label} {values.map((v) => ( {v} ))}
); } /** * Two views of the same subject, so they share a page rather than a rail entry. * * The tab lives in the hash like every other toggle here, which is what makes * `#/parity?tab=findings` a link somebody can be sent — and the findings text * exists to be linked to. */ /** * Two views of one subject, so they share a page rather than a rail entry: the * generated diff, and the write-up of what has actually been resolved. * * The tab lives in the hash like every other toggle here, which is what makes * `#/parity?tab=findings` a link somebody can be sent — and the findings text * exists to be linked to. */ export function ParityPage() { const [tab, setTab] = useParam('tab', 'report'); const m = useMessages(); const TABS = [ ['report', m.parity.tabs.report], ['findings', m.parity.tabs.findings], ] as const; return (

{m.header.foundations}

{m.parity.title}

{TABS.map(([value, label]) => ( {label} ))}
); } function ParityReport() { const m = useMessages(); const byFigmaSet = new Map(); for (const entry of entries) { if (entry.figma) byFigmaSet.set(norm(entry.figma.set), entry); } const reports: SetReport[] = SETS.map((figma) => ({ figma, entry: byFigmaSet.get(norm(figma.set)), rows: compare(figma, byFigmaSet.get(norm(figma.set))), })); const linked = reports.filter((r) => r.entry); const unlinked = reports.filter((r) => !r.entry); const kitOnly = entries.filter((e) => !e.figma); const differing = linked.filter((r) => r.rows.some((x) => x.kind === 'diff' || x.kind === 'no-prop')); return (

{m.parity.lead}

{m.parity.stateNote}

{m.parity.slotsNote}

{[ [m.parity.stats.sets, SETS.length], [m.parity.stats.linked, linked.length], [m.parity.stats.gaps, differing.length], [m.parity.stats.kitOnly, kitOnly.length], ].map(([label, value]) => (
{label}
{value}
))}

{m.parity.linkedSets}

{linked.map(({ figma, entry, rows }, i) => ( /* CBAR draws two separate RadioMark sets on the same page, so name + page is not unique. */
{figma.set} {m.parity.pageMeta(figma.page, figma.variants)}
{entry ? ( {entry.name} → ) : null}
    {rows.map((row, i) => (
  • {m.parity.badge[LABEL[row.kind]]} {row.figmaAxis ?? row.kitAxis} {row.kitAxis && row.figmaAxis && norm(row.kitAxis) !== norm(row.figmaAxis) ? ` → ${row.kitAxis}` : ''}
    {row.kind === 'match' ? ( ) : null} {row.kind === 'state' ? ( ) : null}
  • ))}
{slotProps(figma).length ? (
{m.parity.slots}
{m.parity.values.takenAsChildren} {slotProps(figma).map((slot) => ( {slot.name} {slot.type === 'BOOLEAN' ? slot.default === true ? ' ✓' : ' ✗' : slot.type === 'TEXT' ? ' ✎' : ' ⇄'} ))}
) : null}
))}

{m.parity.unlinkedTitle(unlinked.length)}

{m.parity.unlinkedNote}

    {unlinked.map(({ figma }, i) => (
  • {figma.set} {' '} · {m.parity.setMeta(figma.page, figma.variants)}
  • ))}

{m.parity.kitOnlyTitle(kitOnly.length)}

{m.parity.kitOnlyNote}

{/* `asChild` on the kit's Badge: its `outline` treatment already lights up on hover when the rendered element is an anchor, which is exactly what a pill that navigates should do. */}
    {kitOnly.map((entry) => (
  • {entry.name}
  • ))}
); }