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. 44
      tailwind.config.js

@ -11,19 +11,36 @@
// TAILWIND COLOR DEFINITION https://tailwindcss.com/docs/customizing-colors#using-css-variables // TAILWIND COLOR DEFINITION https://tailwindcss.com/docs/customizing-colors#using-css-variables
@layer base { @layer base {
:root { :root {
--color-primary-base: 46, 117, 163; --color-primary-base: 46 117 163;
--color-primary-gradient: 156, 194, 218; --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-base: 243 126 47;
--color-secondary-gradient: 224, 100, 16; --color-secondary-gradient: 224 100 16;
--color-secondary-button-text: 255 255 255;
--color-tertiary-base: 0, 0, 0; --color-tertiary-base: 51 51 51;
--color-tertiary-gradient: 51, 51, 51; --color-tertiary-gradient: 0 0 0;
--color-tertiary-button-text: 255 255 255;
--color-success-base: 119, 170, 12; --color-success-base: 119 170 12;
--color-success-gradient: 84, 119, 8; --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> <template>
<Button <Button
class="cursor-pointer"
:aria-label="onlyIcon ? label : undefined" :aria-label="onlyIcon ? label : undefined"
:class="buttonClass" :class="buttonClass"
:disabled="disabled" :disabled="disabled"
@ -19,7 +20,7 @@
<script setup> <script setup>
import Button from "primevue/button" import Button from "primevue/button"
import { computed } from "vue" import { computed } from "vue"
import { chamiloIconToClass } from "./ChamiloIcons"; import { chamiloIconToClass } from "./ChamiloIcons"
import { buttonTypeValidator, iconValidator, sizeValidator } from "./validators" import { buttonTypeValidator, iconValidator, sizeValidator } from "./validators"
const props = defineProps({ const props = defineProps({
@ -95,27 +96,35 @@ const buttonClass = computed(() => {
} }
let commonDisabled = 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) { switch (props.type) {
case "primary": case "primary":
result += `border-primary hover:bg-primary text-primary hover:text-white ${commonDisabled} `; result += `bg-white border-primary text-primary-button-text hover:bg-primary hover:text-white ${commonDisabled} `
break; break
case "primary-alternative":
result += `bg-primary text-primary-button-alternative-text hover:bg-primary-gradient ${commonDisabled} `
break
case "secondary": case "secondary":
result += result += `bg-secondary text-secondary-button-text hover:bg-secondary-gradient disabled:bg-secondary-bgdisabled disabled:text-fontdisabled ${commonDisabled}`
"bg-secondary text-white hover:bg-secondary-gradient disabled:bg-secondary-bgdisabled disabled:text-fontdisabled"; break
break;
case "success": case "success":
result += `bg-success hover:bg-success-gradient ${commonDisabled} `; result += `bg-success text-success-button-text hover:bg-success-gradient ${commonDisabled} `
break; 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": case "danger":
result += `border-error hover:bg-error text-error hover:text-white ${commonDisabled} `; result += `bg-white border-danger text-danger hover:bg-danger-gradient hover:text-white ${commonDisabled}`
break; break
case "black": case "black":
result += "bg-white text-tertiary border-tertiary hover:bg-tertiary-gradient hover:text-white"; result += `bg-white border-tertiary text-tertiary hover:bg-tertiary hover:text-white ${commonDisabled}`
break; break
} }
return result; return result
}); })
// https://primevue.org/button/#outlined // https://primevue.org/button/#outlined
const primeOutlinedProperty = computed(() => { const primeOutlinedProperty = computed(() => {

@ -1,27 +1,86 @@
<template> <template>
<div class="flex flex-col justify-center"> <div class="flex flex-col justify-center gap-0">
<span v-if="label">{{ label }}</span> <p v-if="label">{{ label }}</p>
<div class="flex flex-row gap-3 h-10">
<ColorPicker <ColorPicker
format="rgb" format="hex"
:model-value="modelValue" :model-value="hexColor"
@update:model-value="emit('update:modelValue', $event)" @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> </div>
</template> </template>
<script setup> <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"
defineProps({ const { t } = useI18n()
const props = defineProps({
// this should be a Color instance from colorjs library
modelValue: { modelValue: {
type: Object, type: Object,
required: true, required: true,
}, },
label: { label: {
type: String, type: String,
default: '', default: "",
} },
error: {
type: String,
default: "",
},
}) })
const emit = defineEmits(["update:modelValue"]) 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> </script>

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

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

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

@ -21,5 +21,5 @@ export const buttonTypeValidator = (value) => {
if (typeof value !== "string") { if (typeof value !== "string") {
return false 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 { onMounted, ref, watch } from "vue"
import Color from "colorjs.io"
export const useTheme = () => { export const useTheme = () => {
let colors = {} let colors = {}
@ -24,26 +25,36 @@ export const useTheme = () => {
} }
const getCssVariableValue = (variableName) => { const getCssVariableValue = (variableName) => {
const colorVariable = getComputedStyle(document.body).getPropertyValue(variableName).split(", ") const colorVariable = getComputedStyle(document.body).getPropertyValue(variableName)
return { return colorFromCSSVariable(colorVariable)
r: parseInt(colorVariable[0]),
g: parseInt(colorVariable[1]),
b: parseInt(colorVariable[2]),
}
} }
const setCssVariableValue = (variableName, color) => { const setCssVariableValue = (variableName, color) => {
document.documentElement.style.setProperty(variableName, `${color.r}, ${color.g}, ${color.b}`) document.documentElement.style.setProperty(variableName, colorToCSSVariable(color))
} }
const getColors = () => { const getColors = () => {
let colorsPlainObject = {} let colorsPlainObject = {}
for (const [key, value] of Object.entries(colors)) { 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 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 { return {
getColorTheme, getColorTheme,
getColors, getColors,

@ -1,57 +1,180 @@
<template class="personal-theme"> <template class="personal-theme">
<h4 class="mb-4">{{ t("Configure chamilo colors") }}</h4> <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 <BaseColorPicker
v-model="primaryColor" v-model="colorPrimary"
:label="t('Pick primary color')" :label="t('Primary color')"
/> />
<BaseColorPicker <BaseColorPicker
v-model="primaryColorGradient" v-model="colorPrimaryGradient"
:label="t('Pick primary color gradient')" :label="t('Primary color hover/background')"
/> />
<BaseColorPicker <BaseColorPicker
v-model="secondaryColor" v-model="colorPrimaryButtonText"
:label="t('Pick secondary color')" :label="t('Primary color button text')"
:error="colorPrimaryButtonTextError"
/> />
<BaseColorPicker <BaseColorPicker
v-model="secondaryColorGradient" v-model="colorPrimaryButtonAlternativeText"
:label="t('Pick secondary color gradient')" :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 <BaseColorPicker
v-model="tertiaryColor" v-model="colorSecondary"
:label="t('Pick tertiary color')" :label="t('Secondary color')"
/> />
<BaseColorPicker <BaseColorPicker
v-model="tertiaryColorGradient" v-model="colorSecondaryGradient"
:label="t('Pick tertiary color gradient')" :label="t('Secondary color hover/background')"
/> />
<BaseColorPicker <BaseColorPicker
v-model="successColor" v-model="colorSecondaryButtonText"
:label="t('Pick success color')" :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 <BaseColorPicker
v-model="successColorGradient" v-model="colorTertiary"
:label="t('Pick success color gradient')" :label="t('Tertiary color')"
/> />
<BaseColorPicker <BaseColorPicker
v-model="dangerColor" v-model="colorTertiaryGradient"
:label="t('Pick danger color')" :label="t('Tertiary color hover/background')"
/> />
</div> </div>
<div class="flex flex-wrap mb-4 gap-4"> <div class="flex flex-col gap-2 mb-3 md:grid md:grid-cols-2 xl:grid-cols-4">
<BaseInputText <BaseColorPicker
v-model="themeTitle" v-model="colorSuccess"
:label="t('Title')" :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 <BaseButton
type="primary" type="primary"
icon="send" icon="send"
:label="t('Save')" :label="t('Save')"
@click="saveColors" @click="saveColors"
/> />
<BaseButton
type="black"
icon="cog"
:label="isAdvancedMode ? t('Hide advanced mode') : t('Show advanced mode')"
@click="isAdvancedMode = !isAdvancedMode"
/>
</div> </div>
<hr /> <hr />
@ -59,19 +182,18 @@
<div class="mb-4"> <div class="mb-4">
<p class="mb-3 text-lg">{{ t("Buttons") }}</p> <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 <BaseButton
class="mr-2 mb-2" class="mr-2 mb-2"
:label="t('Button')" :label="t('Primary')"
type="primary" type="primary"
icon="eye-on" icon="eye-on"
/> />
<BaseButton <BaseButton
class="mr-2 mb-2" class="mr-2 mb-2"
:label="t('Disabled')" :label="t('Primary alternative')"
type="primary" type="primary-alternative"
icon="eye-on" icon="eye-on"
disabled
/> />
<BaseButton <BaseButton
class="mr-2 mb-2" class="mr-2 mb-2"
@ -85,16 +207,24 @@
type="black" type="black"
icon="eye-on" icon="eye-on"
/> />
</div>
<div class="flex flex-row flex-wrap mb-3">
<BaseButton <BaseButton
class="mr-2 mb-2" class="mr-2 mb-2"
type="primary" :label="t('Success')"
icon="cog" type="success"
only-icon icon="send"
/> />
<BaseButton <BaseButton
class="mr-2 mb-2" class="mr-2 mb-2"
:label="t('Success')" :label="t('Info')"
type="success" type="info"
icon="send"
/>
<BaseButton
class="mr-2 mb-2"
:label="t('Warning')"
type="warning"
icon="send" icon="send"
/> />
<BaseButton <BaseButton
@ -104,12 +234,28 @@
icon="delete" icon="delete"
/> />
</div> </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>
<div class="mb-4"> <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 <BaseButton
class="mr-2 mb-2" class="mr-3 mb-2"
type="primary" type="primary"
icon="cog" icon="cog"
popup-identifier="menu" popup-identifier="menu"
@ -120,7 +266,31 @@
id="menu" id="menu"
ref="menu" ref="menu"
:model="menuItems" :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>
<div class="mb-4"> <div class="mb-4">
@ -147,15 +317,39 @@
</div> </div>
<div class="mb-4"> <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 <BaseInputText
v-model="inputText" :label="t('This is a form with an error')"
:label="t('This is a text example')" :is-invalid="true"
:model-value="null"
/>
<BaseInputDate
id="date"
:label="t('Date')"
class="w-32"
/> />
</div> </div>
<div class="mb-4"> <div class="mb-4">
<p class="mb-3 text-lg">Dialogs</p> <p class="mb-3 text-lg">{{ t("Dialogs") }}</p>
<BaseButton <BaseButton
:label="t('Show dialog')" :label="t('Show dialog')"
type="black" type="black"
@ -169,13 +363,25 @@
@cancel-clicked="isDialogVisible = false" @cancel-clicked="isDialogVisible = false"
/> />
</div> </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> </template>
<script setup> <script setup>
import BaseButton from "../../components/basecomponents/BaseButton.vue" import BaseButton from "../../components/basecomponents/BaseButton.vue"
import { useI18n } from "vue-i18n" import { useI18n } from "vue-i18n"
import BaseMenu from "../../components/basecomponents/BaseMenu.vue" 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 BaseCheckbox from "../../components/basecomponents/BaseCheckbox.vue"
import BaseRadioButtons from "../../components/basecomponents/BaseRadioButtons.vue" import BaseRadioButtons from "../../components/basecomponents/BaseRadioButtons.vue"
import BaseDialogConfirmCancel from "../../components/basecomponents/BaseDialogConfirmCancel.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 BaseColorPicker from "../../components/basecomponents/BaseColorPicker.vue"
import { useTheme } from "../../composables/theme" import { useTheme } from "../../composables/theme"
import axios from "axios" 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 { t } = useI18n()
const { getColorTheme, getColors } = useTheme() 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() const themeTitle = ref()
let primaryColor = getColorTheme("--color-primary-base") let colorSuccess = getColorTheme("--color-success-base")
let primaryColorGradient = getColorTheme("--color-primary-gradient") let colorSuccessGradient = getColorTheme("--color-success-gradient")
let secondaryColor = getColorTheme("--color-secondary-base") let colorSuccessButtonText = getColorTheme("--color-success-button-text")
let secondaryColorGradient = getColorTheme("--color-secondary-gradient")
let tertiaryColor = getColorTheme("--color-tertiary-base") let colorInfo = getColorTheme("--color-info-base")
let tertiaryColorGradient = getColorTheme("--color-tertiary-gradient") let colorInfoGradient = getColorTheme("--color-info-gradient")
let successColor = getColorTheme("--color-success-base") let colorInfoButtonText = getColorTheme("--color-info-button-text")
let successColorGradient = getColorTheme("--color-success-gradient")
let dangerColor = getColorTheme("--color-danger-base") 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 () => { const saveColors = async () => {
let colors = getColors() let colors = getColors()
// TODO send colors to backend, then notify if was correct or incorrect try {
await axios.post("/api/color_themes", { await axios.post("/api/color_themes", {
title: themeTitle.value, title: themeTitle.value,variables: colors,
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 menu = ref("menu")
const menuItems = [{ label: t("Item 1") }, { label: t("Item 2") }, { label: t("Item 3") }] const menuItems = [{ label: t("Item 1") }, { label: t("Item 2") }, { label: t("Item 3") }]
const toggle = (event) => { const toggle = (event) => {
menu.value.toggle(event) menu.value.toggle(event)
} }
const dropdown = ref("")
const checkbox1 = ref(true) const checkbox1 = ref(true)
const checkbox2 = ref(false) const checkbox2 = ref(false)
@ -226,5 +606,11 @@ const radioValue = ref("value1")
const isDialogVisible = ref(false) 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> </script>

@ -39,6 +39,7 @@
"bootstrap-daterangepicker": "^3.1.0", "bootstrap-daterangepicker": "^3.1.0",
"bootstrap-select": "^1.13.18", "bootstrap-select": "^1.13.18",
"chart.js": "^3.9.1", "chart.js": "^3.9.1",
"colorjs.io": "^0.5.0",
"cropper": "^4.1.0", "cropper": "^4.1.0",
"datepair.js": "^0.4.17", "datepair.js": "^0.4.17",
"dotenv": "^16.3.1", "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 // from tailwind youtube channel https://youtu.be/MAtaT8BZEAo?t=1023
const colorWithOpacity = (variableName) => { const colorWithOpacity = (variableName) => {
return ({opacityValue}) => { return ({ opacityValue }) => {
if (opacityValue !== undefined) { 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}))` return `rgb(var(${variableName}))`
} }
@ -24,16 +26,20 @@ module.exports = {
gradient: colorWithOpacity("--color-primary-gradient"), gradient: colorWithOpacity("--color-primary-gradient"),
bgdisabled: "#fafafa", bgdisabled: "#fafafa",
borderdisabled: "#e4e9ed", borderdisabled: "#e4e9ed",
"button-text": colorWithOpacity("--color-primary-button-text"),
"button-alternative-text": colorWithOpacity("--color-primary-button-alternative-text"),
}, },
secondary: { secondary: {
DEFAULT: colorWithOpacity("--color-secondary-base"), DEFAULT: colorWithOpacity("--color-secondary-base"),
gradient: colorWithOpacity("--color-secondary-gradient"), gradient: colorWithOpacity("--color-secondary-gradient"),
bgdisabled: '#e4e9ed', bgdisabled: "#e4e9ed",
hover: "#d35e0f", hover: "#d35e0f",
"button-text": colorWithOpacity("--color-secondary-button-text"),
}, },
tertiary: { tertiary: {
DEFAULT: colorWithOpacity("--color-tertiary-base"), DEFAULT: colorWithOpacity("--color-tertiary-base"),
gradient: colorWithOpacity("--color-tertiary-gradient"), gradient: colorWithOpacity("--color-tertiary-gradient"),
"button-text": colorWithOpacity("--color-tertiary-button-text"),
}, },
gray: { gray: {
5: "rgba(250, 250, 250, 0.5)", 5: "rgba(250, 250, 250, 0.5)",
@ -53,20 +59,37 @@ module.exports = {
5: "#e06410", 5: "#e06410",
6: "#faf7f5", 6: "#faf7f5",
}, },
warning: "#f5ce01",
success: { success: {
DEFAULT: colorWithOpacity("--color-success-base"), DEFAULT: colorWithOpacity("--color-success-base"),
gradient: colorWithOpacity("--color-success-gradient"), 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"), error: colorWithOpacity("--color-danger-base"),
info: "#0d7bfd",
form: colorWithOpacity("--color-form-base"),
white: colors.white, white: colors.white,
black: colors.black, black: colors.black,
transparent: colors.transparent, transparent: colors.transparent,
current: colors.current, current: colors.current,
fontdisabled: '#a2a6b0', fontdisabled: "#a2a6b0",
}, },
extend: { extend: {
fontFamily: { fontFamily: {
@ -83,8 +106,5 @@ module.exports = {
corePlugins: { corePlugins: {
aspectRatio: true, aspectRatio: true,
}, },
plugins: [ plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography")],
require("@tailwindcss/forms"), }
require("@tailwindcss/typography"),
],
};

Loading…
Cancel
Save