"use client" /** * ProductWordmark + ProductMark — render any product brand as a logo. * * - `ProductWordmark` renders `${prefix} ${suffix}` as HTML text: * • `prefix` (e.g. "Exxat") in `font-sans` extra-bold (Inter 800), neutral. * • `suffix` (e.g. "One" / "Prism" / "Pulse") in **Ivy Presto Italic** * (`var(--font-heading)`, Adobe Fonts kit `wuk5wqn` preloaded in * `app/layout.tsx`) tinted with `brandColor`. * * We render real font glyphs rather than baked-in SVG paths so a new product * only needs `{ prefix, suffix, brandColor }` — no path-tracing required. * * - `ProductMark` renders the same "E"-style circular mark used by Exxat, * recolored with the brand's gradient / fill. The SVG geometry stays * constant so existing layouts keep working. * * `variant="mutedSuffix"` (sidebar / switcher): prefix recedes in dark mode; * suffix always uses `wordmarkColor` (Exxat pink) for brand recognition. */ import * as React from "react" import { cn } from "@/lib/utils" import type { ProductBrandConfig } from "@/lib/product-brand" export type ProductWordmarkVariant = "default" | "mutedSuffix" export interface ProductWordmarkProps { config: ProductBrandConfig variant?: ProductWordmarkVariant className?: string } /* ── Wordmark ──────────────────────────────────────────────────────────────── */ /** * Inline product wordmark. Sizing is height-driven — the parent sets the * height (e.g. `className="h-7"`) and the text scales via `text-[...]` * derived from `--wordmark-size` (set inline from the rendered font-size). * * Use `aria-hidden` because the wordmark is decorative — pair it with an * `aria-label` on the trigger/link (see {@link productBrandLabel}). */ export function ProductWordmark({ config, variant: _variant = "default", className, }: ProductWordmarkProps) { const prefix = config.prefix ?? "Exxat" const { suffix, brandColor, wordmarkColor } = config const suffixColor = wordmarkColor ?? brandColor return ( ) } /* ── Circular mark ─────────────────────────────────────────────────────────── */ export interface ProductMarkProps { config: ProductBrandConfig className?: string cutoutColor?: string } /** * Generate a stable id suffix for SVG gradient defs so multiple marks on the * same page never collide. Strip colons because IDs in HTML/SVG can't legally * include them (Radix uses `:`-style IDs by default). */ function useMarkGradientId(brandId: string) { const raw = React.useId().replace(/:/g, "") return `pmk-${brandId.replace(/[^a-z0-9-]/gi, "")}-${raw}` } /** * Defer SVG `` (gradient refs) until after mount so server HTML matches * the first client paint. `useId()` returns different suffixes in SSR vs CSR * trees that conditionally mount the sidebar. */ function useBrowserPaintReady() { const [ready, setReady] = React.useState(false) React.useLayoutEffect(() => { setReady(true) }, []) return ready } /** * Recoloured Exxat "E" mark. Same geometry as the canonical brand mark, so * existing pixel-aligned layouts (sidebar header, dropdown rows) don't shift. * * Fills: * - Outer circle: `markGradient` if provided, else flat `brandColor`. * - Inner shadow plate: `markShadow` (defaults to `brandColor`). * - Cut-out "E" strokes: always white in product chrome (sidebar / switcher). */ export function ProductMark({ config, className, cutoutColor = "white" }: ProductMarkProps) { const ready = useBrowserPaintReady() const gradId = useMarkGradientId(config.id) const [from, to] = config.markGradient ?? [config.brandColor, config.brandColor] const shadow = config.markShadow ?? config.brandColor // No size default. Callers MUST set explicit dimensions (`size-7`, `h-full // w-auto`, etc.). A `size-*` default here loses to a downstream `h-full / // w-auto` only when `tailwind-merge` correctly identifies `size-7` as a // `w-7 + h-7` shorthand — fragile across versions and causes the mark to // render at the default size instead of the parent's height (see // `ExxatProductLogo` h-full mark → 32 px in h-8 parent). Aspect-square stays // so the mark renders as a circle when only one of width/height is set. const sharedClass = cn( "box-border block aspect-square shrink-0 flex-none object-contain", className, ) if (!ready) { return ( ) } return ( ) } /* ── Mark + wordmark combo ─────────────────────────────────────────────────── */ export interface ProductLogoProps { config: ProductBrandConfig variant?: ProductWordmarkVariant /** Render only the mark (omit the wordmark). */ markOnly?: boolean /** Render only the wordmark (omit the mark). */ wordmarkOnly?: boolean className?: string /** Class applied to the inner mark — useful for sizing it independently. */ markClassName?: string /** Class applied to the inner wordmark. */ wordmarkClassName?: string } /** * Mark + wordmark composed inline. Pass `markOnly` for collapsed sidebar / * favicon-like contexts, or `wordmarkOnly` if you've already rendered the * mark separately (e.g. switcher dropdown rows). */ export function ProductLogo({ config, variant = "default", markOnly = false, wordmarkOnly = false, className, markClassName, wordmarkClassName, }: ProductLogoProps) { if (markOnly) { return } if (wordmarkOnly) { return } return ( ) }