Skip to content

Instantly share code, notes, and snippets.

@nickyonge
Created February 12, 2026 04:33
Show Gist options
  • Select an option

  • Save nickyonge/afe8c5260b57521ace664f41ffe21516 to your computer and use it in GitHub Desktop.

Select an option

Save nickyonge/afe8c5260b57521ace664f41ffe21516 to your computer and use it in GitHub Desktop.
Decap CMS Collection Title Color Display
/** DECAP COLLECTION SUMMARY COLOR DISPLAY
*
* Replaces a color string in your Decap CMS Collection's title text
* with a visual indicator of that color, including a color swatch,
* text output, and/or updating the collection item's background color.
*
*
* MARK: Setup
* Using marks for VSCode minimap navigation
*
* INSTALLATION
*
* 1) Load the script into your HTML
* Drop/load this script the directory with Decap's index.html, and add
<script src="decapColorDisplay.js"></script>
* to index.html's body
*
* 2) Check your config.yml Collections settings
* For this to work, the title of your Collection must have a
* CSS-recognizable color in it. The easiest way to do this is
* to have a "color" widget in your collection's fields, preferably
* required and with a default value, and to specify it in your
* collection's "summary". Eg, an example config.yml:
config.yml
collections:
- name: 'demo'
label: 'Demo'
create: true
folder: 'content/demo/'
summary: "{{title}} {{color}}"
fields:
- { label: "Title", name: "title", widget: "string" }
- { label: "Color", name: 'color', widget: 'color', default: '#beeeef'}
* Ultimately though, so long as a CSS-valid color appears anywhere
* in the text, it can be converted to a swatch. This is why it's useful
* to set "preferColorOrder" in config to -1, keep your "color" field
* required, give it a default value, and ensure that "{{color}}" is at
* the END of your "summary", so that there will always be a color to catch.
*
* In the above example, by defining summary as "{{title}} {{color}}", the
* color is always rendered last. Then, with "preferOrderColor" as -1, whatever
* value is defined in the color widget will ALWAYS be the detected color.
*
* Also note: if you set "allowInput:true" in your color widget, users can
* type their own manual text into the color field. In this case, you should
* set "validColors" in config to "BASIC", "CSS", or "FULL", so that colors
* like "red" or "rebeccapurple" can be validly identified.
*
* Collections docs: https://decapcms.org/docs/configuration-options/#collections
* Color widget: https://decapcms.org/docs/widgets/#Color
*
* 3) Adjust the configuration settings in this script to your liking. See below.
*
* CONFIGURATION
* Adjust the settings in the config parameters below as needed.
*
* First, define the behaviour config:
* - validColors: What constitutes a "color"? Options are #ff0000 hex codes,
* rgb()/rgba()/hsl()/hsla() values, common color names like "red", or special
* color keywords like "transparent" or "currentcolor".
* Typically "CSS" is ideal, but choose what's best for your setup.
* - preferColorOrder: When searching for colors, what order should the search be
* performed in? 0 is front to back, -1 is back to front, other positive numbers
* are specific values within the summary title
*
* Next, set the appearance display flags:
* - displayColorBackground: Show the color on the background of the collection item?
* - displayColorSwatch: Display a color swatch on the collection item itself?
* - displayColorText: Show the color's text value on the collection item (and in the swatch, if it's rendered)?
*
* Last, update specific appearance settings:
*
* For the background,
* - backgroundUsesGradient: Should the background color, if used, be drawn as a gradient?
* - swatchFadesBackgroundColor: If using both swatch and background colors, should the BG color be faded?
*
* For the swatch,
* - swatchRightJustify: Should the swatch, if used, be right-justified?
* - removeColorTextFromCollection: Should the detected color text be removed from the collection title's text?
* - swatchWidth: Width of the swatch, if it's used
* - swatchMaxWidth: Max width of the swatch, if it's used
* - swatchHeight: Height of the swatch, if it's used
*
* For the text,
* - textSize: Size of the color text, if it's used
* - textColor: How to apply color to text, if it's used. Generally "AUTO" works best
* - textShadow: Should a drop shadow render behind the text?
*
*/
(() => {
'use strict';
// MARK: Config
/* ======================================== */
/* ============= CONFIG ============= */
/* ======================================== */
/** Is this script enabled? */
const enabled = true;
/* ===== Behaviour Config ===== */
/**
* What type of colors are considered valid?
* - `"HEX"`: only hex-code colors, like `#F0F`, `#F0F8`, `#FF00FF`. or `#FF00FF88`
* - `"NAME"`: only named color keywords like `"red"` and `"darkseagreen"`
* - `"BASIC"`: hex-code colors like `#FF00FF`, *and* named color keywords like `"red"` and `"darkseagreen"`, but *not* RGB/HSL
* - `"DATA"`: hex colors *and* `rgb()`, `rgba()`, `hsl()`, and `hsla()` colors, but *not* any named color keywords
* - `"CSS"`: hex, RGB/HSL, and color keywords like `"red"` and `"darkseagreen"`, and common technical keywords `"transparent"`, `"currentcolor"`, and `"accentcolor"`
* - `"FULL"`: all CSS-functional colors in `"CSS"`, plus technical keywords like `"activetext"`, `"buttonface"`, `"canvas"`, `"mark"`, etc
* @type {"HEX"|"NAME"|"BASIC"|"DATA"|"CSS"|"FULL"}
*/
const validColors = "CSS";
/**
* Which order of color found in the summary to prefer?
* - `-1`: last found color, closest to the end of the string
* - `0`: first found color
* - `>= 1`: zero-inclusive Nth found color
* (eg, `2` would return the 3rd found color). If there are fewer
* colors found than specified, returns the last found color.
* - `< -1`: invalid
*/
const preferColorOrder = -1;
/* ===== Appearance Config ===== */
// --- Main Display Flags ---
/** Update the background color of each collection item? */
const displayColorBackground = true;
/** Display a color swatch for item? */
const displayColorSwatch = false;
/** Display the color text on the item? Typically meant to appear within the swatch, but it can display even if {@linkcode displayColorSwatch} is `false` */
const displayColorText = true;
// --- Background Apperance ---
/** Should the background color use a gradient? */
const backgroundUsesGradient = true;
/** If both the swatch and background color are rendered, should the background color be slightly faded? */
const swatchFadesBackgroundColor = true;
// --- Swatch Apperance ---
/** Right-justify the swatch in the collection bar? */
const swatchRightJustify = true;
/** Remove the color text from the collection's summary text? */
const removeColorTextFromCollection = true;
/** CSS `width` of the swatch, in any valid CSS unit */
const swatchWidth = '200px';
/** CSS `max-width` of the swatch, in any valid CSS unit (ignored if blank or null) */
const swatchMaxWidth = '100%';
/** CSS `height` of the swatch, in any valid CSS unit.
* - 28px is a satisfyingly large swatch
* - 22.5px is the default height of the container */
const swatchHeight = '28px';
// --- Text Apperance ---
/** If {@linkcode displayColorText} is `true`, what size should the text be? Adjust this for {@linkcode swatchHeight} changes. */
const textSize = '9pt';
/** If {@linkcode displayColorText} is `true`, should the text adjust to the general brightness of the color? */
/**
* What type of colors are considered valid?
* - `"DEFAULT"`: Don't modify the CSS text color value
* - `"AUTO"`: Automatically use light or dark text depending on the detected color
* - `"LIGHT"`: Use a light text color
* - `"DARK"`: Use a dark text color
* - `"COLOR"`: Match the detected color
*
* @type {"DEFAULT"|"AUTO"|"LIGHT"|"DARK"|"COLOR"}
*/
const textColor = 'AUTO';
/** Apply a shadow on the text? It automatically detects light or dark. Mainly useful if textColor is COLOR, and there's no BG/swatch */
const textShadow = false;
/* ===== End Config ===== */
// MARK: Internal
/* ===== Internal Params ===== */
/** regex for finding only `#RGB`, `#RGBA`, `#RRGGBB`, and `#RRGGBBAA` hex colors in a string */
const _COLOR_REGEX_HEX = /#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})(?![0-9a-f])/gi;
/** regex for finding color hex codes *and* `rgb()`, `rgba()`, `hsl()`, and `hsla()` colors in a string */
const _COLOR_REGEX_DATA = /#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})(?![0-9a-fA-F])|\b(?:rgba?|hsla?)\(\s*[^)]*\s*\)/gi;
/** All CSS color keyword strings, from `"aliceblue"` to `"yellowgreen"` */
const _CSS_COLOR_NAMES = new Set(["aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige", "bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown", "burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue", "cornsilk", "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod", "darkgray", "darkgreen", "darkgrey", "darkkhaki", "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen", "darkslateblue", "darkslategray", "darkslategrey", "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgray", "dimgrey", "dodgerblue", "firebrick", "floralwhite", "forestgreen", "fuchsia", "gainsboro", "ghostwhite", "gold", "goldenrod", "gray", "green", "greenyellow", "grey", "honeydew", "hotpink", "indianred", "indigo", "ivory", "khaki", "lavender", "lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral", "lightcyan", "lightgoldenrodyellow", "lightgray", "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen", "lightskyblue", "lightslategray", "lightslategrey", "lightsteelblue", "lightyellow", "lime", "limegreen", "linen", "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise", "mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin", "navajowhite", "navy", "oldlace", "olive", "olivedrab", "orange", "orangered", "orchid", "palegoldenrod", "palegreen", "paleturquoise", "palevioletred", "papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue", "purple", "rebeccapurple", "red", "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen", "seashell", "sienna", "silver", "skyblue", "slateblue", "slategray", "slategrey", "snow", "springgreen", "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet", "wheat", "white", "whitesmoke", "yellow", "yellowgreen"]);
/** Relatively common special CSS color keywords: `"transparent"`, `"currentcolor"`, and `"accentcolor"` */
const _CSS_COLOR_KEYWORDS_COMMON = new Set(['transparent', 'currentcolor', 'accentcolor']);
/** All special CSS color keywords, including {@link _CSS_COLOR_KEYWORDS_COMMON the common ones}, and also: `"accentcolortext"`, `"activetext"`, `"buttonborder"`, `"buttonface"`, `"buttontext"`, `"canvas"`, `"canvastext"`, `"field"`, `"fieldtext"`, `"graytext"`, `"highlight"`, `"highlighttext"`, `"linktext"`, `"mark"`, `"marktext"`, `"selecteditem"`, `"selecteditemtext"`, and `"visitedtext"`. */
const _CSS_COLOR_KEYWORDS_COMPREHENSIVE = new Set([..._CSS_COLOR_KEYWORDS_COMMON, 'accentcolortext', 'activetext', 'buttonborder', 'buttonface', 'buttontext', 'canvas', 'canvastext', 'field', 'fieldtext', 'graytext', 'highlight', 'highlighttext', 'linktext', 'mark', 'marktext', 'selecteditem', 'selecteditemtext', 'visitedtext']);
/** Regex expression to search colors with, determined by {@linkcode validColors} */
const _COLOR_REGEX =
// @ts-ignore ignore "const value mismatch" cuz users are allowed to customize their configs, TYPESCRIPT >:c
(validColors === 'DATA' || validColors === 'CSS' || validColors === 'FULL') ?
_COLOR_REGEX_DATA : _COLOR_REGEX_HEX;
/** Set of CSS-valid color keywords, determined by {@linkcode validColors} */
const _CSS_COLORS =
// @ts-ignore
validColors === 'FULL' ?
new Set([..._CSS_COLOR_NAMES, ..._CSS_COLOR_KEYWORDS_COMPREHENSIVE]) :
// @ts-ignore
validColors === 'CSS' ?
new Set([..._CSS_COLOR_NAMES, ..._CSS_COLOR_KEYWORDS_COMMON]) :
new Set([..._CSS_COLOR_NAMES]);
/**
* Searches for a CSS-valid color within the given string.
* Returns a `[string,number]` tuple from the given string,
* where the first param
* @param {string} colorString Color string to search
* @param {number} [order=0] Order of color to return. Default `0`
* - `0`: return the
* @returns {[string, number]}
*/
function cssColorFromString(colorString, order = 0) {
// validity checks
if (colorString == null || colorString.trim() === '') { return null; }
if (order < -1) { console.warn(`Invalid order ${order}, must be -1 or greater`); return null; }
// prep to lower
colorString = colorString.toLowerCase();
/** colors stored with their index in the entire string @type {[string, number][]} */
const colors = [];
let index = 0;
// keywords
switch (validColors) {
case 'NAME':
case 'BASIC':
case 'CSS':
case 'FULL':
const words = colorString.match(/[A-Za-z]+/g) ?? [];
if (words.length === 0) { return null; }
for (const word of words) {
if (_CSS_COLORS.has(word)) {
if (isValidCssColor(word)) {
index += colorString.slice(index).indexOf(word);
colors.push([word, index]);
}
}
}
index = 0;
break;
}
// hex / rgb[a] / hsl[a]
switch (validColors) {
case 'NAME':
// skip any hex/rgba/hsla values
break;
default:
const matches = colorString.match(_COLOR_REGEX);
if (matches && matches.length > 0) {
for (const match of matches) {
if (isValidCssColor(match)) {
index += colorString.slice(index).indexOf(match);
colors.push([match, index]);
}
}
}
break;
}
switch (colors.length) {
case 0:
return null;
case 1:
return colors[0];
}
// sort by index
colors.sort((a, b) => a[1] - b[1]);
switch (order) {
case -1:
return colors.at(-1)
case 0:
return colors[0];
default:
if (order >= colors.length) {
console.warn(`Invalid order, can't get the ${order}+1 Nth color, only ${colors.length} colors found, returning last color`);
return colors.at(-1);
}
return colors[order];
}
}
/**
* Creates a gradient that fades from 0% opacity. Returns the `"linear-gradient()"` string, or `null` if creation failed
* @param {Element} element Element to apply the background gradient to
* @param {string} [colorString] Color to to the background gradient. If blank or undefined, the color will be read from {@linkcode element}
* @param {Number} [startPercent = 0] Percent, from `0` to `100`, that the gradient will *begin* at. Default `0`
* @param {Number} [endPercent = 100] Percent, from `0` to `100`, that the gradient will *end* at. Default `100`
* @param {string} [direction='90deg'] Direction of the gradient. Default `"90deg"`
* @param {boolean} [useAlpha=true] Use the given color's alpha? If `true`, fades from `0` to the color's given opacity. If `false`, fades from `0` to `1` opacity. Default `true`
* @returns {string}
*/
function createFadeGradient(element, colorString, startPercent = 0, endPercent = 100, direction = '90deg', useAlpha = true) {
// validity checks,
if (element == null) { return null; }
// check if colorString is valid
if (colorString == null || colorString.trim() === '') {
// not valid, read from element
colorString = getComputedStyle(element).backgroundColor;
}
const rgba = getColorRGBA(colorString, useAlpha);
return `linear-gradient(${direction}, ${rgbaToColor(rgba, 0)} ${startPercent}%, ${rgbaToColor(rgba)} ${endPercent}%)`;
}
/** @typedef {{r:Number, g:Number, b:Number, a:Number}} rgba */
/**
* Gets a given color as an object with `rgba.r`, `.g`, `.b`, and `.a` numeric values.
*
* The `r`, `g`, and `b` values are numbers 0-255, and `a` is 0.0 to 1.0
* @param {string} colorString Color to parse
* @param {boolean} [useAlpha=true] Preserve any alpha value found in the color? If `false`, set `a` to `1`. Default `true`
* @returns {rgba}
*/
function getColorRGBA(colorString, useAlpha = true) {
if (!isValidCssColor(colorString)) { return null; }
// create temporary span element, apply its background color
const tempSpan = document.createElement('span');
tempSpan.style.cssText = `display:none;background-color:${colorString}`;
document.body.append(tempSpan);
// get the computer background color from the temporary element and remove it
const bgColor = tempSpan.style.backgroundColor && getComputedStyle(tempSpan).backgroundColor;
tempSpan.remove();
if (bgColor == null || bgColor.trim() === '') { return null; }
// extract regex colors from bg string, parse to RGB
const regexMatch = bgColor.match(/rgba?\(([\d.]+),\s*([\d.]+),\s*([\d.]+)(?:,\s*([\d.]+))?/);
if (!regexMatch) { return null; }
const [r, g, b, a = 1] = regexMatch.slice(1).map(Number);
const alpha = useAlpha ? isNaN(a) ? 1 : a : 1;
return { r, g, b, a: alpha };
}
/**
* Converts an `rgba` object to a CSS-valid string
* @param {rgba} rgba Object to convert
* @param {number} [forceAlphaTo=null] If non-null and if {@linkcode rgbOnly} is `false`, overrides the returned `a` value to this
* @param {boolean} [rgbOnly=false] If `true`, returns `rgb()`. If `false`, returns `rgba()`. Default `false`
* @returns {string}
*/
function rgbaToColor(rgba, forceAlphaTo = null, rgbOnly = false) {
return `rgb${rgbOnly ? '' : 'a'}(${rgba.r},${rgba.g},${rgba.b}${rgbOnly ? '' : `,${(forceAlphaTo != null && !isNaN(forceAlphaTo)) ? forceAlphaTo : rgba.a}`})`;
}
/** checks if the given color is valid css color @param {string} color @returns {boolean} */
function isValidCssColor(color) {
if (color == null || color.trim() === '') { return false; }
try {
return typeof CSS !== 'undefined' && CSS.supports && CSS.supports('color', color);
} catch {
if (!CSS.supports) {
console.warn(`Cannot check if CSS supports color ${color}, 'CSS.supports' unavaialble`);
}
return false;
}
}
/**
* Is the given color "dark" (`true`, closer to black) or "light" (`false`, closer to white)
* @param {string} color Color to inspect
* @param {number} [brightnessCutoff=120] Overall luminance value below which a color is considered "dark". Default `120`
* @returns {boolean} */
function isDark(color, brightnessCutoff = 120) {
const tempDiv = /** @type {HTMLElement} */ (document.body.appendChild(document.createElement('div')));
tempDiv.style.cssText = `position:fixed;left:-9999px;top:-9999px;background-color:${color}`;
const regexMatch = getComputedStyle(tempDiv).backgroundColor.match(/[\d.]+/g);
tempDiv.remove();
const [r, g, b, a = 1] = regexMatch.map(Number);
if (a === 0) { return false; }
// see: https://en.wikipedia.org/wiki/Relative_luminance
return ((0.2126 * r) + (0.7152 * g) + (0.0722 * b)) < brightnessCutoff;
}
/** creates an inline color swatch CSS object @param {string} color @returns {HTMLSpanElement} */
function makeSwatch(color) {
const wrap = document.createElement('span');
wrap.style.display = 'flex';
wrap.style.marginLeft = swatchRightJustify ? 'auto' : '0.69em';
wrap.style.whiteSpace = 'nowrap';
wrap.style.flex = '0 0 auto';
wrap.style.justifySelf = 'flex-end';
const box = document.createElement('span');
box.setAttribute('aria-hidden', 'true');
box.style.width = swatchWidth;
if (swatchMaxWidth != null &&
// @ts-ignore Ignore the const assignment to swatchMaxWidth =_=
swatchMaxWidth !== '') {
box.style.maxWidth = swatchMaxWidth;
}
box.style.height = swatchHeight;
box.style.borderRadius = '2.13px';
if (displayColorSwatch) {
box.style.backgroundColor = color;
box.style.border = `1px solid rgba(${isDark(color) ? '213,213,213,0.213' : '0,0,0,0.069'})`;
}
box.style.boxSizing = 'border-box';
box.style.display = 'flex';
box.style.justifyContent = 'flex-end';
box.style.alignItems = 'center';
box.style.alignSelf = 'center';
// in-swatch text
if (displayColorText) {
const lightTextColor = 'rgba(252, 251, 253, 0.69)';
const darkTextColor = 'rgba(2, 1, 3, 0.5)';
const lightTextShadow = '#F2F1F3';
const darkTextShadow = '#222123';
let shadow = darkTextShadow;
switch (textColor) {
case 'COLOR':
box.style.color = color;
if (textShadow) { shadow = isDark(color, 180) ? lightTextShadow : darkTextShadow; }
break;
case 'AUTO':
const darkText = isDark(color, 180);
box.style.color = darkText ? lightTextColor : darkTextColor;
if (textShadow) { shadow = darkText ? darkTextShadow : lightTextShadow; }
break;
case 'LIGHT':
box.style.color = lightTextColor;
if (textShadow) { shadow = darkTextShadow; }
break;
case 'DARK':
box.style.color = darkTextColor;
if (textShadow) { shadow = lightTextShadow; }
break;
default:
// default, do nothing
break;
}
if (textShadow) {
box.style.textShadow = `0 0 1px ${shadow}69, 0 0 2px ${shadow}FF, 0 0.5px 3px ${shadow}FF, 0 2px 16px ${shadow}FF`;
}
box.style.padding = '1px 5px';
box.style.whiteSpace = 'nowrap';
box.style.fontSize = textSize;
box.innerText = color;
if (displayColorSwatch) {
box.style.overflow = 'hidden';
}
}
wrap.appendChild(box);
return wrap;
}
/**
* lerp two rgba objects, or an rgba object to target number
* @param {rgba} cA
* @param {rgba|number} cB
* @param {number} t
* @param {boolean} [alsoLerpAlpha=false]
* @returns {rgba}
*/
function lerpColor(cA, cB, t, alsoLerpAlpha = false) {
if (typeof cB === 'number') {
cA.r = lerpNumber(cA.r, cB, t);
cA.g = lerpNumber(cA.g, cB, t);
cA.b = lerpNumber(cA.b, cB, t);
if (alsoLerpAlpha) {
cA.a = lerpNumber(cA.a, cB, t);
}
} else {
cA.r = lerpNumber(cA.r, cB.r, t);
cA.g = lerpNumber(cA.g, cB.g, t);
cA.b = lerpNumber(cA.b, cB.b, t);
if (alsoLerpAlpha) {
cA.a = lerpNumber(cA.a, cB.a, t);
}
}
return cA;
}
/**
* lerp two numbers
* @param {number} a
* @param {number} b
* @param {number} t
* @returns {number}
*/
function lerpNumber(a, b, t) { return a + (b - a) * t; }
/** find color text in titles, apply visual swatches @param {Document} [root=document] */
function colorizeCollection(root = document) {
if (!enabled) { return; }
// search for appropriate headers
const candidates = root.querySelectorAll('h2');
candidates.forEach((h2) => {
if (!h2) { return; }
if (h2.dataset.decapColorized === '1') { return; }
const parentElement = h2.parentElement;
if (!parentElement || parentElement.tagName?.toLowerCase() !== 'a') { return; }
const icons = h2.querySelector('div');
if (!icons) { return; }
const textNode = Array.from(h2.childNodes).find((n) => n.nodeType === Node.TEXT_NODE);
if (!textNode) { return; }
const rawNode = (textNode.nodeValue || '').trimEnd();
const match = cssColorFromString(rawNode, preferColorOrder);
if (!match || !match[0] || match[0].trim() === '') { return; }
const color = match[0];
const prefix = match[1] > 0 ? rawNode.slice(0, match[1]) : '';
const suffix = rawNode.slice(prefix.length + color.length);
textNode.nodeValue = prefix.trim() + (removeColorTextFromCollection ? '' : ` ${color}`) + suffix.trim();
if (displayColorSwatch || displayColorText) {
const swatchElement = makeSwatch(color);
if (swatchRightJustify) {
const prevDisplay = getComputedStyle(h2).display;
if (prevDisplay && prevDisplay !== 'flex') {
h2.style.display = 'flex';
h2.style.alignItems = 'center';
h2.style.gap = '0.5em';
}
h2.insertBefore(swatchElement, icons);
icons.style.flex = '0 0 auto';
} else {
h2.style.justifyContent = 'flex-start';
h2.insertBefore(swatchElement, icons);
}
}
if (displayColorBackground) {
const fade = displayColorSwatch && swatchFadesBackgroundColor;
if (backgroundUsesGradient) {
const gradient = createFadeGradient(parentElement, color, fade ? 0 : 10, fade ? 200 : 80);
parentElement.style.backgroundImage = gradient;
} else {
const rgba = fade ? lerpColor(getColorRGBA(color), 255, 0.82) : getColorRGBA(color);
parentElement.style.backgroundColor = rgbaToColor(rgba);
}
}
// failsafe to ensure colorization only happens once
h2.dataset.decapColorized = '1';
});
}
colorizeCollection();
// watch for dom or url changes
const mutationObservert = new MutationObserver(() => colorizeCollection());
mutationObservert.observe(document.documentElement, { childList: true, subtree: true });
window.addEventListener('hashchange', () => { colorizeCollection(); });
})();
// MARK: License
/**
* LICENSE INFO
*
* This script was written by Nick Yonge, 2026, and is released
* under the terms of The Unlicense:
*
* This is free and unencumbered software released into the public domain.
*
* Anyone is free to copy, modify, publish, use, compile, sell, or
* distribute this software, either in source code form or as a compiled
* binary, for any purpose, commercial or non-commercial, and by any
* means.
*
* In jurisdictions that recognize copyright laws, the author or authors
* of this software dedicate any and all copyright interest in the
* software to the public domain. We make this dedication for the benefit
* of the public at large and to the detriment of our heirs and
* successors. We intend this dedication to be an overt act of
* relinquishment in perpetuity of all present and future rights to this
* software under copyright law.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* For more information, please refer to <https://unlicense.org/>
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment