From d01074cef1d3823255c2fd6b07a239ae8d4383a3 Mon Sep 17 00:00:00 2001 From: axolotle Date: Wed, 14 Feb 2024 14:09:07 +0100 Subject: [PATCH] feat: add user defined theme variable override --- composables/states.ts | 7 ++++ composables/theming.ts | 93 ++++++++++++++++++++++++++++++++++++++++++ locales/en.json | 25 ++++++++++++ pages/edit.vue | 86 ++++++++++++++++++++++++++++++++++++++ utils/common.ts | 4 ++ 5 files changed, 215 insertions(+) create mode 100644 composables/theming.ts diff --git a/composables/states.ts b/composables/states.ts index d975021..4986249 100644 --- a/composables/states.ts +++ b/composables/states.ts @@ -1,3 +1,5 @@ +import { useThemeOverrideState } from '@/composables/theming' + // AUTH export const useIsLoggedIn = () => { @@ -133,6 +135,11 @@ export const useSettings = async () => { const colorMode = useColorMode() colorMode.preference = settings.value.portal_theme + + const themeOverride = useThemeOverrideState() + if (themeOverride.value) { + useThemeOverride().init(themeOverride.value) + } } return settings diff --git a/composables/theming.ts b/composables/theming.ts new file mode 100644 index 0000000..03e8773 --- /dev/null +++ b/composables/theming.ts @@ -0,0 +1,93 @@ +import { reactive } from 'vue' +import C from 'colorjs.io' + +import { keysOf } from '@/utils/common' + +enum Color { + primary = 'p', + secondary = 's', + accent = 'a', + neutral = 'n', + 'base-100' = 'b1', + 'base-200' = 'b2', + 'base-300' = 'b3', + 'base-content' = 'bc', + info = 'in', + success = 'su', + warning = 'wa', + error = 'er', +} + +enum Size { + 'card-radius' = 'rounded-box', + 'btn-radius' = 'rounded-btn', + 'btn-border' = 'border-btn', + // Other possible vars, not ~used atm + // "rounded-badge", + // "animation-btn", + // "animation-input", + // "btn-focus-scale", + // "tab-border", + // "tab-radius", +} + +type Colors = Record +type Sizes = Record +type Theme = Colors & Sizes + +export const useThemeOverrideState = () => + useState('customTheme', () => { + const theme = localStorage.getItem('customTheme') + return theme ? JSON.parse(theme) : null + }) + +export const useThemeOverride = () => { + const themeOverride = useThemeOverrideState() + const colorNames = keysOf(Color) + const sizeNames = keysOf(Size) + const colors = reactive( + Object.fromEntries( + colorNames.map((key) => [key, themeOverride.value?.[key] ?? '']), + ) as Colors, + ) + const sizes = reactive( + Object.fromEntries( + sizeNames.map((key) => [key, themeOverride.value?.[key] ?? '']), + ) as Sizes, + ) + + const toCss = (theme: Theme) => { + return keysOf(theme) + .reduce((cssVars, key) => { + if (theme[key] === '') return cssVars + if (key in Color) { + const oklch = new C(theme[key]) + .to('oklch') + .coords.map((n) => (isNaN(n) ? 0 : n)) + .join(' ') + cssVars.push(`--${Color[key as keyof Colors]}: ${oklch};`) + } else { + cssVars.push(`--${Size[key as keyof Sizes]}: ${theme[key]}rem;`) + } + return cssVars + }, []) + .join('') + } + + const update = (theme: Theme | null) => { + localStorage.setItem('customTheme', theme ? JSON.stringify(theme) : '') + document + .querySelector('html')! + .setAttribute('style', theme ? toCss(theme) : '') + } + + watch([sizes, colors], () => { + update({ ...colors, ...sizes }) + }) + + return { + init: update, + colors, + sizes, + } +} diff --git a/locales/en.json b/locales/en.json index 69306e2..601619a 100644 --- a/locales/en.json +++ b/locales/en.json @@ -14,6 +14,31 @@ "automatic": "Automatic ({name})", "back_to_apps": "Go back to app list", "cancel": "Cancel", + "theming": { + "color_picker": "Pick a color for {colorName}", + "color_hex": "Enter a color hex value for {colorName}", + "colors": { + "primary": "Primary", + "secondary": "Secondary", + "accent": "Accentuated", + "neutral": "Neutral", + "base-100": "Site background", + "base-200": "Disabled state", + "base-300": "Card borders and headers", + "base-content": "Text", + "info": "Information", + "success": "Success/Valid", + "warning": "Warning", + "error": "Error/Invalid" + }, + "override": "Theme override", + "size": "Define a size in rem for {sizeName}", + "sizes": { + "card-radius": "Card radius", + "btn-radius": "Button radius", + "btn-border": "Button border size" + } + }, "change_password": "Change password", "confirm_new_password": "Confirm new password", "current_password": "Current password", diff --git a/pages/edit.vue b/pages/edit.vue index 53800d0..096c7f3 100644 --- a/pages/edit.vue +++ b/pages/edit.vue @@ -26,6 +26,8 @@ const localesAsOptions = computed(() => { return options }) +const { colors, sizes } = useThemeOverride() + const themesAsOptions = [ 'auto', 'system', @@ -131,7 +133,91 @@ const themesAsOptions = [ + +
+ {{ $t('theming.override') }} + +
+ + + +
+ + + + + + + + {{ $t(`theming.colors.${colorName}`) }} + +
+ +
+ + + + + {{ $t(`theming.sizes.${sizeName}`) }} + + +
+
+ + diff --git a/utils/common.ts b/utils/common.ts index eb12cc4..5c447eb 100644 --- a/utils/common.ts +++ b/utils/common.ts @@ -16,3 +16,7 @@ export function exclude( }) return filtered } + +export function keysOf>(obj: T) { + return Object.keys(obj) as Array +}