Gh 3793 upload color theme to server and improve color pickers (#5286)

* Admin: save personalized colors

* Fix buttons with incorrect cursor

* Minor: format with prettier

* Internal: add alternative to primary button

* Fis style of primary button

* Admin: Improve color picker with hexadecimal value

* Format base input text and add property to change
input class
* Install colorjs to handle colors

* Admin: Add more example elements to custom colors screen

* Core: Fix dropdown to allow any kind of values

* Minor: minor improvements to base components

* Minor: use dev-server for enabling hot reloading

* This will automatic reload the page
when something changes
https://symfony.com/doc/current/frontend/encore/dev-server.html

* Internal: add colors from design

* Use the same name for every property to be consistent

* Internal: use correct colors for buttons and add missing ones

* Minor: fix type definition for some base components

* Internal: use new color library in base color picker

* Admin: Add advanced mode to pick colors of chamilo

* Admin: Use standard rgb format for colors

* rgba is a legacy feature of browsers, that still works,
 but use standard feature will ensure better compatibility

* Admin: Add contrast checker for color picker

* Admin: Simple mode by default in color picker

* Minor: remove not needed change in package.json
snyk-fix-0707082af6333e258bba6802257d9315
Daniel 2 years ago committed by GitHub
parent dd6ecf87df
commit f822a0da50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 35
      assets/css/app.scss
  2. 39
      assets/vue/components/basecomponents/BaseButton.vue
  3. 77
      assets/vue/components/basecomponents/BaseColorPicker.vue
  4. 3
      assets/vue/components/basecomponents/BaseDropdown.vue
  5. 1
      assets/vue/components/basecomponents/BaseInputDate.vue
  6. 37
      assets/vue/components/basecomponents/BaseInputText.vue
  7. 2
      assets/vue/components/basecomponents/validators.js
  8. 27
      assets/vue/composables/theme.js
  9. 494
      assets/vue/views/admin/AdminConfigureColors.vue
  10. 1
      package.json
  11. 42
      tailwind.config.js

@ -11,19 +11,36 @@
// TAILWIND COLOR DEFINITION https://tailwindcss.com/docs/customizing-colors#using-css-variables
@layer base {
:root {
--color-primary-base: 46, 117, 163;
--color-primary-gradient: 156, 194, 218;
--color-primary-base: 46 117 163;
--color-primary-gradient: 36 77 103;
--color-primary-button-text: 46 117 163;
--color-primary-button-alternative-text: 255 255 255;
--color-secondary-base: 243, 126, 47;
--color-secondary-gradient: 224, 100, 16;
--color-secondary-base: 243 126 47;
--color-secondary-gradient: 224 100 16;
--color-secondary-button-text: 255 255 255;
--color-tertiary-base: 0, 0, 0;
--color-tertiary-gradient: 51, 51, 51;
--color-tertiary-base: 51 51 51;
--color-tertiary-gradient: 0 0 0;
--color-tertiary-button-text: 255 255 255;
--color-success-base: 119, 170, 12;
--color-success-gradient: 84, 119, 8;
--color-success-base: 119 170 12;
--color-success-gradient: 83 127 0;
--color-success-button-text: 255 255 255;
--color-danger-base: 223, 59, 59;
--color-info-base: 13 123 253;
--color-info-gradient: 0 84 211;
--color-info-button-text: 255 255 255;
--color-warning-base: 245 206 1;
--color-warning-gradient: 186 152 0;
--color-warning-button-text: 0 0 0;
--color-danger-base: 223 59 59;
--color-danger-gradient: 180 0 21;
--color-danger-button-text: 255 255 255;
--color-form-base: 46 117 163;
}
}

@ -1,5 +1,6 @@
<template>
<Button
class="cursor-pointer"
:aria-label="onlyIcon ? label : undefined"
:class="buttonClass"
:disabled="disabled"
@ -19,7 +20,7 @@
<script setup>
import Button from "primevue/button"
import { computed } from "vue"
import { chamiloIconToClass } from "./ChamiloIcons";
import { chamiloIconToClass } from "./ChamiloIcons"
import { buttonTypeValidator, iconValidator, sizeValidator } from "./validators"
const props = defineProps({
@ -95,27 +96,35 @@ const buttonClass = computed(() => {
}
let commonDisabled =
"disabled:bg-primary-bgdisabled disabled:border disabled:border-primary-borderdisabled disabled:text-fontdisabled";
"disabled:bg-primary-bgdisabled disabled:border disabled:border-primary-borderdisabled disabled:text-fontdisabled disabled:pointer-events-auto disabled:cursor-not-allowed"
switch (props.type) {
case "primary":
result += `border-primary hover:bg-primary text-primary hover:text-white ${commonDisabled} `;
break;
result += `bg-white border-primary text-primary-button-text hover:bg-primary hover:text-white ${commonDisabled} `
break
case "primary-alternative":
result += `bg-primary text-primary-button-alternative-text hover:bg-primary-gradient ${commonDisabled} `
break
case "secondary":
result +=
"bg-secondary text-white hover:bg-secondary-gradient disabled:bg-secondary-bgdisabled disabled:text-fontdisabled";
break;
result += `bg-secondary text-secondary-button-text hover:bg-secondary-gradient disabled:bg-secondary-bgdisabled disabled:text-fontdisabled ${commonDisabled}`
break
case "success":
result += `bg-success hover:bg-success-gradient ${commonDisabled} `;
break;
result += `bg-success text-success-button-text hover:bg-success-gradient ${commonDisabled} `
break
case "info":
result += `bg-info text-info-button-text hover:bg-info-gradient ${commonDisabled} `
break
case "warning":
result += `bg-warning text-warning-button-text hover:bg-warning-gradient ${commonDisabled} `
break
case "danger":
result += `border-error hover:bg-error text-error hover:text-white ${commonDisabled} `;
break;
result += `bg-white border-danger text-danger hover:bg-danger-gradient hover:text-white ${commonDisabled}`
break
case "black":
result += "bg-white text-tertiary border-tertiary hover:bg-tertiary-gradient hover:text-white";
break;
result += `bg-white border-tertiary text-tertiary hover:bg-tertiary hover:text-white ${commonDisabled}`
break
}
return result;
});
return result
})
// https://primevue.org/button/#outlined
const primeOutlinedProperty = computed(() => {

@ -1,27 +1,86 @@
<template>
<div class="flex flex-col justify-center">
<span v-if="label">{{ label }}</span>
<div class="flex flex-col justify-center gap-0">
<p v-if="label">{{ label }}</p>
<div class="flex flex-row gap-3 h-10">
<ColorPicker
format="rgb"
:model-value="modelValue"
@update:model-value="emit('update:modelValue', $event)"
format="hex"
:model-value="hexColor"
@update:model-value="colorPicked"
/>
<BaseInputText
label=""
class="max-w-32"
input-class="mb-0"
:model-value="hexColor"
:error-text="inputHexError"
:is-invalid="inputHexError !== ''"
:form-submitted="inputHexError !== ''"
@update:model-value="colorPicked"
/>
</div>
<small
v-if="error"
class="text-danger h-4"
>
{{ error }}
</small>
<div v-else class="h-4"></div>
</div>
</template>
<script setup>
import ColorPicker from 'primevue/colorpicker'
import ColorPicker from "primevue/colorpicker"
import Color from "colorjs.io"
import { computed, ref } from "vue"
import { useI18n } from "vue-i18n"
import BaseInputText from "./BaseInputText.vue"
const { t } = useI18n()
defineProps({
const props = defineProps({
// this should be a Color instance from colorjs library
modelValue: {
type: Object,
required: true,
},
label: {
type: String,
default: '',
}
default: "",
},
error: {
type: String,
default: "",
},
})
const emit = defineEmits(["update:modelValue"])
const hexColor = computed(() => {
let hex = props.modelValue.toString({ format: "hex" })
// convert #fff color format to full #ffffff, because otherwise input does not set the color right
if (hex.length === 4) {
hex = hex + hex.slice(1)
}
return hex
})
const inputHexError = ref("")
function colorPicked(newHexColor) {
inputHexError.value = ""
if (!newHexColor.startsWith("#")) {
newHexColor = `#${newHexColor}`
}
if (newHexColor.length < 7) {
inputHexError.value = t("Invalid format")
return
}
try {
let color = new Color(newHexColor)
emit("update:modelValue", color)
} catch (error) {
inputHexError.value = t("Invalid format")
}
}
</script>

@ -27,8 +27,9 @@ defineProps({
type: String,
required: true,
},
// type null allow all kind of values, like prime vue does
modelValue: {
type: Object,
type: null,
required: true,
default: () => {},
},

@ -20,7 +20,6 @@ import Calendar from "primevue/calendar";
defineProps({
modelValue: {
type: String,
required: true,
default: () => "",
},
id: {

@ -4,15 +4,30 @@
<InputText
:id="id"
:model-value="modelValue"
:class="{ 'p-invalid': isInvalid }"
:class="{ 'p-invalid': isInvalid, [inputClass]: true }"
:aria-label="label"
type="text"
@update:model-value="updateValue"
/>
<label :for="id" :class="{ 'p-error': isInvalid }">{{ label }}</label>
<label
:for="id"
:class="{ 'p-error': isInvalid }"
>
{{ label }}
</label>
</div>
<small v-if="formSubmitted && isInvalid" class="p-error">{{ errorText }}</small>
<small v-if="helpText" class="form-text text-muted">{{ helpText }}</small>
<small
v-if="formSubmitted && isInvalid"
class="p-error"
>
{{ errorText }}
</small>
<small
v-if="helpText"
class="form-text text-muted"
>
{{ helpText }}
</small>
</div>
</template>
@ -31,7 +46,7 @@ const props = defineProps({
default: "",
},
modelValue: {
type: String,
type: [String, null],
required: true,
},
errorText: {
@ -41,17 +56,23 @@ const props = defineProps({
},
isInvalid: {
type: Boolean,
required: false,
default: false,
},
required: {
type: Boolean,
default: false,
},
helpText: String,
helpText: {
type: String,
default: "",
},
formSubmitted: {
type: Boolean,
default: false
default: false,
},
inputClass: {
type: String,
default: "",
},
})

@ -21,5 +21,5 @@ export const buttonTypeValidator = (value) => {
if (typeof value !== "string") {
return false
}
return ["primary", "secondary", "black", "success", "danger"].includes(value)
return ["primary", "primary-alternative", "secondary", "black", "success", "info", "warning", "danger"].includes(value)
}

@ -1,4 +1,5 @@
import { onMounted, ref, watch } from "vue"
import Color from "colorjs.io"
export const useTheme = () => {
let colors = {}
@ -24,26 +25,36 @@ export const useTheme = () => {
}
const getCssVariableValue = (variableName) => {
const colorVariable = getComputedStyle(document.body).getPropertyValue(variableName).split(", ")
return {
r: parseInt(colorVariable[0]),
g: parseInt(colorVariable[1]),
b: parseInt(colorVariable[2]),
}
const colorVariable = getComputedStyle(document.body).getPropertyValue(variableName)
return colorFromCSSVariable(colorVariable)
}
const setCssVariableValue = (variableName, color) => {
document.documentElement.style.setProperty(variableName, `${color.r}, ${color.g}, ${color.b}`)
document.documentElement.style.setProperty(variableName, colorToCSSVariable(color))
}
const getColors = () => {
let colorsPlainObject = {}
for (const [key, value] of Object.entries(colors)) {
colorsPlainObject[key] = `${value.value.r}, ${value.value.g}, ${value.value.b}`
colorsPlainObject[key] = colorToCSSVariable(value.value)
}
return colorsPlainObject
}
const colorToCSSVariable = (color) => {
// according to documentation https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/rgb#syntax
// the format "r g b" should work but because how rules in css are defined does not
// so, we need the format "r, g, b" for variables,
const r = Math.round(color.r * 255)
const g = Math.round(color.g * 255)
const b = Math.round(color.b * 255)
return `${r} ${g} ${b}`
}
const colorFromCSSVariable = (variable) => {
return new Color(`rgb(${variable})`)
}
return {
getColorTheme,
getColors,

@ -1,57 +1,180 @@
<template class="personal-theme">
<h4 class="mb-4">{{ t("Configure chamilo colors") }}</h4>
<div class="grid grid-cols-2 gap-2 mb-8">
<!-- Advanced mode -->
<div v-show="isAdvancedMode">
<div class="flex flex-col gap-2 mb-3 md:grid md:grid-cols-2 xl:grid-cols-4">
<BaseColorPicker
v-model="primaryColor"
:label="t('Pick primary color')"
v-model="colorPrimary"
:label="t('Primary color')"
/>
<BaseColorPicker
v-model="primaryColorGradient"
:label="t('Pick primary color gradient')"
v-model="colorPrimaryGradient"
:label="t('Primary color hover/background')"
/>
<BaseColorPicker
v-model="secondaryColor"
:label="t('Pick secondary color')"
v-model="colorPrimaryButtonText"
:label="t('Primary color button text')"
:error="colorPrimaryButtonTextError"
/>
<BaseColorPicker
v-model="secondaryColorGradient"
:label="t('Pick secondary color gradient')"
v-model="colorPrimaryButtonAlternativeText"
:label="t('Primary color button alternative text')"
:error="colorPrimaryButtonAlternativeTextError"
/>
</div>
<div class="flex flex-col gap-2 mb-3 md:grid md:grid-cols-2 xl:grid-cols-4">
<BaseColorPicker
v-model="tertiaryColor"
:label="t('Pick tertiary color')"
v-model="colorSecondary"
:label="t('Secondary color')"
/>
<BaseColorPicker
v-model="tertiaryColorGradient"
:label="t('Pick tertiary color gradient')"
v-model="colorSecondaryGradient"
:label="t('Secondary color hover/background')"
/>
<BaseColorPicker
v-model="successColor"
:label="t('Pick success color')"
v-model="colorSecondaryButtonText"
:label="t('Secondary color button text')"
:error="colorSecondaryButtonTextError"
/>
</div>
<div class="flex flex-col gap-2 mb-3 md:grid md:grid-cols-2 xl:grid-cols-4">
<BaseColorPicker
v-model="successColorGradient"
:label="t('Pick success color gradient')"
v-model="colorTertiary"
:label="t('Tertiary color')"
/>
<BaseColorPicker
v-model="dangerColor"
:label="t('Pick danger color')"
v-model="colorTertiaryGradient"
:label="t('Tertiary color hover/background')"
/>
</div>
<div class="flex flex-wrap mb-4 gap-4">
<BaseInputText
v-model="themeTitle"
:label="t('Title')"
<div class="flex flex-col gap-2 mb-3 md:grid md:grid-cols-2 xl:grid-cols-4">
<BaseColorPicker
v-model="colorSuccess"
:label="t('Success color')"
/>
<BaseColorPicker
v-model="colorSuccessGradient"
:label="t('Success color hover/background')"
/>
<BaseColorPicker
v-model="colorSuccessButtonText"
:label="t('Success color button text')"
:error="colorSuccessButtonTextError"
/>
</div>
<div class="flex flex-col gap-2 mb-3 md:grid md:grid-cols-2 xl:grid-cols-4">
<BaseColorPicker
v-model="colorInfo"
:label="t('Info color')"
/>
<BaseColorPicker
v-model="colorInfoGradient"
:label="t('Info color hover/background')"
/>
<BaseColorPicker
v-model="colorInfoButtonText"
:label="t('Info color button text')"
:error="colorInfoButtonTextError"
/>
</div>
<div class="flex flex-col gap-2 mb-3 md:grid md:grid-cols-2 xl:grid-cols-4">
<BaseColorPicker
v-model="colorWarning"
:label="t('Warning color')"
/>
<BaseColorPicker
v-model="colorWarningGradient"
:label="t('Warning color hover/background')"
/>
<BaseColorPicker
v-model="colorWarningButtonText"
:label="t('Warning color button text')"
:error="colorWarningButtonTextError"
/>
</div>
<div class="flex flex-col gap-2 mb-3 md:grid md:grid-cols-2 xl:grid-cols-4">
<BaseColorPicker
v-model="colorDanger"
:label="t('Danger color')"
/>
<BaseColorPicker
v-model="colorDangerGradient"
:label="t('Danger color hover/background')"
/>
</div>
<div class="flex flex-col gap-2 mb-3 md:grid md:grid-cols-2 xl:grid-cols-4">
<BaseColorPicker
v-model="formColor"
:label="t('Form outline color')"
/>
</div>
</div>
<!-- Simple mode -->
<div v-show="!isAdvancedMode">
<div class="flex flex-col gap-2 mb-3 md:grid md:grid-cols-2 xl:grid-cols-4">
<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="flex flex-col gap-2 mb-3 md:grid md:grid-cols-2 xl:grid-cols-4">
<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="flex flex-col gap-2 mb-3 md:grid md:grid-cols-2 xl:grid-cols-4">
<BaseColorPicker
v-model="formColor"
:label="t('Form outline color')"
/>
</div>
</div>
<div class="flex flex-wrap mb-4 gap-3">
<BaseButton
type="primary"
icon="send"
:label="t('Save')"
@click="saveColors"
/>
<BaseButton
type="black"
icon="cog"
:label="isAdvancedMode ? t('Hide advanced mode') : t('Show advanced mode')"
@click="isAdvancedMode = !isAdvancedMode"
/>
</div>
<hr />
@ -59,19 +182,18 @@
<div class="mb-4">
<p class="mb-3 text-lg">{{ t("Buttons") }}</p>
<div class="flex flex-row flex-wrap">
<div class="flex flex-row flex-wrap mb-3">
<BaseButton
class="mr-2 mb-2"
:label="t('Button')"
:label="t('Primary')"
type="primary"
icon="eye-on"
/>
<BaseButton
class="mr-2 mb-2"
:label="t('Disabled')"
type="primary"
:label="t('Primary alternative')"
type="primary-alternative"
icon="eye-on"
disabled
/>
<BaseButton
class="mr-2 mb-2"
@ -85,16 +207,24 @@
type="black"
icon="eye-on"
/>
</div>
<div class="flex flex-row flex-wrap mb-3">
<BaseButton
class="mr-2 mb-2"
type="primary"
icon="cog"
only-icon
:label="t('Success')"
type="success"
icon="send"
/>
<BaseButton
class="mr-2 mb-2"
:label="t('Success')"
type="success"
:label="t('Info')"
type="info"
icon="send"
/>
<BaseButton
class="mr-2 mb-2"
:label="t('Warning')"
type="warning"
icon="send"
/>
<BaseButton
@ -104,12 +234,28 @@
icon="delete"
/>
</div>
<div class="flex flex-row flex-wrap mb-3">
<BaseButton
class="mr-2 mb-2"
:label="t('Disabled')"
type="primary"
icon="eye-on"
disabled
/>
<BaseButton
class="mr-2 mb-2"
type="primary"
icon="cog"
only-icon
/>
</div>
</div>
<div class="mb-4">
<p class="mb-3 text-lg">{{ t("Menu on button pressed") }}</p>
<p class="mb-3 text-lg">{{ t("Dropdowns") }}</p>
<div class="flex flex-row gap-3">
<BaseButton
class="mr-2 mb-2"
class="mr-3 mb-2"
type="primary"
icon="cog"
popup-identifier="menu"
@ -120,7 +266,31 @@
id="menu"
ref="menu"
:model="menuItems"
></BaseMenu>
/>
<BaseDropdown
v-model="dropdown"
class="w-36"
input-id="dropdown"
option-label="label"
option-value="value"
:label="t('Dropdown')"
:options="[
{
label: t('Option 1'),
value: 'option_1',
},
{
label: t('Option 2'),
value: 'option_2',
},
{
label: t('Option 3'),
value: 'option_3',
},
]"
name="dropdown"
/>
</div>
</div>
<div class="mb-4">
@ -147,15 +317,39 @@
</div>
<div class="mb-4">
<p class="mb-3 text-lg">Forms</p>
<p class="mb-3 text-lg">{{ t("Toggle") }}</p>
<BaseToggleButton
:model-value="toggleState"
:off-label="t('Show all')"
:on-label="t('Hide all')"
off-icon="eye-on"
on-icon="eye-off"
size="normal"
without-borders
@update:model-value="toggleState = !toggleState"
/>
</div>
<div class="mb-4">
<p class="mb-3 text-lg">{{ t("Forms") }}</p>
<BaseInputText
:label="t('This is the default form')"
:model-value="null"
/>
<BaseInputText
v-model="inputText"
:label="t('This is a text example')"
:label="t('This is a form with an error')"
:is-invalid="true"
:model-value="null"
/>
<BaseInputDate
id="date"
:label="t('Date')"
class="w-32"
/>
</div>
<div class="mb-4">
<p class="mb-3 text-lg">Dialogs</p>
<p class="mb-3 text-lg">{{ t("Dialogs") }}</p>
<BaseButton
:label="t('Show dialog')"
type="black"
@ -169,13 +363,25 @@
@cancel-clicked="isDialogVisible = false"
/>
</div>
<div class="mb-4">
<p class="mb-3 text-lg">{{ t("Some more elements") }}</p>
<div class="course-tool cursor-pointer">
<div class="course-tool__link hover:primary-gradient hover:bg-primary-gradient/10">
<span
class="course-tool__icon mdi mdi-bookshelf"
aria-hidden="true"
/>
</div>
<p class="course-tool__title">{{ t("Documents") }}</p>
</div>
</div>
</template>
<script setup>
import BaseButton from "../../components/basecomponents/BaseButton.vue"
import { useI18n } from "vue-i18n"
import BaseMenu from "../../components/basecomponents/BaseMenu.vue"
import { ref } from "vue"
import { provide, ref, watch } from "vue"
import BaseCheckbox from "../../components/basecomponents/BaseCheckbox.vue"
import BaseRadioButtons from "../../components/basecomponents/BaseRadioButtons.vue"
import BaseDialogConfirmCancel from "../../components/basecomponents/BaseDialogConfirmCancel.vue"
@ -183,36 +389,210 @@ import BaseInputText from "../../components/basecomponents/BaseInputText.vue"
import BaseColorPicker from "../../components/basecomponents/BaseColorPicker.vue"
import { useTheme } from "../../composables/theme"
import axios from "axios"
import { useNotification } from "../../composables/notification"
import BaseDropdown from "../../components/basecomponents/BaseDropdown.vue"
import BaseInputDate from "../../components/basecomponents/BaseInputDate.vue"
import BaseToggleButton from "../../components/basecomponents/BaseToggleButton.vue"
import Color from "colorjs.io"
const { t } = useI18n()
const { getColorTheme, getColors } = useTheme()
const { showSuccessNotification, showErrorNotification } = useNotification()
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")
const themeTitle = ref()
let primaryColor = getColorTheme("--color-primary-base")
let primaryColorGradient = getColorTheme("--color-primary-gradient")
let secondaryColor = getColorTheme("--color-secondary-base")
let secondaryColorGradient = getColorTheme("--color-secondary-gradient")
let tertiaryColor = getColorTheme("--color-tertiary-base")
let tertiaryColorGradient = getColorTheme("--color-tertiary-gradient")
let successColor = getColorTheme("--color-success-base")
let successColorGradient = getColorTheme("--color-success-gradient")
let dangerColor = getColorTheme("--color-danger-base")
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 saveColors = async () => {
let colors = getColors()
// TODO send colors to backend, then notify if was correct or incorrect
try {
await axios.post("/api/color_themes", {
title: themeTitle.value,
variables: colors,
title: themeTitle.value,variables: colors,
})
showSuccessNotification(t("Colors updated"))
} 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 = ""
}
}
}
// properties for example components
const menu = ref("menu")
const menuItems = [{ label: t("Item 1") }, { label: t("Item 2") }, { label: t("Item 3") }]
const toggle = (event) => {
menu.value.toggle(event)
}
const dropdown = ref("")
const checkbox1 = ref(true)
const checkbox2 = ref(false)
@ -226,5 +606,11 @@ const radioValue = ref("value1")
const isDialogVisible = ref(false)
const inputText = ref("")
const toggleState = ref(true)
// needed for course tool
const isSorting = ref(false)
const isCustomizing = ref(false)
provide("isSorting", isSorting)
provide("isCustomizing", isCustomizing)
</script>

@ -39,6 +39,7 @@
"bootstrap-daterangepicker": "^3.1.0",
"bootstrap-select": "^1.13.18",
"chart.js": "^3.9.1",
"colorjs.io": "^0.5.0",
"cropper": "^4.1.0",
"datepair.js": "^0.4.17",
"dotenv": "^16.3.1",

@ -1,10 +1,12 @@
const colors = require("tailwindcss/colors");
const colors = require("tailwindcss/colors")
// from tailwind youtube channel https://youtu.be/MAtaT8BZEAo?t=1023
const colorWithOpacity = (variableName) => {
return ({ opacityValue }) => {
if (opacityValue !== undefined) {
return `rgba(var(${variableName}), ${opacityValue})`
// use standard syntax rgb (color / alpha) rgba is legacy
// check https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/rgb
return `rgb(var(${variableName}) / ${opacityValue})`
}
return `rgb(var(${variableName}))`
}
@ -24,16 +26,20 @@ module.exports = {
gradient: colorWithOpacity("--color-primary-gradient"),
bgdisabled: "#fafafa",
borderdisabled: "#e4e9ed",
"button-text": colorWithOpacity("--color-primary-button-text"),
"button-alternative-text": colorWithOpacity("--color-primary-button-alternative-text"),
},
secondary: {
DEFAULT: colorWithOpacity("--color-secondary-base"),
gradient: colorWithOpacity("--color-secondary-gradient"),
bgdisabled: '#e4e9ed',
bgdisabled: "#e4e9ed",
hover: "#d35e0f",
"button-text": colorWithOpacity("--color-secondary-button-text"),
},
tertiary: {
DEFAULT: colorWithOpacity("--color-tertiary-base"),
gradient: colorWithOpacity("--color-tertiary-gradient"),
"button-text": colorWithOpacity("--color-tertiary-button-text"),
},
gray: {
5: "rgba(250, 250, 250, 0.5)",
@ -53,20 +59,37 @@ module.exports = {
5: "#e06410",
6: "#faf7f5",
},
warning: "#f5ce01",
success: {
DEFAULT: colorWithOpacity("--color-success-base"),
gradient: colorWithOpacity("--color-success-gradient"),
"button-text": colorWithOpacity("--color-success-button-text"),
},
info: {
DEFAULT: colorWithOpacity("--color-info-base"),
gradient: colorWithOpacity("--color-info-gradient"),
"button-text": colorWithOpacity("--color-info-button-text"),
},
warning: {
DEFAULT: colorWithOpacity("--color-warning-base"),
gradient: colorWithOpacity("--color-warning-gradient"),
"button-text": colorWithOpacity("--color-warning-button-text"),
},
danger: {
DEFAULT: colorWithOpacity("--color-danger-base"),
gradient: colorWithOpacity("--color-danger-gradient"),
"button-text": colorWithOpacity("--color-danger-button-text"),
},
// error is used in some places in css to this is the same color as danger DEFAULT
error: colorWithOpacity("--color-danger-base"),
info: "#0d7bfd",
form: colorWithOpacity("--color-form-base"),
white: colors.white,
black: colors.black,
transparent: colors.transparent,
current: colors.current,
fontdisabled: '#a2a6b0',
fontdisabled: "#a2a6b0",
},
extend: {
fontFamily: {
@ -83,8 +106,5 @@ module.exports = {
corePlugins: {
aspectRatio: true,
},
plugins: [
require("@tailwindcss/forms"),
require("@tailwindcss/typography"),
],
};
plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography")],
}

Loading…
Cancel
Save