{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "readable-text", "title": "Readable Text", "description": "APCA contrast guard: renders text in an accent color while it stays readable on the current glass surface, and swaps to a readable foreground otherwise. Reactive to the live glass tint and light/dark.", "dependencies": [], "registryDependencies": [ "@sistine/oklch-utils", "@sistine/theme" ], "files": [ { "path": "components/readable-text.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n apcaContrast,\n formatOklch,\n glassSurface,\n type OklchColor,\n parseOklch,\n READABLE_USAGE,\n type ReadableUsage,\n readableForeground,\n} from \"@/lib/oklch-utils\";\n\n/**\n * Readable foreground for a KNOWN surface color — pure and memoized (no DOM read, no observer), so it\n * scales to hundreds of off-theme surfaces (e.g. colored tool-call bubbles) at negligible cost.\n * Returns an oklch() string banded for `usage` (ARC Bronze; default \"body\"). For theme-surface text,\n * prefer the CSS tokens (text-foreground / -soft / -strong) or .\n *\n * const fg = useReadableForeground({ l: 70, c: 0.18, h: 50 }, \"ui\"); // readable icon on orange\n */\nexport function useReadableForeground(surface: string | OklchColor, usage: ReadableUsage = \"body\"): string {\n const css = typeof surface === \"string\" ? surface : formatOklch(surface);\n return React.useMemo(() => {\n const s = parseOklch(css);\n return s\n ? formatOklch(\n readableForeground(s, {\n usage,\n }),\n )\n : \"inherit\";\n }, [\n css,\n usage,\n ]);\n}\n\nconst cssNum = (cs: CSSStyleDeclaration, name: string, fallback: number): number => {\n const v = Number.parseFloat(cs.getPropertyValue(name));\n return Number.isNaN(v) ? fallback : v;\n};\n\n/**\n * Dynamic APCA contrast guard. Renders its text in `accent` (a CSS custom-property name such as\n * \"--primary\") *while that color stays readable* on the surface, otherwise a soft readable foreground\n * via readableForeground — legible without the pure-black/white spike. The surface is the live glass\n * (reactive to tint + light/dark) by default; pass `on` (an oklch string) to band against an arbitrary\n * surface, e.g. an off-theme bubble. `usage` selects the ARC-Bronze band. Reuses lib/oklch-utils.\n */\nexport function ReadableText({\n accent,\n on,\n usage,\n minLc,\n className,\n children,\n}: {\n /** CSS custom-property name to use when readable, e.g. \"--primary\". */\n accent: string;\n /** Surface to judge against — an oklch() string. Defaults to the live glass surface. */\n on?: string;\n /** ARC-Bronze band (\"body\" | \"large\" | \"ui\" | …) — sets the keep-accent floor + the soft fallback. */\n usage?: ReadableUsage;\n /** Minimum |APCA Lc| to keep the accent. Defaults to the usage floor, else 45. */\n minLc?: number;\n className?: string;\n children: React.ReactNode;\n}) {\n const [color, setColor] = React.useState();\n\n React.useEffect(() => {\n const root = document.documentElement;\n const floor = minLc ?? (usage ? READABLE_USAGE[usage].floor : 45);\n const pick = () => {\n const cs = getComputedStyle(root);\n const intended = parseOklch(cs.getPropertyValue(accent).trim());\n if (!intended) return;\n const dark = root.classList.contains(\"dark\");\n // `on` (a fixed surface color) bands against an off-theme surface; otherwise the live glass.\n const surface = on\n ? parseOklch(on)\n : glassSurface(dark, {\n h: cssNum(cs, \"--glass-tint-h\", intended.h),\n c: cssNum(cs, \"--glass-tint-c\", 0),\n a: cssNum(cs, \"--glass-tint-a\", 0),\n });\n if (!surface) return;\n const readable = Math.abs(apcaContrast(intended, surface)) >= floor;\n setColor(\n formatOklch(\n readable\n ? intended\n : readableForeground(surface, {\n usage: usage ?? \"body\",\n }),\n ),\n );\n };\n pick();\n const observer = new MutationObserver(pick);\n observer.observe(root, {\n attributes: true,\n attributeFilter: [\n \"style\",\n \"class\",\n \"data-glass-tint\",\n ],\n });\n return () => observer.disconnect();\n }, [\n accent,\n on,\n usage,\n minLc,\n ]);\n\n return (\n \n {children}\n \n );\n}\n", "type": "registry:component", "target": "components/readable-text.tsx" } ], "type": "registry:component" }