"use client" /** * BrandColorPicker — single-anchor palette popover. * * Why "one anchor per family" instead of 16 shades: * The product theme system derives **all** chrome tints (sidebar bg, accent, * secondary, muted, hover, ring, etc.) from a single `--custom-product-brand-color` * variable using OKLCH `from var(…)` formulas. Letting users pick a specific * shade (Pink 500 vs Pink 700) was misleading because the renderer * re-tints everything anyway. One anchor per family is what actually maps * to product reality: "this product is the blue one", not "this product is * Blue 750". * * Layout: * - Header: preview chip + family label + hex + optional Reset link. * - Body: 10 anchor tiles (one per palette family), 5 across, 2 rows. * Each tile carries a small "used by X" pip overlay when another product * already claims that anchor — see `usedBy`. * - Footer: hue sidebar — custom picks change **hue only** (fixed L/C) so * theme tints and Settings panel washes stay predictable. * * `usedBy` is a `{ [anchorHex]: productLabel }` map. Callers compute it from * the active brand registry / overrides so users can see at a glance that * picking Pink would clash with another product. The pip uses the same color * as the anchor on a contrasting background, with a tooltip naming the * product. */ import * as React from "react" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { Button } from "@/components/ui/button" import { Tip } from "@/components/ui/tip" import { EXXAT_PALETTE_BY_FAMILY, type ExxatPaletteFamilyId, type ExxatPaletteSwatch, } from "@/lib/exxat-palette" import { cn } from "@/lib/utils" import { brandColorsEquivalent } from "@/lib/brand-color-match" import { brandAccentOklchFromHue, BRAND_HUE_GRADIENT, isPaletteAnchorColor, normalizeBrandAccentColor, parseOklchHue, } from "@/lib/brand-accent-color" /** Anchor shade — `500` is the canonical "true" tone for each palette family. */ const ANCHOR_SHADE = "500" /** * Palette families that are **brand-color candidates** for the picker. * * Excludes `sapphireGrayBlack` and `neutral` because both are gray-scale * families (chroma ≈ 0.005–0.04) intended for UI text / borders, not for * product identity. When the theme chrome formula * (`oklch(from var(--custom-product-brand-color) 0.965 0.018 h)`) is fed a * near-gray colour, the resulting tint is visually indistinguishable from * white — which is what the user was hitting with "Sapphire doesn't apply * like the rest of the colours". The Custom hex input still accepts any CSS * colour, so off-palette greys remain reachable. */ const BRAND_FAMILY_IDS: ReadonlyArray = [ "exxatPink", "exxatBlue", "exxatIndigo", "purple", "teal", "green", "orange", "red", ] /** * Resolved anchor list — one swatch per **brand** palette family at * `ANCHOR_SHADE`. Kept as a module constant so multiple picker instances share * the same array identity (cheap referential equality for downstream memo). */ export const BRAND_PICKER_ANCHORS: ReadonlyArray = EXXAT_PALETTE_BY_FAMILY.filter(family => BRAND_FAMILY_IDS.includes(family.id)).flatMap( family => family.swatches.filter(swatch => swatch.shade === ANCHOR_SHADE), ) /** Find the anchor whose stored value matches `value` (hex or OKLCH). */ function findAnchor(value: string | null | undefined): ExxatPaletteSwatch | undefined { if (!value) return undefined const v = value.trim().toLowerCase() if (!v) return undefined return BRAND_PICKER_ANCHORS.find( s => s.hex.toLowerCase() === v || s.oklch.toLowerCase() === v, ) } function BrandHueSlider({ id, hue, onHueChange, }: { id: string hue: number onHueChange: (hue: number) => void }) { return (
) } export interface BrandColorPickerProps { /** Current brand color (any CSS color — palette OKLCH, hex, or free text). */ value: string /** Fired when the user selects a palette swatch or commits a custom value. */ onChange: (value: string) => void /** * Optional registry / theme default. When provided and different from * `value`, the popover surfaces a "Reset" link in the header that calls * `onChange(defaultValue)`. */ defaultValue?: string /** * Anchors already claimed by other products. Key by lower-case hex **or** * lower-case OKLCH string (both forms checked). The matching tile shows a * small pip + tooltip naming the product. */ usedBy?: Readonly> /** Trigger id — wire to a `