import * as React from "react" import { useSearchParams } from "next/navigation" import { useQueryStates } from "nuqs" import { createLoader, createSerializer, parseAsBoolean, parseAsInteger, parseAsString, parseAsStringLiteral, type inferParserType, type Options, } from "nuqs/server" import { decodePreset, isPresetCode } from "shadcn/preset" import { BASE_COLORS, BASES, DEFAULT_CONFIG, getThemesForBaseColor, iconLibraries, MENU_ACCENTS, MENU_COLORS, RADII, STYLES, THEMES, type BaseColorName, type BaseName, type ChartColorName, type FontHeadingValue, type FontValue, type IconLibraryName, type MenuAccentValue, type MenuColorValue, type RadiusValue, type StyleName, type ThemeName, } from "@/registry/config" import { FONTS } from "@/app/(app)/(create)/lib/fonts" import { getPresetCode } from "@/app/(app)/(create)/lib/preset-code" import { resolvePresetOverrides } from "@/app/(app)/(create)/lib/preset-query" const designSystemSearchParams = { preset: parseAsString.withDefault("b0"), base: parseAsStringLiteral(BASES.map((b) => b.name)).withDefault( DEFAULT_CONFIG.base ), item: parseAsString.withDefault("preview-02").withOptions({ shallow: true }), iconLibrary: parseAsStringLiteral( Object.values(iconLibraries).map((i) => i.name) ).withDefault(DEFAULT_CONFIG.iconLibrary), style: parseAsStringLiteral(STYLES.map((s) => s.name)).withDefault( DEFAULT_CONFIG.style ), theme: parseAsStringLiteral(THEMES.map((t) => t.name)).withDefault( DEFAULT_CONFIG.theme ), chartColor: parseAsStringLiteral( THEMES.map((t) => t.name) ).withDefault(DEFAULT_CONFIG.chartColor ?? "neutral"), font: parseAsStringLiteral(FONTS.map((f) => f.value)).withDefault( DEFAULT_CONFIG.font ), fontHeading: parseAsStringLiteral([ "inherit", ...FONTS.map((f) => f.value), ]).withDefault(DEFAULT_CONFIG.fontHeading), baseColor: parseAsStringLiteral( BASE_COLORS.map((b) => b.name) ).withDefault(DEFAULT_CONFIG.baseColor), menuAccent: parseAsStringLiteral( MENU_ACCENTS.map((a) => a.value) ).withDefault(DEFAULT_CONFIG.menuAccent), menuColor: parseAsStringLiteral( MENU_COLORS.map((m) => m.value) ).withDefault(DEFAULT_CONFIG.menuColor), radius: parseAsStringLiteral( RADII.map((r) => r.name) ).withDefault("default"), template: parseAsStringLiteral([ "next", "next-monorepo", "start", "start-monorepo", "react-router", "react-router-monorepo", "vite", "vite-monorepo", "astro", "astro-monorepo", "laravel", ] as const).withDefault("next"), rtl: parseAsBoolean.withDefault(false), pointer: parseAsBoolean.withDefault(false), size: parseAsInteger.withDefault(100), custom: parseAsBoolean.withDefault(false), } // Design system param keys that get encoded into the preset code. const DESIGN_SYSTEM_KEYS = [ "style", "baseColor", "theme", "chartColor", "iconLibrary", "font", "fontHeading", "radius", "menuAccent", "menuColor", ] as const function normalizeFontHeading( font: FontValue, fontHeading: FontHeadingValue ): FontHeadingValue { // Persist "same as body" as an explicit inherit sentinel so the body font // can change later without freezing headings to a concrete previous value. return fontHeading === font ? "inherit" : fontHeading } // Non-design-system keys that get passed through as-is. // `base` is not encoded in preset codes — it's an architectural choice, not visual. const NON_DESIGN_SYSTEM_KEYS = [ "base", "item", "preset", "template", "rtl", "pointer", "size", "custom", ] as const // Shared across hook instances so only one mount effect encodes the initial URL. let hasSyncedPresetToUrl = false export const loadDesignSystemSearchParams = createLoader( designSystemSearchParams ) export const serializeDesignSystemSearchParams = createSerializer( designSystemSearchParams ) export type DesignSystemSearchParams = inferParserType< typeof designSystemSearchParams > export function isTranslucentMenuColor( menuColor?: MenuColorValue | null ): menuColor is "default-translucent" | "inverted-translucent" { return ( menuColor === "default-translucent" || menuColor === "inverted-translucent" ) } function normalizePartialDesignSystemParams( params: Partial ): Partial { if ( params.menuAccent === "bold" && isTranslucentMenuColor(params.menuColor ?? undefined) ) { return { ...params, menuAccent: "subtle", } } return params } function normalizeDesignSystemParams( params: DesignSystemSearchParams ): DesignSystemSearchParams { let result = { ...params, fontHeading: normalizeFontHeading(params.font, params.fontHeading), } // Validate theme and chartColor against baseColor. if (result.baseColor) { const available = getThemesForBaseColor(result.baseColor) const themeValid = available.some((t) => t.name === result.theme) const chartColorValid = available.some((t) => t.name === result.chartColor) if (!themeValid || !chartColorValid) { const fallback = (available[0]?.name ?? result.baseColor) as ThemeName result = { ...result, ...(!themeValid && { theme: fallback }), ...(!chartColorValid && { chartColor: fallback as ChartColorName }), } } } if ( result.menuAccent === "bold" && isTranslucentMenuColor(result.menuColor) ) { return { ...result, menuAccent: "subtle", } } return result } // If preset param exists, decode it and overlay on raw params. // V1 presets don't encode chartColor — fall back to the colored // theme that base-color themes originally borrowed charts from. type SearchParamsLike = Pick function resolvePresetParams( rawParams: DesignSystemSearchParams, searchParams: SearchParamsLike ) { if (rawParams.preset && isPresetCode(rawParams.preset)) { const decoded = decodePreset(rawParams.preset) if (decoded) { const presetOverrides = resolvePresetOverrides(searchParams, decoded) return normalizeDesignSystemParams({ ...decoded, ...presetOverrides, base: rawParams.base, item: rawParams.item, preset: rawParams.preset, template: rawParams.template, rtl: rawParams.rtl, pointer: rawParams.pointer, size: rawParams.size, custom: rawParams.custom, }) } } return normalizeDesignSystemParams(rawParams) } export function buildPresetUrlUpdate( merged: DesignSystemSearchParams, resolvedUpdates: Partial = {} ) { const code = getPresetCode(merged) const rawUpdate: Record = { preset: code } for (const key of DESIGN_SYSTEM_KEYS) { rawUpdate[key] = null } for (const key of NON_DESIGN_SYSTEM_KEYS) { if (key === "preset") { continue } rawUpdate[key] = key in resolvedUpdates ? (resolvedUpdates as Record)[key] : merged[key] } return rawUpdate } // Wraps nuqs useQueryStates with transparent preset encoding/decoding. // - Reads: if ?preset=CODE is in the URL, decodes it and returns individual values. // - Writes: when design system params are set, encodes them into a preset code. // // Default options use shallow: true so picker selections do not trigger a full // Next.js server navigation. This prevents the customizer panel from flickering // or resetting while the URL update propagates (#10910, #11060). export function useDesignSystemSearchParams(options: Options = {}) { const searchParams = useSearchParams() const [rawParams, rawSetParams] = useQueryStates(designSystemSearchParams, { shallow: true, history: "push", ...options, }) const params = React.useMemo( () => resolvePresetParams(rawParams, searchParams), [rawParams, searchParams] ) // Use ref so setParams callback stays stable across renders. const paramsRef = React.useRef(params) React.useEffect(() => { paramsRef.current = params }, [params]) React.useEffect(() => { if (hasSyncedPresetToUrl || searchParams.has("preset")) { return } hasSyncedPresetToUrl = true const merged = normalizeDesignSystemParams(paramsRef.current) void rawSetParams(buildPresetUrlUpdate(merged) as RawSetParamsInput, { history: "replace", }) }, [rawSetParams, searchParams]) type RawSetParamsInput = Parameters[0] const setParams = React.useCallback( ( updates: | Partial | (( old: DesignSystemSearchParams ) => Partial), setOptions?: Options ) => { const resolvedUpdates = normalizePartialDesignSystemParams( typeof updates === "function" ? updates(paramsRef.current) : updates ) const hasDesignSystemUpdate = DESIGN_SYSTEM_KEYS.some( (key) => key in resolvedUpdates ) if (!hasDesignSystemUpdate) { // No design system change, pass through directly. return rawSetParams(resolvedUpdates as RawSetParamsInput, setOptions) } // Merge current decoded values with updates. const merged = normalizeDesignSystemParams({ ...paramsRef.current, ...resolvedUpdates, }) return rawSetParams( buildPresetUrlUpdate(merged, resolvedUpdates) as RawSetParamsInput, setOptions ) }, [rawSetParams] ) return [params, setParams] as const }