mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-11 09:51:40 +00:00
fix
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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<T>(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
|
||||
|
||||
@@ -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<DesignSystemSearchParams>
|
||||
): Partial<DesignSystemSearchParams> {
|
||||
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<DesignSystemSearchParams>),
|
||||
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,
|
||||
|
||||
@@ -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<string, string \| object>` | 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<string, string \| object>` | Custom registry URLs. Keys must start with `@`. |
|
||||
|
||||
```json title="custom-base.json" showLineNumbers
|
||||
{
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
},
|
||||
"menuColor": {
|
||||
"type": "string",
|
||||
"enum": ["default", "inverted"]
|
||||
"enum": ["default", "inverted", "default-translucent", "inverted-translucent"]
|
||||
},
|
||||
"menuAccent": {
|
||||
"type": "string",
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user