diff --git a/bun.lock b/bun.lock index bcd3420..50a7693 100644 --- a/bun.lock +++ b/bun.lock @@ -83,6 +83,27 @@ "wrangler": "^4.135.0", }, }, + "packages/appearance": { + "name": "@workspace/appearance", + "version": "0.0.0", + "dependencies": { + "@icones/react": "^0.0.1", + "@tanstack/react-hotkeys": "^0.10.0", + "@workspace/i18n": "workspace:*", + "@workspace/preferences": "workspace:*", + "@workspace/ui": "workspace:*", + "react": "^19.2.6", + }, + "devDependencies": { + "@testing-library/react": "^16.3.2", + "@types/react": "^19", + "@types/react-dom": "^19", + "jsdom": "^30.0.1", + "react-dom": "^19.2.6", + "typescript": "~6", + "vitest": "^4.1.10", + }, + }, "packages/blocks": { "name": "@workspace/blocks", "version": "0.0.0", @@ -999,6 +1020,8 @@ "@vue/shared": ["@vue/shared@3.5.40", "", {}, "sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg=="], + "@workspace/appearance": ["@workspace/appearance@workspace:packages/appearance"], + "@workspace/blocks": ["@workspace/blocks@workspace:packages/blocks"], "@workspace/i18n": ["@workspace/i18n@workspace:packages/i18n"], diff --git a/packages/appearance/package.json b/packages/appearance/package.json new file mode 100644 index 0000000..3575dca --- /dev/null +++ b/packages/appearance/package.json @@ -0,0 +1,38 @@ +{ + "name": "@workspace/appearance", + "version": "0.0.0", + "type": "module", + "private": true, + "scripts": { + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@icones/react": "^0.0.1", + "@tanstack/react-hotkeys": "^0.10.0", + "@workspace/i18n": "workspace:*", + "@workspace/preferences": "workspace:*", + "@workspace/ui": "workspace:*", + "react": "^19.2.6" + }, + "devDependencies": { + "@testing-library/react": "^16.3.2", + "@types/react": "^19", + "@types/react-dom": "^19", + "jsdom": "^30.0.1", + "react-dom": "^19.2.6", + "typescript": "~6", + "vitest": "^4.1.10" + }, + "exports": { + ".": "./src/index.ts", + "./components/*": "./src/components/*.tsx", + "./effects": "./src/effects.ts", + "./globals.css": "./src/styles/globals.css", + "./locales": "./src/locales/catalogs.ts", + "./locales/*": "./src/locales/*.ts", + "./preferences": "./src/preferences.ts", + "./theme": "./src/theme.ts", + "./use-resolved-theme": "./src/use-resolved-theme.ts" + } +} \ No newline at end of file diff --git a/packages/appearance/src/components/appearance-atoms.test.tsx b/packages/appearance/src/components/appearance-atoms.test.tsx new file mode 100644 index 0000000..d162b0e --- /dev/null +++ b/packages/appearance/src/components/appearance-atoms.test.tsx @@ -0,0 +1,48 @@ +// @vitest-environment jsdom + +import { cleanup, fireEvent, render, screen } from "@testing-library/react" +import { I18nProvider } from "@workspace/i18n" +import { afterEach, describe, expect, it, vi } from "vitest" + +import { messages as englishMessages } from "../locales/en-US" +import { CompactLayoutField } from "./compact-layout-field" +import { accentColorSwatches, ThemeColorPicker } from "./theme-color-picker" + +afterEach(cleanup) + +function renderLocalized(node: React.ReactNode) { + return render( + + {node} + + ) +} + +describe("appearance atoms", () => { + it("reports compact layout changes without owning state", () => { + const onCheckedChange = vi.fn() + + renderLocalized( + + ) + + fireEvent.click(screen.getByRole("switch")) + expect(onCheckedChange).toHaveBeenCalledWith(true) + }) + + it("reports the selected theme color", () => { + const onValueChange = vi.fn() + + renderLocalized( + + ) + + fireEvent.click(screen.getByRole("button", { name: "Rose" })) + expect(onValueChange).toHaveBeenCalledWith("rose") + }) +}) diff --git a/packages/appearance/src/components/appearance-controller.tsx b/packages/appearance/src/components/appearance-controller.tsx new file mode 100644 index 0000000..484a1dd --- /dev/null +++ b/packages/appearance/src/components/appearance-controller.tsx @@ -0,0 +1,55 @@ +import { useCallback, useState } from "react" +import { usePreference } from "@workspace/preferences" + +import { useResolvedTheme } from "../use-resolved-theme" +import { + AppearanceDialog, + AppearanceDialogContent, + appearanceDialogHandle, + type AppearanceDialogHandle, +} from "./appearance-dialog" +import { AppearanceHotkeys } from "./appearance-hotkeys" + +export interface AppearanceControllerProps { + handle?: AppearanceDialogHandle + onOpenChange?: (open: boolean) => void +} + +export function AppearanceController({ + handle = appearanceDialogHandle, + onOpenChange, +}: AppearanceControllerProps) { + const [isOpen, setIsOpen] = useState(false) + const [, setThemeMode] = usePreference("theme-mode") + const resolvedTheme = useResolvedTheme() + + const toggleDialog = useCallback(() => { + if (isOpen) { + handle.close() + } else { + handle.open(null) + } + }, [handle, isOpen]) + + const toggleTheme = useCallback(() => { + setThemeMode(resolvedTheme === "dark" ? "light" : "dark") + }, [resolvedTheme, setThemeMode]) + + const handleOpenChange = useCallback( + (open: boolean) => { + setIsOpen(open) + onOpenChange?.(open) + }, + [onOpenChange] + ) + + return ( + + + + + ) +} diff --git a/packages/appearance/src/components/appearance-dialog.tsx b/packages/appearance/src/components/appearance-dialog.tsx new file mode 100644 index 0000000..6d6e571 --- /dev/null +++ b/packages/appearance/src/components/appearance-dialog.tsx @@ -0,0 +1,93 @@ +import type { ComponentProps, ReactNode } from "react" +import { LocalizedText } from "@workspace/i18n" +import { + createDialogHandle, + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, + type DialogHandle, +} from "@workspace/ui/components/dialog" +import { cn } from "@workspace/ui/lib/utils" + +import { appearanceMessages } from "../messages" +import { AppearanceSettings } from "./appearance-settings" + +export type AppearanceDialogHandle = DialogHandle + +export function createAppearanceDialogHandle(): AppearanceDialogHandle { + return createDialogHandle() +} + +export const appearanceDialogHandle = createAppearanceDialogHandle() + +export interface AppearanceDialogProps { + children?: ReactNode + handle?: AppearanceDialogHandle + onOpenChange?: (open: boolean) => void +} + +export function AppearanceDialog({ + children, + handle = appearanceDialogHandle, + onOpenChange, +}: AppearanceDialogProps) { + return ( + + {children ?? } + + ) +} + +export function AppearanceDialogHeader() { + return ( + + + + + + + + + ) +} + +export interface AppearanceDialogContentProps extends Omit< + ComponentProps, + "children" +> { + children?: ReactNode +} + +export function AppearanceDialogContent({ + children, + className, + ...props +}: AppearanceDialogContentProps) { + return ( + + + + {children ?? } + + + ) +} + +export interface AppearanceDialogTriggerProps extends Omit< + ComponentProps, + "handle" +> { + handle?: AppearanceDialogHandle +} + +export function AppearanceDialogTrigger({ + handle = appearanceDialogHandle, + ...props +}: AppearanceDialogTriggerProps) { + return +} + +export const AppearanceTrigger = AppearanceDialogTrigger diff --git a/packages/appearance/src/components/appearance-hotkeys.tsx b/packages/appearance/src/components/appearance-hotkeys.tsx new file mode 100644 index 0000000..6bba125 --- /dev/null +++ b/packages/appearance/src/components/appearance-hotkeys.tsx @@ -0,0 +1,45 @@ +import { useHotkey } from "@tanstack/react-hotkeys" +import { useMessage } from "@workspace/i18n" + +import { appearanceMessages } from "../messages" + +export interface AppearanceHotkeysProps { + onToggleDialog: VoidFunction + onToggleTheme: VoidFunction +} + +export function AppearanceHotkeys({ + onToggleDialog, + onToggleTheme, +}: AppearanceHotkeysProps) { + const appearanceTitle = useMessage(appearanceMessages.appearance) + const appearanceDescription = useMessage( + appearanceMessages.appearanceCommandDescription + ) + const toggleThemeTitle = useMessage(appearanceMessages.toggleTheme) + const toggleThemeDescription = useMessage( + appearanceMessages.toggleThemeDescription + ) + + useHotkey("Mod+,", onToggleDialog, { + ignoreInputs: true, + preventDefault: true, + stopPropagation: false, + meta: { + name: appearanceTitle, + description: appearanceDescription, + }, + }) + + useHotkey("Mod+D", onToggleTheme, { + ignoreInputs: true, + preventDefault: true, + stopPropagation: false, + meta: { + name: toggleThemeTitle, + description: toggleThemeDescription, + }, + }) + + return null +} diff --git a/packages/appearance/src/components/appearance-settings.tsx b/packages/appearance/src/components/appearance-settings.tsx new file mode 100644 index 0000000..d38d66b --- /dev/null +++ b/packages/appearance/src/components/appearance-settings.tsx @@ -0,0 +1,47 @@ +import { cn } from "@workspace/ui/lib/utils" +import { usePreference } from "@workspace/preferences" + +import { useResolvedTheme } from "../use-resolved-theme" +import { CompactLayoutField } from "./compact-layout-field" +import { ThemeConfigCard } from "./theme-config-card" +import { ThemeModeField } from "./theme-mode-field" + +export interface AppearanceSettingsProps { + className?: string +} + +export function AppearanceSettings({ className }: AppearanceSettingsProps) { + const [themeMode, setThemeMode] = usePreference("theme-mode") + const [compacted, setCompacted] = usePreference("main-compacted") + const [lightTheme, setLightTheme] = usePreference("theme-light") + const [darkTheme, setDarkTheme] = usePreference("theme-dark") + const resolvedTheme = useResolvedTheme() + + return ( + + + + + + + + + + + ) +} diff --git a/packages/appearance/src/components/compact-layout-field.tsx b/packages/appearance/src/components/compact-layout-field.tsx new file mode 100644 index 0000000..fbee7d3 --- /dev/null +++ b/packages/appearance/src/components/compact-layout-field.tsx @@ -0,0 +1,33 @@ +import { LocalizedText } from "@workspace/i18n" +import { Switch } from "@workspace/ui/components/switch" +import { cn } from "@workspace/ui/lib/utils" + +import { appearanceMessages } from "../messages" + +export interface CompactLayoutFieldProps { + checked: boolean + className?: string + onCheckedChange: (checked: boolean) => void +} + +export function CompactLayoutField({ + checked, + className, + onCheckedChange, +}: CompactLayoutFieldProps) { + return ( + + + + + + + + onCheckedChange(nextChecked)} + /> + + ) +} diff --git a/packages/appearance/src/components/theme-color-picker.tsx b/packages/appearance/src/components/theme-color-picker.tsx new file mode 100644 index 0000000..3dfd2f2 --- /dev/null +++ b/packages/appearance/src/components/theme-color-picker.tsx @@ -0,0 +1,121 @@ +import type { CSSProperties, ReactNode } from "react" +import { useMessage } from "@workspace/i18n" +import { cn } from "@workspace/ui/lib/utils" + +import { themeColorMessages } from "../messages" +import { + accentColors, + baseColors, + getTheme, + type AccentColor, + type BaseColor, +} from "../theme" + +export type ThemeColor = BaseColor | AccentColor + +export interface ThemeColorSwatch { + dark: ThemeColorSwatchScheme + light: ThemeColorSwatchScheme + name: T +} + +export interface ThemeColorSwatchScheme { + background: string + foreground: string +} + +export function createThemeColorSwatches( + colors: readonly T[] +): ThemeColorSwatch[] { + return colors.map((name) => { + const theme = getTheme(name)! + + return { + name, + light: { + background: theme.cssVars.light.primary, + foreground: theme.cssVars.light["primary-foreground"], + }, + dark: { + background: theme.cssVars.dark.primary, + foreground: theme.cssVars.dark["primary-foreground"], + }, + } + }) +} + +export const baseColorSwatches = createThemeColorSwatches(baseColors) +export const accentColorSwatches = createThemeColorSwatches(accentColors) + +export interface ThemeColorPickerProps { + className?: string + onValueChange: (color: T) => void + swatches: readonly ThemeColorSwatch[] + title: ReactNode + value: T +} + +export function ThemeColorPicker({ + className, + onValueChange, + swatches, + title, + value, +}: ThemeColorPickerProps) { + return ( + + + {title} + + + {swatches.map((swatch) => ( + onValueChange(swatch.name)} + /> + ))} + + + ) +} + +export interface ThemeColorSwatchButtonProps { + onClick: VoidFunction + pressed: boolean + swatch: ThemeColorSwatch +} + +export function ThemeColorSwatchButton({ + onClick, + pressed, + swatch, +}: ThemeColorSwatchButtonProps) { + const swatchTitle = useMessage(themeColorMessages[swatch.name]) + const style = { + "--light-bg": swatch.light.background, + "--light-fg": swatch.light.foreground, + "--dark-bg": swatch.dark.background, + "--dark-fg": swatch.dark.foreground, + } as CSSProperties + + return ( + + {swatchTitle} + + ) +} diff --git a/packages/appearance/src/components/theme-config-card.tsx b/packages/appearance/src/components/theme-config-card.tsx new file mode 100644 index 0000000..8917f8b --- /dev/null +++ b/packages/appearance/src/components/theme-config-card.tsx @@ -0,0 +1,82 @@ +import { LocalizedText } from "@workspace/i18n" +import { Badge } from "@workspace/ui/components/badge" +import { MoonIcon, SunIcon } from "@workspace/ui/components/icon" +import { cn } from "@workspace/ui/lib/utils" + +import { appearanceMessages } from "../messages" +import type { ThemeColorConfig, ThemeScheme } from "../theme" +import { + accentColorSwatches, + baseColorSwatches, + ThemeColorPicker, +} from "./theme-color-picker" +import { ThemePreview } from "./theme-preview" + +export interface ThemeConfigCardProps { + active?: boolean + className?: string + compacted?: boolean + config: ThemeColorConfig + onConfigChange: (config: ThemeColorConfig) => void + scheme: ThemeScheme +} + +export function ThemeConfigCard({ + active = false, + className, + compacted = false, + config, + onConfigChange, + scheme, +}: ThemeConfigCardProps) { + const titleMessage = + scheme === "light" + ? appearanceMessages.lightTheme + : appearanceMessages.darkTheme + const descriptionMessage = + scheme === "light" + ? appearanceMessages.lightThemeDescription + : appearanceMessages.darkThemeDescription + + return ( + + + {scheme === "light" ? ( + + ) : ( + + )} + + + + {active && ( + + + + )} + + + + + + + } + value={config.baseColor} + onValueChange={(baseColor) => + onConfigChange({ ...config, baseColor }) + } + /> + } + value={config.accentColor} + onValueChange={(accentColor) => + onConfigChange({ ...config, accentColor }) + } + /> + + + ) +} diff --git a/packages/appearance/src/components/theme-mode-field.tsx b/packages/appearance/src/components/theme-mode-field.tsx new file mode 100644 index 0000000..b5a114a --- /dev/null +++ b/packages/appearance/src/components/theme-mode-field.tsx @@ -0,0 +1,84 @@ +import { MonitorIcon, MoonIcon, SunIcon } from "@workspace/ui/components/icon" +import { useMessage } from "@workspace/i18n" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@workspace/ui/components/select" +import { cn } from "@workspace/ui/lib/utils" + +import { appearanceMessages } from "../messages" +import type { ThemeMode } from "../preferences" + +export interface ThemeModeFieldProps { + className?: string + onValueChange: (value: ThemeMode) => void + value: ThemeMode +} + +export function ThemeModeIcon({ mode }: { mode: ThemeMode | null }) { + if (mode === "light") return + if (mode === "dark") return + return +} + +export function ThemeModeField({ + className, + onValueChange, + value, +}: ThemeModeFieldProps) { + const lightLabel = useMessage(appearanceMessages.lightMode) + const darkLabel = useMessage(appearanceMessages.darkMode) + const systemLabel = useMessage(appearanceMessages.followSystem) + const lightDescription = useMessage(appearanceMessages.lightModeDescription) + const darkDescription = useMessage(appearanceMessages.darkModeDescription) + const systemDescription = useMessage( + appearanceMessages.followSystemDescription + ) + const title = useMessage(appearanceMessages.themeMode) + const modeItems: ReadonlyArray<{ label: string; value: ThemeMode }> = [ + { label: lightLabel, value: "light" }, + { label: darkLabel, value: "dark" }, + { label: systemLabel, value: "system" }, + ] + const description = { + light: lightDescription, + dark: darkDescription, + system: systemDescription, + }[value] + + return ( + + {title} + {description} + { + if (mode) onValueChange(mode) + }} + > + + + {(selectedValue: ThemeMode | null) => ( + <> + + {modeItems.find((item) => item.value === selectedValue)?.label} + > + )} + + + + {modeItems.map((item) => ( + + + {item.label} + + ))} + + + + ) +} diff --git a/packages/appearance/src/components/theme-preview.tsx b/packages/appearance/src/components/theme-preview.tsx new file mode 100644 index 0000000..38ed0f4 --- /dev/null +++ b/packages/appearance/src/components/theme-preview.tsx @@ -0,0 +1,105 @@ +import type { CSSProperties } from "react" +import { + getTheme, + getThemeCssVariables, + type ThemeColorConfig, + type ThemeScheme, +} from "../theme" +import { cn } from "@workspace/ui/lib/utils" + +export interface ThemePreviewProps { + className?: string + compacted?: boolean + config: ThemeColorConfig + scheme: ThemeScheme +} + +export function ThemePreview({ + scheme, + config, + className, + compacted = false, +}: ThemePreviewProps) { + const themeVariables = getThemeCssVariables(scheme, config) + const baseTheme = getTheme(config.baseColor) ?? getTheme("neutral")! + const style = { + ...Object.fromEntries( + Object.entries(themeVariables).map(([key, value]) => [`--${key}`, value]) + ), + "--preview-base": baseTheme.cssVars[scheme].primary, + } as CSSProperties + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/packages/appearance/src/components/theme-toggle-button.tsx b/packages/appearance/src/components/theme-toggle-button.tsx new file mode 100644 index 0000000..aa16a6c --- /dev/null +++ b/packages/appearance/src/components/theme-toggle-button.tsx @@ -0,0 +1,63 @@ +import type { ComponentProps } from "react" +import { MoonIcon, SunIcon } from "@workspace/ui/components/icon" +import { useMessage } from "@workspace/i18n" +import { usePreference } from "@workspace/preferences" +import { Button } from "@workspace/ui/components/button" +import { cn } from "@workspace/ui/lib/utils" + +import { appearanceMessages } from "../messages" +import type { ThemeScheme } from "../theme" +import { useResolvedTheme } from "../use-resolved-theme" + +export interface ThemeToggleProps extends Omit< + ComponentProps, + "children" | "onClick" +> { + onThemeChange: (theme: ThemeScheme) => void + theme: ThemeScheme +} + +export function ThemeToggle({ + className, + onThemeChange, + theme, + ...props +}: ThemeToggleProps) { + const switchToDarkLabel = useMessage(appearanceMessages.switchToDark) + const switchToLightLabel = useMessage(appearanceMessages.switchToLight) + const nextTheme = theme === "dark" ? "light" : "dark" + + return ( + onThemeChange(nextTheme)} + > + {theme === "dark" ? : } + + ) +} + +export type ThemeToggleButtonProps = Omit< + ThemeToggleProps, + "onThemeChange" | "theme" +> + +export function ThemeToggleButton(props: ThemeToggleButtonProps) { + const [, setThemeMode] = usePreference("theme-mode") + const resolvedTheme = useResolvedTheme() + + return ( + + ) +} diff --git a/packages/appearance/src/effects.test.tsx b/packages/appearance/src/effects.test.tsx new file mode 100644 index 0000000..0f659d3 --- /dev/null +++ b/packages/appearance/src/effects.test.tsx @@ -0,0 +1,86 @@ +// @vitest-environment jsdom + +import { cleanup, fireEvent, render, screen } from "@testing-library/react" +import { PreferencesProvider, usePreference } from "@workspace/preferences" +import { + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, + vi, +} from "vitest" + +import { appearancePreferenceEffect } from "./effects" +import { defaultAppearancePreferences } from "./preferences" +import { THEME_STYLE_ELEMENT_ID } from "./theme" + +beforeAll(() => { + vi.stubGlobal( + "matchMedia", + vi.fn().mockImplementation((query: string) => ({ + addEventListener: vi.fn(), + matches: false, + media: query, + removeEventListener: vi.fn(), + })) + ) + vi.stubGlobal( + "requestAnimationFrame", + vi.fn(() => 1) + ) + vi.stubGlobal("cancelAnimationFrame", vi.fn()) +}) + +beforeEach(() => { + const style = document.createElement("style") + style.id = THEME_STYLE_ELEMENT_ID + document.head.append(style) +}) + +afterEach(() => { + cleanup() + document.getElementById(THEME_STYLE_ELEMENT_ID)?.remove() + document.documentElement.className = "" + document.documentElement.style.colorScheme = "" +}) + +function PreferenceProbe() { + const [themeMode, setThemeMode] = usePreference("theme-mode") + + return ( + setThemeMode("dark")}> + {themeMode} + + ) +} + +describe("appearancePreferenceEffect", () => { + it("integrates with the shared PreferencesProvider", () => { + const onPreferenceChange = vi.fn() + + render( + + + + ) + + expect(document.documentElement.classList.contains("ui:light")).toBe(true) + expect( + document.getElementById(THEME_STYLE_ELEMENT_ID)?.textContent + ).toContain("--background:") + + fireEvent.click(screen.getByRole("button")) + + expect(document.documentElement.classList.contains("ui:dark")).toBe(true) + expect(onPreferenceChange).toHaveBeenCalledWith({ + key: "theme-mode", + value: "dark", + }) + }) +}) diff --git a/packages/appearance/src/effects.ts b/packages/appearance/src/effects.ts new file mode 100644 index 0000000..e1353eb --- /dev/null +++ b/packages/appearance/src/effects.ts @@ -0,0 +1,114 @@ +import { + type PreferenceEffect, + type PreferenceEffectCallback, +} from "@workspace/preferences" + +import type { AppearancePreferences } from "./preferences" +import { + buildThemeStyleSheet, + THEME_STYLE_ELEMENT_ID, + type ThemeScheme, +} from "./theme" + +const systemThemeQuery = "(prefers-color-scheme: dark)" +const themePreferenceKeys = [ + "theme-mode", + "theme-light", + "theme-dark", +] as const satisfies readonly (keyof AppearancePreferences)[] + +let restoreThemeTransitionFrame: number | undefined + +function disableThemeTransitionsTemporarily() { + if (typeof document === "undefined") return + + const root = document.documentElement + root.classList.add("ui:theme-changing") + + if (restoreThemeTransitionFrame !== undefined) { + cancelAnimationFrame(restoreThemeTransitionFrame) + } + + restoreThemeTransitionFrame = requestAnimationFrame(() => { + restoreThemeTransitionFrame = requestAnimationFrame(() => { + root.classList.remove("ui:theme-changing") + restoreThemeTransitionFrame = undefined + }) + }) +} + +function getSystemTheme(): ThemeScheme { + return window.matchMedia(systemThemeQuery).matches ? "dark" : "light" +} + +export function applyAppearancePreferences(preferences: AppearancePreferences) { + if (typeof document === "undefined") return + + const root = document.documentElement + const resolvedTheme = + preferences["theme-mode"] === "system" + ? getSystemTheme() + : preferences["theme-mode"] + const themeStyle = document.getElementById(THEME_STYLE_ELEMENT_ID) + + if (themeStyle) { + themeStyle.textContent = buildThemeStyleSheet( + preferences["theme-light"], + preferences["theme-dark"] + ) + } + + root.classList.toggle("ui:compacted", preferences["main-compacted"]) + root.classList.toggle("ui:collapsed", preferences["sidebar-collapsed"]) + root.classList.toggle("ui:dark", resolvedTheme === "dark") + root.classList.toggle("ui:light", resolvedTheme === "light") + root.style.colorScheme = resolvedTheme +} + +/** Applies appearance preferences before the browser paints the updated UI. */ +export const applyAppearancePreferenceEffect: PreferenceEffectCallback = ({ + previousPreferences, + preferences, +}) => { + const appearancePreferences = preferences as AppearancePreferences + const previousAppearancePreferences = + previousPreferences as AppearancePreferences + + if ( + themePreferenceKeys.some( + (key) => + !Object.is( + previousAppearancePreferences[key], + appearancePreferences[key] + ) + ) + ) { + disableThemeTransitionsTemporarily() + } + + applyAppearancePreferences(appearancePreferences) +} + +/** Keeps system-mode themes synchronized with the operating system setting. */ +export const subscribeSystemThemePreferenceEffect: PreferenceEffectCallback = ({ + preferences, + store, +}) => { + const appearancePreferences = preferences as AppearancePreferences + if (appearancePreferences["theme-mode"] !== "system") return + + const mediaQuery = window.matchMedia(systemThemeQuery) + const applySystemTheme = () => { + disableThemeTransitionsTemporarily() + applyAppearancePreferences(store.getSnapshot() as AppearancePreferences) + } + + mediaQuery.addEventListener("change", applySystemTheme) + return () => mediaQuery.removeEventListener("change", applySystemTheme) +} + +/** Register this effect with the application's shared `PreferencesProvider`. */ +export const appearancePreferenceEffect: PreferenceEffect = { + layoutEffect: applyAppearancePreferenceEffect, + effect: subscribeSystemThemePreferenceEffect, +} diff --git a/packages/appearance/src/index.ts b/packages/appearance/src/index.ts new file mode 100644 index 0000000..d6808cc --- /dev/null +++ b/packages/appearance/src/index.ts @@ -0,0 +1,80 @@ +export { + AppearanceController, + type AppearanceControllerProps, +} from "./components/appearance-controller" +export { + AppearanceDialog, + AppearanceDialogContent, + AppearanceDialogHeader, + AppearanceDialogTrigger, + AppearanceTrigger, + appearanceDialogHandle, + createAppearanceDialogHandle, + type AppearanceDialogContentProps, + type AppearanceDialogHandle, + type AppearanceDialogProps, + type AppearanceDialogTriggerProps, +} from "./components/appearance-dialog" +export { + AppearanceHotkeys, + type AppearanceHotkeysProps, +} from "./components/appearance-hotkeys" +export { + AppearanceSettings, + type AppearanceSettingsProps, +} from "./components/appearance-settings" +export { + CompactLayoutField, + type CompactLayoutFieldProps, +} from "./components/compact-layout-field" +export { + ThemeColorPicker, + ThemeColorSwatchButton, + accentColorSwatches, + baseColorSwatches, + createThemeColorSwatches, + type ThemeColor, + type ThemeColorPickerProps, + type ThemeColorSwatch, + type ThemeColorSwatchButtonProps, + type ThemeColorSwatchScheme, +} from "./components/theme-color-picker" +export { + ThemeConfigCard, + type ThemeConfigCardProps, +} from "./components/theme-config-card" +export { + ThemeModeField, + ThemeModeIcon, + type ThemeModeFieldProps, +} from "./components/theme-mode-field" +export { + ThemePreview, + type ThemePreviewProps, +} from "./components/theme-preview" +export { + ThemeToggle, + ThemeToggleButton, + type ThemeToggleButtonProps, + type ThemeToggleProps, +} from "./components/theme-toggle-button" +export { + applyAppearancePreferenceEffect, + applyAppearancePreferences, + appearancePreferenceEffect, + subscribeSystemThemePreferenceEffect, +} from "./effects" +export { + appearancePreferenceDefinitions, + defaultAppearancePreferences, + defaultThemeColorConfig, + isAppearancePreferenceUpdate, + isThemeColorConfig, + type AppearancePreferenceKey, + type AppearancePreferences, + type AppearancePreferenceUpdate, + type AppearancePreferenceValue, + type ThemeMode, +} from "./preferences" +export * from "./theme" +export { useResolvedTheme } from "./use-resolved-theme" diff --git a/packages/appearance/src/locales/catalogs.test.ts b/packages/appearance/src/locales/catalogs.test.ts new file mode 100644 index 0000000..ecaa5c0 --- /dev/null +++ b/packages/appearance/src/locales/catalogs.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest" + +import { appearanceMessages, themeColorMessages } from "../messages" +import { appearanceCatalogLocales } from "./catalogs" +import { messages as enUS } from "./en-US" +import { messages as zhHans } from "./zh-Hans" + +describe("appearance locale catalogs", () => { + it("ships every message in every built-in locale", () => { + const ids = [ + ...Object.values(appearanceMessages), + ...Object.values(themeColorMessages), + ].map((descriptor) => descriptor.id) + const catalogs = { "en-US": enUS, "zh-Hans": zhHans } + + expect(appearanceCatalogLocales).toEqual(["en-US", "zh-Hans"]) + for (const locale of appearanceCatalogLocales) { + expect(Object.keys(catalogs[locale])).toHaveLength(ids.length) + expect(ids.every((id) => id in catalogs[locale])).toBe(true) + } + }) +}) diff --git a/packages/appearance/src/locales/catalogs.ts b/packages/appearance/src/locales/catalogs.ts new file mode 100644 index 0000000..29ce6bd --- /dev/null +++ b/packages/appearance/src/locales/catalogs.ts @@ -0,0 +1,19 @@ +import type { appearanceMessages, themeColorMessages } from "../messages" + +export const appearanceCatalogLocales = ["en-US", "zh-Hans"] as const + +interface MessageDescriptorMap { + readonly [key: string]: { + readonly id: string + } +} + +type MessageId = T[keyof T]["id"] + +export type AppearanceCatalogLocale = (typeof appearanceCatalogLocales)[number] +export type AppearanceMessageCatalog = Readonly< + Record< + MessageId | MessageId, + string + > +> diff --git a/packages/appearance/src/locales/en-US.ts b/packages/appearance/src/locales/en-US.ts new file mode 100644 index 0000000..e5fec97 --- /dev/null +++ b/packages/appearance/src/locales/en-US.ts @@ -0,0 +1,25 @@ +import type { MessageDescriptor } from "@workspace/i18n" + +import { appearanceMessages, themeColorMessages } from "../messages.ts" +import type { AppearanceMessageCatalog } from "./catalogs" + +export const locale = "en-US" +export const languageTag = "en-US" + +function createSourceCatalog( + ...groups: readonly Record[] +): AppearanceMessageCatalog { + return Object.fromEntries( + groups.flatMap((group) => + Object.values(group).map((descriptor) => [ + descriptor.id, + descriptor.message, + ]) + ) + ) as AppearanceMessageCatalog +} + +export const messages = createSourceCatalog( + appearanceMessages, + themeColorMessages +) satisfies AppearanceMessageCatalog diff --git a/packages/appearance/src/locales/zh-Hans.ts b/packages/appearance/src/locales/zh-Hans.ts new file mode 100644 index 0000000..d93d098 --- /dev/null +++ b/packages/appearance/src/locales/zh-Hans.ts @@ -0,0 +1,54 @@ +import type { AppearanceMessageCatalog } from "./catalogs" + +export const locale = "zh-Hans" +export const languageTag = "zh-CN" + +export const messages = { + "appearance.active": "使用中", + "appearance.baseColor": "基础颜色", + "appearance.color.amber": "琥珀色", + "appearance.color.blue": "蓝色", + "appearance.color.cyan": "青色", + "appearance.color.emerald": "翠绿色", + "appearance.color.fuchsia": "紫红色", + "appearance.color.green": "绿色", + "appearance.color.indigo": "靛蓝色", + "appearance.color.lime": "青柠色", + "appearance.color.mauve": "紫灰", + "appearance.color.mist": "雾灰", + "appearance.color.neutral": "中性灰", + "appearance.color.olive": "橄榄灰", + "appearance.color.orange": "橙色", + "appearance.color.pink": "粉色", + "appearance.color.purple": "紫色", + "appearance.color.red": "红色", + "appearance.color.rose": "玫红色", + "appearance.color.sky": "天蓝色", + "appearance.color.stone": "石灰", + "appearance.color.taupe": "灰褐", + "appearance.color.teal": "蓝绿色", + "appearance.color.violet": "紫罗兰色", + "appearance.color.yellow": "黄色", + "appearance.color.zinc": "锌灰", + "appearance.command.description": "打开主题与布局偏好", + "appearance.compact.description": "启用后,页面主内容将使用紧凑宽度", + "appearance.compact.title": "紧凑布局", + "appearance.description": "主题与布局偏好", + "appearance.mode.dark": "深色", + "appearance.mode.dark.description": "界面将始终使用深色主题", + "appearance.mode.light": "浅色", + "appearance.mode.light.description": "界面将始终使用浅色主题", + "appearance.mode.system": "系统", + "appearance.mode.system.description": "界面主题将跟随系统外观设置", + "appearance.mode.title": "主题模式", + "appearance.primaryColor": "主要颜色", + "appearance.theme.dark": "深色主题", + "appearance.theme.dark.description": "当系统设置为深色模式时,将使用此主题", + "appearance.theme.light": "浅色主题", + "appearance.theme.light.description": "当系统设置为浅色模式时,将使用此主题", + "appearance.title": "界面外观", + "appearance.toggleTheme": "切换主题", + "appearance.toggleTheme.dark": "切换为深色模式", + "appearance.toggleTheme.description": "在浅色与深色主题之间切换", + "appearance.toggleTheme.light": "切换为浅色模式", +} as const satisfies AppearanceMessageCatalog diff --git a/packages/appearance/src/messages.ts b/packages/appearance/src/messages.ts new file mode 100644 index 0000000..dce1d0b --- /dev/null +++ b/packages/appearance/src/messages.ts @@ -0,0 +1,195 @@ +import type { MessageDescriptor } from "@workspace/i18n" + +export const appearanceMessages = { + active: /* i18n */ { + id: "appearance.active", + message: "Active", + }, + appearance: /* i18n */ { + id: "appearance.title", + message: "Appearance", + }, + appearanceCommandDescription: /* i18n */ { + id: "appearance.command.description", + message: "Open theme and layout preferences", + }, + baseColor: /* i18n */ { + id: "appearance.baseColor", + message: "Base color", + }, + compactDescription: /* i18n */ { + id: "appearance.compact.description", + message: "Use a compact width for the main page content", + }, + compactLayout: /* i18n */ { + id: "appearance.compact.title", + message: "Compact layout", + }, + darkMode: /* i18n */ { + id: "appearance.mode.dark", + message: "Dark", + }, + darkModeDescription: /* i18n */ { + id: "appearance.mode.dark.description", + message: "The interface will always use the dark theme", + }, + darkTheme: /* i18n */ { + id: "appearance.theme.dark", + message: "Dark theme", + }, + darkThemeDescription: /* i18n */ { + id: "appearance.theme.dark.description", + message: "This theme is used when the system is in dark mode", + }, + followSystem: /* i18n */ { + id: "appearance.mode.system", + message: "System", + }, + followSystemDescription: /* i18n */ { + id: "appearance.mode.system.description", + message: "The interface theme will follow the system appearance", + }, + lightMode: /* i18n */ { + id: "appearance.mode.light", + message: "Light", + }, + lightModeDescription: /* i18n */ { + id: "appearance.mode.light.description", + message: "The interface will always use the light theme", + }, + lightTheme: /* i18n */ { + id: "appearance.theme.light", + message: "Light theme", + }, + lightThemeDescription: /* i18n */ { + id: "appearance.theme.light.description", + message: "This theme is used when the system is in light mode", + }, + primaryColor: /* i18n */ { + id: "appearance.primaryColor", + message: "Primary color", + }, + themeAndLayoutPreferences: /* i18n */ { + id: "appearance.description", + message: "Theme and layout preferences", + }, + themeMode: /* i18n */ { + id: "appearance.mode.title", + message: "Theme mode", + }, + toggleTheme: /* i18n */ { + id: "appearance.toggleTheme", + message: "Toggle theme", + }, + toggleThemeDescription: /* i18n */ { + id: "appearance.toggleTheme.description", + message: "Switch between light and dark themes", + }, + switchToDark: /* i18n */ { + id: "appearance.toggleTheme.dark", + message: "Switch to dark mode", + }, + switchToLight: /* i18n */ { + id: "appearance.toggleTheme.light", + message: "Switch to light mode", + }, +} as const satisfies Record + +export const themeColorMessages = { + amber: /* i18n */ { + id: "appearance.color.amber", + message: "Amber", + }, + blue: /* i18n */ { + id: "appearance.color.blue", + message: "Blue", + }, + cyan: /* i18n */ { + id: "appearance.color.cyan", + message: "Cyan", + }, + emerald: /* i18n */ { + id: "appearance.color.emerald", + message: "Emerald", + }, + fuchsia: /* i18n */ { + id: "appearance.color.fuchsia", + message: "Fuchsia", + }, + green: /* i18n */ { + id: "appearance.color.green", + message: "Green", + }, + indigo: /* i18n */ { + id: "appearance.color.indigo", + message: "Indigo", + }, + lime: /* i18n */ { + id: "appearance.color.lime", + message: "Lime", + }, + mauve: /* i18n */ { + id: "appearance.color.mauve", + message: "Mauve", + }, + mist: /* i18n */ { + id: "appearance.color.mist", + message: "Mist", + }, + neutral: /* i18n */ { + id: "appearance.color.neutral", + message: "Neutral", + }, + olive: /* i18n */ { + id: "appearance.color.olive", + message: "Olive", + }, + orange: /* i18n */ { + id: "appearance.color.orange", + message: "Orange", + }, + pink: /* i18n */ { + id: "appearance.color.pink", + message: "Pink", + }, + purple: /* i18n */ { + id: "appearance.color.purple", + message: "Purple", + }, + red: /* i18n */ { + id: "appearance.color.red", + message: "Red", + }, + rose: /* i18n */ { + id: "appearance.color.rose", + message: "Rose", + }, + sky: /* i18n */ { + id: "appearance.color.sky", + message: "Sky", + }, + stone: /* i18n */ { + id: "appearance.color.stone", + message: "Stone", + }, + taupe: /* i18n */ { + id: "appearance.color.taupe", + message: "Taupe", + }, + teal: /* i18n */ { + id: "appearance.color.teal", + message: "Teal", + }, + violet: /* i18n */ { + id: "appearance.color.violet", + message: "Violet", + }, + yellow: /* i18n */ { + id: "appearance.color.yellow", + message: "Yellow", + }, + zinc: /* i18n */ { + id: "appearance.color.zinc", + message: "Zinc", + }, +} as const satisfies Record diff --git a/packages/appearance/src/preferences.test.ts b/packages/appearance/src/preferences.test.ts new file mode 100644 index 0000000..2ce1cba --- /dev/null +++ b/packages/appearance/src/preferences.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest" + +import { + appearancePreferenceDefinitions, + defaultAppearancePreferences, + isAppearancePreferenceUpdate, +} from "./preferences" + +describe("appearance preferences", () => { + it("creates stable defaults", () => { + expect(defaultAppearancePreferences).toEqual({ + "main-compacted": false, + "sidebar-collapsed": false, + "theme-dark": { accentColor: "blue", baseColor: "neutral" }, + "theme-light": { accentColor: "blue", baseColor: "neutral" }, + "theme-mode": "system", + }) + }) + + it("serializes and validates preference updates", () => { + const compacted = appearancePreferenceDefinitions["main-compacted"] + const theme = appearancePreferenceDefinitions["theme-light"] + + expect(compacted.parse(compacted.serialize(true))).toBe(true) + expect( + theme.parse( + theme.serialize({ + accentColor: "violet", + baseColor: "zinc", + }) + ) + ).toEqual({ accentColor: "violet", baseColor: "zinc" }) + expect( + isAppearancePreferenceUpdate({ key: "theme-mode", value: "dark" }) + ).toBe(true) + expect( + isAppearancePreferenceUpdate({ key: "theme-mode", value: "unknown" }) + ).toBe(false) + }) +}) diff --git a/packages/appearance/src/preferences.ts b/packages/appearance/src/preferences.ts new file mode 100644 index 0000000..bdf3863 --- /dev/null +++ b/packages/appearance/src/preferences.ts @@ -0,0 +1,115 @@ +import { + createDefaultPreferences, + createPreferenceUpdateGuard, + type PreferenceDefinitions, + type PreferenceUpdateFor, +} from "@workspace/preferences" + +import { accentColors, baseColors, type ThemeColorConfig } from "./theme" + +export type ThemeMode = "light" | "dark" | "system" + +export interface AppearancePreferences { + "main-compacted": boolean + "sidebar-collapsed": boolean + "theme-mode": ThemeMode + "theme-light": ThemeColorConfig + "theme-dark": ThemeColorConfig +} + +declare module "@workspace/preferences" { + interface PreferencesCustom extends AppearancePreferences {} +} + +export type AppearancePreferenceKey = keyof AppearancePreferences +export type AppearancePreferenceValue = + AppearancePreferences[Key] +export type AppearancePreferenceUpdate = + PreferenceUpdateFor + +export const defaultThemeColorConfig: ThemeColorConfig = { + accentColor: "blue", + baseColor: "neutral", +} + +export function isThemeColorConfig(value: unknown): value is ThemeColorConfig { + if (!value || typeof value !== "object") return false + + const config = value as { + accentColor?: unknown + baseColor?: unknown + } + + return ( + typeof config.baseColor === "string" && + baseColors.some((color) => color === config.baseColor) && + typeof config.accentColor === "string" && + accentColors.some((color) => color === config.accentColor) + ) +} + +function isBoolean(value: unknown): value is boolean { + return typeof value === "boolean" +} + +function isThemeMode(value: unknown): value is ThemeMode { + return value === "light" || value === "dark" || value === "system" +} + +function parseThemeColorConfig(value: string | undefined): ThemeColorConfig { + if (!value) return { ...defaultThemeColorConfig } + + try { + const parsed: unknown = JSON.parse(value) + return isThemeColorConfig(parsed) ? parsed : { ...defaultThemeColorConfig } + } catch { + return { ...defaultThemeColorConfig } + } +} + +export const appearancePreferenceDefinitions: PreferenceDefinitions = + { + "main-compacted": { + cookie: "ui-main-compacted", + defaultValue: false, + is: isBoolean, + parse: (value) => value === "1", + serialize: (value) => (value ? "1" : "0"), + }, + "sidebar-collapsed": { + cookie: "ui-sidebar-collapsed", + defaultValue: false, + is: isBoolean, + parse: (value) => value === "true" || value === "1", + serialize: (value) => (value ? "1" : "0"), + }, + "theme-mode": { + cookie: "ui-theme", + defaultValue: "system", + is: isThemeMode, + parse: (value) => (isThemeMode(value) ? value : "system"), + serialize: (value) => value, + }, + "theme-light": { + cookie: "ui-theme-light", + defaultValue: { ...defaultThemeColorConfig }, + is: isThemeColorConfig, + parse: parseThemeColorConfig, + serialize: JSON.stringify, + }, + "theme-dark": { + cookie: "ui-theme-dark", + defaultValue: { ...defaultThemeColorConfig }, + is: isThemeColorConfig, + parse: parseThemeColorConfig, + serialize: JSON.stringify, + }, + } + +export const defaultAppearancePreferences = createDefaultPreferences( + appearancePreferenceDefinitions +) + +export const isAppearancePreferenceUpdate = createPreferenceUpdateGuard( + appearancePreferenceDefinitions +) diff --git a/packages/appearance/src/styles/globals.css b/packages/appearance/src/styles/globals.css new file mode 100644 index 0000000..f3275b1 --- /dev/null +++ b/packages/appearance/src/styles/globals.css @@ -0,0 +1 @@ +@source "../"; diff --git a/packages/appearance/src/theme.test.ts b/packages/appearance/src/theme.test.ts new file mode 100644 index 0000000..1705b86 --- /dev/null +++ b/packages/appearance/src/theme.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "vitest" + +import { + accentColors, + baseColors, + buildThemeStyleSheet, + getTheme, + getThemeCssVariables, + themes, +} from "./theme" + +describe("appearance themes", () => { + it("defines every selectable base and accent color exactly once", () => { + const selectableColors = [...baseColors, ...accentColors] + + expect(themes.map((theme) => theme.name)).toEqual(selectableColors) + expect(new Set(selectableColors).size).toBe(selectableColors.length) + expect(new Set(themes.map((theme) => theme.name)).size).toBe(themes.length) + }) + + it("combines base and accent palettes", () => { + const variables = getThemeCssVariables("light", { + accentColor: "rose", + baseColor: "stone", + }) + + expect(variables.background).toBe( + getTheme("stone")?.cssVars.light.background + ) + expect(variables.primary).toBe(getTheme("rose")?.cssVars.light.primary) + }) + + it("builds explicit and system color-scheme rules", () => { + const styleSheet = buildThemeStyleSheet( + { accentColor: "blue", baseColor: "neutral" }, + { accentColor: "violet", baseColor: "zinc" } + ) + + expect(styleSheet).toContain(":root,.ui\\:light{") + expect(styleSheet).toContain(".ui\\:dark{") + expect(styleSheet).toContain("@media (prefers-color-scheme:dark)") + expect(styleSheet).toContain("--background:") + expect(styleSheet).toContain("--primary:") + }) +}) diff --git a/packages/appearance/src/theme.ts b/packages/appearance/src/theme.ts new file mode 100644 index 0000000..25bf25d --- /dev/null +++ b/packages/appearance/src/theme.ts @@ -0,0 +1,58 @@ +import { + themes, + type AccentColor, + type BaseColor, + type ThemeColorConfig, + type ThemeScheme, +} from "./themes" + +export { + accentColors, + baseColors, + themes, + type AccentColor, + type BaseColor, + type Theme, + type ThemeColorConfig, + type ThemeScheme, +} from "./themes" + +export const THEME_STYLE_ELEMENT_ID = "ui-theme-variables" + +export const getTheme = (name: BaseColor | AccentColor) => + themes.find((theme) => theme.name === name) + +export function getThemeCssVariables( + scheme: ThemeScheme, + config: ThemeColorConfig +) { + const base = getTheme(config.baseColor) ?? getTheme("neutral")! + const accent = getTheme(config.accentColor) ?? getTheme("blue")! + + return { + ...base.cssVars[scheme], + ...accent.cssVars[scheme], + } +} + +function serializeThemeVariables(theme: Record): string { + return Object.entries(theme) + .map(([key, value]) => `--${key}:${value}`) + .join(";") +} + +export function buildThemeStyleSheet( + lightConfig: ThemeColorConfig, + darkConfig: ThemeColorConfig +) { + const light = serializeThemeVariables( + getThemeCssVariables("light", lightConfig) + ) + const dark = serializeThemeVariables(getThemeCssVariables("dark", darkConfig)) + + return [ + `:root,.ui\\:light{${light}}`, + `.ui\\:dark{${dark}}`, + `@media (prefers-color-scheme:dark){:root:not(.ui\\:light):not(.ui\\:dark){${dark}}}`, + ].join("") +} diff --git a/packages/appearance/src/themes.ts b/packages/appearance/src/themes.ts new file mode 100644 index 0000000..d8e8751 --- /dev/null +++ b/packages/appearance/src/themes.ts @@ -0,0 +1,1132 @@ +export type BaseColor = + | "neutral" + | "stone" + | "zinc" + | "mauve" + | "olive" + | "mist" + | "taupe" + +export type AccentColor = + | "amber" + | "blue" + | "cyan" + | "emerald" + | "fuchsia" + | "green" + | "indigo" + | "lime" + | "orange" + | "pink" + | "purple" + | "red" + | "rose" + | "sky" + | "teal" + | "violet" + | "yellow" + +export type ThemeScheme = "light" | "dark" + +export type ThemeColorConfig = { + baseColor: BaseColor + accentColor: AccentColor +} + +export const accentColors: AccentColor[] = [ + "amber", + "blue", + "cyan", + "emerald", + "fuchsia", + "green", + "indigo", + "lime", + "orange", + "pink", + "purple", + "red", + "rose", + "sky", + "teal", + "violet", + "yellow", +] + +export const baseColors: BaseColor[] = [ + "neutral", + "stone", + "zinc", + "mauve", + "olive", + "mist", + "taupe", +] + +export type Theme = { + name: BaseColor | AccentColor + title: string + cssVars: { + light: Record + dark: Record + } +} + +export const themes: Theme[] = [ + { + name: "neutral", + title: "Neutral", + cssVars: { + light: { + background: "oklch(1 0 0)", + foreground: "oklch(0.145 0 0)", + 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)", + border: "oklch(0.922 0 0)", + input: "oklch(0.922 0 0)", + ring: "oklch(0.708 0 0)", + "chart-1": "oklch(0.87 0 0)", + "chart-2": "oklch(0.556 0 0)", + "chart-3": "oklch(0.439 0 0)", + "chart-4": "oklch(0.371 0 0)", + "chart-5": "oklch(0.269 0 0)", + 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.708 0 0)", + }, + dark: { + background: "oklch(0.145 0 0)", + foreground: "oklch(0.985 0 0)", + card: "oklch(0.205 0 0)", + "card-foreground": "oklch(0.985 0 0)", + popover: "oklch(0.205 0 0)", + "popover-foreground": "oklch(0.985 0 0)", + primary: "oklch(0.922 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.704 0.191 22.216)", + border: "oklch(1 0 0 / 10%)", + input: "oklch(1 0 0 / 15%)", + ring: "oklch(0.556 0 0)", + "chart-1": "oklch(0.87 0 0)", + "chart-2": "oklch(0.556 0 0)", + "chart-3": "oklch(0.439 0 0)", + "chart-4": "oklch(0.371 0 0)", + "chart-5": "oklch(0.269 0 0)", + 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(1 0 0 / 10%)", + "sidebar-ring": "oklch(0.556 0 0)", + }, + }, + }, + { + name: "stone", + title: "Stone", + cssVars: { + light: { + background: "oklch(1 0 0)", + foreground: "oklch(0.147 0.004 49.25)", + card: "oklch(1 0 0)", + "card-foreground": "oklch(0.147 0.004 49.25)", + popover: "oklch(1 0 0)", + "popover-foreground": "oklch(0.147 0.004 49.25)", + primary: "oklch(0.216 0.006 56.043)", + "primary-foreground": "oklch(0.985 0.001 106.423)", + secondary: "oklch(0.97 0.001 106.424)", + "secondary-foreground": "oklch(0.216 0.006 56.043)", + muted: "oklch(0.97 0.001 106.424)", + "muted-foreground": "oklch(0.553 0.013 58.071)", + accent: "oklch(0.97 0.001 106.424)", + "accent-foreground": "oklch(0.216 0.006 56.043)", + destructive: "oklch(0.577 0.245 27.325)", + border: "oklch(0.923 0.003 48.717)", + input: "oklch(0.923 0.003 48.717)", + ring: "oklch(0.709 0.01 56.259)", + "chart-1": "oklch(0.869 0.005 56.366)", + "chart-2": "oklch(0.553 0.013 58.071)", + "chart-3": "oklch(0.444 0.011 73.639)", + "chart-4": "oklch(0.374 0.01 67.558)", + "chart-5": "oklch(0.268 0.007 34.298)", + radius: "0.625rem", + sidebar: "oklch(0.985 0.001 106.423)", + "sidebar-foreground": "oklch(0.147 0.004 49.25)", + "sidebar-primary": "oklch(0.216 0.006 56.043)", + "sidebar-primary-foreground": "oklch(0.985 0.001 106.423)", + "sidebar-accent": "oklch(0.97 0.001 106.424)", + "sidebar-accent-foreground": "oklch(0.216 0.006 56.043)", + "sidebar-border": "oklch(0.923 0.003 48.717)", + "sidebar-ring": "oklch(0.709 0.01 56.259)", + }, + dark: { + background: "oklch(0.147 0.004 49.25)", + foreground: "oklch(0.985 0.001 106.423)", + card: "oklch(0.216 0.006 56.043)", + "card-foreground": "oklch(0.985 0.001 106.423)", + popover: "oklch(0.216 0.006 56.043)", + "popover-foreground": "oklch(0.985 0.001 106.423)", + primary: "oklch(0.923 0.003 48.717)", + "primary-foreground": "oklch(0.216 0.006 56.043)", + secondary: "oklch(0.268 0.007 34.298)", + "secondary-foreground": "oklch(0.985 0.001 106.423)", + muted: "oklch(0.268 0.007 34.298)", + "muted-foreground": "oklch(0.709 0.01 56.259)", + accent: "oklch(0.268 0.007 34.298)", + "accent-foreground": "oklch(0.985 0.001 106.423)", + destructive: "oklch(0.704 0.191 22.216)", + border: "oklch(1 0 0 / 10%)", + input: "oklch(1 0 0 / 15%)", + ring: "oklch(0.553 0.013 58.071)", + "chart-1": "oklch(0.869 0.005 56.366)", + "chart-2": "oklch(0.553 0.013 58.071)", + "chart-3": "oklch(0.444 0.011 73.639)", + "chart-4": "oklch(0.374 0.01 67.558)", + "chart-5": "oklch(0.268 0.007 34.298)", + sidebar: "oklch(0.216 0.006 56.043)", + "sidebar-foreground": "oklch(0.985 0.001 106.423)", + "sidebar-primary": "oklch(0.488 0.243 264.376)", + "sidebar-primary-foreground": "oklch(0.985 0.001 106.423)", + "sidebar-accent": "oklch(0.268 0.007 34.298)", + "sidebar-accent-foreground": "oklch(0.985 0.001 106.423)", + "sidebar-border": "oklch(1 0 0 / 10%)", + "sidebar-ring": "oklch(0.553 0.013 58.071)", + }, + }, + }, + { + name: "zinc", + title: "Zinc", + cssVars: { + light: { + background: "oklch(1 0 0)", + foreground: "oklch(0.141 0.005 285.823)", + card: "oklch(1 0 0)", + "card-foreground": "oklch(0.141 0.005 285.823)", + popover: "oklch(1 0 0)", + "popover-foreground": "oklch(0.141 0.005 285.823)", + primary: "oklch(0.21 0.006 285.885)", + "primary-foreground": "oklch(0.985 0 0)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + muted: "oklch(0.967 0.001 286.375)", + "muted-foreground": "oklch(0.552 0.016 285.938)", + accent: "oklch(0.967 0.001 286.375)", + "accent-foreground": "oklch(0.21 0.006 285.885)", + destructive: "oklch(0.577 0.245 27.325)", + border: "oklch(0.92 0.004 286.32)", + input: "oklch(0.92 0.004 286.32)", + ring: "oklch(0.705 0.015 286.067)", + "chart-1": "oklch(0.871 0.006 286.286)", + "chart-2": "oklch(0.552 0.016 285.938)", + "chart-3": "oklch(0.442 0.017 285.786)", + "chart-4": "oklch(0.37 0.013 285.805)", + "chart-5": "oklch(0.274 0.006 286.033)", + radius: "0.625rem", + sidebar: "oklch(0.985 0 0)", + "sidebar-foreground": "oklch(0.141 0.005 285.823)", + "sidebar-primary": "oklch(0.21 0.006 285.885)", + "sidebar-primary-foreground": "oklch(0.985 0 0)", + "sidebar-accent": "oklch(0.967 0.001 286.375)", + "sidebar-accent-foreground": "oklch(0.21 0.006 285.885)", + "sidebar-border": "oklch(0.92 0.004 286.32)", + "sidebar-ring": "oklch(0.705 0.015 286.067)", + }, + dark: { + background: "oklch(0.141 0.005 285.823)", + foreground: "oklch(0.985 0 0)", + card: "oklch(0.21 0.006 285.885)", + "card-foreground": "oklch(0.985 0 0)", + popover: "oklch(0.21 0.006 285.885)", + "popover-foreground": "oklch(0.985 0 0)", + primary: "oklch(0.92 0.004 286.32)", + "primary-foreground": "oklch(0.21 0.006 285.885)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + muted: "oklch(0.274 0.006 286.033)", + "muted-foreground": "oklch(0.705 0.015 286.067)", + accent: "oklch(0.274 0.006 286.033)", + "accent-foreground": "oklch(0.985 0 0)", + destructive: "oklch(0.704 0.191 22.216)", + border: "oklch(1 0 0 / 10%)", + input: "oklch(1 0 0 / 15%)", + ring: "oklch(0.552 0.016 285.938)", + "chart-1": "oklch(0.871 0.006 286.286)", + "chart-2": "oklch(0.552 0.016 285.938)", + "chart-3": "oklch(0.442 0.017 285.786)", + "chart-4": "oklch(0.37 0.013 285.805)", + "chart-5": "oklch(0.274 0.006 286.033)", + sidebar: "oklch(0.21 0.006 285.885)", + "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.274 0.006 286.033)", + "sidebar-accent-foreground": "oklch(0.985 0 0)", + "sidebar-border": "oklch(1 0 0 / 10%)", + "sidebar-ring": "oklch(0.552 0.016 285.938)", + }, + }, + }, + { + name: "mauve", + title: "Mauve", + cssVars: { + light: { + background: "oklch(1 0 0)", + foreground: "oklch(0.145 0.008 326)", + card: "oklch(1 0 0)", + "card-foreground": "oklch(0.145 0.008 326)", + popover: "oklch(1 0 0)", + "popover-foreground": "oklch(0.145 0.008 326)", + primary: "oklch(0.212 0.019 322.12)", + "primary-foreground": "oklch(0.985 0 0)", + secondary: "oklch(0.96 0.003 325.6)", + "secondary-foreground": "oklch(0.212 0.019 322.12)", + muted: "oklch(0.96 0.003 325.6)", + "muted-foreground": "oklch(0.542 0.034 322.5)", + accent: "oklch(0.96 0.003 325.6)", + "accent-foreground": "oklch(0.212 0.019 322.12)", + destructive: "oklch(0.577 0.245 27.325)", + border: "oklch(0.922 0.005 325.62)", + input: "oklch(0.922 0.005 325.62)", + ring: "oklch(0.711 0.019 323.02)", + "chart-1": "oklch(0.865 0.012 325.68)", + "chart-2": "oklch(0.542 0.034 322.5)", + "chart-3": "oklch(0.435 0.029 321.78)", + "chart-4": "oklch(0.364 0.029 323.89)", + "chart-5": "oklch(0.263 0.024 320.12)", + radius: "0.625rem", + sidebar: "oklch(0.985 0 0)", + "sidebar-foreground": "oklch(0.145 0.008 326)", + "sidebar-primary": "oklch(0.212 0.019 322.12)", + "sidebar-primary-foreground": "oklch(0.985 0 0)", + "sidebar-accent": "oklch(0.96 0.003 325.6)", + "sidebar-accent-foreground": "oklch(0.212 0.019 322.12)", + "sidebar-border": "oklch(0.922 0.005 325.62)", + "sidebar-ring": "oklch(0.711 0.019 323.02)", + }, + dark: { + background: "oklch(0.145 0.008 326)", + foreground: "oklch(0.985 0 0)", + card: "oklch(0.212 0.019 322.12)", + "card-foreground": "oklch(0.985 0 0)", + popover: "oklch(0.212 0.019 322.12)", + "popover-foreground": "oklch(0.985 0 0)", + primary: "oklch(0.922 0.005 325.62)", + "primary-foreground": "oklch(0.212 0.019 322.12)", + secondary: "oklch(0.263 0.024 320.12)", + "secondary-foreground": "oklch(0.985 0 0)", + muted: "oklch(0.263 0.024 320.12)", + "muted-foreground": "oklch(0.711 0.019 323.02)", + accent: "oklch(0.263 0.024 320.12)", + "accent-foreground": "oklch(0.985 0 0)", + destructive: "oklch(0.704 0.191 22.216)", + border: "oklch(1 0 0 / 10%)", + input: "oklch(1 0 0 / 15%)", + ring: "oklch(0.542 0.034 322.5)", + "chart-1": "oklch(0.865 0.012 325.68)", + "chart-2": "oklch(0.542 0.034 322.5)", + "chart-3": "oklch(0.435 0.029 321.78)", + "chart-4": "oklch(0.364 0.029 323.89)", + "chart-5": "oklch(0.263 0.024 320.12)", + sidebar: "oklch(0.212 0.019 322.12)", + "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.263 0.024 320.12)", + "sidebar-accent-foreground": "oklch(0.985 0 0)", + "sidebar-border": "oklch(1 0 0 / 10%)", + "sidebar-ring": "oklch(0.542 0.034 322.5)", + }, + }, + }, + { + name: "olive", + title: "Olive", + cssVars: { + light: { + background: "oklch(1 0 0)", + foreground: "oklch(0.153 0.006 107.1)", + card: "oklch(1 0 0)", + "card-foreground": "oklch(0.153 0.006 107.1)", + popover: "oklch(1 0 0)", + "popover-foreground": "oklch(0.153 0.006 107.1)", + primary: "oklch(0.228 0.013 107.4)", + "primary-foreground": "oklch(0.988 0.003 106.5)", + secondary: "oklch(0.966 0.005 106.5)", + "secondary-foreground": "oklch(0.228 0.013 107.4)", + muted: "oklch(0.966 0.005 106.5)", + "muted-foreground": "oklch(0.58 0.031 107.3)", + accent: "oklch(0.966 0.005 106.5)", + "accent-foreground": "oklch(0.228 0.013 107.4)", + destructive: "oklch(0.577 0.245 27.325)", + border: "oklch(0.93 0.007 106.5)", + input: "oklch(0.93 0.007 106.5)", + ring: "oklch(0.737 0.021 106.9)", + "chart-1": "oklch(0.88 0.011 106.6)", + "chart-2": "oklch(0.58 0.031 107.3)", + "chart-3": "oklch(0.466 0.025 107.3)", + "chart-4": "oklch(0.394 0.023 107.4)", + "chart-5": "oklch(0.286 0.016 107.4)", + radius: "0.625rem", + sidebar: "oklch(0.988 0.003 106.5)", + "sidebar-foreground": "oklch(0.153 0.006 107.1)", + "sidebar-primary": "oklch(0.228 0.013 107.4)", + "sidebar-primary-foreground": "oklch(0.988 0.003 106.5)", + "sidebar-accent": "oklch(0.966 0.005 106.5)", + "sidebar-accent-foreground": "oklch(0.228 0.013 107.4)", + "sidebar-border": "oklch(0.93 0.007 106.5)", + "sidebar-ring": "oklch(0.737 0.021 106.9)", + }, + dark: { + background: "oklch(0.153 0.006 107.1)", + foreground: "oklch(0.988 0.003 106.5)", + card: "oklch(0.228 0.013 107.4)", + "card-foreground": "oklch(0.988 0.003 106.5)", + popover: "oklch(0.228 0.013 107.4)", + "popover-foreground": "oklch(0.988 0.003 106.5)", + primary: "oklch(0.93 0.007 106.5)", + "primary-foreground": "oklch(0.228 0.013 107.4)", + secondary: "oklch(0.286 0.016 107.4)", + "secondary-foreground": "oklch(0.988 0.003 106.5)", + muted: "oklch(0.286 0.016 107.4)", + "muted-foreground": "oklch(0.737 0.021 106.9)", + accent: "oklch(0.286 0.016 107.4)", + "accent-foreground": "oklch(0.988 0.003 106.5)", + destructive: "oklch(0.704 0.191 22.216)", + border: "oklch(1 0 0 / 10%)", + input: "oklch(1 0 0 / 15%)", + ring: "oklch(0.58 0.031 107.3)", + "chart-1": "oklch(0.88 0.011 106.6)", + "chart-2": "oklch(0.58 0.031 107.3)", + "chart-3": "oklch(0.466 0.025 107.3)", + "chart-4": "oklch(0.394 0.023 107.4)", + "chart-5": "oklch(0.286 0.016 107.4)", + sidebar: "oklch(0.228 0.013 107.4)", + "sidebar-foreground": "oklch(0.988 0.003 106.5)", + "sidebar-primary": "oklch(0.488 0.243 264.376)", + "sidebar-primary-foreground": "oklch(0.988 0.003 106.5)", + "sidebar-accent": "oklch(0.286 0.016 107.4)", + "sidebar-accent-foreground": "oklch(0.988 0.003 106.5)", + "sidebar-border": "oklch(1 0 0 / 10%)", + "sidebar-ring": "oklch(0.58 0.031 107.3)", + }, + }, + }, + { + name: "mist", + title: "Mist", + cssVars: { + light: { + background: "oklch(1 0 0)", + foreground: "oklch(0.148 0.004 228.8)", + card: "oklch(1 0 0)", + "card-foreground": "oklch(0.148 0.004 228.8)", + popover: "oklch(1 0 0)", + "popover-foreground": "oklch(0.148 0.004 228.8)", + primary: "oklch(0.218 0.008 223.9)", + "primary-foreground": "oklch(0.987 0.002 197.1)", + secondary: "oklch(0.963 0.002 197.1)", + "secondary-foreground": "oklch(0.218 0.008 223.9)", + muted: "oklch(0.963 0.002 197.1)", + "muted-foreground": "oklch(0.56 0.021 213.5)", + accent: "oklch(0.963 0.002 197.1)", + "accent-foreground": "oklch(0.218 0.008 223.9)", + destructive: "oklch(0.577 0.245 27.325)", + border: "oklch(0.925 0.005 214.3)", + input: "oklch(0.925 0.005 214.3)", + ring: "oklch(0.723 0.014 214.4)", + "chart-1": "oklch(0.872 0.007 219.6)", + "chart-2": "oklch(0.56 0.021 213.5)", + "chart-3": "oklch(0.45 0.017 213.2)", + "chart-4": "oklch(0.378 0.015 216)", + "chart-5": "oklch(0.275 0.011 216.9)", + radius: "0.625rem", + sidebar: "oklch(0.987 0.002 197.1)", + "sidebar-foreground": "oklch(0.148 0.004 228.8)", + "sidebar-primary": "oklch(0.218 0.008 223.9)", + "sidebar-primary-foreground": "oklch(0.987 0.002 197.1)", + "sidebar-accent": "oklch(0.963 0.002 197.1)", + "sidebar-accent-foreground": "oklch(0.218 0.008 223.9)", + "sidebar-border": "oklch(0.925 0.005 214.3)", + "sidebar-ring": "oklch(0.723 0.014 214.4)", + }, + dark: { + background: "oklch(0.148 0.004 228.8)", + foreground: "oklch(0.987 0.002 197.1)", + card: "oklch(0.218 0.008 223.9)", + "card-foreground": "oklch(0.987 0.002 197.1)", + popover: "oklch(0.218 0.008 223.9)", + "popover-foreground": "oklch(0.987 0.002 197.1)", + primary: "oklch(0.925 0.005 214.3)", + "primary-foreground": "oklch(0.218 0.008 223.9)", + secondary: "oklch(0.275 0.011 216.9)", + "secondary-foreground": "oklch(0.987 0.002 197.1)", + muted: "oklch(0.275 0.011 216.9)", + "muted-foreground": "oklch(0.723 0.014 214.4)", + accent: "oklch(0.275 0.011 216.9)", + "accent-foreground": "oklch(0.987 0.002 197.1)", + destructive: "oklch(0.704 0.191 22.216)", + border: "oklch(1 0 0 / 10%)", + input: "oklch(1 0 0 / 15%)", + ring: "oklch(0.56 0.021 213.5)", + "chart-1": "oklch(0.872 0.007 219.6)", + "chart-2": "oklch(0.56 0.021 213.5)", + "chart-3": "oklch(0.45 0.017 213.2)", + "chart-4": "oklch(0.378 0.015 216)", + "chart-5": "oklch(0.275 0.011 216.9)", + sidebar: "oklch(0.218 0.008 223.9)", + "sidebar-foreground": "oklch(0.987 0.002 197.1)", + "sidebar-primary": "oklch(0.488 0.243 264.376)", + "sidebar-primary-foreground": "oklch(0.987 0.002 197.1)", + "sidebar-accent": "oklch(0.275 0.011 216.9)", + "sidebar-accent-foreground": "oklch(0.987 0.002 197.1)", + "sidebar-border": "oklch(1 0 0 / 10%)", + "sidebar-ring": "oklch(0.56 0.021 213.5)", + }, + }, + }, + { + name: "taupe", + title: "Taupe", + cssVars: { + light: { + background: "oklch(1 0 0)", + foreground: "oklch(0.147 0.004 49.3)", + card: "oklch(1 0 0)", + "card-foreground": "oklch(0.147 0.004 49.3)", + popover: "oklch(1 0 0)", + "popover-foreground": "oklch(0.147 0.004 49.3)", + primary: "oklch(0.214 0.009 43.1)", + "primary-foreground": "oklch(0.986 0.002 67.8)", + secondary: "oklch(0.96 0.002 17.2)", + "secondary-foreground": "oklch(0.214 0.009 43.1)", + muted: "oklch(0.96 0.002 17.2)", + "muted-foreground": "oklch(0.547 0.021 43.1)", + accent: "oklch(0.96 0.002 17.2)", + "accent-foreground": "oklch(0.214 0.009 43.1)", + destructive: "oklch(0.577 0.245 27.325)", + border: "oklch(0.922 0.005 34.3)", + input: "oklch(0.922 0.005 34.3)", + ring: "oklch(0.714 0.014 41.2)", + "chart-1": "oklch(0.868 0.007 39.5)", + "chart-2": "oklch(0.547 0.021 43.1)", + "chart-3": "oklch(0.438 0.017 39.3)", + "chart-4": "oklch(0.367 0.016 35.7)", + "chart-5": "oklch(0.268 0.011 36.5)", + radius: "0.625rem", + sidebar: "oklch(0.986 0.002 67.8)", + "sidebar-foreground": "oklch(0.147 0.004 49.3)", + "sidebar-primary": "oklch(0.214 0.009 43.1)", + "sidebar-primary-foreground": "oklch(0.986 0.002 67.8)", + "sidebar-accent": "oklch(0.96 0.002 17.2)", + "sidebar-accent-foreground": "oklch(0.214 0.009 43.1)", + "sidebar-border": "oklch(0.922 0.005 34.3)", + "sidebar-ring": "oklch(0.714 0.014 41.2)", + }, + dark: { + background: "oklch(0.147 0.004 49.3)", + foreground: "oklch(0.986 0.002 67.8)", + card: "oklch(0.214 0.009 43.1)", + "card-foreground": "oklch(0.986 0.002 67.8)", + popover: "oklch(0.214 0.009 43.1)", + "popover-foreground": "oklch(0.986 0.002 67.8)", + primary: "oklch(0.922 0.005 34.3)", + "primary-foreground": "oklch(0.214 0.009 43.1)", + secondary: "oklch(0.268 0.011 36.5)", + "secondary-foreground": "oklch(0.986 0.002 67.8)", + muted: "oklch(0.268 0.011 36.5)", + "muted-foreground": "oklch(0.714 0.014 41.2)", + accent: "oklch(0.268 0.011 36.5)", + "accent-foreground": "oklch(0.986 0.002 67.8)", + destructive: "oklch(0.704 0.191 22.216)", + border: "oklch(1 0 0 / 10%)", + input: "oklch(1 0 0 / 15%)", + ring: "oklch(0.547 0.021 43.1)", + "chart-1": "oklch(0.868 0.007 39.5)", + "chart-2": "oklch(0.547 0.021 43.1)", + "chart-3": "oklch(0.438 0.017 39.3)", + "chart-4": "oklch(0.367 0.016 35.7)", + "chart-5": "oklch(0.268 0.011 36.5)", + sidebar: "oklch(0.214 0.009 43.1)", + "sidebar-foreground": "oklch(0.986 0.002 67.8)", + "sidebar-primary": "oklch(0.488 0.243 264.376)", + "sidebar-primary-foreground": "oklch(0.986 0.002 67.8)", + "sidebar-accent": "oklch(0.268 0.011 36.5)", + "sidebar-accent-foreground": "oklch(0.986 0.002 67.8)", + "sidebar-border": "oklch(1 0 0 / 10%)", + "sidebar-ring": "oklch(0.547 0.021 43.1)", + }, + }, + }, + { + name: "amber", + title: "Amber", + cssVars: { + light: { + primary: "oklch(0.555 0.163 48.998)", + "primary-foreground": "oklch(0.987 0.022 95.277)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.879 0.169 91.605)", + "chart-2": "oklch(0.769 0.188 70.08)", + "chart-3": "oklch(0.666 0.179 58.318)", + "chart-4": "oklch(0.555 0.163 48.998)", + "chart-5": "oklch(0.473 0.137 46.201)", + "sidebar-primary": "oklch(0.666 0.179 58.318)", + "sidebar-primary-foreground": "oklch(0.987 0.022 95.277)", + }, + dark: { + primary: "oklch(0.473 0.137 46.201)", + "primary-foreground": "oklch(0.987 0.022 95.277)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.879 0.169 91.605)", + "chart-2": "oklch(0.769 0.188 70.08)", + "chart-3": "oklch(0.666 0.179 58.318)", + "chart-4": "oklch(0.555 0.163 48.998)", + "chart-5": "oklch(0.473 0.137 46.201)", + "sidebar-primary": "oklch(0.769 0.188 70.08)", + "sidebar-primary-foreground": "oklch(0.279 0.077 45.635)", + }, + }, + }, + { + name: "blue", + title: "Blue", + cssVars: { + light: { + primary: "oklch(0.488 0.243 264.376)", + "primary-foreground": "oklch(0.97 0.014 254.604)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.809 0.105 251.813)", + "chart-2": "oklch(0.623 0.214 259.815)", + "chart-3": "oklch(0.546 0.245 262.881)", + "chart-4": "oklch(0.488 0.243 264.376)", + "chart-5": "oklch(0.424 0.199 265.638)", + "sidebar-primary": "oklch(0.546 0.245 262.881)", + "sidebar-primary-foreground": "oklch(0.97 0.014 254.604)", + }, + dark: { + primary: "oklch(0.424 0.199 265.638)", + "primary-foreground": "oklch(0.97 0.014 254.604)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.809 0.105 251.813)", + "chart-2": "oklch(0.623 0.214 259.815)", + "chart-3": "oklch(0.546 0.245 262.881)", + "chart-4": "oklch(0.488 0.243 264.376)", + "chart-5": "oklch(0.424 0.199 265.638)", + "sidebar-primary": "oklch(0.623 0.214 259.815)", + "sidebar-primary-foreground": "oklch(0.97 0.014 254.604)", + }, + }, + }, + { + name: "cyan", + title: "Cyan", + cssVars: { + light: { + primary: "oklch(0.52 0.105 223.128)", + "primary-foreground": "oklch(0.984 0.019 200.873)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.865 0.127 207.078)", + "chart-2": "oklch(0.715 0.143 215.221)", + "chart-3": "oklch(0.609 0.126 221.723)", + "chart-4": "oklch(0.52 0.105 223.128)", + "chart-5": "oklch(0.45 0.085 224.283)", + "sidebar-primary": "oklch(0.609 0.126 221.723)", + "sidebar-primary-foreground": "oklch(0.984 0.019 200.873)", + }, + dark: { + primary: "oklch(0.45 0.085 224.283)", + "primary-foreground": "oklch(0.984 0.019 200.873)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.865 0.127 207.078)", + "chart-2": "oklch(0.715 0.143 215.221)", + "chart-3": "oklch(0.609 0.126 221.723)", + "chart-4": "oklch(0.52 0.105 223.128)", + "chart-5": "oklch(0.45 0.085 224.283)", + "sidebar-primary": "oklch(0.715 0.143 215.221)", + "sidebar-primary-foreground": "oklch(0.302 0.056 229.695)", + }, + }, + }, + { + name: "emerald", + title: "Emerald", + cssVars: { + light: { + primary: "oklch(0.508 0.118 165.612)", + "primary-foreground": "oklch(0.979 0.021 166.113)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.845 0.143 164.978)", + "chart-2": "oklch(0.696 0.17 162.48)", + "chart-3": "oklch(0.596 0.145 163.225)", + "chart-4": "oklch(0.508 0.118 165.612)", + "chart-5": "oklch(0.432 0.095 166.913)", + "sidebar-primary": "oklch(0.596 0.145 163.225)", + "sidebar-primary-foreground": "oklch(0.979 0.021 166.113)", + }, + dark: { + primary: "oklch(0.432 0.095 166.913)", + "primary-foreground": "oklch(0.979 0.021 166.113)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.845 0.143 164.978)", + "chart-2": "oklch(0.696 0.17 162.48)", + "chart-3": "oklch(0.596 0.145 163.225)", + "chart-4": "oklch(0.508 0.118 165.612)", + "chart-5": "oklch(0.432 0.095 166.913)", + "sidebar-primary": "oklch(0.696 0.17 162.48)", + "sidebar-primary-foreground": "oklch(0.262 0.051 172.552)", + }, + }, + }, + { + name: "fuchsia", + title: "Fuchsia", + cssVars: { + light: { + primary: "oklch(0.518 0.253 323.949)", + "primary-foreground": "oklch(0.977 0.017 320.058)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.833 0.145 321.434)", + "chart-2": "oklch(0.667 0.295 322.15)", + "chart-3": "oklch(0.591 0.293 322.896)", + "chart-4": "oklch(0.518 0.253 323.949)", + "chart-5": "oklch(0.452 0.211 324.591)", + "sidebar-primary": "oklch(0.591 0.293 322.896)", + "sidebar-primary-foreground": "oklch(0.977 0.017 320.058)", + }, + dark: { + primary: "oklch(0.452 0.211 324.591)", + "primary-foreground": "oklch(0.977 0.017 320.058)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.833 0.145 321.434)", + "chart-2": "oklch(0.667 0.295 322.15)", + "chart-3": "oklch(0.591 0.293 322.896)", + "chart-4": "oklch(0.518 0.253 323.949)", + "chart-5": "oklch(0.452 0.211 324.591)", + "sidebar-primary": "oklch(0.667 0.295 322.15)", + "sidebar-primary-foreground": "oklch(0.977 0.017 320.058)", + }, + }, + }, + { + name: "green", + title: "Green", + cssVars: { + light: { + primary: "oklch(0.527 0.154 150.069)", + "primary-foreground": "oklch(0.982 0.018 155.826)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.871 0.15 154.449)", + "chart-2": "oklch(0.723 0.219 149.579)", + "chart-3": "oklch(0.627 0.194 149.214)", + "chart-4": "oklch(0.527 0.154 150.069)", + "chart-5": "oklch(0.448 0.119 151.328)", + "sidebar-primary": "oklch(0.627 0.194 149.214)", + "sidebar-primary-foreground": "oklch(0.982 0.018 155.826)", + }, + dark: { + primary: "oklch(0.448 0.119 151.328)", + "primary-foreground": "oklch(0.982 0.018 155.826)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.871 0.15 154.449)", + "chart-2": "oklch(0.723 0.219 149.579)", + "chart-3": "oklch(0.627 0.194 149.214)", + "chart-4": "oklch(0.527 0.154 150.069)", + "chart-5": "oklch(0.448 0.119 151.328)", + "sidebar-primary": "oklch(0.723 0.219 149.579)", + "sidebar-primary-foreground": "oklch(0.982 0.018 155.826)", + }, + }, + }, + { + name: "indigo", + title: "Indigo", + cssVars: { + light: { + primary: "oklch(0.457 0.24 277.023)", + "primary-foreground": "oklch(0.962 0.018 272.314)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.785 0.115 274.713)", + "chart-2": "oklch(0.585 0.233 277.117)", + "chart-3": "oklch(0.511 0.262 276.966)", + "chart-4": "oklch(0.457 0.24 277.023)", + "chart-5": "oklch(0.398 0.195 277.366)", + "sidebar-primary": "oklch(0.511 0.262 276.966)", + "sidebar-primary-foreground": "oklch(0.962 0.018 272.314)", + }, + dark: { + primary: "oklch(0.398 0.195 277.366)", + "primary-foreground": "oklch(0.962 0.018 272.314)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.785 0.115 274.713)", + "chart-2": "oklch(0.585 0.233 277.117)", + "chart-3": "oklch(0.511 0.262 276.966)", + "chart-4": "oklch(0.457 0.24 277.023)", + "chart-5": "oklch(0.398 0.195 277.366)", + "sidebar-primary": "oklch(0.585 0.233 277.117)", + "sidebar-primary-foreground": "oklch(0.962 0.018 272.314)", + }, + }, + }, + { + name: "lime", + title: "Lime", + cssVars: { + light: { + primary: "oklch(0.841 0.238 128.85)", + "primary-foreground": "oklch(0.405 0.101 131.063)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.897 0.196 126.665)", + "chart-2": "oklch(0.768 0.233 130.85)", + "chart-3": "oklch(0.648 0.2 131.684)", + "chart-4": "oklch(0.532 0.157 131.589)", + "chart-5": "oklch(0.453 0.124 130.933)", + "sidebar-primary": "oklch(0.648 0.2 131.684)", + "sidebar-primary-foreground": "oklch(0.986 0.031 120.757)", + }, + dark: { + primary: "oklch(0.768 0.233 130.85)", + "primary-foreground": "oklch(0.405 0.101 131.063)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.897 0.196 126.665)", + "chart-2": "oklch(0.768 0.233 130.85)", + "chart-3": "oklch(0.648 0.2 131.684)", + "chart-4": "oklch(0.532 0.157 131.589)", + "chart-5": "oklch(0.453 0.124 130.933)", + "sidebar-primary": "oklch(0.768 0.233 130.85)", + "sidebar-primary-foreground": "oklch(0.274 0.072 132.109)", + }, + }, + }, + { + name: "orange", + title: "Orange", + cssVars: { + light: { + primary: "oklch(0.553 0.195 38.402)", + "primary-foreground": "oklch(0.98 0.016 73.684)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.837 0.128 66.29)", + "chart-2": "oklch(0.705 0.213 47.604)", + "chart-3": "oklch(0.646 0.222 41.116)", + "chart-4": "oklch(0.553 0.195 38.402)", + "chart-5": "oklch(0.47 0.157 37.304)", + "sidebar-primary": "oklch(0.646 0.222 41.116)", + "sidebar-primary-foreground": "oklch(0.98 0.016 73.684)", + }, + dark: { + primary: "oklch(0.47 0.157 37.304)", + "primary-foreground": "oklch(0.98 0.016 73.684)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.837 0.128 66.29)", + "chart-2": "oklch(0.705 0.213 47.604)", + "chart-3": "oklch(0.646 0.222 41.116)", + "chart-4": "oklch(0.553 0.195 38.402)", + "chart-5": "oklch(0.47 0.157 37.304)", + "sidebar-primary": "oklch(0.705 0.213 47.604)", + "sidebar-primary-foreground": "oklch(0.98 0.016 73.684)", + }, + }, + }, + { + name: "pink", + title: "Pink", + cssVars: { + light: { + primary: "oklch(0.525 0.223 3.958)", + "primary-foreground": "oklch(0.971 0.014 343.198)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.823 0.12 346.018)", + "chart-2": "oklch(0.656 0.241 354.308)", + "chart-3": "oklch(0.592 0.249 0.584)", + "chart-4": "oklch(0.525 0.223 3.958)", + "chart-5": "oklch(0.459 0.187 3.815)", + "sidebar-primary": "oklch(0.592 0.249 0.584)", + "sidebar-primary-foreground": "oklch(0.971 0.014 343.198)", + }, + dark: { + primary: "oklch(0.459 0.187 3.815)", + "primary-foreground": "oklch(0.971 0.014 343.198)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.823 0.12 346.018)", + "chart-2": "oklch(0.656 0.241 354.308)", + "chart-3": "oklch(0.592 0.249 0.584)", + "chart-4": "oklch(0.525 0.223 3.958)", + "chart-5": "oklch(0.459 0.187 3.815)", + "sidebar-primary": "oklch(0.656 0.241 354.308)", + "sidebar-primary-foreground": "oklch(0.971 0.014 343.198)", + }, + }, + }, + { + name: "purple", + title: "Purple", + cssVars: { + light: { + primary: "oklch(0.496 0.265 301.924)", + "primary-foreground": "oklch(0.977 0.014 308.299)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.827 0.119 306.383)", + "chart-2": "oklch(0.627 0.265 303.9)", + "chart-3": "oklch(0.558 0.288 302.321)", + "chart-4": "oklch(0.496 0.265 301.924)", + "chart-5": "oklch(0.438 0.218 303.724)", + "sidebar-primary": "oklch(0.558 0.288 302.321)", + "sidebar-primary-foreground": "oklch(0.977 0.014 308.299)", + }, + dark: { + primary: "oklch(0.438 0.218 303.724)", + "primary-foreground": "oklch(0.977 0.014 308.299)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.827 0.119 306.383)", + "chart-2": "oklch(0.627 0.265 303.9)", + "chart-3": "oklch(0.558 0.288 302.321)", + "chart-4": "oklch(0.496 0.265 301.924)", + "chart-5": "oklch(0.438 0.218 303.724)", + "sidebar-primary": "oklch(0.627 0.265 303.9)", + "sidebar-primary-foreground": "oklch(0.977 0.014 308.299)", + }, + }, + }, + { + name: "red", + title: "Red", + cssVars: { + light: { + primary: "oklch(0.505 0.213 27.518)", + "primary-foreground": "oklch(0.971 0.013 17.38)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.808 0.114 19.571)", + "chart-2": "oklch(0.637 0.237 25.331)", + "chart-3": "oklch(0.577 0.245 27.325)", + "chart-4": "oklch(0.505 0.213 27.518)", + "chart-5": "oklch(0.444 0.177 26.899)", + "sidebar-primary": "oklch(0.577 0.245 27.325)", + "sidebar-primary-foreground": "oklch(0.971 0.013 17.38)", + }, + dark: { + primary: "oklch(0.444 0.177 26.899)", + "primary-foreground": "oklch(0.971 0.013 17.38)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.808 0.114 19.571)", + "chart-2": "oklch(0.637 0.237 25.331)", + "chart-3": "oklch(0.577 0.245 27.325)", + "chart-4": "oklch(0.505 0.213 27.518)", + "chart-5": "oklch(0.444 0.177 26.899)", + "sidebar-primary": "oklch(0.637 0.237 25.331)", + "sidebar-primary-foreground": "oklch(0.971 0.013 17.38)", + }, + }, + }, + { + name: "rose", + title: "Rose", + cssVars: { + light: { + primary: "oklch(0.514 0.222 16.935)", + "primary-foreground": "oklch(0.969 0.015 12.422)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.81 0.117 11.638)", + "chart-2": "oklch(0.645 0.246 16.439)", + "chart-3": "oklch(0.586 0.253 17.585)", + "chart-4": "oklch(0.514 0.222 16.935)", + "chart-5": "oklch(0.455 0.188 13.697)", + "sidebar-primary": "oklch(0.586 0.253 17.585)", + "sidebar-primary-foreground": "oklch(0.969 0.015 12.422)", + }, + dark: { + primary: "oklch(0.455 0.188 13.697)", + "primary-foreground": "oklch(0.969 0.015 12.422)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.81 0.117 11.638)", + "chart-2": "oklch(0.645 0.246 16.439)", + "chart-3": "oklch(0.586 0.253 17.585)", + "chart-4": "oklch(0.514 0.222 16.935)", + "chart-5": "oklch(0.455 0.188 13.697)", + sidebar: "oklch(0.21 0.006 285.885)", + "sidebar-primary": "oklch(0.645 0.246 16.439)", + "sidebar-primary-foreground": "oklch(0.969 0.015 12.422)", + }, + }, + }, + { + name: "sky", + title: "Sky", + cssVars: { + light: { + primary: "oklch(0.5 0.134 242.749)", + "primary-foreground": "oklch(0.977 0.013 236.62)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.828 0.111 230.318)", + "chart-2": "oklch(0.685 0.169 237.323)", + "chart-3": "oklch(0.588 0.158 241.966)", + "chart-4": "oklch(0.5 0.134 242.749)", + "chart-5": "oklch(0.443 0.11 240.79)", + "sidebar-primary": "oklch(0.588 0.158 241.966)", + "sidebar-primary-foreground": "oklch(0.977 0.013 236.62)", + }, + dark: { + primary: "oklch(0.443 0.11 240.79)", + "primary-foreground": "oklch(0.977 0.013 236.62)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.828 0.111 230.318)", + "chart-2": "oklch(0.685 0.169 237.323)", + "chart-3": "oklch(0.588 0.158 241.966)", + "chart-4": "oklch(0.5 0.134 242.749)", + "chart-5": "oklch(0.443 0.11 240.79)", + "sidebar-primary": "oklch(0.685 0.169 237.323)", + "sidebar-primary-foreground": "oklch(0.293 0.066 243.157)", + }, + }, + }, + { + name: "teal", + title: "Teal", + cssVars: { + light: { + primary: "oklch(0.511 0.096 186.391)", + "primary-foreground": "oklch(0.984 0.014 180.72)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.855 0.138 181.071)", + "chart-2": "oklch(0.704 0.14 182.503)", + "chart-3": "oklch(0.6 0.118 184.704)", + "chart-4": "oklch(0.511 0.096 186.391)", + "chart-5": "oklch(0.437 0.078 188.216)", + "sidebar-primary": "oklch(0.6 0.118 184.704)", + "sidebar-primary-foreground": "oklch(0.984 0.014 180.72)", + }, + dark: { + primary: "oklch(0.437 0.078 188.216)", + "primary-foreground": "oklch(0.984 0.014 180.72)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.855 0.138 181.071)", + "chart-2": "oklch(0.704 0.14 182.503)", + "chart-3": "oklch(0.6 0.118 184.704)", + "chart-4": "oklch(0.511 0.096 186.391)", + "chart-5": "oklch(0.437 0.078 188.216)", + "sidebar-primary": "oklch(0.704 0.14 182.503)", + "sidebar-primary-foreground": "oklch(0.277 0.046 192.524)", + }, + }, + }, + { + name: "violet", + title: "Violet", + cssVars: { + light: { + primary: "oklch(0.491 0.27 292.581)", + "primary-foreground": "oklch(0.969 0.016 293.756)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.811 0.111 293.571)", + "chart-2": "oklch(0.606 0.25 292.717)", + "chart-3": "oklch(0.541 0.281 293.009)", + "chart-4": "oklch(0.491 0.27 292.581)", + "chart-5": "oklch(0.432 0.232 292.759)", + "sidebar-primary": "oklch(0.541 0.281 293.009)", + "sidebar-primary-foreground": "oklch(0.969 0.016 293.756)", + }, + dark: { + primary: "oklch(0.432 0.232 292.759)", + "primary-foreground": "oklch(0.969 0.016 293.756)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.811 0.111 293.571)", + "chart-2": "oklch(0.606 0.25 292.717)", + "chart-3": "oklch(0.541 0.281 293.009)", + "chart-4": "oklch(0.491 0.27 292.581)", + "chart-5": "oklch(0.432 0.232 292.759)", + "sidebar-primary": "oklch(0.606 0.25 292.717)", + "sidebar-primary-foreground": "oklch(0.969 0.016 293.756)", + }, + }, + }, + { + name: "yellow", + title: "Yellow", + cssVars: { + light: { + primary: "oklch(0.852 0.199 91.936)", + "primary-foreground": "oklch(0.421 0.095 57.708)", + secondary: "oklch(0.967 0.001 286.375)", + "secondary-foreground": "oklch(0.21 0.006 285.885)", + "chart-1": "oklch(0.905 0.182 98.111)", + "chart-2": "oklch(0.795 0.184 86.047)", + "chart-3": "oklch(0.681 0.162 75.834)", + "chart-4": "oklch(0.554 0.135 66.442)", + "chart-5": "oklch(0.476 0.114 61.907)", + "sidebar-primary": "oklch(0.681 0.162 75.834)", + "sidebar-primary-foreground": "oklch(0.987 0.026 102.212)", + }, + dark: { + primary: "oklch(0.795 0.184 86.047)", + "primary-foreground": "oklch(0.421 0.095 57.708)", + secondary: "oklch(0.274 0.006 286.033)", + "secondary-foreground": "oklch(0.985 0 0)", + "chart-1": "oklch(0.905 0.182 98.111)", + "chart-2": "oklch(0.795 0.184 86.047)", + "chart-3": "oklch(0.681 0.162 75.834)", + "chart-4": "oklch(0.554 0.135 66.442)", + "chart-5": "oklch(0.476 0.114 61.907)", + "sidebar-primary": "oklch(0.795 0.184 86.047)", + "sidebar-primary-foreground": "oklch(0.987 0.026 102.212)", + }, + }, + }, +] diff --git a/packages/appearance/src/use-resolved-theme.ts b/packages/appearance/src/use-resolved-theme.ts new file mode 100644 index 0000000..4c3d153 --- /dev/null +++ b/packages/appearance/src/use-resolved-theme.ts @@ -0,0 +1,32 @@ +import React from "react" +import { usePreference } from "@workspace/preferences" + +import type { ThemeMode } from "./preferences" +import type { ThemeScheme } from "./theme" + +const systemThemeQuery = "(prefers-color-scheme: dark)" + +function subscribeToSystemTheme(listener: VoidFunction) { + const mediaQuery = window.matchMedia(systemThemeQuery) + mediaQuery.addEventListener("change", listener) + return () => mediaQuery.removeEventListener("change", listener) +} + +function getSystemTheme(): ThemeScheme { + return window.matchMedia(systemThemeQuery).matches ? "dark" : "light" +} + +function getServerSystemTheme(): ThemeScheme { + return "light" +} + +export function useResolvedTheme(): ThemeScheme { + const themeMode: ThemeMode = usePreference("theme-mode")[0] + const systemTheme = React.useSyncExternalStore( + subscribeToSystemTheme, + getSystemTheme, + getServerSystemTheme + ) + + return themeMode === "system" ? systemTheme : themeMode +} diff --git a/packages/appearance/tsconfig.json b/packages/appearance/tsconfig.json new file mode 100644 index 0000000..a8c40d3 --- /dev/null +++ b/packages/appearance/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "jsx": "react-jsx", + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "paths": { + "@workspace/appearance": ["./src/index.ts"], + "@workspace/appearance/*": ["./src/*"], + "@workspace/i18n": ["../i18n/src/index.ts"], + "@workspace/preferences": ["../preferences/src/index.tsx"], + "@workspace/ui/*": ["../ui/src/*"] + } + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file
+ +
{description}