mirror of
https://github.com/unraid/api.git
synced 2025-12-31 13:39:52 -06:00
feat: update theme application logic and color picker (#1181)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Upgraded the theme customization interface with a dropdown that lets you choose from multiple themes (Light, Dark, Azure, Gray). Users can now adjust options like text colors, background color, gradients, and banner display more intuitively. - Introduced a structured approach to theme variables, enhancing compatibility and customization options. - **Style** - Enhanced the header’s visual presentation by introducing dynamic background imagery and refined layout adjustments for a more polished look. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,92 +1,77 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Input, Label, Switch } from '@unraid/ui';
|
import { Input, Label, Switch, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@unraid/ui';
|
||||||
|
import type { Theme } from '~/themes/types';
|
||||||
import type { Theme } from '~/store/theme';
|
import { useThemeStore } from '~/store/theme';
|
||||||
|
import { defaultColors } from '~/themes/default';
|
||||||
import { defaultColors, useThemeStore } from '~/store/theme';
|
|
||||||
|
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
const { darkMode } = toRefs(themeStore);
|
|
||||||
|
|
||||||
const setDarkMode = ref<boolean>(false);
|
// Form state
|
||||||
const setGradient = ref<boolean>(false);
|
const form = reactive({
|
||||||
const setDescription = ref<boolean>(true);
|
selectedTheme: 'white',
|
||||||
const setBanner = ref<boolean>(true);
|
gradient: false,
|
||||||
|
description: true,
|
||||||
const toggleSwitch = (value: boolean) => {
|
banner: true,
|
||||||
setDarkMode.value = value;
|
textPrimary: '',
|
||||||
};
|
textSecondary: '',
|
||||||
const toggleGradient = (value: boolean) => {
|
bgColor: ''
|
||||||
setGradient.value = value;
|
|
||||||
};
|
|
||||||
const toggleDescription = (value: boolean) => {
|
|
||||||
setDescription.value = value;
|
|
||||||
};
|
|
||||||
const toggleBanner = (value: boolean) => {
|
|
||||||
setBanner.value = value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const textPrimary = ref<string>('');
|
|
||||||
const textSecondary = ref<string>('');
|
|
||||||
const bgColor = ref<string>('');
|
|
||||||
|
|
||||||
const textPrimaryToSet = computed(() => {
|
|
||||||
if (textPrimary.value) {
|
|
||||||
return textPrimary.value;
|
|
||||||
}
|
|
||||||
return darkMode.value ? defaultColors.dark.headerTextPrimary : defaultColors.light.headerTextPrimary;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const textSecondaryToSet = computed(() => {
|
// Watch for changes and update theme
|
||||||
if (textSecondary.value) {
|
watch([form], () => {
|
||||||
return textSecondary.value;
|
// Enable gradient if banner is enabled
|
||||||
|
if (form.banner && !form.gradient) {
|
||||||
|
form.gradient = true;
|
||||||
}
|
}
|
||||||
return darkMode.value
|
|
||||||
? defaultColors.dark.headerTextSecondary
|
|
||||||
: defaultColors.light.headerTextSecondary;
|
|
||||||
});
|
|
||||||
|
|
||||||
const bgColorToSet = computed(() => {
|
|
||||||
if (bgColor.value) {
|
|
||||||
return bgColor.value;
|
|
||||||
}
|
|
||||||
return darkMode.value
|
|
||||||
? defaultColors.dark.headerBackgroundColor
|
|
||||||
: defaultColors.light.headerBackgroundColor;
|
|
||||||
});
|
|
||||||
|
|
||||||
watch([setDarkMode, bgColorToSet, textSecondaryToSet, textPrimaryToSet], (newVal) => {
|
|
||||||
console.log(newVal);
|
|
||||||
const themeToSet: Theme = {
|
const themeToSet: Theme = {
|
||||||
banner: setBanner.value,
|
banner: form.banner,
|
||||||
bannerGradient: setGradient.value,
|
bannerGradient: form.gradient,
|
||||||
descriptionShow: setDescription.value,
|
descriptionShow: form.description,
|
||||||
textColor: textPrimaryToSet.value,
|
textColor: form.textPrimary ?? defaultColors[form.selectedTheme]['--header-text-primary']!,
|
||||||
metaColor: textSecondaryToSet.value,
|
metaColor: form.textSecondary ?? defaultColors[form.selectedTheme]['--header-text-secondary']!,
|
||||||
bgColor: bgColorToSet.value,
|
bgColor: form.bgColor ?? defaultColors[form.selectedTheme]['--header-background-color']!,
|
||||||
name: setDarkMode.value ? 'black' : 'light',
|
name: form.selectedTheme
|
||||||
};
|
};
|
||||||
themeStore.setTheme(themeToSet);
|
themeStore.setTheme(themeToSet);
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-2 border-solid border-2 p-2 border-r-2">
|
<div class="flex flex-col gap-2 border-solid border-2 p-2 border-r-2">
|
||||||
<h1 class="text-lg">Color Theme Customization</h1>
|
<h1 class="text-lg">Color Theme Customization</h1>
|
||||||
|
|
||||||
|
<Label for="theme-select">Theme</Label>
|
||||||
|
<Select v-model="form.selectedTheme">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a theme" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="white">Light</SelectItem>
|
||||||
|
<SelectItem value="black">Dark</SelectItem>
|
||||||
|
<SelectItem value="azure">Azure</SelectItem>
|
||||||
|
<SelectItem value="gray">Gray</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
<Label for="primary-text-color">Header Primary Text Color</Label>
|
<Label for="primary-text-color">Header Primary Text Color</Label>
|
||||||
<Input id="primary-text-color" v-model="textPrimary" />
|
<Input id="primary-text-color" v-model="form.textPrimary" />
|
||||||
<Label for="primary-text-color">Header Secondary Text Color</Label>
|
|
||||||
<Input id="primary-text-color" v-model="textSecondary" />
|
<Label for="secondary-text-color">Header Secondary Text Color</Label>
|
||||||
<Label for="primary-text-color">Header Background Color</Label>
|
<Input id="secondary-text-color" v-model="form.textSecondary" />
|
||||||
<Input id="primary-text-color" v-model="bgColor" />
|
|
||||||
<Label for="dark-mode">Dark Mode</Label>
|
<Label for="background-color">Header Background Color</Label>
|
||||||
<Switch id="dark-mode" @update:checked="toggleSwitch" />
|
<Input id="background-color" v-model="form.bgColor" />
|
||||||
|
|
||||||
<Label for="gradient">Gradient</Label>
|
<Label for="gradient">Gradient</Label>
|
||||||
<Switch id="gradient" @update:checked="toggleGradient" />
|
<Switch id="gradient" v-model:checked="form.gradient" />
|
||||||
|
|
||||||
<Label for="description">Description</Label>
|
<Label for="description">Description</Label>
|
||||||
<Switch id="description" @update:checked="toggleDescription" />
|
<Switch id="description" v-model:checked="form.description" />
|
||||||
|
|
||||||
<Label for="banner">Banner</Label>
|
<Label for="banner">Banner</Label>
|
||||||
<Switch id="banner" @update:checked="toggleBanner" />
|
<Switch id="banner" v-model:checked="form.banner" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ import AES from 'crypto-js/aes';
|
|||||||
import type { SendPayloads } from '~/store/callback';
|
import type { SendPayloads } from '~/store/callback';
|
||||||
|
|
||||||
import SsoButtonCe from '~/components/SsoButton.ce.vue';
|
import SsoButtonCe from '~/components/SsoButton.ce.vue';
|
||||||
|
import { useThemeStore } from '~/store/theme';
|
||||||
|
|
||||||
const serverStore = useDummyServerStore();
|
const serverStore = useDummyServerStore();
|
||||||
const { serverState } = storeToRefs(serverStore);
|
const { serverState } = storeToRefs(serverStore);
|
||||||
const { registerEntry } = useCustomElements();
|
const { registerEntry } = useCustomElements();
|
||||||
|
const { theme } = storeToRefs(useThemeStore());
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
registerEntry('UnraidComponents');
|
registerEntry('UnraidComponents');
|
||||||
});
|
});
|
||||||
@@ -75,6 +77,13 @@ onMounted(() => {
|
|||||||
'forUpc'
|
'forUpc'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const bannerImage = watch(theme, () => {
|
||||||
|
if (theme.value.banner) {
|
||||||
|
return `url(https://picsum.photos/1920/200?${Math.round(Math.random() * 100)})`;
|
||||||
|
}
|
||||||
|
return 'none';
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -86,7 +95,14 @@ onMounted(() => {
|
|||||||
<ColorSwitcherCe />
|
<ColorSwitcherCe />
|
||||||
<h2 class="text-xl font-semibold font-mono">Vue Components</h2>
|
<h2 class="text-xl font-semibold font-mono">Vue Components</h2>
|
||||||
<h3 class="text-lg font-semibold font-mono">UserProfileCe</h3>
|
<h3 class="text-lg font-semibold font-mono">UserProfileCe</h3>
|
||||||
<header class="bg-header-background-color py-4 flex flex-row justify-between items-center">
|
<header
|
||||||
|
class="bg-header-background-color flex justify-between items-center"
|
||||||
|
:style="{
|
||||||
|
backgroundImage: bannerImage,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center'
|
||||||
|
}"
|
||||||
|
>
|
||||||
<div class="inline-flex flex-col gap-4 items-start px-4">
|
<div class="inline-flex flex-col gap-4 items-start px-4">
|
||||||
<a href="https://unraid.net" target="_blank">
|
<a href="https://unraid.net" target="_blank">
|
||||||
<BrandLogo class="w-[100px] sm:w-[150px]" />
|
<BrandLogo class="w-[100px] sm:w-[150px]" />
|
||||||
|
|||||||
@@ -1,129 +1,33 @@
|
|||||||
import { createPinia, defineStore, setActivePinia } from 'pinia';
|
import { createPinia, defineStore, setActivePinia } from 'pinia';
|
||||||
|
|
||||||
|
import { defaultColors } from '~/themes/default';
|
||||||
import hexToRgba from 'hex-to-rgba';
|
import hexToRgba from 'hex-to-rgba';
|
||||||
|
|
||||||
|
import type { Theme, ThemeVariables } from '~/themes/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||||
* @see https://github.com/vuejs/pinia/discussions/1085
|
* @see https://github.com/vuejs/pinia/discussions/1085
|
||||||
*/
|
*/
|
||||||
setActivePinia(createPinia());
|
setActivePinia(createPinia());
|
||||||
|
|
||||||
export interface Theme {
|
|
||||||
banner: boolean;
|
|
||||||
bannerGradient: boolean;
|
|
||||||
bgColor: string;
|
|
||||||
descriptionShow: boolean;
|
|
||||||
metaColor: string;
|
|
||||||
name: string;
|
|
||||||
textColor: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ThemeVariables {
|
|
||||||
[key: string]: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultColors: Record<string, ThemeVariables> = {
|
|
||||||
dark: {
|
|
||||||
'--background': '0 0% 3.9%',
|
|
||||||
'--foreground': '0 0% 98%',
|
|
||||||
'--muted': '0 0% 14.9%',
|
|
||||||
'--muted-foreground': '0 0% 63.9%',
|
|
||||||
'--popover': '0 0% 3.9%',
|
|
||||||
'--popover-foreground': '0 0% 98%',
|
|
||||||
'--card': '0 0% 14.9%',
|
|
||||||
'--card-foreground': '0 0% 98%',
|
|
||||||
'--border': '0 0% 20%',
|
|
||||||
'--input': '0 0% 14.9%',
|
|
||||||
'--primary': '24 100% 50%',
|
|
||||||
'--primary-foreground': '0 0% 98%',
|
|
||||||
'--secondary': '0 0% 14.9%',
|
|
||||||
'--secondary-foreground': '0 0% 77%',
|
|
||||||
'--accent': '0 0% 14.9%',
|
|
||||||
'--accent-foreground': '0 0% 98%',
|
|
||||||
'--destructive': '0 62.8% 30.6%',
|
|
||||||
'--destructive-foreground': '0 0% 98%',
|
|
||||||
'--ring': '0 0% 83.1%',
|
|
||||||
'--header-text-primary': '#1c1c1c',
|
|
||||||
'--header-text-secondary': '#999999',
|
|
||||||
'--header-background-color': '#f2f2f2',
|
|
||||||
'--gradient-start': 'rgba(0, 0, 0, 0)',
|
|
||||||
'--gradient-end': 'var(--header-background-color)',
|
|
||||||
},
|
|
||||||
light: {
|
|
||||||
'--background': '0 0% 100%',
|
|
||||||
'--foreground': '0 0% 3.9%',
|
|
||||||
'--muted': '0 0% 96.1%',
|
|
||||||
'--muted-foreground': '0 0% 45.1%',
|
|
||||||
'--popover': '0 0% 100%',
|
|
||||||
'--popover-foreground': '0 0% 3.9%',
|
|
||||||
'--card': '0 0% 100%',
|
|
||||||
'--card-foreground': '0 0% 3.9%',
|
|
||||||
'--border': '0 0% 89.8%',
|
|
||||||
'--input': '0 0% 89.8%',
|
|
||||||
'--primary': '24 100% 50%',
|
|
||||||
'--primary-foreground': '0 0% 98%',
|
|
||||||
'--secondary': '0 0% 96.1%',
|
|
||||||
'--secondary-foreground': '0 0% 45%',
|
|
||||||
'--accent': '0 0% 96.1%',
|
|
||||||
'--accent-foreground': '0 0% 9%',
|
|
||||||
'--destructive': '0 84.2% 60.2%',
|
|
||||||
'--destructive-foreground': '0 0% 98%',
|
|
||||||
'--ring': '0 0% 3.9%',
|
|
||||||
'--radius': '0.5rem',
|
|
||||||
'--header-text-primary': '#f2f2f2',
|
|
||||||
'--header-text-secondary': '#999999',
|
|
||||||
'--header-background-color': '#1c1b1b',
|
|
||||||
'--gradient-start': 'rgba(0, 0, 0, 0)',
|
|
||||||
'--gradient-end': 'var(--header-background-color)',
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unraid default theme colors do not have consistent colors for the header.
|
|
||||||
* This is a workaround to set the correct colors for the header.
|
|
||||||
* DARK THEMES: black, gray
|
|
||||||
* DARK HEADER THEMES: white, gray
|
|
||||||
* LIGHT THEMES: white, gray
|
|
||||||
* LIGHT HEADER THEMES: black, gray
|
|
||||||
*/
|
|
||||||
export const defaultAzureGrayHeaderColors: ThemeVariables = {
|
|
||||||
// azure and gray header colors are the same but the background color is different
|
|
||||||
'--header-text-primary': '#39587f',
|
|
||||||
'--header-text-secondary': '#606e7f',
|
|
||||||
};
|
|
||||||
export const defaultHeaderColors: Record<string, ThemeVariables> = {
|
|
||||||
azure: {
|
|
||||||
...defaultAzureGrayHeaderColors,
|
|
||||||
'--header-background-color': '#1c1b1b',
|
|
||||||
},
|
|
||||||
black: {
|
|
||||||
'--header-text-primary': '#1c1c1c',
|
|
||||||
'--header-text-secondary': '#999999',
|
|
||||||
'--header-background-color': '#f2f2f2',
|
|
||||||
},
|
|
||||||
gray: {
|
|
||||||
...defaultAzureGrayHeaderColors,
|
|
||||||
'--header-background-color': '#f2f2f2',
|
|
||||||
},
|
|
||||||
white: {
|
|
||||||
'--header-text-primary': '#f2f2f2',
|
|
||||||
'--header-text-secondary': '#999999',
|
|
||||||
'--header-background-color': '#1c1b1b',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// used to swap the UPC text color when using the azure or gray theme
|
// used to swap the UPC text color when using the azure or gray theme
|
||||||
export const DARK_THEMES = ['black', 'gray'] as const;
|
export const DARK_THEMES = ['black', 'gray'] as const;
|
||||||
|
|
||||||
export const useThemeStore = defineStore('theme', () => {
|
export const useThemeStore = defineStore('theme', () => {
|
||||||
// State
|
// State
|
||||||
const theme = ref<Theme | undefined>();
|
const theme = ref<Theme>({
|
||||||
|
name: 'white',
|
||||||
const activeColorVariables = ref<ThemeVariables>({
|
banner: false,
|
||||||
...defaultColors.light,
|
bannerGradient: false,
|
||||||
...defaultHeaderColors['white'],
|
bgColor: '',
|
||||||
|
descriptionShow: false,
|
||||||
|
metaColor: '',
|
||||||
|
textColor: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const activeColorVariables = ref<ThemeVariables>(defaultColors.white);
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
const darkMode = computed<boolean>(
|
const darkMode = computed<boolean>(
|
||||||
() => DARK_THEMES.includes(theme.value?.name as (typeof DARK_THEMES)[number]) ?? false
|
() => DARK_THEMES.includes(theme.value?.name as (typeof DARK_THEMES)[number]) ?? false
|
||||||
@@ -133,8 +37,8 @@ export const useThemeStore = defineStore('theme', () => {
|
|||||||
if (!theme.value?.banner || !theme.value?.bannerGradient) {
|
if (!theme.value?.banner || !theme.value?.bannerGradient) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const start = theme.value?.bgColor ? 'var(--gradient-start)' : 'rgba(0, 0, 0, 0)';
|
const start = theme.value?.bgColor ? 'var(--header-gradient-start)' : 'rgba(0, 0, 0, 0)';
|
||||||
const end = theme.value?.bgColor ? 'var(--gradient-end)' : 'var(--header-background-color)';
|
const end = theme.value?.bgColor ? 'var(--header-gradient-end)' : 'var(--header-background-color)';
|
||||||
return `background-image: linear-gradient(90deg, ${start} 0, ${end} 30%);`;
|
return `background-image: linear-gradient(90deg, ${start} 0, ${end} 30%);`;
|
||||||
});
|
});
|
||||||
// Actions
|
// Actions
|
||||||
@@ -143,74 +47,68 @@ export const useThemeStore = defineStore('theme', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const setCssVars = () => {
|
const setCssVars = () => {
|
||||||
const customColorVariables = structuredClone(defaultColors);
|
const selectedTheme = theme.value.name;
|
||||||
const body = document.body;
|
const customTheme = { ...defaultColors[selectedTheme] };
|
||||||
const selectedMode = darkMode.value ? 'dark' : 'light';
|
|
||||||
|
|
||||||
// set the default header colors for the current theme
|
|
||||||
const themeName = theme.value?.name;
|
|
||||||
if (themeName && themeName in defaultHeaderColors) {
|
|
||||||
customColorVariables[selectedMode] = {
|
|
||||||
...customColorVariables[selectedMode],
|
|
||||||
...defaultHeaderColors[themeName],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set banner gradient if enabled
|
// Set banner gradient if enabled
|
||||||
if (theme.value?.banner && theme.value?.bannerGradient) {
|
if (theme.value.banner && theme.value.bannerGradient) {
|
||||||
const start = theme.value?.bgColor
|
const start = theme.value.bgColor
|
||||||
? hexToRgba(theme.value.bgColor, 0)
|
? hexToRgba(theme.value.bgColor, 0)
|
||||||
: customColorVariables[selectedMode]['--gradient-start'];
|
: customTheme['--header-gradient-start'];
|
||||||
const end = theme.value?.bgColor
|
const end = theme.value.bgColor
|
||||||
? hexToRgba(theme.value.bgColor, 0.7)
|
? hexToRgba(theme.value.bgColor, 0.7)
|
||||||
: customColorVariables[selectedMode]['--gradient-end'];
|
: customTheme['--header-gradient-end'];
|
||||||
body.style.setProperty('--banner-gradient', `linear-gradient(90deg, ${start} 0, ${end} 30%)`);
|
|
||||||
} else {
|
// set the banner gradient
|
||||||
body.style.removeProperty('--banner-gradient');
|
customTheme['--banner-gradient'] = `linear-gradient(90deg, ${start} 0, ${end} 30%)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// overwrite with hex colors set in webGUI @ /Settings/DisplaySettings
|
// overwrite with hex colors set in webGUI @ /Settings/DisplaySettings
|
||||||
if (theme.value?.textColor) {
|
if (theme.value.textColor) {
|
||||||
body.style.setProperty('--header-text-primary', theme.value.textColor);
|
customTheme['--header-text-primary'] = theme.value.textColor;
|
||||||
} else {
|
|
||||||
body.style.setProperty(
|
|
||||||
'--header-text-primary',
|
|
||||||
customColorVariables[selectedMode]['--header-text-primary']
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (theme.value?.metaColor) {
|
if (theme.value.metaColor) {
|
||||||
body.style.setProperty('--header-text-secondary', theme.value.metaColor);
|
customTheme['--header-text-secondary'] = theme.value.metaColor;
|
||||||
} else {
|
|
||||||
body.style.setProperty(
|
|
||||||
'--header-text-secondary',
|
|
||||||
customColorVariables[selectedMode]['--header-text-secondary']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (theme.value?.bgColor) {
|
|
||||||
body.style.setProperty('--header-background-color', theme.value.bgColor);
|
|
||||||
body.style.setProperty('--gradient-start', hexToRgba(theme.value.bgColor, 0));
|
|
||||||
body.style.setProperty('--gradient-end', hexToRgba(theme.value.bgColor, 0.7));
|
|
||||||
} else {
|
|
||||||
body.style.setProperty(
|
|
||||||
'--header-background-color',
|
|
||||||
customColorVariables[selectedMode]['--header-background-color']
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply all other CSS variables
|
if (theme.value.bgColor) {
|
||||||
Object.entries(customColorVariables[selectedMode]).forEach(([key, value]) => {
|
customTheme['--header-background-color'] = theme.value.bgColor;
|
||||||
if (!key.startsWith('--header-')) {
|
customTheme['--header-gradient-start'] = hexToRgba(theme.value.bgColor, 0);
|
||||||
body.style.setProperty(key, value);
|
customTheme['--header-gradient-end'] = hexToRgba(theme.value.bgColor, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (darkMode.value) {
|
||||||
|
document.body.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('dark');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.body.style.cssText = createCssText(customTheme, document.body);
|
||||||
|
activeColorVariables.value = customTheme;
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (darkMode.value) {
|
/**
|
||||||
document.body.classList.add('dark');
|
* Creates a string of CSS rules preserving existing rules that are not defined in the theme variables
|
||||||
} else {
|
* @param themeVariables - The theme variables to apply
|
||||||
document.body.classList.remove('dark');
|
* @param body - The body element to apply the CSS to
|
||||||
}
|
* @returns A string of CSS rules
|
||||||
|
*/
|
||||||
|
const createCssText = (themeVariables: ThemeVariables, body: HTMLElement) => {
|
||||||
|
const existingStyles = body.style.cssText
|
||||||
|
.split(';')
|
||||||
|
.filter((rule) => rule.trim())
|
||||||
|
.filter((rule) => {
|
||||||
|
// Keep rules that aren't in our theme variables
|
||||||
|
return !Object.keys(themeVariables).some((themeVar) => rule.startsWith(themeVar));
|
||||||
|
});
|
||||||
|
|
||||||
activeColorVariables.value = customColorVariables[selectedMode];
|
const themeStyles = Object.entries(themeVariables).reduce((acc, [key, value]) => {
|
||||||
|
if (value) acc.push(`${key}: ${value}`);
|
||||||
|
return acc;
|
||||||
|
}, [] as string[]);
|
||||||
|
|
||||||
|
return [...existingStyles, ...themeStyles].join(';');
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(theme, () => {
|
watch(theme, () => {
|
||||||
|
|||||||
104
web/themes/default.ts
Normal file
104
web/themes/default.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import type { LegacyThemeVariables, ThemeVariables } from '~/themes/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines legacy colors that are kept for backwards compatibility
|
||||||
|
*
|
||||||
|
* Allows theme-engine to be updated without breaking existing themes
|
||||||
|
*/
|
||||||
|
export const legacyColors = {
|
||||||
|
'--color-alpha': 'var(--header-background-color)',
|
||||||
|
'--color-beta': 'var(--header-text-primary)',
|
||||||
|
'--color-gamma': 'var(--header-text-secondary)',
|
||||||
|
'--color-gamma-opaque': 'rgba(153, 153, 153, .5)',
|
||||||
|
'--color-customgradient-start': 'rgba(242, 242, 242, .0)',
|
||||||
|
'--color-customgradient-end': 'rgba(242, 242, 242, .85)',
|
||||||
|
'--shadow-beta': '0 25px 50px -12px rgba(242, 242, 242, .15)',
|
||||||
|
} as const satisfies LegacyThemeVariables;
|
||||||
|
|
||||||
|
export const defaultLight: ThemeVariables = {
|
||||||
|
'--background': '0 0% 3.9%',
|
||||||
|
'--foreground': '0 0% 98%',
|
||||||
|
'--muted': '0 0% 14.9%',
|
||||||
|
'--muted-foreground': '0 0% 63.9%',
|
||||||
|
'--popover': '0 0% 3.9%',
|
||||||
|
'--popover-foreground': '0 0% 98%',
|
||||||
|
'--card': '0 0% 14.9%',
|
||||||
|
'--card-foreground': '0 0% 98%',
|
||||||
|
'--border': '0 0% 20%',
|
||||||
|
'--input': '0 0% 14.9%',
|
||||||
|
'--primary': '24 100% 50%',
|
||||||
|
'--primary-foreground': '0 0% 98%',
|
||||||
|
'--secondary': '0 0% 14.9%',
|
||||||
|
'--secondary-foreground': '0 0% 77%',
|
||||||
|
'--accent': '0 0% 14.9%',
|
||||||
|
'--accent-foreground': '0 0% 98%',
|
||||||
|
'--destructive': '0 62.8% 30.6%',
|
||||||
|
'--destructive-foreground': '0 0% 98%',
|
||||||
|
'--ring': '0 0% 83.1%',
|
||||||
|
'--radius': '0.5rem',
|
||||||
|
'--header-text-primary': '#f2f2f2',
|
||||||
|
'--header-text-secondary': '#999999',
|
||||||
|
'--header-background-color': '#1c1b1b',
|
||||||
|
'--header-gradient-start': 'rgba(0, 0, 0, 0)',
|
||||||
|
'--header-gradient-end': 'var(--header-background-color)',
|
||||||
|
'--banner-gradient': null,
|
||||||
|
...legacyColors,
|
||||||
|
} as const satisfies ThemeVariables;
|
||||||
|
|
||||||
|
export const defaultDark: ThemeVariables = {
|
||||||
|
'--background': '0 0% 100%',
|
||||||
|
'--foreground': '0 0% 3.9%',
|
||||||
|
'--muted': '0 0% 96.1%',
|
||||||
|
'--muted-foreground': '0 0% 45.1%',
|
||||||
|
'--popover': '0 0% 100%',
|
||||||
|
'--popover-foreground': '0 0% 3.9%',
|
||||||
|
'--card': '0 0% 100%',
|
||||||
|
'--card-foreground': '0 0% 3.9%',
|
||||||
|
'--border': '0 0% 89.8%',
|
||||||
|
'--input': '0 0% 89.8%',
|
||||||
|
'--primary': '24 100% 50%',
|
||||||
|
'--primary-foreground': '0 0% 98%',
|
||||||
|
'--secondary': '0 0% 96.1%',
|
||||||
|
'--secondary-foreground': '0 0% 45%',
|
||||||
|
'--accent': '0 0% 96.1%',
|
||||||
|
'--accent-foreground': '0 0% 9%',
|
||||||
|
'--destructive': '0 84.2% 60.2%',
|
||||||
|
'--destructive-foreground': '0 0% 98%',
|
||||||
|
'--ring': '0 0% 3.9%',
|
||||||
|
'--radius': '0.5rem',
|
||||||
|
'--header-text-primary': '#1c1c1c',
|
||||||
|
'--header-text-secondary': '#999999',
|
||||||
|
'--header-background-color': '#f2f2f2',
|
||||||
|
'--header-gradient-start': 'rgba(0, 0, 0, 0)',
|
||||||
|
'--header-gradient-end': 'var(--header-background-color)',
|
||||||
|
'--banner-gradient': null,
|
||||||
|
...legacyColors,
|
||||||
|
} as const satisfies ThemeVariables;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Color Explanation:
|
||||||
|
* White (base light theme): has dark header background and light text
|
||||||
|
* Black (base dark theme): has light header background and dark text
|
||||||
|
* Gray (base dark theme): has dark header background and light text
|
||||||
|
* Azure (base light theme): has light header background and dark text
|
||||||
|
*/
|
||||||
|
export const defaultColors: Record<string, ThemeVariables> = {
|
||||||
|
white: {
|
||||||
|
...defaultLight,
|
||||||
|
},
|
||||||
|
black: {
|
||||||
|
...defaultDark,
|
||||||
|
},
|
||||||
|
gray: {
|
||||||
|
...defaultDark,
|
||||||
|
'--header-text-primary': '#39587f',
|
||||||
|
'--header-text-secondary': '#606e7f',
|
||||||
|
'--header-background-color': '#1c1b1b',
|
||||||
|
},
|
||||||
|
azure: {
|
||||||
|
...defaultDark,
|
||||||
|
'--header-text-primary': '#39587f',
|
||||||
|
'--header-text-secondary': '#606e7f',
|
||||||
|
'--header-background-color': '#f2f2f2',
|
||||||
|
},
|
||||||
|
} as const satisfies Record<string, ThemeVariables>;
|
||||||
50
web/themes/types.d.ts
vendored
Normal file
50
web/themes/types.d.ts
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
export interface Theme {
|
||||||
|
banner: boolean;
|
||||||
|
bannerGradient: boolean;
|
||||||
|
bgColor: string;
|
||||||
|
descriptionShow: boolean;
|
||||||
|
metaColor: string;
|
||||||
|
name: string;
|
||||||
|
textColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseThemeVariables = {
|
||||||
|
'--background': string;
|
||||||
|
'--foreground': string;
|
||||||
|
'--muted': string;
|
||||||
|
'--muted-foreground': string;
|
||||||
|
'--popover': string;
|
||||||
|
'--popover-foreground': string;
|
||||||
|
'--card': string;
|
||||||
|
'--card-foreground': string;
|
||||||
|
'--border': string;
|
||||||
|
'--input': string;
|
||||||
|
'--primary': string;
|
||||||
|
'--primary-foreground': string;
|
||||||
|
'--secondary': string;
|
||||||
|
'--secondary-foreground': string;
|
||||||
|
'--accent': string;
|
||||||
|
'--accent-foreground': string;
|
||||||
|
'--destructive': string;
|
||||||
|
'--destructive-foreground': string;
|
||||||
|
'--ring': string;
|
||||||
|
'--radius': string;
|
||||||
|
'--header-text-primary': string;
|
||||||
|
'--header-text-secondary': string;
|
||||||
|
'--header-background-color': string;
|
||||||
|
'--header-gradient-start': string;
|
||||||
|
'--header-gradient-end': string;
|
||||||
|
'--banner-gradient': string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LegacyThemeVariables = {
|
||||||
|
'--color-alpha': string;
|
||||||
|
'--color-beta': string;
|
||||||
|
'--color-gamma': string;
|
||||||
|
'--color-gamma-opaque': string;
|
||||||
|
'--color-customgradient-start': string;
|
||||||
|
'--color-customgradient-end': string;
|
||||||
|
'--shadow-beta': string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ThemeVariables = BaseThemeVariables & LegacyThemeVariables;
|
||||||
Reference in New Issue
Block a user