mirror of
https://github.com/YunoHost/yunohost-portal.git
synced 2024-09-03 20:06:23 +02:00
feat: add user defined theme variable override
This commit is contained in:
parent
2d9099d25b
commit
d01074cef1
5 changed files with 215 additions and 0 deletions
|
@ -1,3 +1,5 @@
|
||||||
|
import { useThemeOverrideState } from '@/composables/theming'
|
||||||
|
|
||||||
// AUTH
|
// AUTH
|
||||||
|
|
||||||
export const useIsLoggedIn = () => {
|
export const useIsLoggedIn = () => {
|
||||||
|
@ -133,6 +135,11 @@ export const useSettings = async () => {
|
||||||
|
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
colorMode.preference = settings.value.portal_theme
|
colorMode.preference = settings.value.portal_theme
|
||||||
|
|
||||||
|
const themeOverride = useThemeOverrideState()
|
||||||
|
if (themeOverride.value) {
|
||||||
|
useThemeOverride().init(themeOverride.value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return settings
|
return settings
|
||||||
|
|
93
composables/theming.ts
Normal file
93
composables/theming.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,31 @@
|
||||||
"automatic": "Automatic ({name})",
|
"automatic": "Automatic ({name})",
|
||||||
"back_to_apps": "Go back to app list",
|
"back_to_apps": "Go back to app list",
|
||||||
"cancel": "Cancel",
|
"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",
|
"change_password": "Change password",
|
||||||
"confirm_new_password": "Confirm new password",
|
"confirm_new_password": "Confirm new password",
|
||||||
"current_password": "Current password",
|
"current_password": "Current password",
|
||||||
|
|
|
@ -26,6 +26,8 @@ const localesAsOptions = computed(() => {
|
||||||
return options
|
return options
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { colors, sizes } = useThemeOverride()
|
||||||
|
|
||||||
const themesAsOptions = [
|
const themesAsOptions = [
|
||||||
'auto',
|
'auto',
|
||||||
'system',
|
'system',
|
||||||
|
@ -131,7 +133,91 @@ const themesAsOptions = [
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.theme-override input.input {
|
||||||
|
min-height: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -16,3 +16,7 @@ export function exclude<T, K extends keyof T>(
|
||||||
})
|
})
|
||||||
return filtered
|
return filtered
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function keysOf<T extends Record<string, string>>(obj: T) {
|
||||||
|
return Object.keys(obj) as Array<keyof T>
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue