From dad006aa1ed396bde39b34693b22a314d7679c83 Mon Sep 17 00:00:00 2001 From: shadcn Date: Tue, 10 Mar 2026 16:55:09 +0400 Subject: [PATCH] fix --- .../app/(create)/components/accent-picker.tsx | 4 +- .../components/design-system-provider.tsx | 5 +- .../app/(create)/components/menu-picker.tsx | 32 +++++++--- apps/v4/app/(create)/hooks/use-random.tsx | 24 +++++-- apps/v4/app/(create)/lib/search-params.ts | 64 +++++++++++++++---- apps/v4/content/docs/registry/examples.mdx | 36 +++++------ apps/v4/public/schema.json | 2 +- apps/v4/registry/config.ts | 4 +- packages/shadcn/src/commands/init.ts | 10 +++ packages/shadcn/src/preset/preset.test.ts | 6 +- packages/shadcn/src/preset/preset.ts | 4 +- packages/shadcn/src/registry/schema.ts | 11 +++- .../utils/transformers/transform-menu.test.ts | 12 ++-- .../src/utils/transformers/transform-menu.ts | 10 +-- 14 files changed, 155 insertions(+), 69 deletions(-) diff --git a/apps/v4/app/(create)/components/accent-picker.tsx b/apps/v4/app/(create)/components/accent-picker.tsx index 32b5abb95..7ac45b58e 100644 --- a/apps/v4/app/(create)/components/accent-picker.tsx +++ b/apps/v4/app/(create)/components/accent-picker.tsx @@ -84,8 +84,8 @@ export function MenuAccentPicker({ closeOnClick={isMobile} disabled={ accent.value === "bold" && - (params.menuColor === "translucent" || - params.menuColor === "translucent-inverted") + (params.menuColor === "default-translucent" || + params.menuColor === "inverted-translucent") } > {accent.label} diff --git a/apps/v4/app/(create)/components/design-system-provider.tsx b/apps/v4/app/(create)/components/design-system-provider.tsx index 8f579618c..d1cb3de84 100644 --- a/apps/v4/app/(create)/components/design-system-provider.tsx +++ b/apps/v4/app/(create)/components/design-system-provider.tsx @@ -184,9 +184,10 @@ export function DesignSystemProvider({ } const isInvertedMenu = - menuColor === "inverted" || menuColor === "translucent-inverted" + menuColor === "inverted" || menuColor === "inverted-translucent" const isTranslucentMenu = - menuColor === "translucent" || menuColor === "translucent-inverted" + menuColor === "default-translucent" || + menuColor === "inverted-translucent" let frameId = 0 const updateMenuElements = () => { diff --git a/apps/v4/app/(create)/components/menu-picker.tsx b/apps/v4/app/(create)/components/menu-picker.tsx index 6119e3ec9..2d8d54251 100644 --- a/apps/v4/app/(create)/components/menu-picker.tsx +++ b/apps/v4/app/(create)/components/menu-picker.tsx @@ -18,7 +18,10 @@ import { PickerSeparator, PickerTrigger, } from "@/app/(create)/components/picker" -import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params" +import { + isTranslucentMenuColor, + useDesignSystemSearchParams, +} from "@/app/(create)/lib/search-params" type ColorChoice = "default" | "inverted" type SurfaceChoice = "solid" | "translucent" @@ -33,17 +36,17 @@ function getMenuColorValue( translucent: boolean ): MenuColorValue { if (color === "default") { - return translucent ? "translucent" : "default" + return translucent ? "default-translucent" : "default" } - return translucent ? "translucent-inverted" : "inverted" + return translucent ? "inverted-translucent" : "inverted" } const MENU_ITEMS: MenuItemConfig[] = [ { value: "default", label: "Default / Solid" }, - { value: "translucent", label: "Default / Translucent" }, + { value: "default-translucent", label: "Default / Translucent" }, { value: "inverted", label: "Inverted / Solid" }, - { value: "translucent-inverted", label: "Inverted / Translucent" }, + { value: "inverted-translucent", label: "Inverted / Translucent" }, ] const ALL_OPTIONS = MENU_ITEMS @@ -57,38 +60,47 @@ export function MenuColorPicker({ const [params, setParams] = useDesignSystemSearchParams() const { resolvedTheme } = useTheme() const mounted = useMounted() + const lastSolidMenuAccentRef = React.useRef(params.menuAccent) const isDark = mounted && resolvedTheme === "dark" const currentMenu = ALL_OPTIONS.find( (menu) => menu.value === params.menuColor ) const colorChoice: ColorChoice = params.menuColor === "inverted" || - params.menuColor === "translucent-inverted" + params.menuColor === "inverted-translucent" ? "inverted" : "default" const surfaceChoice: SurfaceChoice = - params.menuColor === "translucent" || - params.menuColor === "translucent-inverted" + params.menuColor === "default-translucent" || + params.menuColor === "inverted-translucent" ? "translucent" : "solid" + React.useEffect(() => { + if (surfaceChoice === "solid") { + lastSolidMenuAccentRef.current = params.menuAccent + } + }, [params.menuAccent, surfaceChoice]) + const setColor = (color: ColorChoice) => { const nextMenuColor = getMenuColorValue( color, surfaceChoice === "translucent" ) + setParams({ menuColor: nextMenuColor, - ...(surfaceChoice === "translucent" && { menuAccent: "subtle" }), + ...(isTranslucentMenuColor(nextMenuColor) && { menuAccent: "subtle" }), }) } const setSurface = (choice: SurfaceChoice) => { const isTranslucent = choice === "translucent" const nextMenuColor = getMenuColorValue(colorChoice, isTranslucent) + setParams({ menuColor: nextMenuColor, - ...(isTranslucent && { menuAccent: "subtle" }), + menuAccent: isTranslucent ? "subtle" : lastSolidMenuAccentRef.current, }) } diff --git a/apps/v4/app/(create)/hooks/use-random.tsx b/apps/v4/app/(create)/hooks/use-random.tsx index b15d29140..950b77344 100644 --- a/apps/v4/app/(create)/hooks/use-random.tsx +++ b/apps/v4/app/(create)/hooks/use-random.tsx @@ -18,7 +18,10 @@ import { RANDOMIZE_BIASES, type RandomizeContext, } from "@/app/(create)/lib/randomize-biases" -import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params" +import { + isTranslucentMenuColor, + useDesignSystemSearchParams, +} from "@/app/(create)/lib/search-params" function randomItem(array: readonly T[]): T { return array[Math.floor(Math.random() * array.length)] @@ -68,12 +71,25 @@ export function useRandom() { const selectedIconLibrary = locks.has("iconLibrary") ? paramsRef.current.iconLibrary : randomItem(Object.values(iconLibraries)).name - const selectedMenuAccent = locks.has("menuAccent") + const lockedMenuAccent = locks.has("menuAccent") ? paramsRef.current.menuAccent - : randomItem(MENU_ACCENTS).value + : undefined + const availableMenuColors = + !locks.has("menuColor") && lockedMenuAccent === "bold" + ? MENU_COLORS.filter((menuColor) => { + return !isTranslucentMenuColor(menuColor.value) + }) + : MENU_COLORS const selectedMenuColor = locks.has("menuColor") ? paramsRef.current.menuColor - : randomItem(MENU_COLORS).value + : randomItem(availableMenuColors).value + const selectedMenuAccent = + locks.has("menuAccent") || isTranslucentMenuColor(selectedMenuColor) + ? paramsRef.current.menuAccent === "bold" && + isTranslucentMenuColor(selectedMenuColor) + ? "subtle" + : paramsRef.current.menuAccent + : randomItem(MENU_ACCENTS).value context.theme = selectedTheme context.font = selectedFont diff --git a/apps/v4/app/(create)/lib/search-params.ts b/apps/v4/app/(create)/lib/search-params.ts index 6f7ea199c..b1679e7f2 100644 --- a/apps/v4/app/(create)/lib/search-params.ts +++ b/apps/v4/app/(create)/lib/search-params.ts @@ -118,6 +118,46 @@ 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 { + if ( + params.menuAccent === "bold" && + isTranslucentMenuColor(params.menuColor) + ) { + return { + ...params, + menuAccent: "subtle", + } + } + + return params +} + // 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. @@ -129,15 +169,13 @@ export function useDesignSystemSearchParams(options: Options = {}) { }) // If preset param exists, decode it and overlay on raw params. - const params = React.useMemo(() => { - if (rawParams.preset && isPresetCode(rawParams.preset)) { - const decoded = decodePreset(rawParams.preset) - if (decoded) { - return { ...rawParams, ...decoded } - } - } - return rawParams - }, [rawParams]) + const params = + rawParams.preset && isPresetCode(rawParams.preset) + ? normalizeDesignSystemParams({ + ...rawParams, + ...(decodePreset(rawParams.preset) ?? {}), + }) + : normalizeDesignSystemParams(rawParams) // Use ref so setParams callback stays stable across renders. const paramsRef = React.useRef(params) @@ -156,8 +194,9 @@ export function useDesignSystemSearchParams(options: Options = {}) { ) => Partial), setOptions?: Options ) => { - const resolvedUpdates = + const resolvedUpdates = normalizePartialDesignSystemParams( typeof updates === "function" ? updates(paramsRef.current) : updates + ) const hasDesignSystemUpdate = DESIGN_SYSTEM_KEYS.some( (key) => key in resolvedUpdates @@ -169,7 +208,10 @@ export function useDesignSystemSearchParams(options: Options = {}) { } // Merge current decoded values with updates. - const merged = { ...paramsRef.current, ...resolvedUpdates } + const merged = normalizeDesignSystemParams({ + ...paramsRef.current, + ...resolvedUpdates, + }) // Encode design system fields into a preset code. // Cast needed: merged values may include null from nuqs resets, diff --git a/apps/v4/content/docs/registry/examples.mdx b/apps/v4/content/docs/registry/examples.mdx index bbd58686e..b2d1b3de3 100644 --- a/apps/v4/content/docs/registry/examples.mdx +++ b/apps/v4/content/docs/registry/examples.mdx @@ -395,24 +395,24 @@ A `registry:base` item is a complete design system base. It defines the full set The `config` field accepts the following properties (all optional): -| Property | Type | Description | -| -------------------- | ---------------------------------- | --------------------------------------------------------------- | -| `style` | `string` | The style name for the base. | -| `iconLibrary` | `string` | The icon library to use (e.g. `lucide`). | -| `rsc` | `boolean` | Whether to enable React Server Components. Defaults to `false`. | -| `tsx` | `boolean` | Whether to use TypeScript. Defaults to `true`. | -| `rtl` | `boolean` | Whether to enable right-to-left support. Defaults to `false`. | -| `menuColor` | `"default" \| "inverted"` | The menu color scheme. Defaults to `"default"`. | -| `menuAccent` | `"subtle" \| "bold"` | The menu accent style. Defaults to `"subtle"`. | -| `tailwind.baseColor` | `string` | The base color name (e.g. `neutral`, `slate`, `zinc`). | -| `tailwind.css` | `string` | Path to the Tailwind CSS file. | -| `tailwind.prefix` | `string` | A prefix to add to all Tailwind classes. | -| `aliases.components` | `string` | Import alias for components. | -| `aliases.utils` | `string` | Import alias for utilities. | -| `aliases.ui` | `string` | Import alias for UI components. | -| `aliases.lib` | `string` | Import alias for lib. | -| `aliases.hooks` | `string` | Import alias for hooks. | -| `registries` | `Record` | Custom registry URLs. Keys must start with `@`. | +| Property | Type | Description | +| -------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------- | +| `style` | `string` | The style name for the base. | +| `iconLibrary` | `string` | The icon library to use (e.g. `lucide`). | +| `rsc` | `boolean` | Whether to enable React Server Components. Defaults to `false`. | +| `tsx` | `boolean` | Whether to use TypeScript. Defaults to `true`. | +| `rtl` | `boolean` | Whether to enable right-to-left support. Defaults to `false`. | +| `menuColor` | `"default" \| "inverted" \| "default-translucent" \| "inverted-translucent"` | The menu color scheme. Defaults to `"default"`. | +| `menuAccent` | `"subtle" \| "bold"` | The menu accent style. Defaults to `"subtle"`. | +| `tailwind.baseColor` | `string` | The base color name (e.g. `neutral`, `slate`, `zinc`). | +| `tailwind.css` | `string` | Path to the Tailwind CSS file. | +| `tailwind.prefix` | `string` | A prefix to add to all Tailwind classes. | +| `aliases.components` | `string` | Import alias for components. | +| `aliases.utils` | `string` | Import alias for utilities. | +| `aliases.ui` | `string` | Import alias for UI components. | +| `aliases.lib` | `string` | Import alias for lib. | +| `aliases.hooks` | `string` | Import alias for hooks. | +| `registries` | `Record` | Custom registry URLs. Keys must start with `@`. | ```json title="custom-base.json" showLineNumbers { diff --git a/apps/v4/public/schema.json b/apps/v4/public/schema.json index a402fba31..3a53bbcc5 100644 --- a/apps/v4/public/schema.json +++ b/apps/v4/public/schema.json @@ -59,7 +59,7 @@ }, "menuColor": { "type": "string", - "enum": ["default", "inverted"] + "enum": ["default", "inverted", "default-translucent", "inverted-translucent"] }, "menuAccent": { "type": "string", diff --git a/apps/v4/registry/config.ts b/apps/v4/registry/config.ts index c4de32cc9..d10e352f6 100644 --- a/apps/v4/registry/config.ts +++ b/apps/v4/registry/config.ts @@ -44,8 +44,8 @@ export type MenuAccentValue = MenuAccent["value"] export const MENU_COLORS = [ { value: "default", label: "Default" }, { value: "inverted", label: "Inverted" }, - { value: "translucent", label: "Translucent" }, - { value: "translucent-inverted", label: "Translucent Inverted" }, + { value: "default-translucent", label: "Default Translucent" }, + { value: "inverted-translucent", label: "Inverted Translucent" }, ] as const export type MenuColor = (typeof MENU_COLORS)[number] diff --git a/packages/shadcn/src/commands/init.ts b/packages/shadcn/src/commands/init.ts index 6b1e66ceb..d762c6df9 100644 --- a/packages/shadcn/src/commands/init.ts +++ b/packages/shadcn/src/commands/init.ts @@ -82,6 +82,16 @@ export const initOptionsSchema = z.object({ existingConfig: z.record(z.unknown()).optional(), installStyleIndex: z.boolean().default(true), registryBaseConfig: rawConfigSchema.deepPartial().optional(), + menuColor: z + .enum([ + "default", + "inverted", + "default-translucent", + "inverted-translucent", + ]) + .optional(), + menuAccent: z.enum(["subtle", "bold"]).optional(), + iconLibrary: z.string().optional(), }) export const init = new Command() diff --git a/packages/shadcn/src/preset/preset.test.ts b/packages/shadcn/src/preset/preset.test.ts index 3cb0d28f0..fad9d57a3 100644 --- a/packages/shadcn/src/preset/preset.test.ts +++ b/packages/shadcn/src/preset/preset.test.ts @@ -137,10 +137,10 @@ describe("encodePreset / decodePreset", () => { } }) - it("should round-trip translucent menu color", () => { - const code = encodePreset({ menuColor: "translucent" }) + it("should round-trip default-translucent menu color", () => { + const code = encodePreset({ menuColor: "default-translucent" }) const decoded = decodePreset(code) - expect(decoded!.menuColor).toBe("translucent") + expect(decoded!.menuColor).toBe("default-translucent") }) }) diff --git a/packages/shadcn/src/preset/preset.ts b/packages/shadcn/src/preset/preset.ts index 8739096e1..c5da9913c 100644 --- a/packages/shadcn/src/preset/preset.ts +++ b/packages/shadcn/src/preset/preset.ts @@ -93,8 +93,8 @@ export const PRESET_MENU_ACCENTS = ["subtle", "bold"] as const export const PRESET_MENU_COLORS = [ "default", "inverted", - "translucent", - "translucent-inverted", + "default-translucent", + "inverted-translucent", ] as const // Field definitions in pack order. Total: 43 bits, 10 bits headroom. diff --git a/packages/shadcn/src/registry/schema.ts b/packages/shadcn/src/registry/schema.ts index c9f4147c3..7501eb2cd 100644 --- a/packages/shadcn/src/registry/schema.ts +++ b/packages/shadcn/src/registry/schema.ts @@ -41,7 +41,12 @@ export const rawConfigSchema = z iconLibrary: z.string().optional(), rtl: z.coerce.boolean().default(false).optional(), menuColor: z - .enum(["default", "inverted", "translucent", "translucent-inverted"]) + .enum([ + "default", + "inverted", + "default-translucent", + "inverted-translucent", + ]) .default("default") .optional(), menuAccent: z.enum(["subtle", "bold"]).default("subtle").optional(), @@ -296,8 +301,8 @@ export const presetSchema = z.object({ menuColor: z.enum([ "default", "inverted", - "translucent", - "translucent-inverted", + "default-translucent", + "inverted-translucent", ]), radius: z.string(), }) diff --git a/packages/shadcn/src/utils/transformers/transform-menu.test.ts b/packages/shadcn/src/utils/transformers/transform-menu.test.ts index cc589bffc..8a20e218c 100644 --- a/packages/shadcn/src/utils/transformers/transform-menu.test.ts +++ b/packages/shadcn/src/utils/transformers/transform-menu.test.ts @@ -312,7 +312,7 @@ export function Component() { `) }) - describe("menuColor is translucent", () => { + describe("menuColor is default-translucent", () => { test("inlines cn-menu-translucent styles", async () => { expect( await transform( @@ -325,7 +325,7 @@ export function Component() { }`, config: { ...testConfig, - menuColor: "translucent", + menuColor: "default-translucent", }, }, [transformMenu] @@ -351,7 +351,7 @@ export function Component() { }`, config: { ...testConfig, - menuColor: "translucent", + menuColor: "default-translucent", }, }, [transformMenu] @@ -366,7 +366,7 @@ export function Component() { }) }) - describe("menuColor is translucent-inverted", () => { + describe("menuColor is inverted-translucent", () => { test("replaces cn-menu-target with dark and inlines cn-menu-translucent", async () => { expect( await transform( @@ -379,7 +379,7 @@ export function Component() { }`, config: { ...testConfig, - menuColor: "translucent-inverted", + menuColor: "inverted-translucent", }, }, [transformMenu] @@ -405,7 +405,7 @@ export function Component() { }`, config: { ...testConfig, - menuColor: "translucent-inverted", + menuColor: "inverted-translucent", }, }, [transformMenu] diff --git a/packages/shadcn/src/utils/transformers/transform-menu.ts b/packages/shadcn/src/utils/transformers/transform-menu.ts index d5b67e0b0..a162d1e79 100644 --- a/packages/shadcn/src/utils/transformers/transform-menu.ts +++ b/packages/shadcn/src/utils/transformers/transform-menu.ts @@ -8,13 +8,13 @@ const TRANSLUCENT_CLASSES = // Transforms cn-menu-target and cn-menu-translucent classes based on config.menuColor. // If menuColor is "inverted", replaces cn-menu-target with "dark" and removes cn-menu-translucent. -// If menuColor is "translucent", removes cn-menu-target and inlines cn-menu-translucent styles. -// If menuColor is "translucent-inverted", replaces cn-menu-target with "dark" and inlines cn-menu-translucent styles. +// If menuColor is "default-translucent", removes cn-menu-target and inlines cn-menu-translucent styles. +// If menuColor is "inverted-translucent", replaces cn-menu-target with "dark" and inlines cn-menu-translucent styles. // Otherwise, removes both cn-menu-target and cn-menu-translucent. export const transformMenu: Transformer = async ({ sourceFile, config }) => { const menuColor = config.menuColor const isTranslucent = - menuColor === "translucent" || menuColor === "translucent-inverted" + menuColor === "default-translucent" || menuColor === "inverted-translucent" for (const attr of sourceFile.getDescendantsOfKind(SyntaxKind.JsxAttribute)) { const attrName = attr.getNameNode().getText() @@ -38,11 +38,11 @@ export const transformMenu: Transformer = async ({ sourceFile, config }) => { let newText = text let needsCleanup = false - if (menuColor === "inverted" || menuColor === "translucent-inverted") { + if (menuColor === "inverted" || menuColor === "inverted-translucent") { // Replace cn-menu-target with "dark". newText = newText.replace(/cn-menu-target/g, "dark") } else { - // Remove cn-menu-target for both "translucent" and "default". + // Remove cn-menu-target for both "default-translucent" and "default". newText = newText.replace(/cn-menu-target/g, "") needsCleanup = true }