"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 (
{prefix}
{suffix}
)
}
/* ── 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 (
)
}