diff --git a/package-lock.json b/package-lock.json
index 21de79387f..a5f7a09ed0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,7 +13,6 @@
"@github/relative-time-element": "4.4.0",
"@github/text-expander-element": "2.6.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
- "@melloware/coloris": "0.23.0",
"@primer/octicons": "19.9.0",
"add-asset-webpack-plugin": "2.0.1",
"ansi_up": "6.0.2",
@@ -54,6 +53,7 @@
"toastify-js": "1.12.0",
"tributejs": "5.1.3",
"uint8-to-base64": "0.2.0",
+ "vanilla-colorful": "0.7.2",
"vue": "3.4.21",
"vue-bar-graph": "2.0.0",
"vue-chartjs": "5.3.0",
@@ -1290,11 +1290,6 @@
"@mcaptcha/core-glue": "^0.1.0-alpha-5"
}
},
- "node_modules/@melloware/coloris": {
- "version": "0.23.0",
- "resolved": "https://registry.npmjs.org/@melloware/coloris/-/coloris-0.23.0.tgz",
- "integrity": "sha512-VGIjI9+IQwg6BHjIE10yl0K2ARYz5bsjn6BgFEs1y1ErPAQymgdoxwVcSVL4Ai5t9OVs8xaCB7JKHqFu2N96Ow=="
- },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -11853,6 +11848,11 @@
"builtins": "^1.0.3"
}
},
+ "node_modules/vanilla-colorful": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/vanilla-colorful/-/vanilla-colorful-0.7.2.tgz",
+ "integrity": "sha512-z2YZusTFC6KnLERx1cgoIRX2CjPRP0W75N+3CC6gbvdX5Ch47rZkEMGO2Xnf+IEmi3RiFLxS18gayMA27iU7Kg=="
+ },
"node_modules/vite": {
"version": "5.2.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.6.tgz",
diff --git a/package.json b/package.json
index beea0e5d86..004ac9e2bf 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,6 @@
"@github/relative-time-element": "4.4.0",
"@github/text-expander-element": "2.6.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
- "@melloware/coloris": "0.23.0",
"@primer/octicons": "19.9.0",
"add-asset-webpack-plugin": "2.0.1",
"ansi_up": "6.0.2",
@@ -53,6 +52,7 @@
"toastify-js": "1.12.0",
"tributejs": "5.1.3",
"uint8-to-base64": "0.2.0",
+ "vanilla-colorful": "0.7.2",
"vue": "3.4.21",
"vue-bar-graph": "2.0.0",
"vue-chartjs": "5.3.0",
diff --git a/web_src/css/features/colorpicker.css b/web_src/css/features/colorpicker.css
index 0c651cfeb3..b7436783df 100644
--- a/web_src/css/features/colorpicker.css
+++ b/web_src/css/features/colorpicker.css
@@ -1,10 +1,6 @@
-/* This is a stripped-down version of coloris's CSS tailored to our needs. It does only include
- opaqua colors, and if more features like opacity are needed, the CSS needs to be extended
- based on upstream: https://github.com/mdbassit/Coloris/blob/main/src/coloris.css. */
-
.js-color-picker-input {
display: flex;
- flex-wrap: wrap;
+ position: relative;
}
.js-color-picker-input input {
@@ -13,152 +9,39 @@
padding-left: 32px !important;
}
-.clr-picker {
- display: none;
- flex-wrap: wrap;
- position: absolute;
- width: 200px;
- z-index: 1002; /* above .ui.modal which has 1001 */
- border-radius: var(--border-radius);
- background-color: var(--color-menu);
- justify-content: flex-end;
- direction: ltr;
- box-shadow: 0 5px 20px var(--color-shadow);
- user-select: none;
-}
-
-.clr-picker.clr-open {
- display: flex;
-}
-
-.clr-gradient {
- position: relative;
- width: 100%;
- height: 100px;
- border-radius: 3px 3px 0 0;
- background: linear-gradient(rgba(0,0,0,0), #000), linear-gradient(90deg, #fff, currentcolor); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
- cursor: pointer;
-}
-
-.clr-marker {
- position: absolute;
- width: 12px;
- height: 12px;
- margin: -6px 0 0 -6px;
- border: 1px solid var(--color-white);
- border-radius: 50%;
- background-color: currentcolor;
- cursor: pointer;
-}
-
-.clr-picker input[type="range"]::-webkit-slider-runnable-track {
- width: 100%;
- height: 16px;
-}
-
-.clr-picker input[type="range"]::-webkit-slider-thumb {
- width: 16px;
- height: 16px;
- -webkit-appearance: none;
-}
-
-.clr-picker input[type="range"]::-moz-range-track {
- width: 100%;
- height: 16px;
- border: 0;
-}
-
-.clr-picker input[type="range"]::-moz-range-thumb {
- width: 16px;
- height: 16px;
- border: 0;
-}
-
-.clr-hue {
- background: linear-gradient(to right, #f00 0%, #ff0 16.66%, #0f0 33.33%, #0ff 50%, #00f 66.66%, #f0f 83.33%, #f00 100%); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
- position: relative;
- width: calc(100% - 40px);
- height: 10px;
- margin: 10px 20px;
- border-radius: 4px;
-}
-
-.clr-hue input[type="range"] {
- position: absolute;
- width: calc(100% + 32px);
- margin: 0;
- background-color: transparent;
- opacity: 0;
- cursor: pointer;
- appearance: none;
-}
-
-.clr-hue div {
- position: absolute;
- width: 16px;
- height: 16px;
- left: 0;
- top: 50%;
- transform: translate(-50%, -50%);
- border: 2px solid var(--color-white);
- border-radius: 50%;
- background-color: currentcolor;
- box-shadow: 0 0 1px var(--color-shadow);
- pointer-events: none;
-}
-
-.clr-field {
- flex: 1;
- position: relative;
- color: transparent;
-}
-
-.clr-field button {
+.js-color-picker-input .preview-square {
position: absolute;
aspect-ratio: 1;
height: 16px;
left: 10px;
top: 50%;
transform: translateY(-50%);
- margin: 0;
- padding: 0;
- border: 0;
- color: inherit;
- pointer-events: none;
border-radius: 2px;
background: repeating-linear-gradient(45deg, #aaa 25%, transparent 25%, transparent 75%, #aaa 75%, #aaa), repeating-linear-gradient(45deg, #aaa 25%, #fff 25%, #fff 75%, #aaa 75%, #aaa); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
background-position: 0 0, 4px 4px;
background-size: 8px 8px;
}
-.clr-field button::after {
+.js-color-picker-input .preview-square::after {
content: "";
- display: block;
position: absolute;
width: 100%;
height: 100%;
- left: 0;
- top: 0;
border-radius: inherit;
background-color: currentcolor;
}
-.clr-marker:focus {
- outline: none;
+hex-color-picker {
+ width: 180px;
+ height: 120px;
}
-.clr-keyboard-nav .clr-marker:focus,
-.clr-keyboard-nav .clr-hue input:focus + div,
-.clr-keyboard-nav .clr-alpha input:focus + div {
- outline: none;
- box-shadow: 0 0 2px 2px var(--color-white);
+hex-color-picker::part(hue-pointer),
+hex-color-picker::part(saturation-pointer) {
+ width: 22px;
+ height: 22px;
}
-.clr-picker .clr-preview,
-.clr-picker .clr-clear,
-.clr-picker .clr-swatches,
-.clr-picker .clr-format,
-.clr-picker .clr-alpha,
-.clr-picker .clr-color {
- display: none;
+hex-color-picker::part(hue) {
+ flex-basis: 16px;
}
diff --git a/web_src/css/modules/tippy.css b/web_src/css/modules/tippy.css
index 76d36b4293..6ac7c37d93 100644
--- a/web_src/css/modules/tippy.css
+++ b/web_src/css/modules/tippy.css
@@ -29,6 +29,17 @@
z-index: 1;
}
+/* bare theme, no styling at all, except box-shadow */
+.tippy-box[data-theme="bare"] {
+ border: none;
+ box-shadow: 0 6px 18px var(--color-shadow);
+}
+
+.tippy-box[data-theme="bare"] .tippy-content {
+ padding: 0;
+ background: transparent;
+}
+
/* tooltip theme for text tooltips */
.tippy-box[data-theme="tooltip"] {
diff --git a/web_src/js/features/colorpicker.js b/web_src/js/features/colorpicker.js
index f342598e66..6d00d908c9 100644
--- a/web_src/js/features/colorpicker.js
+++ b/web_src/js/features/colorpicker.js
@@ -1,31 +1,66 @@
-export async function initColorPickers(selector = '.js-color-picker-input input', opts = {}) {
- const inputEls = document.querySelectorAll(selector);
- if (!inputEls.length) return;
+import {createTippy} from '../modules/tippy.js';
- const [{coloris, init}] = await Promise.all([
- import(/* webpackChunkName: "colorpicker" */'@melloware/coloris'),
+export async function initColorPickers() {
+ const els = document.getElementsByClassName('js-color-picker-input');
+ if (!els.length) return;
+
+ await Promise.all([
+ import(/* webpackChunkName: "colorpicker" */'vanilla-colorful/hex-color-picker.js'),
import(/* webpackChunkName: "colorpicker" */'../../css/features/colorpicker.css'),
]);
- init();
- coloris({
- el: selector,
- alpha: false,
- focusInput: true,
- selectInput: false,
- ...opts,
- });
-
- for (const inputEl of inputEls) {
- const parent = inputEl.closest('.js-color-picker-input');
- // prevent tabbing on the color preview `button` inside the input
- parent.querySelector('button').tabIndex = -1;
- // init precolors
- for (const el of parent.querySelectorAll('.precolors .color')) {
- el.addEventListener('click', (e) => {
- inputEl.value = e.target.getAttribute('data-color-hex');
- inputEl.dispatchEvent(new Event('input', {bubbles: true}));
- });
- }
+ for (const el of els) {
+ initPicker(el);
+ }
+}
+
+function updateSquare(el, newValue) {
+ el.style.color = /#[0-9a-f]{6}/i.test(newValue) ? newValue : 'transparent';
+}
+
+function updatePicker(el, newValue) {
+ el.setAttribute('color', newValue);
+}
+
+function initPicker(el) {
+ const input = el.querySelector('input');
+
+ const square = document.createElement('div');
+ square.classList.add('preview-square');
+ updateSquare(square, input.value);
+ el.append(square);
+
+ const picker = document.createElement('hex-color-picker');
+ picker.addEventListener('color-changed', (e) => {
+ input.value = e.detail.value;
+ input.focus();
+ updateSquare(square, e.detail.value);
+ });
+
+ input.addEventListener('input', (e) => {
+ updateSquare(square, e.target.value);
+ updatePicker(picker, e.target.value);
+ });
+
+ createTippy(input, {
+ trigger: 'focus click',
+ theme: 'bare',
+ hideOnClick: true,
+ content: picker,
+ placement: 'bottom-start',
+ interactive: true,
+ onShow() {
+ updatePicker(picker, input.value);
+ },
+ });
+
+ // init precolors
+ for (const colorEl of el.querySelectorAll('.precolors .color')) {
+ colorEl.addEventListener('click', (e) => {
+ const newValue = e.target.getAttribute('data-color-hex');
+ input.value = newValue;
+ input.dispatchEvent(new Event('input', {bubbles: true}));
+ updateSquare(square, newValue);
+ });
}
}
diff --git a/web_src/js/modules/tippy.js b/web_src/js/modules/tippy.js
index 220c9e5512..83b28e5745 100644
--- a/web_src/js/modules/tippy.js
+++ b/web_src/js/modules/tippy.js
@@ -3,11 +3,12 @@ import {isDocumentFragmentOrElementNode} from '../utils/dom.js';
import {formatDatetime} from '../utils/time.js';
const visibleInstances = new Set();
+const arrowSvg = ``;
export function createTippy(target, opts = {}) {
// the callback functions should be destructured from opts,
// because we should use our own wrapper functions to handle them, do not let the user override them
- const {onHide, onShow, onDestroy, role, theme, ...other} = opts;
+ const {onHide, onShow, onDestroy, role, theme, arrow, ...other} = opts;
const instance = tippy(target, {
appendTo: document.body,
@@ -35,9 +36,9 @@ export function createTippy(target, opts = {}) {
visibleInstances.add(instance);
return onShow?.(instance);
},
- arrow: ``,
+ arrow: arrow || (theme === 'bare' ? false : arrowSvg),
role: role || 'menu', // HTML role attribute
- theme: theme || role || 'menu', // CSS theme, either "tooltip", "menu" or "box-with-header"
+ theme: theme || role || 'menu', // CSS theme, either "tooltip", "menu", "box-with-header" or "bare"
plugins: [followCursor],
...other,
});