This commit is contained in:
Axolotle 2024-08-19 01:59:32 +02:00 committed by GitHub
commit 38741b0b0b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 317 additions and 16 deletions

View file

@ -34,7 +34,8 @@ body {
/* GLOBAL */
.btn,
.select,
.input {
.input.input,
.join-item.btn {
min-height: 2.5rem;
height: 2.5rem;
}

View file

@ -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

93
composables/theming.ts Normal file
View file

@ -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<keyof typeof Color, string>
type Sizes = Record<keyof typeof Size, string>
type Theme = Colors & Sizes
export const useThemeOverrideState = () =>
useState<Theme | null>('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<string[]>((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,
}
}

View file

@ -37,7 +37,7 @@ async function logout() {
</script>
<template>
<div class="container mx-auto p-10 min-h-screen flex flex-col">
<div class="container mx-auto p-6 md:p-10 min-h-screen flex flex-col">
<BaseAlert
v-if="queryMsg"
variant="warning"

View file

@ -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",

View file

@ -31,6 +31,7 @@
},
"dependencies": {
"@vee-validate/yup": "^4.10.8",
"colorjs.io": "^0.4.5",
"nuxt-icons": "^3.2.1",
"vee-validate": "^4.10.8"
}

View file

@ -26,11 +26,15 @@ const localesAsOptions = computed(() => {
return options
})
const { colors, sizes } = useThemeOverride()
const themesAsOptions = [
'auto',
'system',
'light',
'dark',
'admin',
'legacy',
'cupcake',
'bumblebee',
'emerald',
@ -76,25 +80,31 @@ const themesAsOptions = [
<div class="lg:flex lg:justify-between">
<section
class="lg:w-1/2 lg:me-20 h-full card card-body border border-neutral my-10"
class="lg:w-1/2 lg:me-20 h-full card card-bordered border-base-300 my-10"
>
<h2 class="text-3xl mb-3">{{ t('edit_personal_settings') }}</h2>
<div class="card-header bg-base-300 py-4 px-8">
<h2 class="text-3xl">{{ t('edit_personal_settings') }}</h2>
</div>
<UserInfoForm />
<UserInfoForm class="p-8" />
</section>
<section class="lg:w-1/2 card card-body border border-neutral my-10">
<h2 class="text-3xl mb-3">{{ $t('change_password') }}</h2>
<section class="lg:w-1/2 card card-bordered border-base-300 my-10">
<div class="card-header bg-base-300 py-4 px-8">
<h2 class="text-3xl">{{ $t('change_password') }}</h2>
</div>
<UserPasswordForm />
<UserPasswordForm class="p-8" />
</section>
</div>
<section class="card card-body border border-neutral my-10">
<h2 class="text-3xl mb-3">{{ t('edit_browser_settings') }}</h2>
<section class="card card-bordered border-base-300 my-10">
<div class="card-header bg-base-300 py-4 px-8">
<h2 class="text-3xl">{{ t('edit_browser_settings') }}</h2>
</div>
<form novalidate @submit.prevent>
<div role="group" class="flex align mb-3">
<form class="p-8" novalidate @submit.prevent>
<div role="group" class="flex flex-wrap align mb-3">
<!-- eslint-disable-next-line vuejs-accessibility/label-has-for -->
<label for="language" class="label me-3">{{ t('language') }}</label>
<select
@ -113,7 +123,7 @@ const themesAsOptions = [
</select>
</div>
<div role="group" class="flex align">
<div role="group" class="flex flex-wrap align">
<!-- eslint-disable-next-line vuejs-accessibility/label-has-for -->
<label for="theme" class="label me-3">{{ t('theme') }}</label>
<select
@ -131,7 +141,95 @@ const themesAsOptions = [
</option>
</select>
</div>
<fieldset class="theme-override mt-8">
<legend class="text-xl mb-6">{{ $t('theming.override') }}</legend>
<div
v-for="(_, colorName) in colors"
:key="colorName"
class="flex flex-wrap mb-2"
>
<FormField
:name="`color-picker-${colorName}`"
:label="
$t('theming.color_picker', {
colorName: $t(`theming.colors.${colorName}`),
})
"
sr-hide-label
>
<!-- eslint-disable-next-line vuejs-accessibility/form-control-has-label -->
<input
:id="`color-picker-${colorName}`"
v-model="colors[colorName]"
type="color"
class="inline-block w-8 h-8 -mr-8 cursor-pointer"
/>
<div
:class="'bg-' + colorName"
class="inline-block w-8 h-8 pointer-events-none border border-base-300"
/>
</FormField>
<FormField
:name="`color-hex-${colorName}`"
:label="
$t('theming.color_hex', {
colorName: $t(`theming.colors.${colorName}`),
})
"
sr-hide-label
>
<input
:id="`color-hex-${colorName}`"
v-model="colors[colorName]"
size="7"
class="input input-bordered px-2 font-mono ml-3"
/>
</FormField>
<span class="ml-3" aria-hidden>
{{ $t(`theming.colors.${colorName}`) }}
</span>
</div>
<div v-for="(_, sizeName) in sizes" :key="sizeName" class="flex mb-2">
<FormField
:name="`size-${sizeName}`"
:label="
$t('theming.size', {
sizeName: $t(`theming.sizes.${sizeName}`),
})
"
sr-hide-label
>
<!-- eslint-disable-next-line vuejs-accessibility/form-control-has-label -->
<input
:id="`size-${sizeName}`"
v-model="sizes[sizeName]"
type="number"
size="6"
class="input input-bordered inline-block pe-1"
/>
<span class="ml-3" aria-hidden>
{{ $t(`theming.sizes.${sizeName}`) }}
</span>
</FormField>
</div>
</fieldset>
</form>
</section>
</div>
</template>
<style scoped>
.card .card-header {
border-top-left-radius: var(--rounded-box);
border-top-right-radius: var(--rounded-box);
}
.theme-override input.input {
min-height: 2rem;
height: 2rem;
}
</style>

View file

@ -87,7 +87,7 @@ async function onSearchSubmit() {
<li
v-for="app in apps"
:key="app.label"
class="flex flex-auto border border-neutral rounded p-4 relative hover:bg-neutral hover:text-neutral-content"
class="flex text-align flex-auto btn btn-outline btn-neutral !h-auto p-5 relative flex-nowrap items-start justify-normal text-left font-normal"
>
<img
v-if="app.logo"

View file

@ -1,8 +1,75 @@
module.exports = {
// Safelisting some classes to avoid content purge
plugins: [require('daisyui')],
safelist: ['safelisted'],
safelist: [
'safelisted',
'bg-primary',
'bg-secondary',
'bg-accent',
'bg-neutral',
'bg-base-100',
'bg-base-200',
'bg-base-300',
'bg-base-content',
'bg-info',
'bg-success',
'bg-warning',
'bg-error',
],
daisyui: {
themes: true,
themes: [
...Object.keys(require('daisyui/src/theming/themes')),
{
legacy: {
primary: '#2980b9',
secondary: '#30333b',
accent: '#7028b8',
neutral: '#999',
info: '#2980b9',
success: '#27ae60',
warning: '#e67e22',
error: '#c0392b',
'base-100': '#41444f',
'base-200': '#999',
'base-300': '#30333b',
'base-content': '#fff',
'primary-content': '#fff',
'secondary-content': '#fff',
'accent-content': '#fff',
'neutral-content': '#fff',
'info-content': '#fff',
'success-content': '#fff',
'warning-content': '#fff',
'error-content': '#fff',
'--rounded-box': '0rem',
'--rounded-btn': '0rem',
},
admin: {
primary: '#53a5fb',
secondary: '#20cb98',
accent: '#b957ea',
neutral: '#EDEDED',
info: '#79e7f9',
success: '#70ea8d',
warning: '#ffd452',
error: '#ff5a5a',
'base-100': '#202020',
'base-200': '#3C3C3C',
'base-300': '#303030',
'base-content': '#fafafa',
'primary-content': '#000',
'secondary-content': '#000',
'accent-content': '#000',
'neutral-content': '#000',
'info-content': '#000',
'success-content': '#000',
'warning-content': '#000',
'error-content': '#000',
'--rounded-box': '0.1875rem',
'--rounded-btn': '0.1875rem',
'--border-btn': '1px',
},
},
],
},
}

View file

@ -16,3 +16,7 @@ export function exclude<T, K extends keyof T>(
})
return filtered
}
export function keysOf<T extends Record<string, string>>(obj: T) {
return Object.keys(obj) as Array<keyof T>
}

View file

@ -2507,6 +2507,11 @@ colorette@^2.0.20:
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
colorjs.io@^0.4.5:
version "0.4.5"
resolved "https://registry.yarnpkg.com/colorjs.io/-/colorjs.io-0.4.5.tgz#7775f787ff90aca7a38f6edb7b7c0f8cce1e6418"
integrity sha512-yCtUNCmge7llyfd/Wou19PMAcf5yC3XXhgFoAh6zsO2pGswhUPBaaUh8jzgHnXtXuZyFKzXZNAnyF5i+apICow==
commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"