Display: Add feature to customize colors of chamilo - refs #4745

* Minor: remove unused styles

* Use css variables to set colors in tailwind

* Minor: add missing icon

* Add BaseColorPicker component

* Adapt base components to use colors from tailwindcss

* Create composable to retrieve css variables

* Add component and route to modify chamilo colors

---------

Co-authored-by: Yannick Warnier <ywarnier@users.noreply.github.com>
Co-authored-by: Angel Fernando Quiroz Campos <angelfqc.18@gmail.com>
pull/5070/head^2^2
Daniel 2 years ago committed by GitHub
parent df76d92d1d
commit 7d6ddd0f28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 201
      assets/css/app.scss
  2. 18
      assets/vue/components/admin/AdminBlock.vue
  3. 26
      assets/vue/components/basecomponents/BaseButton.vue
  4. 12
      assets/vue/components/basecomponents/BaseCheckbox.vue
  5. 27
      assets/vue/components/basecomponents/BaseColorPicker.vue
  6. 8
      assets/vue/components/basecomponents/BaseInputText.vue
  7. 55
      assets/vue/composables/theme.js
  8. 9
      assets/vue/router/admin.js
  9. 136
      assets/vue/views/admin/AdminConfigureColors.vue
  10. 17
      assets/vue/views/admin/AdminIndex.vue
  11. 1
      src/CoreBundle/Controller/IndexController.php
  12. 33
      tailwind.config.js

@ -2,18 +2,31 @@
@tailwind components;
@tailwind utilities;
.btn-blue {
@apply bg-primary text-white;
}
.btn-blue:hover {
}
@import '~cropper/dist/cropper.css';
//@import "~flag-icons/sass/flag-icons.scss";
@import "~select2/dist/css/select2.css";
@import "~bootstrap-daterangepicker/daterangepicker.css";
//@import '~jquery-ui/themes/base/all.css';
// 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-secondary-base: 243, 126, 47;
--color-secondary-gradient: 224, 100, 16;
--color-tertiary-base: 0, 0, 0;
--color-tertiary-gradient: 51, 51, 51;
--color-success-base: 119, 170, 12;
--color-success-gradient: 84, 119, 8;
--color-danger-base: 223, 59, 59;
}
}
@layer utilities {
.custom-collapse {
display: none;
@ -242,9 +255,6 @@
text-decoration: underline;
}
/* ****************************************************
CSS SKILL
**************************************************** */
#skillList .header-title{
padding: 8px;
border-bottom: 2px solid #ddd;
@ -502,179 +512,6 @@ table#skill_holder {
color: #0a0a0a;
}
/* ****************************************************
END SKILL
**************************************************** */
/* Grid System */
#legacy_content .section-content .row h3 {
width: 100%;
display: block;
margin: 0;
}
#legacy_content .row {
display: flex;
flex-wrap: wrap;
margin: 15px -15px;
width: 100%;
}
#legacy_content .col-md-2 {
position: relative;
width: 100%;
padding-right: 15px;
padding-left: 15px;
flex: 0 0 16.666667%;
max-width: 16.666667%;
}
#legacy_content .col-md-10 {
position: relative;
width: 100%;
padding-right: 15px;
padding-left: 15px;
flex: 0 0 83.333333%;
max-width: 83.333333%;
}
#legacy_content .col-md-6 {
position: relative;
width: 100%;
padding-right: 15px;
padding-left: 15px;
flex: 0 0 50%;
max-width: 50%;
}
#legacy_content .col-md-4 {
position: relative;
width: 100%;
padding-right: 15px;
padding-left: 15px;
flex: 0 0 33.333333%;
max-width: 33.333333%;
}
.pull-right {
float: right;
}
/* Buttons */
.btn {
display: inline-block;
font-weight: 400;
text-align: center;
white-space: nowrap;
vertical-align: middle;
user-select: none;
border: 1px solid transparent;
padding: 0.375rem 0.75rem;
font-size: 1rem;
line-height: 1.5;
border-radius: 0.25rem;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out,
border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.btn--plain {
background-color: transparent;
color: #007bff;
}
.btn--primary {
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
.btn--primary:hover {
color: #fff;
background-color: #0069d9;
border-color: #0062cc;
}
/* Images */
.social-groups-image {
max-width: 100%;
height: auto;
}
/* Style for the tabs */
.nav {
list-style: none;
padding: 0;
margin: 0;
}
/* Style for the navigation tabs */
.nav-tabs {
border-bottom: 2px solid #ddd;
}
.nav-item {
display: inline-block;
margin-bottom: -1px;
}
.nav-link {
text-decoration: none;
padding: 10px 20px;
color: #333;
}
.nav-link:hover {
background-color: #f5f5f5;
}
.panel {
background-color: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
padding: 20px;
}
.panel-title {
color: #333;
font-size: 24px;
}
.panel-text {
color: #666;
font-size: 16px;
}
.panel-header {
background-color: #007bff;
color: #fff;
padding: 10px;
}
.panel-body {
background-color: #fff;
padding: 10px;
}
/* Style for the tab content */
.tab-content {
padding: 15px;
border: 1px solid #ddd;
border-top: none;
border-radius: 0 0 .25rem .25rem;
}
/* Custom style for a button with class btn--plain */
.btn.btn--plain {
background-color: transparent;
border: 1px solid #333;
color: #333;
}
.btn.btn--plain:hover {
background-color: #333;
color: #fff;
}
//@import 'primevue-md-light-indigo/theme.css';
//@import '~primevue/resources/primevue.min.css';
//@import '~primeflex/primeflex.css';

@ -21,6 +21,24 @@
</form>
</div>
<div class="p-menu p-component p-ripple-disabled">
<ul class="p-menu-list p-reset" role="menu">
<li
v-for="(item, index) in visibleItems"
:key="index"
:aria-label="t(item.label)"
:class="item.className"
class="p-menuitem"
role="menuitem"
>
<div class="p-menuitem-content">
<a :href="item.url" class="p-menuitem-link">
<span class="p-menuitem-text" v-text="item.label" />
</a>
</div>
</li>
<slot></slot>
</ul>
<div class="p-menu p-component p-ripple-disabled">
<ul class="p-menu-list p-reset" role="menu">
<li

@ -81,7 +81,7 @@ const primePlainProperty = computed(() => {
const buttonClass = computed(() => {
if (props.onlyIcon) {
return "p-3"
return "p-3 text-tertiary hover:bg-tertiary-gradient/30"
}
let result = ""
switch (props.size) {
@ -92,8 +92,28 @@ const buttonClass = computed(() => {
result += "py-2 px-3.5 "
}
return result
})
let commonDisabled =
"disabled:bg-primary-bgdisabled disabled:border disabled:border-primary-borderdisabled disabled:text-fontdisabled";
switch (props.type) {
case "primary":
result += `border-primary hover:bg-primary text-primary hover:text-white ${commonDisabled} `;
break;
case "secondary":
result +=
"bg-secondary text-white hover:bg-secondary-gradient disabled:bg-secondary-bgdisabled disabled:text-fontdisabled";
break;
case "success":
result += `bg-success hover:bg-success-gradient ${commonDisabled} `;
break;
case "danger":
result += `border-error hover:bg-error text-error hover:text-white ${commonDisabled} `;
break;
case "black":
result += "bg-white text-tertiary border-tertiary hover:bg-tertiary-gradient hover:text-white";
break;
}
return result;
});
// https://primevue.org/button/#outlined
const primeOutlinedProperty = computed(() => {

@ -6,13 +6,23 @@
:name="name"
:input-id="id"
@update:model-value="$emit('update:modelValue', $event)"
/>
>
<template #icon="{checked}">
<BaseIcon
v-if="checked"
icon="confirm"
size="small"
class="bg-primary text-white rounded"
/>
</template>
</Checkbox>
<label :for="id" class="ml-2 cursor-pointer">{{ label }}</label>
</div>
</template>
<script setup>
import Checkbox from "primevue/checkbox";
import BaseIcon from "./BaseIcon.vue";
defineProps({
id: {

@ -0,0 +1,27 @@
<template>
<div class="flex flex-col justify-center">
<span v-if="label">{{ label }}</span>
<ColorPicker
format="rgb"
:model-value="modelValue"
@update:model-value="emit('update:modelValue', $event)"
/>
</div>
</template>
<script setup>
import ColorPicker from 'primevue/colorpicker'
defineProps({
modelValue: {
type: Object,
required: true,
},
label: {
type: String,
default: '',
}
})
const emit = defineEmits(["update:modelValue"])
</script>

@ -5,11 +5,17 @@
:id="id"
:model-value="modelValue"
:class="{ 'p-invalid': isInvalid }"
class="border-primary-gradient hover:border-primary focus:ring-primary"
:aria-label="label"
type="text"
@update:model-value="$emit('update:modelValue', $event)"
/>
<label v-t="label" :class="{ 'p-error': isInvalid }" :for="id" />
<label
v-t="label"
:class="{ 'p-error': isInvalid }"
:for="id"
class="text-primary/40"
/>
</div>
<slot name="errors">
<small v-if="isInvalid" v-t="errorText" class="p-error" />

@ -0,0 +1,55 @@
import {onMounted, ref, watch} from "vue";
export const useTheme = () => {
let colors = {}
onMounted(() => {
// getCssVariableValue return empty if called too early, refresh colors when html is mounted
// to ensure all values are set correctly
for (const [key, value] of Object.entries(colors)) {
value.value = getCssVariableValue(key)
}
})
const getColorTheme = (variableName) => {
if (Object.hasOwn(colors, variableName)) {
return colors[variableName]
}
const colorRef = ref(getCssVariableValue(variableName))
watch(colorRef, (newColor) => {
setCssVariableValue(variableName, newColor)
})
colors[variableName] = colorRef
return colorRef
}
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 setCssVariableValue = (variableName, color) => {
document.documentElement.style
.setProperty(variableName, `${color.r}, ${color.g}, ${color.b}`)
}
const getColors = () => {
let colorsPlainObject = {}
for (const [key, value] of Object.entries(colors)) {
colorsPlainObject[key] = `${value.value.r} ${value.value.g} ${value.value.b}`
}
return colorsPlainObject
}
return {
getColorTheme,
getColors,
}
}

@ -8,7 +8,14 @@ export default {
path: '',
name: 'AdminIndex',
meta: { requiresAdmin: true, requiresSessionAdmin: true },
component: () => import('../views/admin/AdminIndex.vue')
component: () => import('../views/admin/AdminIndex.vue'),
},
{
name: 'AdminConfigurationColors',
path: 'configuration/colors',
meta: { requiresAdmin: true, requiresSessionAdmin: true },
component: () => import('../views/admin/AdminConfigureColors.vue'),
}
],
};

@ -0,0 +1,136 @@
<template class="personal-theme">
<h4 class="mb-4">{{ t('Configure chamilo colors') }}</h4>
<div class="grid grid-cols-2 gap-2 mb-8">
<BaseColorPicker v-model="primaryColor" :label="t('Pick primary color')"/>
<BaseColorPicker v-model="primaryColorGradient" :label="t('Pick primary color gradient')"/>
<BaseColorPicker v-model="secondaryColor" :label="t('Pick secondary color')"/>
<BaseColorPicker v-model="secondaryColorGradient" :label="t('Pick secondary color gradient')"/>
<BaseColorPicker v-model="tertiaryColor" :label="t('Pick tertiary color')"/>
<BaseColorPicker v-model="tertiaryColorGradient" :label="t('Pick tertiary color gradient')"/>
<BaseColorPicker v-model="successColor" :label="t('Pick success color')"/>
<BaseColorPicker v-model="successColorGradient" :label="t('Pick success color gradient')"/>
<BaseColorPicker v-model="dangerColor" :label="t('Pick danger color')"/>
</div>
<div class="flex flex-wrap mb-4">
<BaseButton type="primary" icon="send" :label="t('Save')" @click="saveColors"/>
</div>
<hr>
<h5 class="mb-4">{{ t('You can see examples of how chamilo will look here') }}</h5>
<div class="mb-4">
<p class="mb-3 text-lg">{{ t('Buttons') }}</p>
<div class="flex flex-row flex-wrap">
<BaseButton class="mr-2 mb-2" :label="t('Button')" type="primary" icon="eye-on"/>
<BaseButton class="mr-2 mb-2" :label="t('Disabled')" type="primary" icon="eye-on" disabled/>
<BaseButton class="mr-2 mb-2" :label="t('Secondary')" type="secondary" icon="eye-on"/>
<BaseButton class="mr-2 mb-2" :label="t('Tertiary')" type="black" icon="eye-on"/>
<BaseButton class="mr-2 mb-2" type="primary" icon="cog" only-icon/>
<BaseButton class="mr-2 mb-2" :label="t('Success')" type="success" icon="send"/>
<BaseButton class="mr-2 mb-2" :label="t('Danger')" type="danger" icon="delete"/>
</div>
</div>
<div class="mb-4">
<p class="mb-3 text-lg">{{ t('Menu on button pressed') }}</p>
<BaseButton
class="mr-2 mb-2"
type="primary"
icon="cog"
popup-identifier="menu"
only-icon
@click="toggle"
/>
<BaseMenu id="menu" ref="menu" :model="menuItems"></BaseMenu>
</div>
<div class="mb-4">
<p class="mb-3 text-lg">{{ t('Checkbox and radio buttons') }}</p>
<BaseCheckbox id="check1" v-model="checkbox1" :label="t('Checkbox 1')" name="checkbox1"/>
<BaseCheckbox id="check2" v-model="checkbox2" :label="t('Checkbox 2')" name="checkbox2"/>
<div class="mb-2"></div>
<BaseRadioButtons
v-model="radioValue"
:options="radioButtons"
:initial-value="radioValue"
name="radio"
/>
</div>
<div class="mb-4">
<p class="mb-3 text-lg">Forms</p>
<BaseInputText
v-model="inputText"
:label="t('This is a text example')"
/>
</div>
<div class="mb-4">
<p class="mb-3 text-lg">Dialogs</p>
<BaseButton :label="t('Show dialog')" type="black" icon="eye-on" @click="isDialogVisible = true"/>
<BaseDialogConfirmCancel
:title="t('Dialog example')"
:is-visible="isDialogVisible"
@confirm-clicked="isDialogVisible = false"
@cancel-clicked="isDialogVisible = false"
/>
</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 BaseCheckbox from "../../components/basecomponents/BaseCheckbox.vue"
import BaseRadioButtons from "../../components/basecomponents/BaseRadioButtons.vue"
import BaseDialogConfirmCancel from "../../components/basecomponents/BaseDialogConfirmCancel.vue"
import BaseInputText from "../../components/basecomponents/BaseInputText.vue"
import BaseColorPicker from "../../components/basecomponents/BaseColorPicker.vue"
import {useTheme} from "../../composables/theme"
const {t} = useI18n()
const {getColorTheme, getColors} = useTheme()
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')
const saveColors = () => {
let colors = getColors()
// TODO send colors to backend, then notify if was correct or incorrect
console.log(colors)
}
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 checkbox1 = ref(true)
const checkbox2 = ref(false)
const radioButtons = [
{label: t('Value 1'), value: 'value1'},
{label: t('Value 2'), value: 'value2'},
{label: t('Value 3'), value: 'value3'},
]
const radioValue = ref('value1')
const isDialogVisible = ref(false)
const inputText = ref('')
</script>

@ -150,7 +150,19 @@
:title="t('Platform management')"
class="block-admin-platform"
icon="admin-settings"
/>
>
<li
:aria-label="t('Colors')"
class="p-menuitem"
role="menuitem"
>
<div class="p-menuitem-content">
<router-link class="p-menuitem-link" :to="{name: 'AdminConfigurationColors'}">
<span class="p-menuitem-text" v-text="t('Colors')" />
</router-link>
</div>
</li>
</AdminBlock>
<AdminBlock
v-if="blockChamilo"
@ -174,7 +186,8 @@ import Skeleton from "primevue/skeleton"
import AdminBlock from "../../components/admin/AdminBlock"
import axios from "axios"
import { usePlatformConfig } from "../../store/platformConfig"
import { usePlatformConfig } from "../../store/platformConfig";
import AdminConfigureColors from "./AdminConfigureColors.vue";
const { t } = useI18n()

@ -30,6 +30,7 @@ class IndexController extends BaseController
*/
#[Route('/sessions', name: 'sessions')]
#[Route('/sessions/{extra}', name: 'sessions_options')]
#[Route('/admin/configuration/colors', name: 'configuration_colors')]
public function indexAction(): Response
{
return $this->render('@ChamiloCore/Index/vue.html.twig');

@ -1,5 +1,15 @@
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})`
}
return `rgb(var(${variableName}))`
}
}
module.exports = {
important: true,
content: [
@ -10,17 +20,20 @@ module.exports = {
theme: {
colors: {
primary: {
DEFAULT: "#2e75a3",
gradient: "#9cc2da",
bgdisabled: '#fafafa',
borderdisabled: '#e4e9eD',
DEFAULT: colorWithOpacity("--color-primary-base"),
gradient: colorWithOpacity("--color-primary-gradient"),
bgdisabled: "#fafafa",
borderdisabled: "#e4e9ed",
},
secondary: {
DEFAULT: "#f37e2f",
gradient: "#e06410",
hover: "#d35e0f",
DEFAULT: colorWithOpacity("--color-secondary-base"),
gradient: colorWithOpacity("--color-secondary-gradient"),
bgdisabled: '#e4e9ed',
},
tertiary: {
DEFAULT: colorWithOpacity("--color-tertiary-base"),
gradient: colorWithOpacity("--color-tertiary-gradient"),
},
gray: {
5: "#fcfcfc",
10: "#fafafa",
@ -41,10 +54,10 @@ module.exports = {
},
warning: "#f5ce01",
success: {
DEFAULT: "#77aa0c",
gradient: "#547708",
DEFAULT: colorWithOpacity("--color-success-base"),
gradient: colorWithOpacity("--color-success-gradient"),
},
error: "#df3b3b",
error: colorWithOpacity("--color-danger-base"),
info: "#0d7bfd",
white: colors.white,

Loading…
Cancel
Save