mirror of
https://github.com/YunoHost/yunohost-portal.git
synced 2024-09-03 20:06:23 +02:00
Merge 1cdadad125
into ff3674aea9
This commit is contained in:
commit
38741b0b0b
11 changed files with 317 additions and 16 deletions
3
app.vue
3
app.vue
|
@ -34,7 +34,8 @@ body {
|
||||||
/* GLOBAL */
|
/* GLOBAL */
|
||||||
.btn,
|
.btn,
|
||||||
.select,
|
.select,
|
||||||
.input {
|
.input.input,
|
||||||
|
.join-item.btn {
|
||||||
min-height: 2.5rem;
|
min-height: 2.5rem;
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,7 +37,7 @@ async function logout() {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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
|
<BaseAlert
|
||||||
v-if="queryMsg"
|
v-if="queryMsg"
|
||||||
variant="warning"
|
variant="warning"
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vee-validate/yup": "^4.10.8",
|
"@vee-validate/yup": "^4.10.8",
|
||||||
|
"colorjs.io": "^0.4.5",
|
||||||
"nuxt-icons": "^3.2.1",
|
"nuxt-icons": "^3.2.1",
|
||||||
"vee-validate": "^4.10.8"
|
"vee-validate": "^4.10.8"
|
||||||
}
|
}
|
||||||
|
|
120
pages/edit.vue
120
pages/edit.vue
|
@ -26,11 +26,15 @@ const localesAsOptions = computed(() => {
|
||||||
return options
|
return options
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { colors, sizes } = useThemeOverride()
|
||||||
|
|
||||||
const themesAsOptions = [
|
const themesAsOptions = [
|
||||||
'auto',
|
'auto',
|
||||||
'system',
|
'system',
|
||||||
'light',
|
'light',
|
||||||
'dark',
|
'dark',
|
||||||
|
'admin',
|
||||||
|
'legacy',
|
||||||
'cupcake',
|
'cupcake',
|
||||||
'bumblebee',
|
'bumblebee',
|
||||||
'emerald',
|
'emerald',
|
||||||
|
@ -76,25 +80,31 @@ const themesAsOptions = [
|
||||||
|
|
||||||
<div class="lg:flex lg:justify-between">
|
<div class="lg:flex lg:justify-between">
|
||||||
<section
|
<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>
|
||||||
|
|
||||||
<section class="lg:w-1/2 card card-body border border-neutral my-10">
|
<section class="lg:w-1/2 card card-bordered border-base-300 my-10">
|
||||||
<h2 class="text-3xl mb-3">{{ $t('change_password') }}</h2>
|
<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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="card card-body border border-neutral my-10">
|
<section class="card card-bordered border-base-300 my-10">
|
||||||
<h2 class="text-3xl mb-3">{{ t('edit_browser_settings') }}</h2>
|
<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>
|
<form class="p-8" novalidate @submit.prevent>
|
||||||
<div role="group" class="flex align mb-3">
|
<div role="group" class="flex flex-wrap align mb-3">
|
||||||
<!-- eslint-disable-next-line vuejs-accessibility/label-has-for -->
|
<!-- eslint-disable-next-line vuejs-accessibility/label-has-for -->
|
||||||
<label for="language" class="label me-3">{{ t('language') }}</label>
|
<label for="language" class="label me-3">{{ t('language') }}</label>
|
||||||
<select
|
<select
|
||||||
|
@ -113,7 +123,7 @@ const themesAsOptions = [
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div role="group" class="flex align">
|
<div role="group" class="flex flex-wrap align">
|
||||||
<!-- eslint-disable-next-line vuejs-accessibility/label-has-for -->
|
<!-- eslint-disable-next-line vuejs-accessibility/label-has-for -->
|
||||||
<label for="theme" class="label me-3">{{ t('theme') }}</label>
|
<label for="theme" class="label me-3">{{ t('theme') }}</label>
|
||||||
<select
|
<select
|
||||||
|
@ -131,7 +141,95 @@ 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>
|
||||||
|
.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>
|
||||||
|
|
|
@ -87,7 +87,7 @@ async function onSearchSubmit() {
|
||||||
<li
|
<li
|
||||||
v-for="app in apps"
|
v-for="app in apps"
|
||||||
:key="app.label"
|
: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
|
<img
|
||||||
v-if="app.logo"
|
v-if="app.logo"
|
||||||
|
|
|
@ -1,8 +1,75 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// Safelisting some classes to avoid content purge
|
// Safelisting some classes to avoid content purge
|
||||||
plugins: [require('daisyui')],
|
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: {
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
}
|
||||||
|
|
|
@ -2507,6 +2507,11 @@ colorette@^2.0.20:
|
||||||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
|
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
|
||||||
integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
|
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:
|
commander@^2.20.0:
|
||||||
version "2.20.3"
|
version "2.20.3"
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||||
|
|
Loading…
Add table
Reference in a new issue