diff --git a/src/components/OmniBar.tsx b/src/components/OmniBar.tsx index 88db3c1a..a6288112 100644 --- a/src/components/OmniBar.tsx +++ b/src/components/OmniBar.tsx @@ -18,6 +18,7 @@ import { focusStore } from '~/core/FocusStore' import { messageTable } from '~/core/message/MessageTable' import { humanizeShortcut } from '~/utils/humanizeShortcut' +import { getThemeKeys, getThemeDisplayNames } from '~/utils/themeConfig' const isSelected = ({ parent, id }: ActionImpl) => { if (parent === 'theme') { @@ -98,6 +99,18 @@ export function RenderResults() { } const useRegisterThemeActions = () => { + const themeKeys = getThemeKeys() + const themeDisplayNames = getThemeDisplayNames() + + const themeActions = themeKeys.map(theme => ({ + id: theme, + name: themeDisplayNames[theme], + keywords: `${theme} theme`, + section: 'Theme', + perform: async () => settingStore.update({ theme }), + parent: 'theme', + })) + useRegisterActions([ { id: 'theme', @@ -105,38 +118,7 @@ const useRegisterThemeActions = () => { keywords: 'interface color dark light', section: 'Preferences', }, - { - id: 'dark', - name: 'Dark', - keywords: 'dark theme', - section: 'Theme', - perform: async () => settingStore.update({ theme: 'dark' }), - parent: 'theme', - }, - { - id: 'dracula', - name: 'Dracula', - keywords: 'dracula theme', - section: 'Theme', - perform: async () => settingStore.update({ theme: 'dracula' }), - parent: 'theme', - }, - { - id: '_system', - name: 'System', - keywords: 'system theme', - section: 'Theme', - perform: async () => settingStore.update({ theme: '_system' }), - parent: 'theme', - }, - { - id: 'garden', - name: 'Light', - keywords: 'light garden theme', - section: 'Theme', - perform: async () => settingStore.update({ theme: 'garden' }), - parent: 'theme', - }, + ...themeActions, ]) } diff --git a/src/components/ThemeSelector.tsx b/src/components/ThemeSelector.tsx index ac01a554..f44b24f8 100644 --- a/src/components/ThemeSelector.tsx +++ b/src/components/ThemeSelector.tsx @@ -3,13 +3,9 @@ import { twMerge } from 'tailwind-merge' import { settingStore } from '~/core/setting/SettingStore' import { Select, SelectItem } from '@heroui/react' +import { getThemeDisplayNames } from '~/utils/themeConfig' -const themes = { - _system: 'System theme', - dark: 'Dark', - dracula: 'Dracula', - garden: 'Light', -} +const themes = getThemeDisplayNames() const ThemeSelector = () => { const selectedTheme = settingStore.setting.theme diff --git a/src/containers/DaisyUiThemeProvider.tsx b/src/containers/DaisyUiThemeProvider.tsx index dbe0c5b6..7fd13798 100644 --- a/src/containers/DaisyUiThemeProvider.tsx +++ b/src/containers/DaisyUiThemeProvider.tsx @@ -2,15 +2,19 @@ import { useEffect, useMemo, type PropsWithChildren } from 'react' import useMedia from 'use-media' import { settingStore } from '~/core/setting/SettingStore' +import { resolveThemeAlias, systemThemeConfig } from '~/utils/themeConfig' const DaisyUiThemeProvider = ({ children }: PropsWithChildren) => { const selectedTheme = settingStore.setting.theme const prefersDarkMode = useMedia('(prefers-color-scheme: dark)') const theme = useMemo(() => { - if (selectedTheme !== '_system') return selectedTheme - - return prefersDarkMode ? 'dark' : 'garden' + if (selectedTheme === '_system') { + const systemTheme = prefersDarkMode ? systemThemeConfig.dark : systemThemeConfig.light + return resolveThemeAlias(systemTheme) + } else { + return resolveThemeAlias(selectedTheme) + } }, [selectedTheme, prefersDarkMode]) useEffect(() => { diff --git a/src/core/BaseTable.ts b/src/core/BaseTable.ts index 04b8e1e6..95dd7d69 100644 --- a/src/core/BaseTable.ts +++ b/src/core/BaseTable.ts @@ -95,7 +95,13 @@ export abstract class BaseTable< if (!entity) return undefined - return this.cache.put(this.schema.parse(entity), false) + const result = this.schema.safeParse(entity) + + if (result.success) { + return this.cache.put(result.data, false) + } else { + return undefined + } } async findByIds(ids: string[]): Promise { diff --git a/src/core/setting/SettingModel.ts b/src/core/setting/SettingModel.ts index c3a34386..54b04c43 100644 --- a/src/core/setting/SettingModel.ts +++ b/src/core/setting/SettingModel.ts @@ -1,15 +1,12 @@ import z from 'zod' import moment from 'moment' +import { getThemeKeys } from '~/utils/themeConfig' export const CURRENT_DB_TIMESTAMP = moment('Oct 28 24', 'MMM DD YY') export const CURRENT_DB_TIMESTAMP_MILLISECONDS = CURRENT_DB_TIMESTAMP.valueOf() -const ThemeOptions = z.union([ - z.literal('_system'), - z.literal('dark'), - z.literal('dracula'), - z.literal('garden'), -]) +const themeKeys = getThemeKeys() as [string, ...string[]] +const ThemeOptions = z.enum(themeKeys) export const SettingModel = z.object({ // setting row will only have one field diff --git a/src/utils/themeConfig.ts b/src/utils/themeConfig.ts new file mode 100644 index 00000000..c40dc3d0 --- /dev/null +++ b/src/utils/themeConfig.ts @@ -0,0 +1,131 @@ +import allThemes from 'daisyui/src/theming/themes' + +export const colors = { + error: 'oklch(51% 0.17 22.1)', +} + +export const systemThemeConfig = { light: 'light', dark: 'dark' } + +export const themeConfig = { + enabledThemes: [ + 'dark', + 'dracula', + { light: 'garden' }, // alias: light -> garden + 'nord', + 'synthwave', + ], + + defaultTheme: 'dracula', + + customizations: { + // Regex patterns for multiple themes (examples) + // '^dark.*': { /* customizations for themes starting with 'dark' */ }, + // '.*night.*': { /* customizations for themes containing 'night' */ }, + // '(dracula|dark|night|black)': { /* customizations for dark-themed themes */ }, + // '^(light|garden|cupcake)$': { /* customizations for specific light themes */ }, + + '*': { + error: colors.error, + }, + + garden: { + primary: 'oklch(62.45% 0.1947 3.83636)', + }, + }, +} + +export const getThemeKeys = (): string[] => { + const themes: string[] = [] + + themes.push('_system') + themeConfig.enabledThemes.forEach(theme => { + if (typeof theme === 'string') { + themes.push(theme) + } else { + themes.push(Object.keys(theme)[0]) // push alias as theme + } + }) + + return themes +} + +export const getThemeDisplayNames = (): Record => { + const themes: Record = {} + + themes['_system'] = 'System Theme' + themeConfig.enabledThemes.forEach(theme => { + if (typeof theme === 'string') { + themes[theme] = theme.charAt(0).toUpperCase() + theme.slice(1) + } else { + const [alias] = Object.keys(theme) + themes[alias] = alias.charAt(0).toUpperCase() + alias.slice(1) + } + }) + + return themes +} + +export const resolveThemeAlias = (themeKey: string): string => { + if (themeKey === '_system') return '_system' + + for (const theme of themeConfig.enabledThemes) { + if (typeof theme === 'object') { + const [alias, actualTheme] = Object.entries(theme)[0] + if (alias === themeKey) { + return actualTheme + } + } + } + + return themeKey +} + +export const getEnabledDaisyThemes = () => { + const enabledThemes: Record = {} + + themeConfig.enabledThemes.forEach(theme => { + let actualThemeName: string + + if (typeof theme === 'string') { + actualThemeName = theme + } else { + // It's an alias object, get the actual theme name + const [alias, actualTheme] = Object.entries(theme)[0] + + // Skip _system theme as it's not a real DaisyUI theme + if (alias === '_system') return + + actualThemeName = actualTheme as string + } + + if ((allThemes as any)[actualThemeName]) { + let themeCustomizations = { ...(allThemes as any)[actualThemeName] } + + // Apply customizations in order of specificity (least to most specific) + Object.entries(themeConfig.customizations).forEach(([pattern, customization]) => { + if (pattern === '*') { + // Global wildcard - applies to all themes + themeCustomizations = { ...themeCustomizations, ...customization } + } else if (pattern === actualThemeName) { + // Exact theme name match - most specific, applied last + themeCustomizations = { ...themeCustomizations, ...customization } + } else { + // Try regex pattern matching + try { + const regex = new RegExp(pattern) + if (regex.test(actualThemeName)) { + themeCustomizations = { ...themeCustomizations, ...customization } + } + } catch (e) { + // Invalid regex pattern, skip silently + console.warn(`Invalid regex pattern in theme customizations: ${pattern}`) + } + } + }) + + enabledThemes[actualThemeName] = themeCustomizations + } + }) + + return enabledThemes +} diff --git a/tailwind.config.ts b/tailwind.config.ts index 3125c150..dcfee5fc 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,54 +1,37 @@ /** @type {import('tailwindcss').Config} */ -import { heroui } from "@heroui/react" -import themes from 'daisyui/src/theming/themes' +import { heroui } from '@heroui/react' +import { getEnabledDaisyThemes, colors } from './src/utils/themeConfig' -const errorColor = 'oklch(51% 0.17 22.1)' +const allThemes = Object.entries(getEnabledDaisyThemes()).map(([name, theme]) => ({ + [name]: theme, +})) module.exports = { - content: [ - 'src/**/*.{js,ts,jsx,tsx}', - "./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}", - ], + content: ['src/**/*.{js,ts,jsx,tsx}', './node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}'], theme: { extend: {}, }, darkMode: 'class', - plugins: [require('@tailwindcss/typography'), require('daisyui'), heroui({ - themes: { - light: { - colors: { - danger: errorColor - } - }, - dark: { - colors: { - danger: errorColor - } - } - } - })], - daisyui: { - themes: [ - { - garden: { - ...themes['garden'], - primary: 'oklch(62.45% 0.1947 3.83636)', - error: errorColor + plugins: [ + require('@tailwindcss/typography'), + require('daisyui'), + heroui({ + themes: { + light: { + colors: { + danger: colors.error, + }, }, - }, - { dark: { - ...themes['dark'], - error: errorColor + colors: { + danger: colors.error, + }, }, }, - { - dracula: { - ...themes['dracula'], - error: errorColor - }, - }, - ], + }), + ], + daisyui: { + themes: allThemes, }, }