Skip to content

Commit

Permalink
feat: generate multiple theme
Browse files Browse the repository at this point in the history
  • Loading branch information
hyoban committed Dec 17, 2023
1 parent 8075cd4 commit afd161a
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 42 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@
"dev": "unbuild --stub",
"lint": "eslint --max-warnings 0 .",
"prepublishOnly": "nr build",
"changelog": "changelogen --clean --output && prettier --write CHANGELOG.md && git add CHANGELOG.md && git commit -m \"docs: update changelog\" && git push",
"release": "bumpp && npm publish",
"changelog": "changelogen --output && prettier --write CHANGELOG.md && git add CHANGELOG.md",
"release": "bumpp --all --execute \"pnpm changelog\" && npm publish",
"start": "esno src/index.ts",
"test": "vitest",
"typecheck": "tsc --noEmit",
Expand Down
69 changes: 47 additions & 22 deletions src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,76 @@ import { mergeDeep } from "unocss"
import { themeCSSVarKeys, themes } from "./themes"

import type { ThemeCSSVarKey, ThemeCSSVars } from "./themes"
import type { ColorOptions } from "./types"

function generateLightVars(
theme: "light" | "dark",
color: ThemeCSSVars,
radius: number,
) {
return [
...Object.entries(color)
.map(([key, value]) => {
if (!themeCSSVarKeys.includes(key as ThemeCSSVarKey)) return ""
return ` --${key}: ${value};`
})
.filter(Boolean),
...(theme === "light" ? [` --radius: ${radius}rem;`] : []),
].join("\n")
import type { ColorOptions, PresetShadcnOptions } from "./types"

function generateColorCSSVars(color: ThemeCSSVars) {
return Object.entries(color)
.map(([key, value]) => {
if (!themeCSSVarKeys.includes(key as ThemeCSSVarKey)) return ""
return ` --${key}: ${value};`
})
.filter(Boolean)
.join("\n")
}

function generateRadiusCSSVars(radius: number) {
return ` --radius: ${radius}rem;`
}

function getBuiltInTheme(name: string) {
const theme = themes.find((t) => t.name === name)
if (!theme) throw new Error(`Unknown color: ${name}`)
return theme.cssVars
return {
name,
...theme.cssVars,
}
}

function getColorTheme(color: ColorOptions) {
let light: ThemeCSSVars
let dark: ThemeCSSVars
let name: string

if (typeof color === "string") {
name = color
;({ light, dark } = getBuiltInTheme(color))
} else if ("base" in color) {
name = color.base
;({ light, dark } = mergeDeep(getBuiltInTheme(color.base), color.color))
} else {
name = color.name
;({ light, dark } = color)
}
return { light, dark }
return { light, dark, name }
}

export function generateCSSVars(color: ColorOptions, radius: number) {
let { light, dark } = getColorTheme(color)
const lightVars = generateLightVars("light", light, radius)
const darkVars = generateLightVars("dark", dark, radius)
export function generateCSSVars(
theme: PresetShadcnOptions,
onlyOne = true,
): string {
if (Array.isArray(theme)) {
return theme.map((t) => generateCSSVars(t, false)).join("\n")
}

const { color, radius } = theme
const { light, dark, name } = getColorTheme(color)
const lightVars = generateColorCSSVars(light)
const darkVars = generateColorCSSVars(dark)

if (!onlyOne) {
return `.theme-${name} {
${lightVars}
${generateRadiusCSSVars(radius)}
}
.dark .theme-${name} {
${darkVars}
}`
}

return `:root {
${lightVars}
${generateRadiusCSSVars(radius)}
}
.dark {
Expand Down
9 changes: 6 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import type { PresetShadcnOptions } from "./types"
import type { Preset } from "unocss"
import type { Theme } from "unocss/preset-mini"

export function presetShadcn(options: PresetShadcnOptions = {}): Preset<Theme> {
const { color = "zinc", radius = 0.5 } = options
export function presetShadcn(options?: PresetShadcnOptions): Preset<Theme> {
const theme = options ?? {
color: "zinc",
radius: 0.5,
}

return {
name: "unocss-preset-shadcn",
Expand All @@ -17,7 +20,7 @@ export function presetShadcn(options: PresetShadcnOptions = {}): Preset<Theme> {
@keyframes shadcn-enter { from{ opacity: var(--un-enter-opacity, 1); transform: translate3d(var(--un-enter-translate-x, 0), var(--un-enter-translate-y, 0), 0) scale3d(var(--un-enter-scale, 1), var(--un-enter-scale, 1), var(--un-enter-scale, 1)) rotate(var(--un-enter-rotate, 0)) } }
@keyframes shadcn-exit { to{ opacity: var(--un-exit-opacity, 1); transform: translate3d(var(--un-exit-translate-x, 0), var(--un-exit-translate-y, 0), 0) scale3d(var(--un-exit-scale, 1), var(--un-exit-scale, 1), var(--un-exit-scale, 1)) rotate(var(--un-exit-rotate, 0)) } }
${generateCSSVars(color, radius)}
${generateCSSVars(theme)}
* {
border-color: hsl(var(--border));
Expand Down
1 change: 1 addition & 0 deletions src/themes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type ThemeCSSVars = {
}

export type ThemeCSSVarsVariant = {
name: string
light: ThemeCSSVars
dark: ThemeCSSVars
}
Expand Down
10 changes: 7 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { DeepPartial } from "unocss"

export type ShadcnThemeColor = ShadcnTheme["name"]

type ArrayOrSingle<T> = T | T[]

export type ColorOptions =
| ShadcnThemeColor
| ThemeCSSVarsVariant
Expand All @@ -11,13 +13,15 @@ export type ColorOptions =
color: DeepPartial<ThemeCSSVarsVariant>
}

export type PresetShadcnOptions = {
export type ThemeOptions = {
/**
* @default 'zinc'
*/
color?: ColorOptions
color: ColorOptions
/**
* @default 0.5
*/
radius?: number
radius: number
}

export type PresetShadcnOptions = ArrayOrSingle<ThemeOptions>
48 changes: 36 additions & 12 deletions test/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,25 @@ import { generateCSSVars } from "../src/generate"

describe("generate-theme-css-var", () => {
it("built in themes", () => {
expect(generateCSSVars("zinc", 0.5)).toMatchFileSnapshot("zinc-0.5.css")
expect(generateCSSVars("neutral", 0.75)).toMatchFileSnapshot(
"neutral-0.75.css",
)
expect(
generateCSSVars({
color: "zinc",
radius: 0.5,
}),
).toMatchFileSnapshot("zinc-0.5.css")
expect(
generateCSSVars({
color: "neutral",
radius: 0.75,
}),
).toMatchFileSnapshot("neutral-0.75.css")
})

it("custom theme", () => {
expect(
generateCSSVars(
{
generateCSSVars({
color: {
name: "custom",
light: {
background: "0 1% 100%",
foreground: "240 10% 3.9%",
Expand Down Expand Up @@ -57,24 +66,39 @@ describe("generate-theme-css-var", () => {
ring: "240 4.9% 83.9%",
},
},
1,
),
radius: 1,
}),
).toMatchFileSnapshot("custom.css")
})

it("custom theme based on built in theme", () => {
expect(
generateCSSVars(
{
generateCSSVars({
color: {
base: "zinc",
color: {
light: {
background: "0 1% 100%",
},
},
},
1,
),
radius: 1,
}),
).toMatchFileSnapshot("custom.css")
})

it("generate multiple themes", () => {
expect(
generateCSSVars([
{
color: "zinc",
radius: 0.5,
},
{
color: "neutral",
radius: 0.75,
},
]),
).toMatchFileSnapshot("multiple.css")
})
})
88 changes: 88 additions & 0 deletions test/multiple.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
.theme-zinc {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5.9% 10%;
--radius: 0.5rem;
}

.dark .theme-zinc {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
}
.theme-neutral {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--radius: 0.75rem;
}

.dark .theme-neutral {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
}

0 comments on commit afd161a

Please sign in to comment.