Admin: Allow to edit and set the current theme while changing the selected one

pull/5649/head
Angel Fernando Quiroz Campos 1 year ago
parent 8d576421d3
commit 41c84559e1
No known key found for this signature in database
GPG Key ID: B284841AE3E562CD
  1. 26
      assets/css/scss/_admin_index.scss
  2. 433
      assets/vue/components/admin/ColorThemeForm.vue
  3. 13
      assets/vue/components/platform/ColorThemeSelector.vue
  4. 65
      assets/vue/composables/theme.js
  5. 22
      assets/vue/services/colorThemeService.js
  6. 424
      assets/vue/views/admin/AdminConfigureColors.vue
  7. 2
      src/CoreBundle/Entity/ColorTheme.php

@ -13,30 +13,18 @@
.admin-colors {
&__container {
@apply flex flex-col mt-6;
@apply flex flex-col md:flex-row mt-6;
}
&__settings {
@apply flex flex-col gap-4
md:flex-row;
&-form {
@apply flex-1;
&-title {
@apply mb-4;
}
&__form {
@apply w-full md:w-3/5;
}
.row-group {
@apply flex flex-col gap-4 items-start
sm:grid sm:grid-cols-2
md:items-end
xl:grid-cols-3;
&__form-fields {
@apply mt-4;
}
&-preview {
@apply space-y-4 flex-1;
}
&__preview {
@apply flex w-full md:w-2/5;
}
}

@ -0,0 +1,433 @@
<script setup>
import { ref, watch } from "vue"
import { useI18n } from "vue-i18n"
import Color from "colorjs.io"
import SectionHeader from "../layout/SectionHeader.vue"
import ColorThemeSelector from "../platform/ColorThemeSelector.vue"
import BaseColorPicker from "../basecomponents/BaseColorPicker.vue"
import BaseButton from "../basecomponents/BaseButton.vue"
import BaseDialogConfirmCancel from "../basecomponents/BaseDialogConfirmCancel.vue"
import BaseInputText from "../basecomponents/BaseInputText.vue"
import colorThemeService from "../../services/colorThemeService"
import { useNotification } from "../../composables/notification"
import { useTheme } from "../../composables/theme"
const { showSuccessNotification, showErrorNotification } = useNotification()
const { getColorTheme, getColors, setColors, makeGradient, makeTextWithContrast, checkColorContrast } = useTheme()
const { t } = useI18n()
const isAdvancedMode = ref(false)
const themeSelectorEl = ref()
const selectedThemeIri = ref()
const newThemeSelected = ref()
let colorPrimary = getColorTheme("--color-primary-base")
let colorPrimaryGradient = getColorTheme("--color-primary-gradient")
let colorPrimaryButtonText = getColorTheme("--color-primary-button-text")
let colorPrimaryButtonAlternativeText = getColorTheme("--color-primary-button-alternative-text")
let colorSecondary = getColorTheme("--color-secondary-base")
let colorSecondaryGradient = getColorTheme("--color-secondary-gradient")
let colorSecondaryButtonText = getColorTheme("--color-secondary-button-text")
let colorTertiary = getColorTheme("--color-tertiary-base")
let colorTertiaryGradient = getColorTheme("--color-tertiary-gradient")
let colorTertiaryButtonText = getColorTheme("--color-tertiary-button-text")
let colorSuccess = getColorTheme("--color-success-base")
let colorSuccessGradient = getColorTheme("--color-success-gradient")
let colorSuccessButtonText = getColorTheme("--color-success-button-text")
let colorInfo = getColorTheme("--color-info-base")
let colorInfoGradient = getColorTheme("--color-info-gradient")
let colorInfoButtonText = getColorTheme("--color-info-button-text")
let colorWarning = getColorTheme("--color-warning-base")
let colorWarningGradient = getColorTheme("--color-warning-gradient")
let colorWarningButtonText = getColorTheme("--color-warning-button-text")
let colorDanger = getColorTheme("--color-danger-base")
let colorDangerGradient = getColorTheme("--color-danger-gradient")
let colorDangerButtonText = getColorTheme("--color-danger-button-text")
let formColor = getColorTheme("--color-form-base")
function onChangeTheme(colorTheme) {
newThemeSelected.value = colorTheme
if (colorTheme) {
setColors(colorTheme.variables)
}
}
const dialogCreateVisible = ref(false)
const themeTitle = ref()
async function onClickSelectColorTheme() {
if (selectedThemeIri.value) {
await colorThemeService.changePlatformColorTheme(selectedThemeIri.value)
}
}
async function onClickUpdate() {
try {
const updatedTheme = await colorThemeService.update({
iri: selectedThemeIri.value,
title: themeTitle.value,
colors: getColors(),
})
showSuccessNotification(t("Color updated"))
await themeSelectorEl.value.loadThemes()
selectedThemeIri.value = updatedTheme["@id"]
} catch (error) {
showErrorNotification(error)
}
}
async function onClickCreate() {
try {
if (!themeTitle.value.trim()) {
return
}
const updatedTheme = await colorThemeService.create({
title: themeTitle.value,
colors: getColors(),
})
showSuccessNotification(t("Color updated"))
await themeSelectorEl.value.loadThemes()
selectedThemeIri.value = updatedTheme["@id"]
} catch (error) {
showErrorNotification(error)
}
dialogCreateVisible.value = false
}
watch(colorPrimary, (newValue) => {
colorPrimaryGradient.value = makeGradient(newValue)
colorPrimaryButtonText.value = newValue
colorPrimaryButtonAlternativeText.value = makeTextWithContrast(newValue)
colorPrimaryButtonAlternativeTextError.value = checkColorContrast(newValue, colorPrimaryButtonText.value)
})
watch(colorSecondary, (newValue) => {
colorSecondaryGradient.value = makeGradient(newValue)
colorSecondaryButtonText.value = makeTextWithContrast(newValue)
colorSecondaryButtonTextError.value = checkColorContrast(newValue, colorSecondaryButtonText.value)
})
watch(colorTertiary, (newValue) => {
colorTertiaryButtonText.value = newValue
colorTertiaryGradient.value = makeGradient(newValue)
})
watch(colorSuccess, (newValue) => {
colorSuccessGradient.value = makeGradient(newValue)
colorSuccessButtonText.value = makeTextWithContrast(newValue)
colorSuccessButtonTextError.value = checkColorContrast(newValue, colorSuccessButtonText.value)
})
watch(colorInfo, (newValue) => {
colorInfoGradient.value = makeGradient(newValue)
colorInfoButtonText.value = makeTextWithContrast(newValue)
colorInfoButtonTextError.value = checkColorContrast(newValue, colorInfoButtonText.value)
})
watch(colorWarning, (newValue) => {
colorWarningGradient.value = makeGradient(newValue)
colorWarningButtonText.value = makeTextWithContrast(newValue)
colorWarningButtonTextError.value = checkColorContrast(newValue, colorWarningButtonText.value)
})
watch(colorDanger, (newValue) => {
colorDangerGradient.value = makeGradient(newValue)
colorDangerButtonText.value = makeTextWithContrast(newValue)
})
// check for contrast of text
const colorPrimaryButtonTextError = ref("")
watch(
colorPrimaryButtonText,
(newValue) => (colorPrimaryButtonTextError.value = checkColorContrast(new Color("white"), newValue)),
)
const colorPrimaryButtonAlternativeTextError = ref("")
watch(
colorPrimaryButtonAlternativeText,
(newValue) => (colorPrimaryButtonAlternativeTextError.value = checkColorContrast(colorPrimary.value, newValue)),
)
const colorSecondaryButtonTextError = ref("")
watch(
colorSecondaryButtonText,
(newValue) => (colorSecondaryButtonTextError.value = checkColorContrast(colorSecondary.value, newValue)),
)
const colorTertiaryButtonTextError = ref("")
watch(
colorTertiaryButtonText,
(newValue) => (colorTertiaryButtonTextError.value = checkColorContrast(colorTertiary.value, newValue)),
)
const colorSuccessButtonTextError = ref("")
watch(
colorSuccessButtonText,
(newValue) => (colorSuccessButtonTextError.value = checkColorContrast(colorSuccess.value, newValue)),
)
const colorInfoButtonTextError = ref("")
watch(
colorInfoButtonText,
(newValue) => (colorInfoButtonTextError.value = checkColorContrast(colorInfo.value, newValue)),
)
const colorWarningButtonTextError = ref("")
watch(
colorWarningButtonText,
(newValue) => (colorWarningButtonTextError.value = checkColorContrast(colorWarning.value, newValue)),
)
const colorDangerButtonTextError = ref("")
watch(
colorDangerButtonText,
(newValue) => (colorDangerButtonTextError.value = checkColorContrast(new Color("white"), newValue)),
)
</script>
<template>
<ColorThemeSelector
ref="themeSelectorEl"
v-model="selectedThemeIri"
@change="onChangeTheme"
/>
<SectionHeader
:title="t('Modify color theme')"
size="6"
>
<BaseButton
:label="isAdvancedMode ? t('Hide advanced mode') : t('Show advanced mode')"
icon="cog"
type="black"
@click="isAdvancedMode = !isAdvancedMode"
/>
</SectionHeader>
<form class="admin-colors__form-fields">
<!-- Advanced mode -->
<div v-show="isAdvancedMode">
<div class="field-group">
<BaseColorPicker
v-model="colorPrimary"
:label="t('Primary color')"
/>
<BaseColorPicker
v-model="colorPrimaryGradient"
:label="t('Primary color hover/background')"
/>
<BaseColorPicker
v-model="colorPrimaryButtonText"
:error="colorPrimaryButtonTextError"
:label="t('Primary color button text')"
/>
<BaseColorPicker
v-model="colorPrimaryButtonAlternativeText"
:error="colorPrimaryButtonAlternativeTextError"
:label="t('Primary color button alternative text')"
/>
</div>
<div class="field-group">
<BaseColorPicker
v-model="colorSecondary"
:label="t('Secondary color')"
/>
<BaseColorPicker
v-model="colorSecondaryGradient"
:label="t('Secondary color hover/background')"
/>
<BaseColorPicker
v-model="colorSecondaryButtonText"
:error="colorSecondaryButtonTextError"
:label="t('Secondary color button text')"
/>
</div>
<div class="field-group">
<BaseColorPicker
v-model="colorTertiary"
:label="t('Tertiary color')"
/>
<BaseColorPicker
v-model="colorTertiaryGradient"
:label="t('Tertiary color hover/background')"
/>
</div>
<div class="field-group">
<BaseColorPicker
v-model="colorSuccess"
:label="t('Success color')"
/>
<BaseColorPicker
v-model="colorSuccessGradient"
:label="t('Success color hover/background')"
/>
<BaseColorPicker
v-model="colorSuccessButtonText"
:error="colorSuccessButtonTextError"
:label="t('Success color button text')"
/>
</div>
<div class="field-group">
<BaseColorPicker
v-model="colorInfo"
:label="t('Info color')"
/>
<BaseColorPicker
v-model="colorInfoGradient"
:label="t('Info color hover/background')"
/>
<BaseColorPicker
v-model="colorInfoButtonText"
:error="colorInfoButtonTextError"
:label="t('Info color button text')"
/>
</div>
<div class="field-group">
<BaseColorPicker
v-model="colorWarning"
:label="t('Warning color')"
/>
<BaseColorPicker
v-model="colorWarningGradient"
:label="t('Warning color hover/background')"
/>
<BaseColorPicker
v-model="colorWarningButtonText"
:error="colorWarningButtonTextError"
:label="t('Warning color button text')"
/>
</div>
<div class="field-group">
<BaseColorPicker
v-model="colorDanger"
:label="t('Danger color')"
/>
<BaseColorPicker
v-model="colorDangerGradient"
:label="t('Danger color hover/background')"
/>
</div>
<div class="field-group">
<BaseColorPicker
v-model="formColor"
:label="t('Form outline color')"
/>
</div>
</div>
<!-- Simple mode -->
<div v-show="!isAdvancedMode">
<div class="field-group">
<BaseColorPicker
v-model="colorPrimary"
:label="t('Primary color')"
/>
<BaseColorPicker
v-model="colorSecondary"
:label="t('Secondary color')"
/>
<BaseColorPicker
v-model="colorTertiary"
:label="t('Tertiary color')"
/>
</div>
<div class="field-group">
<BaseColorPicker
v-model="colorSuccess"
:label="t('Success color')"
/>
<BaseColorPicker
v-model="colorInfo"
:label="t('Info color')"
/>
<BaseColorPicker
v-model="colorWarning"
:label="t('Warning color')"
/>
<BaseColorPicker
v-model="colorDanger"
:label="t('Danger color')"
/>
</div>
<div class="field-group">
<BaseColorPicker
v-model="formColor"
:label="t('Form outline color')"
/>
</div>
</div>
<div class="field-group">
<BaseButton
:disabled="!selectedThemeIri"
:label="t('Select as current theme')"
icon="save"
type="primary"
@click="onClickSelectColorTheme"
/>
<BaseButton
:disabled="!selectedThemeIri"
:label="t('Save')"
icon="send"
type="primary"
@click="onClickUpdate"
/>
<BaseButton
:label="t('Save as new theme')"
icon="send"
type="primary"
@click="dialogCreateVisible = true"
/>
</div>
</form>
<BaseDialogConfirmCancel
v-model:is-visible="dialogCreateVisible"
:cancel-label="t('Cancel')"
:confirm-label="t('Save')"
:title="t('New color theme')"
@confirm-clicked="onClickCreate"
@cancel-clicked="dialogCreateVisible = false"
>
<BaseInputText
v-model="themeTitle"
:label="t('Title')"
/>
</BaseDialogConfirmCancel>
</template>

@ -1,6 +1,6 @@
<script setup>
import BaseSelect from "../basecomponents/BaseSelect.vue"
import { ref } from "vue"
import { computed, ref } from "vue"
import themeService from "../../services/colorThemeService"
import { useI18n } from "vue-i18n"
import { useNotification } from "../../composables/notification"
@ -34,17 +34,26 @@ defineExpose({
loadThemes,
})
const emit = defineEmits(["change"])
loadThemes()
function onChange({ value }) {
const themeSelected = serverThemes.value.find((accessUrlRelColorTheme) => accessUrlRelColorTheme["@id"] === value)
emit("change", themeSelected)
}
</script>
<template>
<BaseSelect
v-model="modelValue"
:is-loading="isServerThemesLoading"
:label="t('Color theme selected')"
:label="t('Theme title')"
:options="serverThemes"
allow-clear
option-label="title"
option-value="@id"
@change="onChange"
/>
</template>

@ -1,8 +1,11 @@
import { onMounted, ref, watch } from "vue"
import { useI18n } from "vue-i18n"
import Color from "colorjs.io"
import { usePlatformConfig } from "../store/platformConfig"
export const useTheme = () => {
const { t } = useI18n()
let colors = {}
onMounted(() => {
@ -27,7 +30,7 @@ export const useTheme = () => {
const getCssVariableValue = (variableName) => {
const colorVariable = getComputedStyle(document.body).getPropertyValue(variableName)
return colorFromCSSVariable(colorVariable)
return getColorFromCSSVariable(colorVariable)
}
const setCssVariableValue = (variableName, color) => {
@ -48,7 +51,7 @@ export const useTheme = () => {
console.error(`Color with key ${key} is on color set`)
continue
}
colors[key].value = colorFromCSSVariable(colorsObj[key])
colors[key].value = getColorFromCSSVariable(colorsObj[key])
}
}
@ -62,28 +65,78 @@ export const useTheme = () => {
return `${r} ${g} ${b}`
}
const colorFromCSSVariable = (variable) => {
const getColorFromCSSVariable = (variable) => {
return new Color(`rgb(${variable})`)
}
/**
* @param {Color} color
* @returns {Color}
*/
function makeGradient(color) {
const light = color.clone().to("oklab").l
// when color is light (lightness > 0.5), darken gradient color
// when color is dark, lighten gradient color
// The values 0.5 and 1.6 were chosen through experimentation, there could be a better way to do this
if (light > 0.5) {
return color
.clone()
.set({ "oklab.l": (l) => l * 0.8 })
.to("srgb")
}
return color
.clone()
.set({ "oklab.l": (l) => l * 1.6 })
.to("srgb")
}
/**
* @param {Color} color
* @returns {Color}
*/
function makeTextWithContrast(color) {
// according to colorjs library https://colorjs.io/docs/contrast#accessible-perceptual-contrast-algorithm-apca
// this algorithm is better than WCAGG 2.1 to check for contrast
// "APCA is being evaluated for use in version 3 of the W3C Web Content Accessibility Guidelines (WCAG)"
let onWhite = Math.abs(color.contrast("white", "APCA"))
let onBlack = Math.abs(color.contrast("black", "APCA"))
return onWhite > onBlack ? new Color("white") : new Color("black")
}
function checkColorContrast(background, foreground) {
// using APCA for text contrast in buttons. In chamilo buttons the text
// has a font size of 16px and weight of 600
// Lc 60 The minimum level recommended for content text that is not body, column, or block text
// https://git.apcacontrast.com/documentation/APCA_in_a_Nutshell#use-case--size-ranges
let contrast = Math.abs(background.contrast(foreground, "APCA"))
if (contrast < 60) {
return t("Does not have enough contrast against background")
}
return ""
}
return {
getColorTheme,
getColors,
setColors,
makeGradient,
makeTextWithContrast,
checkColorContrast,
}
}
export function useVisualTheme() {
const platformConfigStore = usePlatformConfig()
const themeName = platformConfigStore.visualTheme
function getThemeAssetUrl(path) {
return `/themes/${platformConfigStore.visualTheme}/${path}`
}
return {
themeName,
getThemeAssetUrl,
}
}

@ -12,22 +12,29 @@ async function findAllByCurrentUrl() {
}
/**
* Update or create a theme with the title
* Create a color theme
*
* @param {string|null} iri
* @param {string} title
* @param {Object} colors
* @returns {Promise<Object>}
*/
async function updateTheme({ iri = null, title, colors }) {
if (iri) {
return await baseService.put(iri, {
async function create({ title, colors }) {
return await baseService.post(url, {
title,
variables: colors,
})
}
return await baseService.post(url, {
/**
* Update a color theme
*
* @param {string} iri
* @param {string} title
* @param {Object} colors
* @returns {Promise<Object>}
*/
async function update({ iri, title, colors }) {
return await baseService.put(iri, {
title,
variables: colors,
})
@ -44,7 +51,8 @@ async function changePlatformColorTheme(iri) {
}
export default {
updateTheme,
create,
update,
findAllByCurrentUrl,
changePlatformColorTheme,
}

@ -3,214 +3,11 @@
<SectionHeader :title="t('Configure Chamilo colors')" />
<div class="admin-colors__container">
<form class="admin-colors__form">
<ColorThemeSelector
ref="themeSelectorEl"
v-model="selectedTheme"
/>
<BaseButton
:label="t('Save')"
icon="save"
type="primary"
@click="onClickChangeColorTheme"
/>
</form>
<BaseDivider />
<div class="admin-colors__settings">
<div class="admin-colors__settings-form">
<h3
v-t="selectedTheme ? 'Edit color theme' : 'Create color theme'"
class="admin-colors__settings-form-title"
/>
<BaseInputText
v-model="themeTitle"
:disabled="!!selectedTheme"
:label="t('Title')"
/>
<!-- Advanced mode -->
<div v-show="isAdvancedMode">
<div class="row-group">
<BaseColorPicker
v-model="colorPrimary"
:label="t('Primary color')"
/>
<BaseColorPicker
v-model="colorPrimaryGradient"
:label="t('Primary color hover/background')"
/>
<BaseColorPicker
v-model="colorPrimaryButtonText"
:error="colorPrimaryButtonTextError"
:label="t('Primary color button text')"
/>
<BaseColorPicker
v-model="colorPrimaryButtonAlternativeText"
:error="colorPrimaryButtonAlternativeTextError"
:label="t('Primary color button alternative text')"
/>
</div>
<div class="row-group">
<BaseColorPicker
v-model="colorSecondary"
:label="t('Secondary color')"
/>
<BaseColorPicker
v-model="colorSecondaryGradient"
:label="t('Secondary color hover/background')"
/>
<BaseColorPicker
v-model="colorSecondaryButtonText"
:error="colorSecondaryButtonTextError"
:label="t('Secondary color button text')"
/>
</div>
<div class="row-group">
<BaseColorPicker
v-model="colorTertiary"
:label="t('Tertiary color')"
/>
<BaseColorPicker
v-model="colorTertiaryGradient"
:label="t('Tertiary color hover/background')"
/>
</div>
<div class="row-group">
<BaseColorPicker
v-model="colorSuccess"
:label="t('Success color')"
/>
<BaseColorPicker
v-model="colorSuccessGradient"
:label="t('Success color hover/background')"
/>
<BaseColorPicker
v-model="colorSuccessButtonText"
:error="colorSuccessButtonTextError"
:label="t('Success color button text')"
/>
</div>
<div class="row-group">
<BaseColorPicker
v-model="colorInfo"
:label="t('Info color')"
/>
<BaseColorPicker
v-model="colorInfoGradient"
:label="t('Info color hover/background')"
/>
<BaseColorPicker
v-model="colorInfoButtonText"
:error="colorInfoButtonTextError"
:label="t('Info color button text')"
/>
</div>
<div class="row-group">
<BaseColorPicker
v-model="colorWarning"
:label="t('Warning color')"
/>
<BaseColorPicker
v-model="colorWarningGradient"
:label="t('Warning color hover/background')"
/>
<BaseColorPicker
v-model="colorWarningButtonText"
:error="colorWarningButtonTextError"
:label="t('Warning color button text')"
/>
</div>
<div class="row-group">
<BaseColorPicker
v-model="colorDanger"
:label="t('Danger color')"
/>
<BaseColorPicker
v-model="colorDangerGradient"
:label="t('Danger color hover/background')"
/>
</div>
<div class="row-group">
<BaseColorPicker
v-model="formColor"
:label="t('Form outline color')"
/>
</div>
</div>
<!-- Simple mode -->
<div v-show="!isAdvancedMode">
<div class="row-group">
<BaseColorPicker
v-model="colorPrimary"
:label="t('Primary color')"
/>
<BaseColorPicker
v-model="colorSecondary"
:label="t('Secondary color')"
/>
<BaseColorPicker
v-model="colorTertiary"
:label="t('Tertiary color')"
/>
</div>
<div class="row-group">
<BaseColorPicker
v-model="colorSuccess"
:label="t('Success color')"
/>
<BaseColorPicker
v-model="colorInfo"
:label="t('Info color')"
/>
<BaseColorPicker
v-model="colorWarning"
:label="t('Warning color')"
/>
<BaseColorPicker
v-model="colorDanger"
:label="t('Danger color')"
/>
</div>
<div class="row-group">
<BaseColorPicker
v-model="formColor"
:label="t('Form outline color')"
/>
</div>
</div>
<div class="field">
<BaseButton
:label="isAdvancedMode ? t('Hide advanced mode') : t('Show advanced mode')"
icon="cog"
type="black"
@click="isAdvancedMode = !isAdvancedMode"
/>
</div>
<div class="field">
<BaseButton
:label="t('Save')"
icon="send"
type="primary"
@click="saveColors"
/>
</div>
<div class="admin-colors__form">
<ColorThemeForm />
</div>
<div class="admin-colors__preview">
<BaseDivider layout="vertical" />
<ColorThemePreview />
@ -220,224 +17,11 @@
</template>
<script setup>
import BaseButton from "../../components/basecomponents/BaseButton.vue"
import { useI18n } from "vue-i18n"
import { ref, watch } from "vue"
import BaseInputText from "../../components/basecomponents/BaseInputText.vue"
import BaseColorPicker from "../../components/basecomponents/BaseColorPicker.vue"
import { useTheme } from "../../composables/theme"
import { useNotification } from "../../composables/notification"
import Color from "colorjs.io"
import themeService from "../../services/colorThemeService"
import BaseDivider from "../../components/basecomponents/BaseDivider.vue"
import SectionHeader from "../../components/layout/SectionHeader.vue"
import ColorThemeSelector from "../../components/platform/ColorThemeSelector.vue"
import ColorThemePreview from "../../components/admin/ColorThemePreview.vue"
import colorThemeService from "../../services/colorThemeService"
import ColorThemeForm from "../../components/admin/ColorThemeForm.vue"
const { t } = useI18n()
const { getColorTheme, getColors, setColors } = useTheme()
const { showSuccessNotification, showErrorNotification } = useNotification()
const themeSelectorEl = ref()
let colorPrimary = getColorTheme("--color-primary-base")
let colorPrimaryGradient = getColorTheme("--color-primary-gradient")
let colorPrimaryButtonText = getColorTheme("--color-primary-button-text")
let colorPrimaryButtonAlternativeText = getColorTheme("--color-primary-button-alternative-text")
let colorSecondary = getColorTheme("--color-secondary-base")
let colorSecondaryGradient = getColorTheme("--color-secondary-gradient")
let colorSecondaryButtonText = getColorTheme("--color-secondary-button-text")
let colorTertiary = getColorTheme("--color-tertiary-base")
let colorTertiaryGradient = getColorTheme("--color-tertiary-gradient")
let colorTertiaryButtonText = getColorTheme("--color-tertiary-button-text")
let colorSuccess = getColorTheme("--color-success-base")
let colorSuccessGradient = getColorTheme("--color-success-gradient")
let colorSuccessButtonText = getColorTheme("--color-success-button-text")
let colorInfo = getColorTheme("--color-info-base")
let colorInfoGradient = getColorTheme("--color-info-gradient")
let colorInfoButtonText = getColorTheme("--color-info-button-text")
let colorWarning = getColorTheme("--color-warning-base")
let colorWarningGradient = getColorTheme("--color-warning-gradient")
let colorWarningButtonText = getColorTheme("--color-warning-button-text")
let colorDanger = getColorTheme("--color-danger-base")
let colorDangerGradient = getColorTheme("--color-danger-gradient")
let colorDangerButtonText = getColorTheme("--color-danger-button-text")
let formColor = getColorTheme("--color-form-base")
const themeTitle = ref()
const selectedTheme = ref()
const saveColors = async () => {
try {
const updatedTheme = await themeService.updateTheme({
iri: selectedTheme.value || undefined,
title: themeTitle.value,
colors: getColors(),
})
showSuccessNotification(t("Color updated"))
await themeSelectorEl.value.loadThemes()
selectedTheme.value = updatedTheme["@id"]
} catch (error) {
showErrorNotification(error)
console.error(error)
}
}
const isAdvancedMode = ref(false)
watch(colorPrimary, (newValue) => {
if (!isAdvancedMode.value) {
colorPrimaryGradient.value = makeGradient(newValue)
colorPrimaryButtonText.value = newValue
colorPrimaryButtonAlternativeText.value = makeTextWithContrast(newValue)
}
checkColorContrast(newValue, colorPrimaryButtonText.value, colorPrimaryButtonAlternativeTextError)
})
watch(colorSecondary, (newValue) => {
if (!isAdvancedMode.value) {
colorSecondaryGradient.value = makeGradient(newValue)
colorSecondaryButtonText.value = makeTextWithContrast(newValue)
}
checkColorContrast(newValue, colorSecondaryButtonText.value, colorSecondaryButtonTextError)
})
watch(colorTertiary, (newValue) => {
if (!isAdvancedMode.value) {
colorTertiaryButtonText.value = newValue
colorTertiaryGradient.value = makeGradient(newValue)
}
})
watch(colorSuccess, (newValue) => {
if (!isAdvancedMode.value) {
colorSuccessGradient.value = makeGradient(newValue)
colorSuccessButtonText.value = makeTextWithContrast(newValue)
}
checkColorContrast(newValue, colorSuccessButtonText.value, colorSuccessButtonTextError)
})
watch(colorInfo, (newValue) => {
if (!isAdvancedMode.value) {
colorInfoGradient.value = makeGradient(newValue)
colorInfoButtonText.value = makeTextWithContrast(newValue)
}
checkColorContrast(newValue, colorInfoButtonText.value, colorInfoButtonTextError)
})
watch(colorWarning, (newValue) => {
if (!isAdvancedMode.value) {
colorWarningGradient.value = makeGradient(newValue)
colorWarningButtonText.value = makeTextWithContrast(newValue)
}
checkColorContrast(newValue, colorWarningButtonText.value, colorWarningButtonTextError)
})
watch(colorDanger, (newValue) => {
if (!isAdvancedMode.value) {
colorDangerGradient.value = makeGradient(newValue)
colorDangerButtonText.value = makeTextWithContrast(newValue)
}
})
function makeGradient(color) {
const light = color.clone().to("oklab").l
// when color is light (lightness > 0.5), darken gradient color
// when color is dark, lighten gradient color
// The values 0.5 and 1.6 were chosen through experimentation, there could be a better way to do this
if (light > 0.5) {
return color
.clone()
.set({ "oklab.l": (l) => l * 0.8 })
.to("srgb")
} else {
return color
.clone()
.set({ "oklab.l": (l) => l * 1.6 })
.to("srgb")
}
}
function makeTextWithContrast(color) {
// according to colorjs library https://colorjs.io/docs/contrast#accessible-perceptual-contrast-algorithm-apca
// this algorithm is better than WCAGG 2.1 to check for contrast
// "APCA is being evaluated for use in version 3 of the W3C Web Content Accessibility Guidelines (WCAG)"
let onWhite = Math.abs(color.contrast("white", "APCA"))
let onBlack = Math.abs(color.contrast("black", "APCA"))
return onWhite > onBlack ? new Color("white") : new Color("black")
}
// check for contrast of text
const colorPrimaryButtonTextError = ref("")
watch(colorPrimaryButtonText, (newValue) => {
checkColorContrast(new Color("white"), newValue, colorPrimaryButtonTextError)
})
const colorPrimaryButtonAlternativeTextError = ref("")
watch(colorPrimaryButtonAlternativeText, (newValue) => {
checkColorContrast(colorPrimary.value, newValue, colorPrimaryButtonAlternativeTextError)
})
const colorSecondaryButtonTextError = ref("")
watch(colorSecondaryButtonText, (newValue) => {
checkColorContrast(colorSecondary.value, newValue, colorSecondaryButtonTextError)
})
const colorTertiaryButtonTextError = ref("")
watch(colorTertiaryButtonText, (newValue) => {
checkColorContrast(colorTertiary.value, newValue, colorTertiaryButtonTextError)
})
const colorSuccessButtonTextError = ref("")
watch(colorSuccessButtonText, (newValue) => {
checkColorContrast(colorSuccess.value, newValue, colorSuccessButtonTextError)
})
const colorInfoButtonTextError = ref("")
watch(colorInfoButtonText, (newValue) => {
checkColorContrast(colorInfo.value, newValue, colorInfoButtonTextError)
})
const colorWarningButtonTextError = ref("")
watch(colorWarningButtonText, (newValue) => {
checkColorContrast(colorWarning.value, newValue, colorWarningButtonTextError)
})
const colorDangerButtonTextError = ref("")
watch(colorDangerButtonText, (newValue) => {
checkColorContrast(new Color("white"), newValue, colorDangerButtonTextError)
})
function checkColorContrast(background, foreground, textErrorRef) {
if (isAdvancedMode.value) {
// using APCA for text contrast in buttons. In chamilo buttons the text
// has a font size of 16px and weight of 600
// Lc 60 The minimum level recommended for content text that is not body, column, or block text
// https://git.apcacontrast.com/documentation/APCA_in_a_Nutshell#use-case--size-ranges
let contrast = Math.abs(background.contrast(foreground, "APCA"))
console.log(`Contrast ${contrast}`)
if (contrast < 60) {
textErrorRef.value = t("Does not have enough contrast against background")
} else {
textErrorRef.value = ""
}
}
}
async function onClickChangeColorTheme() {
if (selectedTheme.value) {
await colorThemeService.changePlatformColorTheme(selectedTheme.value)
}
}
</script>

@ -46,7 +46,7 @@ class ColorTheme
/**
* @var array<string, mixed>
*/
#[Groups(['color_theme:write'])]
#[Groups(['color_theme:write', 'access_url_rel_color_theme:read'])]
#[ORM\Column]
private array $variables = [];

Loading…
Cancel
Save