Skip to content

Commit a11788a

Browse files
authored
Merge branch 'main' into pr-factory/issue-9780-switch-class-style
2 parents 7f6d46d + 5b40b9d commit a11788a

File tree

631 files changed

+13471
-1499
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

631 files changed

+13471
-1499
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
description: Keep registry base and radix trees in sync when editing shared UI
3+
globs: apps/v4/registry/bases/**/*
4+
alwaysApply: false
5+
---
6+
7+
# Registry bases: Base UI ↔ Radix parity
8+
9+
`apps/v4/registry/bases/base` and `apps/v4/registry/bases/radix` are **parallel registries**. Anything that exists in both trees for the same purpose (preview blocks, mirrored examples, shared card layouts, etc.) **must stay in sync**.
10+
11+
## When editing
12+
13+
- If you change a file under **`bases/base/...`**, apply the **same behavioral and visual change** to the matching path under **`bases/radix/...`** (and the reverse).
14+
- Only diverge where APIs differ (e.g. import paths like `@/registry/bases/base/ui/*` vs `@/registry/bases/radix/ui/*`, or Base UI vs Radix component props).
15+
- Do **not** update only one side unless the user explicitly asks for a single-base change.
16+
17+
## Typical mirrored paths
18+
19+
- `blocks/preview/**` — preview cards and blocks
20+
- Parallel `ui/*` components when both exist for the same component
21+
22+
After edits, briefly confirm both trees were updated (or state why one side is intentionally unchanged).

apps/v4/app/(app)/docs/changelog/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export default function ChangelogPage() {
7979
})}
8080
{olderPages.length > 0 && (
8181
<div id="more-updates" className="mb-24 scroll-mt-24">
82-
<h2 className="font-heading mb-6 text-xl font-semibold tracking-tight">
82+
<h2 className="mb-6 font-heading text-xl font-semibold tracking-tight">
8383
More Updates
8484
</h2>
8585
<div className="grid auto-rows-fr gap-3 sm:grid-cols-2">
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
5+
import { useMounted } from "@/hooks/use-mounted"
6+
import {
7+
BASE_COLORS,
8+
getThemesForBaseColor,
9+
type ChartColorName,
10+
} from "@/registry/config"
11+
import { LockButton } from "@/app/(create)/components/lock-button"
12+
import {
13+
Picker,
14+
PickerContent,
15+
PickerGroup,
16+
PickerRadioGroup,
17+
PickerRadioItem,
18+
PickerSeparator,
19+
PickerTrigger,
20+
} from "@/app/(create)/components/picker"
21+
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
22+
23+
export function ChartColorPicker({
24+
isMobile,
25+
anchorRef,
26+
}: {
27+
isMobile: boolean
28+
anchorRef: React.RefObject<HTMLDivElement | null>
29+
}) {
30+
const mounted = useMounted()
31+
const [params, setParams] = useDesignSystemSearchParams()
32+
33+
const availableChartColors = React.useMemo(
34+
() => getThemesForBaseColor(params.baseColor),
35+
[params.baseColor]
36+
)
37+
38+
const currentChartColor = React.useMemo(
39+
() =>
40+
availableChartColors.find((theme) => theme.name === params.chartColor),
41+
[availableChartColors, params.chartColor]
42+
)
43+
44+
const currentChartColorIsBaseColor = React.useMemo(
45+
() => BASE_COLORS.find((baseColor) => baseColor.name === params.chartColor),
46+
[params.chartColor]
47+
)
48+
49+
React.useEffect(() => {
50+
if (!currentChartColor && availableChartColors.length > 0) {
51+
setParams({ chartColor: availableChartColors[0].name })
52+
}
53+
}, [currentChartColor, availableChartColors, setParams])
54+
55+
return (
56+
<div className="group/picker relative">
57+
<Picker>
58+
<PickerTrigger>
59+
<div className="flex flex-col justify-start text-left">
60+
<div className="text-xs text-muted-foreground">Chart Color</div>
61+
<div className="text-sm font-medium text-foreground">
62+
{currentChartColor?.title}
63+
</div>
64+
</div>
65+
{mounted && (
66+
<div
67+
style={
68+
{
69+
"--color":
70+
currentChartColor?.cssVars?.dark?.[
71+
currentChartColorIsBaseColor
72+
? "muted-foreground"
73+
: "primary"
74+
],
75+
} as React.CSSProperties
76+
}
77+
className="pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 rounded-full bg-(--color) select-none md:right-2.5"
78+
/>
79+
)}
80+
</PickerTrigger>
81+
<PickerContent
82+
anchor={isMobile ? anchorRef : undefined}
83+
side={isMobile ? "top" : "right"}
84+
align={isMobile ? "center" : "start"}
85+
className="max-h-92"
86+
>
87+
<PickerRadioGroup
88+
value={currentChartColor?.name}
89+
onValueChange={(value) => {
90+
setParams({ chartColor: value as ChartColorName })
91+
}}
92+
>
93+
<PickerGroup>
94+
{availableChartColors
95+
.filter((theme) =>
96+
BASE_COLORS.find((baseColor) => baseColor.name === theme.name)
97+
)
98+
.map((theme) => (
99+
<PickerRadioItem
100+
key={theme.name}
101+
value={theme.name}
102+
closeOnClick={isMobile}
103+
>
104+
{theme.title}
105+
</PickerRadioItem>
106+
))}
107+
</PickerGroup>
108+
<PickerSeparator />
109+
<PickerGroup>
110+
{availableChartColors
111+
.filter(
112+
(theme) =>
113+
!BASE_COLORS.find(
114+
(baseColor) => baseColor.name === theme.name
115+
)
116+
)
117+
.map((theme) => (
118+
<PickerRadioItem
119+
key={theme.name}
120+
value={theme.name}
121+
closeOnClick={isMobile}
122+
>
123+
{theme.title}
124+
</PickerRadioItem>
125+
))}
126+
</PickerGroup>
127+
</PickerRadioGroup>
128+
</PickerContent>
129+
</Picker>
130+
<LockButton
131+
param="chartColor"
132+
className="absolute top-1/2 right-8 -translate-y-1/2"
133+
/>
134+
</div>
135+
)
136+
}

apps/v4/app/(create)/components/customizer.tsx

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ import {
77
CardFooter,
88
CardHeader,
99
} from "@/examples/base/ui/card"
10-
import { FieldGroup } from "@/examples/base/ui/field"
11-
import { Separator } from "@/examples/base/ui/separator"
12-
import { CardTitle } from "@/examples/radix/ui/card"
10+
import { FieldGroup, FieldSeparator } from "@/examples/base/ui/field"
1311
import { type RegistryItem } from "shadcn/schema"
1412

1513
import { useIsMobile } from "@/hooks/use-mobile"
@@ -18,19 +16,19 @@ import { MenuAccentPicker } from "@/app/(create)/components/accent-picker"
1816
import { ActionMenu } from "@/app/(create)/components/action-menu"
1917
import { BaseColorPicker } from "@/app/(create)/components/base-color-picker"
2018
import { BasePicker } from "@/app/(create)/components/base-picker"
19+
import { ChartColorPicker } from "@/app/(create)/components/chart-color-picker"
2120
import { CopyPreset } from "@/app/(create)/components/copy-preset"
2221
import { FontPicker } from "@/app/(create)/components/font-picker"
2322
import { IconLibraryPicker } from "@/app/(create)/components/icon-library-picker"
2423
import { MainMenu } from "@/app/(create)/components/main-menu"
2524
import { MenuColorPicker } from "@/app/(create)/components/menu-picker"
26-
import { ProjectForm } from "@/app/(create)/components/project-form"
2725
import { RadiusPicker } from "@/app/(create)/components/radius-picker"
2826
import { RandomButton } from "@/app/(create)/components/random-button"
2927
import { ResetDialog } from "@/app/(create)/components/reset-button"
3028
import { StylePicker } from "@/app/(create)/components/style-picker"
3129
import { ThemePicker } from "@/app/(create)/components/theme-picker"
3230
import { V0Button } from "@/app/(create)/components/v0-button"
33-
import { FONTS } from "@/app/(create)/lib/fonts"
31+
import { FONT_HEADING_OPTIONS, FONTS } from "@/app/(create)/lib/fonts"
3432
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
3533

3634
export function Customizer({
@@ -57,22 +55,40 @@ export function Customizer({
5755
<MainMenu />
5856
</CardHeader>
5957
<CardContent className="no-scrollbar min-h-0 flex-1 overflow-x-auto overflow-y-hidden md:overflow-y-auto">
60-
<FieldGroup className="flex-row gap-2.5 py-px md:flex-col md:gap-3.25">
61-
<BasePicker isMobile={isMobile} anchorRef={anchorRef} />
58+
<FieldGroup className="flex-row gap-2.5 py-px **:data-[slot=field-separator]:-mx-4 **:data-[slot=field-separator]:w-auto md:flex-col md:gap-3.25">
59+
{isMobile && <BasePicker isMobile={isMobile} anchorRef={anchorRef} />}
6260
<StylePicker
6361
styles={STYLES}
6462
isMobile={isMobile}
6563
anchorRef={anchorRef}
6664
/>
65+
<FieldSeparator className="hidden md:block" />
6766
<BaseColorPicker isMobile={isMobile} anchorRef={anchorRef} />
6867
<ThemePicker
6968
themes={availableThemes}
7069
isMobile={isMobile}
7170
anchorRef={anchorRef}
7271
/>
72+
<ChartColorPicker isMobile={isMobile} anchorRef={anchorRef} />
73+
<FieldSeparator className="hidden md:block" />
74+
<FontPicker
75+
label="Heading"
76+
param="fontHeading"
77+
fonts={FONT_HEADING_OPTIONS}
78+
isMobile={isMobile}
79+
anchorRef={anchorRef}
80+
/>
81+
<FontPicker
82+
label="Font"
83+
param="font"
84+
fonts={FONTS}
85+
isMobile={isMobile}
86+
anchorRef={anchorRef}
87+
/>
88+
<FieldSeparator className="hidden md:block" />
7389
<IconLibraryPicker isMobile={isMobile} anchorRef={anchorRef} />
74-
<FontPicker fonts={FONTS} isMobile={isMobile} anchorRef={anchorRef} />
7590
<RadiusPicker isMobile={isMobile} anchorRef={anchorRef} />
91+
<FieldSeparator className="hidden md:block" />
7692
<MenuColorPicker isMobile={isMobile} anchorRef={anchorRef} />
7793
<MenuAccentPicker isMobile={isMobile} anchorRef={anchorRef} />
7894
</FieldGroup>

apps/v4/app/(create)/components/design-system-provider.tsx

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,18 +64,37 @@ export function DesignSystemProvider({
6464
history: "replace", // …or push updates into the iframe history.
6565
})
6666
const [isReady, setIsReady] = React.useState(false)
67-
const { style, theme, font, baseColor, menuAccent, menuColor, radius } =
68-
searchParams
67+
const {
68+
style,
69+
theme,
70+
font,
71+
fontHeading,
72+
baseColor,
73+
chartColor,
74+
menuAccent,
75+
menuColor,
76+
radius,
77+
} = searchParams
6978
const effectiveRadius = style === "lyra" ? "none" : radius
7079
const selectedFont = React.useMemo(
7180
() => FONTS.find((fontOption) => fontOption.value === font),
7281
[font]
7382
)
83+
const selectedHeadingFont = React.useMemo(() => {
84+
if (fontHeading === "inherit" || fontHeading === font) {
85+
return selectedFont
86+
}
87+
88+
return FONTS.find((fontOption) => fontOption.value === fontHeading)
89+
}, [font, fontHeading, selectedFont])
7490
const initialFontSansRef = React.useRef<string | null>(null)
91+
const initialFontHeadingRef = React.useRef<string | null>(null)
7592

7693
React.useEffect(() => {
7794
initialFontSansRef.current =
7895
document.documentElement.style.getPropertyValue("--font-sans")
96+
initialFontHeadingRef.current =
97+
document.documentElement.style.getPropertyValue("--font-heading")
7998

8099
return () => {
81100
removeManagedBodyClasses(document.body)
@@ -86,10 +105,18 @@ export function DesignSystemProvider({
86105
"--font-sans",
87106
initialFontSansRef.current
88107
)
89-
return
108+
} else {
109+
document.documentElement.style.removeProperty("--font-sans")
90110
}
91111

92-
document.documentElement.style.removeProperty("--font-sans")
112+
if (initialFontHeadingRef.current) {
113+
document.documentElement.style.setProperty(
114+
"--font-heading",
115+
initialFontHeadingRef.current
116+
)
117+
} else {
118+
document.documentElement.style.removeProperty("--font-heading")
119+
}
93120
}
94121
}, [])
95122

@@ -124,12 +151,29 @@ export function DesignSystemProvider({
124151
// Always set --font-sans for the preview so the selected font is visible.
125152
// The font type (sans/serif/mono) is metadata for the CLI updater.
126153
if (selectedFont) {
127-
const fontFamily = selectedFont.font.style.fontFamily
128-
document.documentElement.style.setProperty("--font-sans", fontFamily)
154+
document.documentElement.style.setProperty(
155+
"--font-sans",
156+
selectedFont.font.style.fontFamily
157+
)
158+
}
159+
160+
if (selectedHeadingFont) {
161+
document.documentElement.style.setProperty(
162+
"--font-heading",
163+
selectedHeadingFont.font.style.fontFamily
164+
)
129165
}
130166

131167
setIsReady(true)
132-
}, [style, theme, font, baseColor, selectedFont])
168+
}, [
169+
style,
170+
theme,
171+
font,
172+
fontHeading,
173+
baseColor,
174+
selectedFont,
175+
selectedHeadingFont,
176+
])
133177

134178
const registryTheme = React.useMemo(() => {
135179
if (!baseColor || !theme || !menuAccent || !effectiveRadius) {
@@ -140,12 +184,13 @@ export function DesignSystemProvider({
140184
...DEFAULT_CONFIG,
141185
baseColor,
142186
theme,
187+
chartColor,
143188
menuAccent,
144189
radius: effectiveRadius,
145190
}
146191

147192
return buildRegistryTheme(config)
148-
}, [baseColor, theme, menuAccent, effectiveRadius])
193+
}, [baseColor, theme, chartColor, menuAccent, effectiveRadius])
149194

150195
// Use useLayoutEffect for synchronous CSS var updates.
151196
React.useLayoutEffect(() => {

0 commit comments

Comments
 (0)