diff --git a/nodes/config/locales/en-US/ui_theme.json b/nodes/config/locales/en-US/ui_theme.json index 3d9f68372..334b8c0f9 100644 --- a/nodes/config/locales/en-US/ui_theme.json +++ b/nodes/config/locales/en-US/ui_theme.json @@ -3,6 +3,8 @@ "label": { "themeName": "Theme Name", "colors": "Colors", + "colorsLight": "Light", + "colorsDark": "Dark", "dashboard": "Dashboard", "header": "Header", "primary": "Primary", @@ -18,7 +20,8 @@ "default": "Default (48px)", "comfortable": "Comfortable (38px)", "compact": "Compact (32px)", - "density": "Row Height" + "density": "Row Height", + "preview": "Preview" } } } \ No newline at end of file diff --git a/nodes/config/ui_base.html b/nodes/config/ui_base.html index f29bdeaff..4811fe620 100644 --- a/nodes/config/ui_base.html +++ b/nodes/config/ui_base.html @@ -1957,14 +1957,47 @@ $('', { class: 'nrdb2-sb-title' }).text(theme.name || theme.id).appendTo(titleRow) $('', { class: 'nrdb2-sb-info' }).text(theme.users.length + ' ' + (theme.users.length > 1 ? c_('layout.pages') : c_('layout.page'))).appendTo(titleRow) - const palette = $('
', { class: 'nrdb2-sb-palette' }).appendTo(titleRow) + // backward compatibility + if (!hasProperty(theme.colors, 'light') && hasProperty(theme.colors, 'surface')) { + // Set default values for light theme + const updatedColors = { + light: { + surface: theme.colors.surface, + primary: theme.colors.primary, + bgPage: theme.colors.bgPage, + groupBg: theme.colors.groupBg, + groupOutline: theme.colors.groupOutline + }, + + // Set default values for dark theme + dark: { + surface: '#111111', + primary: theme.colors.primary, + bgPage: '#222222', + groupBg: '#333333', + groupOutline: '#cccccc' + } + } + + theme.colors = updatedColors + } + + const paletteLight = $('
', { class: 'nrdb2-sb-palette' }).appendTo(titleRow) const colors = theme.colors - palette.append($('
', { class: 'nrdb2-sb-palette-color', style: `background-color: ${colors.surface}` })) - palette.append($('
', { class: 'nrdb2-sb-palette-color', style: `background-color: ${colors.primary}` })) - palette.append($('
', { class: 'nrdb2-sb-palette-color', style: `background-color: ${colors.bgPage}` })) - palette.append($('
', { class: 'nrdb2-sb-palette-color', style: `background-color: ${colors.groupBg}` })) - palette.append($('
', { class: 'nrdb2-sb-palette-color', style: `background-color: ${colors.groupOutline}` })) + paletteLight.append($('
', { class: 'nrdb2-sb-palette-color', style: `background-color: ${colors.light.surface}` })) + paletteLight.append($('
', { class: 'nrdb2-sb-palette-color', style: `background-color: ${colors.light.primary}` })) + paletteLight.append($('
', { class: 'nrdb2-sb-palette-color', style: `background-color: ${colors.light.bgPage}` })) + paletteLight.append($('
', { class: 'nrdb2-sb-palette-color', style: `background-color: ${colors.light.groupBg}` })) + paletteLight.append($('
', { class: 'nrdb2-sb-palette-color', style: `background-color: ${colors.light.groupOutline}` })) + + const paletteDark = $('
', { class: 'nrdb2-sb-palette' }).appendTo(titleRow) + + paletteDark.append($('
', { class: 'nrdb2-sb-palette-color', style: `background-color: ${colors.dark.surface}` })) + paletteDark.append($('
', { class: 'nrdb2-sb-palette-color', style: `background-color: ${colors.dark.primary}` })) + paletteDark.append($('
', { class: 'nrdb2-sb-palette-color', style: `background-color: ${colors.dark.bgPage}` })) + paletteDark.append($('
', { class: 'nrdb2-sb-palette-color', style: `background-color: ${colors.dark.groupBg}` })) + paletteDark.append($('
', { class: 'nrdb2-sb-palette-color', style: `background-color: ${colors.dark.groupOutline}` })) // theme - actions const actions = $('
', { class: 'nrdb2-sb-list-header-actions' }).appendTo(titleRow) diff --git a/nodes/config/ui_theme.html b/nodes/config/ui_theme.html index f7a38654a..79dbd16ff 100644 --- a/nodes/config/ui_theme.html +++ b/nodes/config/ui_theme.html @@ -6,16 +6,30 @@ value: RED._('@flowfuse/node-red-dashboard/ui-theme:ui-theme.label.themeName'), required: true }, + // colors colors: { value: { - surface: '#ffffff', - primary: '#0094CE', - bgPage: '#eeeeee', - groupBg: '#ffffff', - groupOutline: '#cccccc' + // light mode + light: { + surface: '#ffffff', + primary: '#0094CE', + bgPage: '#eeeeee', + groupBg: '#ffffff', + groupOutline: '#cccccc' + }, + + // dark mode + dark: { + surface: '#111111', + primary: '#0094CE', + bgPage: '#222222', + groupBg: '#333333', + groupOutline: '#cccccc' + } } }, + // sizes sizes: { value: { @@ -33,15 +47,44 @@ } if (!this.colors) { - this.colors = {} - // set default values - this.colors.surface = '#ffffff' - this.colors.primary = '#0094CE' - // pages - this.colors.bgPage = '#eeeeee' - // groups - this.colors.groupBg = '#ffffff' - this.colors.groupOutline = '#cccccc' + this.colors = { + light: { + surface: '#ffffff', + primary: '#0094CE', + bgPage: '#eeeeee', + groupBg: '#ffffff', + groupOutline: '#cccccc' + }, + dark: { + surface: '#111111', + primary: '#0094CE', + bgPage: '#222222', + groupBg: '#333333', + groupOutline: '#cccccc' + } + } + } else if (!hasProperty(this.colors, 'light') && hasProperty(this.colors, 'surface')) { + // Set default values for light theme + const updatedColors = { + light: { + surface: this.colors.surface, + primary: this.colors.primary, + bgPage: this.colors.bgPage, + groupBg: this.colors.groupBg, + groupOutline: this.colors.groupOutline + }, + + // Set default values for dark theme + dark: { + surface: '#111111', + primary: this.colors.primary, + bgPage: '#222222', + groupBg: '#333333', + groupOutline: '#cccccc' + } + } + + this.colors = updatedColors } if (!this.sizes) { @@ -65,11 +108,12 @@ } // loop over keys in this.colors - Object.keys(this.colors).forEach((color) => { - // get the value of the key - const value = this.colors[color] - // set the value of the input - $('#node-config-input-' + color).val(value) + Object.keys(this.colors).forEach((theme) => { + const colors = this.colors[theme] + Object.keys(colors).forEach((color) => { + const value = colors[color] + $('#node-config-input-' + theme + '-' + color).val(value) + }) }) // loop over keys in this.sizes @@ -81,7 +125,118 @@ // set the value of the input $('#node-config-input-' + size).val(value) }) + + function hexToRgb (hex) { + const bigint = parseInt(hex.replace('#', ''), 16) + const r = (bigint >> 16) & 255 + const g = (bigint >> 8) & 255 + const b = bigint & 255 + return [r, g, b] + } + // get the contrast of background color for text color + function getContrast (bg) { + const bgRgb = hexToRgb(bg) + + // http://www.w3.org/TR/AERT#color-contrast + const brightness = Math.round(((parseInt(bgRgb[0]) * 299) + + (parseInt(bgRgb[1]) * 587) + + (parseInt(bgRgb[2]) * 114)) / 1000) + + const textColor = (brightness > 125) ? '#000000' : '#ffffff' + return textColor + } + + let activeTheme = 'light' + + function setElementColor (colorPicker) { + const colorPickerId = colorPicker.attr('id') + const selectedColor = colorPicker.val() + const contrastColor = getContrast(selectedColor) + if (activeTheme === 'light') { + switch (colorPickerId) { + case 'node-config-input-light-surface': + $('#header').css({ + 'background-color': selectedColor, + color: contrastColor + }) + break + case 'node-config-input-light-primary': + $('.primary-btn').css({ + 'background-color': selectedColor, + color: contrastColor + }) + break + case 'node-config-input-light-bgPage': + $('#bgPage').css({ + 'background-color': selectedColor, + color: contrastColor + }) + break + case 'node-config-input-light-groupBg': + $('.group').each(function () { + $(this).css({ + 'background-color': selectedColor, + color: contrastColor + }) + if (!$(this).css('border-color')) { + $(this).css('border-color', 'initial') // Ensures border color is not overwritten if not set + } + }) + break + case 'node-config-input-light-groupOutline': + $('.group').each(function () { + $(this).css('border-color', selectedColor) + if (!$(this).css('background-color')) { + $(this).css('background-color', 'initial') // Ensures background color is not overwritten if not set + } + }) + break + } + } else if (activeTheme === 'dark') { + switch (colorPickerId) { + case 'node-config-input-dark-surface': + $('#header').css({ + 'background-color': selectedColor, + color: contrastColor + }) + break + case 'node-config-input-dark-primary': + $('.primary-btn').css({ + 'background-color': selectedColor, + color: contrastColor + }) + break + case 'node-config-input-dark-bgPage': + $('#bgPage').css({ + 'background-color': selectedColor, + color: contrastColor + }) + break + case 'node-config-input-dark-groupBg': + $('.group').each(function () { + $(this).css({ + 'background-color': selectedColor, + color: contrastColor + }) + if (!$(this).css('border-color')) { + $(this).css('border-color', 'initial') // Ensures border color is not overwritten if not set + } + }) + break + case 'node-config-input-dark-groupOutline': + $('.group').each(function () { + $(this).css('border-color', selectedColor) + if (!$(this).css('background-color')) { + $(this).css('background-color', 'initial') // Ensures background color is not overwritten if not set + } + }) + break + } + } + } + + // update label bg to match the colour input values // update label b/g to match the colour input values // this provides nicer styling than the default browser styling const colorWrappers = $('.color-picker-wrapper') @@ -90,15 +245,48 @@ const colorPicker = wrapper.children("input[type='color']").eq(0) colorPicker.on('change', () => { wrapper.css('background-color', colorPicker.val()) + setElementColor(colorPicker) }) wrapper.css('background-color', colorPicker.val()) + setElementColor(colorPicker) + }) + + const that = this + const tabs = RED.tabs.create({ + id: 'color-tabs', + onchange: function (tab) { + $('#color-tabs-content').children().hide() + $('#' + tab.id).show() + activeTheme = tab.id === 'color-tab-light' ? 'light' : 'dark' + // Update colors based on the active theme + colorWrappers.each(function (i) { + const wrapper = $(this) + const colorPicker = wrapper.children("input[type='color']").eq(0) + setElementColor(colorPicker) + }) + } + }) + tabs.addTab({ + id: 'color-tab-light', + iconClass: 'fa fa-sun-o', + label: that._('ui-theme.label.colorsLight') }) + + tabs.addTab({ + id: 'color-tab-dark', + iconClass: 'fa fa-moon-o', + label: that._('ui-theme.label.colorsDark') + }) + + tabs.activateTab('color-tab-light') }, oneditsave: function () { // update our config values from the input values - Object.keys(this.colors).forEach((color) => { - // set the value of the input - this.colors[color] = $('#node-config-input-' + color).val() + Object.keys(this.colors).forEach((theme) => { + const colors = this.colors[theme] + Object.keys(colors).forEach((color) => { + this.colors[theme][color] = $('#node-config-input-' + theme + '-' + color).val() + }) }) // update our config values from the input values @@ -124,49 +312,124 @@

-
-

-
-
-
- - -
-
- - -
-
-
-

+ + +
+
    -
    - - -
    -
    -

    -
    -
    -
    - - +
    + -
    - - + +
    +

    @@ -192,6 +455,31 @@

    +

    +

    +

    *Only shows theme colors

    + +
    + +
    +
    +

    Group 1

    + +
    +
    +

    Group 2

    + +
    +
    +

    Group 3

    + +
    +
    +
    + \ No newline at end of file diff --git a/nodes/config/ui_theme.js b/nodes/config/ui_theme.js index daaefbba1..e96c5021c 100644 --- a/nodes/config/ui_theme.js +++ b/nodes/config/ui_theme.js @@ -32,7 +32,33 @@ module.exports = function (RED) { sizes.widgetGap = '12px' } - node.colors = { ...rest.colors } + const colors = { ...rest.colors } + node.colors = colors + + if (!hasProperty(colors, 'light') && hasProperty(colors, 'surface')) { + // Set default values for light theme + const updatedColors = { + light: { + surface: colors.surface, + primary: colors.primary, + bgPage: colors.bgPage, + groupBg: colors.groupBg, + groupOutline: colors.groupOutline + }, + + // Set default values for dark theme + dark: { + surface: '#111111', + primary: colors.primary, + bgPage: '#222222', + groupBg: '#333333', + groupOutline: '#cccccc' + } + } + + node.colors = updatedColors + } + node.sizes = sizes let uiBase = null diff --git a/test/nodes/fixtures/ui-theme.json b/test/nodes/fixtures/ui-theme.json index 719d82568..55d2d33a9 100644 --- a/test/nodes/fixtures/ui-theme.json +++ b/test/nodes/fixtures/ui-theme.json @@ -1,12 +1,21 @@ { - "id": "config-ui-theme", - "type": "ui-theme", - "name": "Theme 1", - "colors": { - "surface": "#ffffff", - "primary": "#0094ce", - "bgPage": "#eeeeee", - "groupBg": "#ffffff", - "groupOutline": "#cccccc" + "id": "config-ui-theme", + "type": "ui-theme", + "name": "Theme 1", + "colors": { + "light": { + "surface": "#ffffff", + "primary": "#0094CE", + "bgPage": "#eeeeee", + "groupBg": "#ffffff", + "groupOutline": "#cccccc" + }, + "dark": { + "surface": "#111111", + "primary": "#0094CE", + "bgPage": "#222222", + "groupBg": "#333333", + "groupOutline": "#cccccc" } + } } diff --git a/ui/src/layouts/Baseline.vue b/ui/src/layouts/Baseline.vue index 860eb01b9..fe5ccdf8b 100644 --- a/ui/src/layouts/Baseline.vue +++ b/ui/src/layouts/Baseline.vue @@ -54,6 +54,18 @@ + + + {{ darkMode ? 'mdi-weather-night' : 'mdi-weather-sunny' }} + +
    @@ -77,14 +89,34 @@ - + +