Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(shadcn): light-dark support #6664

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 33 additions & 68 deletions apps/v4/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,74 +5,39 @@
@custom-variant dark (&:is(.dark *));

:root {
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.87 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.87 0 0);
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
}

.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
--card: light-dark(oklch(1 0 0), oklch(0.145 0 0));
--card-foreground: light-dark(oklch(0.145 0 0), oklch(0.985 0 0));
--popover: light-dark(oklch(1 0 0), oklch(0.145 0 0));
--popover-foreground: light-dark(oklch(0.145 0 0), oklch(0.985 0 0));
--primary: light-dark(oklch(0.205 0 0), oklch(0.985 0 0));
--primary-foreground: light-dark(oklch(0.985 0 0), oklch(0.205 0 0));
--secondary: light-dark(oklch(0.97 0 0), oklch(0.269 0 0));
--secondary-foreground: light-dark(oklch(0.205 0 0), oklch(0.985 0 0));
--muted: light-dark(oklch(0.97 0 0), oklch(0.269 0 0));
--muted-foreground: light-dark(oklch(0.556 0 0), oklch(0.708 0 0));
--accent: light-dark(oklch(0.97 0 0), oklch(0.269 0 0));
--accent-foreground: light-dark(oklch(0.205 0 0), oklch(0.985 0 0));
--destructive: light-dark(oklch(0.577 0.245 27.325), oklch(0.396 0.141 25.723));
--destructive-foreground: light-dark(oklch(0.577 0.245 27.325), oklch(0.637 0.237 25.331));
--border: light-dark(oklch(0.922 0 0), oklch(0.269 0 0));
--input: light-dark(oklch(0.922 0 0), oklch(0.269 0 0));
--ring: light-dark(oklch(0.87 0 0), oklch(0.439 0 0));
--chart-1: light-dark(oklch(0.646 0.222 41.116), oklch(0.488 0.243 264.376));
--chart-2: light-dark(oklch(0.6 0.118 184.704), oklch(0.696 0.17 162.48));
--chart-3: light-dark(oklch(0.398 0.07 227.392), oklch(0.769 0.188 70.08));
--chart-4: light-dark(oklch(0.828 0.189 84.429), oklch(0.627 0.265 303.9));
--chart-5: light-dark(oklch(0.769 0.188 70.08), oklch(0.645 0.246 16.439));
--radius: 0.625rem;;
--sidebar: light-dark(oklch(0.985 0 0), oklch(0.205 0 0));
--sidebar-foreground: light-dark(oklch(0.145 0 0), oklch(0.985 0 0));
--sidebar-primary: light-dark(oklch(0.205 0 0), oklch(0.488 0.243 264.376));
--sidebar-primary-foreground: light-dark(oklch(0.985 0 0), oklch(0.985 0 0));
--sidebar-accent: light-dark(oklch(0.97 0 0), oklch(0.269 0 0));
--sidebar-accent-foreground: light-dark(oklch(0.205 0 0), oklch(0.985 0 0));
--sidebar-border: light-dark(oklch(0.922 0 0), oklch(0.269 0 0));
--sidebar-ring: light-dark(oklch(0.87 0 0), oklch(0.439 0 0));
--background: light-dark(oklch(1 0 0), oklch(0.145 0 0));
--foreground: light-dark(oklch(0.145 0 0), oklch(0.985 0 0));
}

.theme-login-one {
Expand Down
142 changes: 113 additions & 29 deletions packages/shadcn/src/utils/updaters/update-css-vars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,54 +353,138 @@ function updateCssVarsPluginV4(
return {
postcssPlugin: "update-css-vars-v4",
Once(root: Root) {
Object.entries(cssVars).forEach(([key, vars]) => {
const selector = key === "light" ? ":root" : `.${key}`
let newCssVars = new Map<
string,
{ light?: string; dark?: string; color: boolean }
>()

function extractVars(selector: string, key: "light" | "dark") {
root.walkRules(selector, (rule) => {
rule.walkDecls((decl) => {
if (!decl.prop.startsWith("--") || !isColorValue(decl.value)) return
const name = decl.prop.substring(2)
if (!newCssVars.has(name)) {
newCssVars.set(name, { color: true })
}
newCssVars.get(name)![key] = decl.value
})
})
}

// Extract existing CSS variables
extractVars(":root", "light")
extractVars(".dark", "dark")

// Add new variables
for (const [key, vars] of Object.entries(cssVars)) {
for (const [v, value] of Object.entries(vars)) {
const isColor = isColorValue(value) || isLocalHSLValue(value)
if (!newCssVars.has(v)) {
newCssVars.set(v, { color: isColor })
}
// Do not override existing declarations.
// We do not want new components to override existing vars.
// Keep user defined vars.
if (key in newCssVars.get(v)!) {
continue
}
newCssVars.get(v)![key as keyof typeof cssVars] = value
}
}

let ruleNode = root.nodes?.find(
// Ensure root and dark rules exist if needed
function getOrCreateRule(selector: string) {
let rule = root.nodes?.find(
(node): node is Rule =>
node.type === "rule" && node.selector === selector
)

if (!ruleNode && Object.keys(vars).length > 0) {
ruleNode = postcss.rule({
if (!rule && newCssVars.size > 0) {
rule = postcss.rule({
selector,
nodes: [],
raws: { semicolon: true, between: " ", before: "\n" },
})
root.append(ruleNode)
root.insertBefore(ruleNode, postcss.comment({ text: "---break---" }))
root.append(rule)
root.insertBefore(rule, postcss.comment({ text: "---break---" }))
}
return rule
}

Object.entries(vars).forEach(([key, value]) => {
let prop = `--${key.replace(/^--/, "")}`
const rootRuleNode = getOrCreateRule(":root")
const darkRuleNode = getOrCreateRule(".dark")

// Special case for sidebar-background.
if (prop === "--sidebar-background") {
prop = "--sidebar"
}
// Insert CSS variables
newCssVars.forEach((value, key) => {
let prop = `--${key.replace(/^--/, "")}`

if (isLocalHSLValue(value)) {
value = `hsl(${value})`
}
// Special case for sidebar-background.
if (prop === "--sidebar-background") {
prop = "--sidebar"
}

const newDecl = postcss.decl({
const formatValue = (val: string) =>
isLocalHSLValue(val) ? `hsl(${val})` : val
const createDecl = (val: string) =>
postcss.decl({
prop,
value,
value: formatValue(val),
raws: { semicolon: true },
})
const existingDecl = ruleNode?.nodes.find(
(node): node is postcss.Declaration =>
node.type === "decl" && node.prop === prop
)

// Do not override existing declarations.
// We do not want new components to override existing vars.
// Keep user defined vars.
if (!existingDecl) {
ruleNode?.append(newDecl)
if (value.light && value.dark) {
if (value.color) {
const newDecl = createDecl(
`light-dark(${formatValue(value.light)}, ${formatValue(
value.dark
)})`
)

const existingDecl = rootRuleNode?.nodes.find(
(n) => n.type === "decl" && n.prop === prop
)

existingDecl
? existingDecl.replaceWith(newDecl)
: rootRuleNode?.append(newDecl)

darkRuleNode?.nodes
.find((n) => n.type === "decl" && n.prop === prop)
?.remove()
} else {
const lightDecl = createDecl(value.light)
const darkDecl = createDecl(value.dark)

const existingRootDecl = rootRuleNode?.nodes.find(
(n) => n.type === "decl" && n.prop === prop
)

existingRootDecl
? existingRootDecl.replaceWith(lightDecl)
: rootRuleNode?.append(lightDecl)

const existingDarkDecl = darkRuleNode?.nodes.find(
(n) => n.type === "decl" && n.prop === prop
)

existingDarkDecl
? existingDarkDecl.replaceWith(darkDecl)
: darkRuleNode?.append(darkDecl)
}
})
} else {
const val = value.light || value.dark
const newDecl = createDecl(val!)
const targetRule = value.light ? rootRuleNode : darkRuleNode
const existingDecl = targetRule?.nodes.find(
(n) => n.type === "decl" && n.prop === prop
)
existingDecl
? existingDecl.replaceWith(newDecl)
: targetRule?.append(newDecl)
}
})

// Remove empty .dark rule
if (darkRuleNode?.nodes.length === 0) darkRuleNode.remove()
},
}
}
Expand Down
Loading