"use client" /** * Tokens & themes — hub client. * * Composition mirrors `Library → Library` and matches every other * primary hub (Placements / Team / Sites / Compliance): * - **Secondary panel** (`tokens`) — category scope lives in the rail * (Colors, Radius, Motion, …) via `TokensSecondaryNav`. Opening the panel * also collapses the main sidebar via `secondary-panel.tsx#openPanel`. * - `PrimaryPageTemplate` + `ListPageTemplate` — same hub frame as * Placements / Library. * - **`HubTable`** (NOT raw ``) — the canonical primitive that * wires `useTableState`, the toolbar (search + filter chips + filter * dropdown + sort), `TablePropertiesDrawerButton`, view-type tiles, * bulk-actions, and conditional rules. Hubs that drop down to raw * `` silently lose filters and Properties; do not do that. * - One view tab (`viewType: "table"`) — category scope is the panel's * job, not the view tabs'. * * Token index (`packages/ui/tokens/hooks-index.json`) is the single source of * truth; visualizers live in `tokens-themes-section.tsx`. */ import * as React from "react" import { Link, useNavigate, useSearchParams } from "react-router-dom" import { PrimaryPageTemplate } from "@/components/templates/primary-page-template" import { PageHeader } from "@/components/page-header" import { Button } from "@/components/ui/button" import { Tip } from "@/components/ui/tip" import { Kbd, KbdGroup } from "@/components/ui/kbd" import { Shortcut } from "@/components/ui/dropdown-menu" import { useModKeyLabel, useAltKeyLabel } from "@/hooks/use-mod-key-label" import { useProductDashboardHref } from "@/contexts/product-route-sync" import { useProduct } from "@/contexts/product-context" import { productPersistKey } from "@/stores/app-store" import { KeyMetrics, type MetricInsight, type MetricItem, } from "@/components/key-metrics" import { HubTable, ListPageBoardCard, ListPageTemplate, type ViewTab, } from "@/components/data-views" import type { ListPageBoardColumnDef } from "@/components/data-views/list-page-board-template" import type { ColumnDef } from "@/components/data-table/types" import { FULL_HUB_SUPPORTED_VIEWS } from "@/lib/data-list-view-registry" import { buildTokensHubRenderers, renderTokenListRow, type TokenHubRow, } from "@/components/tokens-hub-auxiliary-views" import { Badge } from "@/components/ui/badge" import { CATEGORY_COUNTS, CATEGORY_TABS, DEPRECATED_COUNT, TOKENS_INDEX, categoryPreview, primaryValueText, type TokenCategory, } from "@/components/tokens-themes-section" import { readTokensCategory, TOKENS_ALL_CATEGORY, type TokensCategoryParam, } from "@/components/tokens-themes-section" /** Row shape consumed by `DataTable` — flat fields make built-in search work out of the box. */ type TokenRow = TokenHubRow & { category: TokenCategory | string } /** Build all token rows once at module load (token index is static at runtime). */ const TOKEN_ROWS: TokenRow[] = (() => { const out: TokenRow[] = [] for (const [name, record] of Object.entries(TOKENS_INDEX.tokens)) { out.push({ id: name, name, namespace: record.namespace, category: record.category, value: primaryValueText(record), deprecated: Boolean(record.deprecated), record, }) } return out.sort((a, b) => a.name.localeCompare(b.name)) })() /** Pre-bucket rows by category so each panel selection slices in O(1). */ const ROWS_BY_CATEGORY = (() => { const map = new Map() map.set(TOKENS_ALL_CATEGORY, TOKEN_ROWS) for (const tab of CATEGORY_TABS) { map.set( tab.id as TokensCategoryParam, TOKEN_ROWS.filter((r) => tab.matches(String(r.category))), ) } return map })() /** Namespace select-filter options — built once from the index. */ const NAMESPACE_OPTIONS = TOKENS_INDEX.namespaces .slice() .sort() .map((ns) => ({ value: ns, label: ns })) const STATUS_OPTIONS = [ { value: "active", label: "Active" }, { value: "deprecated", label: "Deprecated" }, ] const TOKENS_HEADER_SUBTITLE = `${TOKENS_INDEX.tokenCount} tokens · ${TOKENS_INDEX.namespaces.length} namespaces · v${TOKENS_INDEX.version}` const TOKENS_VIEW_TABS: ViewTab[] = [ { id: "tokens-table", label: "Tokens", viewType: "table", icon: "fa-table", filterId: "all", }, ] /** Same seven views as Library / Column types — each has a renderer on `HubTable` below. */ const TOKENS_SUPPORTED_VIEWS = FULL_HUB_SUPPORTED_VIEWS /** * Canonical KPI shape (matches `placement-kpi.ts` precedent): * - every `MetricItem` is clickable — tiles drive the secondary-panel * category by pushing `?category=…`, the same URL the panel rail uses, * - a `MetricInsight` summarizes the current scope on the right. * See `apps/web/docs/kpi-flat-band-pattern.md` + `exxat-kpi-trends.mdc`. */ function buildMetrics( navigate: (category: TokensCategoryParam) => void, ): MetricItem[] { return [ { id: "total", label: "Total tokens", value: TOKENS_INDEX.tokenCount, delta: "", trend: "neutral", trendPolarity: "informational", metricVariant: "hero", description: `${TOKENS_INDEX.namespaces.length} namespaces`, onClick: () => navigate(TOKENS_ALL_CATEGORY), }, { id: "color", label: "Color tokens", value: CATEGORY_COUNTS.color ?? 0, delta: "", trend: "neutral", trendPolarity: "informational", description: "semantic + alias", onClick: () => navigate("color" as TokensCategoryParam), }, { id: "motion", label: "Motion tokens", value: CATEGORY_COUNTS.transition ?? 0, delta: "", trend: "neutral", trendPolarity: "informational", description: "easings + durations", onClick: () => navigate("motion" as TokensCategoryParam), }, { id: "deprecated", label: "Deprecated", value: DEPRECATED_COUNT, delta: "", trend: "neutral", trendPolarity: "lower_is_better", description: DEPRECATED_COUNT > 0 ? "scheduled for removal" : "none scheduled for removal", onClick: () => navigate("deprecated" as TokensCategoryParam), }, ] } function buildInsight(activeCategory: TokensCategoryParam, rowCount: number): MetricInsight { const label = categoryDisplayLabel(activeCategory) return { title: activeCategory === TOKENS_ALL_CATEGORY ? "Token index" : `${label} in scope`, description: activeCategory === TOKENS_ALL_CATEGORY ? `${rowCount.toLocaleString()} tokens across ${TOKENS_INDEX.namespaces.length} namespaces. Click any KPI above to scope by category, or use the rail on the left for finer slicing.` : `${rowCount.toLocaleString()} ${label.toLowerCase()}. Filter by namespace or status from the table toolbar, or jump back to the full index from the rail.`, severity: "info", actionLabel: "Ask Leo", } } /** Friendly display label for the category currently scoped from the panel. */ function categoryDisplayLabel(category: TokensCategoryParam): string { if (category === TOKENS_ALL_CATEGORY) return "All tokens" return CATEGORY_TABS.find((c) => c.id === category)?.label ?? "Tokens" } /* ── Cell renderers ───────────────────────────────────────────────────── */ function useClipboard() { const [copied, setCopied] = React.useState(null) const copy = React.useCallback((text: string) => { if (typeof navigator === "undefined" || !navigator.clipboard) return navigator.clipboard.writeText(text).then(() => { setCopied(text) window.setTimeout(() => setCopied((c) => (c === text ? null : c)), 1200) }).catch(() => {}) }, []) return { copied, copy } } function TokenNameCell({ row, onCopy, copiedNow, }: { row: TokenRow onCopy: (text: string) => void copiedNow: boolean }) { const cssRef = `var(${row.name})` // Icon-only per-row Copy is mouse-only (`tabIndex={-1}`). With ~163 token // rows, making each row's hidden Copy a default tabstop produces a wall of // sequential focus stops in the table body — keyboard users perceive it as // a "Tab trap" and never reach the page chrome (sidebar drill-in, header // ⋯ menu, ⌘K trigger). WCAG 2.4.3 (Focus Order) + Exxat DS rule // `exxat-kbd-shortcuts.mdc` rule 6 ("skip dense tables, icon-only row // actions that already have aria-label"). Keyboard users keep the visible // `` text (selectable by native browser copy) and can use ⌘K table // search to scope quickly; mouse users still see/click the button on // hover. return (
{row.name}