"use client" /** * Tokens & themes — visualizer primitives + token index. * * This module exports the building blocks consumed by `tokens-themes-client.tsx` * (which wires them into `PrimaryPageTemplate` + `ListPageTemplate`). It does * NOT render the page shell. * * Each category gets a representation that matches the token's nature: * * | Tab | Renders tokens as… | * |--------------|---------------------------------------------------------------| * | Colors | 56-px swatch (`background: var(--name)`) | * | Gradients | 96×40 fill swatch (paint-based, value usually multi-line) | * | Radius | 64×64 muted box with `border-radius: var(--name)` | * | Size | bar with `height: var(--name)` (scaled visually) | * | Shadow | floating mini-card with `box-shadow: var(--name)` | * | Typography | "Aa Sample" in `font-family: var(--name)` | * | Motion | a dot that translates on hover using `transition: var(--name)`| * | Aliases | `name → var(--target)` row (resolves the indirection) | * | Other | raw text value | * * All tiles share click-to-copy on the `var(--name)` reference. The token * index is the single source of truth: `packages/ui/tokens/hooks-index.json`. */ import * as React from "react" import { useTheme } from "@exxatdesignux/ui/hooks/use-color-scheme" import tokensIndex from "@exxatdesignux/ui/tokens/hooks-index.json" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Tip } from "@/components/ui/tip" import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" import { cn } from "@/lib/utils" /* ── Token index types ────────────────────────────────────────────────── */ export type TokenCategory = | "color" | "gradient" | "radius" | "size" | "shadow" | "typography" | "transition" | "alias" | "other" export type TokenRecord = { namespace: string category: TokenCategory | string values: Record tailwindUtilities?: string[] deprecated?: boolean deprecatedMessage?: string | null } type TokensIndex = { version: string tokenCount: number namespaces: string[] themeKeys: string[] tokens: Record } export const TOKENS_INDEX = tokensIndex as unknown as TokensIndex /** First available theme value — used for the "raw value" text under each tile. */ export function primaryValueText(t: TokenRecord): string { return t.values.light ?? Object.values(t.values)[0] ?? "" } /* ── Category tab catalogue ────────────────────────────────────────────── */ export interface CategoryTabDef { id: TokenCategory label: string icon: string matches: (cat: string) => boolean } /** * URL value for "show everything". Centralised here so consumers (sidebar * drill-in, header subtitle, client query reader) all agree on the default. * * Lives next to `CATEGORY_TABS` because reading the active category from a * `URLSearchParams` (`readTokensCategory`) needs to validate against the same * tab list. Before the `SidebarDrillIn` rewrite these helpers lived in * `tokens-secondary-nav.tsx`; that file is gone. */ export const TOKENS_ALL_CATEGORY = "all" as const export type TokensCategoryParam = "all" | TokenCategory /** Read the active category from a `URLSearchParams`. Falls back to `"all"`. */ export function readTokensCategory(params: URLSearchParams | null): TokensCategoryParam { const raw = (params?.get("category") ?? "").toLowerCase() if (raw === TOKENS_ALL_CATEGORY) return TOKENS_ALL_CATEGORY const match = CATEGORY_TABS.find((c) => c.id === raw) return match ? (match.id as TokenCategory) : TOKENS_ALL_CATEGORY } export const CATEGORY_TABS: CategoryTabDef[] = [ { id: "color", label: "Colors", icon: "fa-palette", matches: (c) => c === "color" }, { id: "gradient", label: "Gradients", icon: "fa-circle-half-stroke", matches: (c) => c === "gradient" }, { id: "radius", label: "Radius", icon: "fa-rectangle-vertical", matches: (c) => c === "radius" }, { id: "size", label: "Size", icon: "fa-ruler-horizontal", matches: (c) => c === "size" }, { id: "shadow", label: "Shadow", icon: "fa-clone", matches: (c) => c === "shadow" }, { id: "typography", label: "Typography", icon: "fa-text-size", matches: (c) => c === "typography" }, { id: "transition", label: "Motion", icon: "fa-wave-sine", matches: (c) => c === "transition" }, { id: "alias", label: "Aliases", icon: "fa-link", matches: (c) => c === "alias" }, { id: "other", label: "Other", icon: "fa-hashtag", matches: (c) => c === "other" }, ] /** Pre-compute counts per category — same shape `getTabCount(filterId)` expects. */ export const CATEGORY_COUNTS: Record = CATEGORY_TABS.reduce( (acc, tab) => { acc[tab.id] = 0; return acc }, {} as Record, ) export const DEPRECATED_COUNT = (() => { let n = 0 for (const t of Object.values(TOKENS_INDEX.tokens)) { if (t.deprecated) n += 1 for (const tab of CATEGORY_TABS) { if (tab.matches(t.category)) { CATEGORY_COUNTS[tab.id] += 1 break } } } return n })() /* ── Theme switcher ────────────────────────────────────────────────────── */ export function TokensThemeSwitcher({ className }: { className?: string }) { const { theme = "system", setTheme } = useTheme() const [mounted, setMounted] = React.useState(false) React.useEffect(() => setMounted(true), []) const value = mounted ? theme : "system" return ( {(["light", "dark", "system"] as const).map((t) => (