Molbap's picture
Molbap HF Staff
push a bunch of updates
e903a32
raw
history blame
18.8 kB
---
const rootId = `palettes-${Math.random().toString(36).slice(2)}`;
---
<div class="palettes" id={rootId} style="width:100%; margin: 10px 0;">
<style is:global>
.palettes {
box-sizing: border-box;
}
.palettes .palettes__grid {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
max-width: 100%;
}
.palettes .palette-card {
position: relative;
display: grid;
grid-template-columns: 1fr minmax(0, 220px);
align-items: stretch;
gap: 12px;
border: 1px solid var(--border-color);
border-radius: 10px;
background: var(--surface-bg);
padding: 12px;
transition:
box-shadow 0.18s ease,
transform 0.18s ease,
border-color 0.18s ease;
min-height: 60px;
}
.palettes .palette-card__preview {
width: 48px;
height: 48px;
border-radius: 999px;
flex: 0 0 auto;
background-size: cover;
background-position: center;
}
.palettes .palette-card__copy {
position: absolute;
top: 50%;
left: 100%;
transform: translateY(-50%);
z-index: 3;
border-left: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.palettes .palette-card__copy svg {
width: 18px;
height: 18px;
fill: currentColor;
display: block;
color: inherit;
}
.palettes .palette-card__swatches {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
grid-auto-rows: 1fr;
gap: 2px;
margin: 0;
min-height: 60px;
}
.palettes .palette-card__swatches .sw {
width: 100%;
min-width: 0;
height: auto;
border-radius: 0;
border: 1px solid var(--border-color);
}
.palettes .palette-card__swatches .sw:first-child {
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
}
.palettes .palette-card__swatches .sw:last-child {
border-top-right-radius: 8px;
border-bottom-right-radius: 8px;
}
.palettes .palette-card__content {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 12px;
min-width: 0;
padding-right: 12px;
}
.palettes .palette-card__preview {
width: 48px;
height: 48px;
border-radius: 999px;
position: relative;
flex: 0 0 auto;
overflow: hidden;
}
.palettes .palette-card__preview .dot {
position: absolute;
width: 4px;
height: 4px;
background: #fff;
border-radius: 999px;
box-shadow: 0 0 6px rgba(0, 0, 0, 1);
}
.palettes .palette-card__preview .donut-hole {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 24px;
height: 24px;
border-radius: 999px;
background: var(--surface-bg);
box-shadow: 0 0 0 1px var(--border-color) inset;
}
.palettes .palette-card__content__info {
display: flex;
flex-direction: column;
}
.palettes .palette-card__title {
text-align: left;
font-weight: 800;
font-size: 15px;
}
.palettes .palette-card__desc {
text-align: left;
color: var(--muted-color);
line-height: 1.5;
font-size: 12px;
}
.palettes .palettes__select {
width: 100%;
max-width: 100%;
border: 1px solid var(--border-color);
background: var(--surface-bg);
color: var(--text-color);
padding: 8px 10px;
border-radius: 8px;
}
.palettes .sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 1px, 1px);
white-space: nowrap;
border: 0;
}
.palettes .palettes__controls {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
margin: 8px 0 14px;
}
.palettes .palettes__field {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
flex: 1 1 280px;
max-width: 100%;
}
.palettes .palettes__label {
font-size: 12px;
color: var(--muted-color);
font-weight: 800;
}
.palettes .palettes__label-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.palettes .ghost-badge {
font-size: 11px;
padding: 1px 6px;
border-radius: 999px;
border: 1px solid var(--border-color);
color: var(--muted-color);
background: transparent;
font-variant-numeric: tabular-nums;
}
.palettes .palettes__count {
display: flex;
align-items: center;
gap: 8px;
max-width: 100%;
}
.palettes .palettes__count input[type="range"] {
width: 100%;
}
.palettes .palettes__count output {
min-width: 28px;
text-align: center;
font-variant-numeric: tabular-nums;
font-size: 12px;
color: var(--muted-color);
}
.palettes input[type="range"] {
-webkit-appearance: none;
appearance: none;
height: 24px;
background: transparent;
cursor: pointer;
accent-color: var(--primary-color);
}
.palettes input[type="range"]:focus {
outline: none;
}
.palettes input[type="range"]::-webkit-slider-runnable-track {
height: 6px;
background: var(--border-color);
border-radius: 999px;
}
.palettes input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
margin-top: -6px;
width: 18px;
height: 18px;
background: var(--primary-color);
border: 2px solid var(--surface-bg);
border-radius: 50%;
}
.palettes input[type="range"]::-moz-range-track {
height: 6px;
background: var(--border-color);
border: none;
border-radius: 999px;
}
.palettes input[type="range"]::-moz-range-progress {
height: 6px;
background: var(--primary-color);
border-radius: 999px;
}
.palettes input[type="range"]::-moz-range-thumb {
width: 18px;
height: 18px;
background: var(--primary-color);
border: 2px solid var(--surface-bg);
border-radius: 50%;
}
html.cb-grayscale,
body.cb-grayscale {
filter: grayscale(1) !important;
}
html.cb-protanopia,
body.cb-protanopia {
filter: url(#cb-protanopia) !important;
}
html.cb-deuteranopia,
body.cb-deuteranopia {
filter: url(#cb-deuteranopia) !important;
}
html.cb-tritanopia,
body.cb-tritanopia {
filter: url(#cb-tritanopia) !important;
}
html.cb-achromatopsia,
body.cb-achromatopsia {
filter: url(#cb-achromatopsia) !important;
}
@media (max-width: 1100px) {
.palettes .palette-card {
grid-template-columns: 1fr;
align-items: stretch;
gap: 10px;
}
.palettes .palette-card__swatches {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.palettes .palette-card__content {
border-right: none;
padding-right: 0;
}
.palettes .palette-card__copy {
display: none;
}
}
</style>
<div class="palettes__controls">
<div class="palettes__field">
<label class="palettes__label" for="cb-select"
>Color vision simulation</label
>
<select id="cb-select" class="palettes__select">
<option value="none"
>Normal color vision — typical for most people</option
>
<option value="achromatopsia">Achromatopsia — no color at all</option>
<option value="protanopia">Protanopia — reduced/absent reds</option>
<option value="deuteranopia"
>Deuteranopia — reduced/absent greens</option
>
<option value="tritanopia">Tritanopia — reduced/absent blues</option>
</select>
</div>
<div class="palettes__field">
<div class="palettes__label-row">
<label class="palettes__label" for="color-count">Number of colors</label
>
<output id="color-count-out" for="color-count" class="ghost-badge"
>8</output
>
</div>
<div class="palettes__count">
<input
id="color-count"
type="range"
min="6"
max="10"
step="1"
value="8"
aria-label="Number of colors"
/>
</div>
</div>
</div>
<div class="palettes__grid"></div>
<div class="palettes__simu" role="group" aria-labelledby="cb-sim-title">
<svg
aria-hidden="true"
focusable="false"
width="0"
height="0"
style="position:absolute; left:-9999px; overflow:hidden;"
>
<defs>
<filter id="cb-protanopia"
><feColorMatrix
type="matrix"
values="0.567 0.433 0 0 0 0.558 0.442 0 0 0 0 0.242 0.758 0 0 0 0 0 1 0"
></feColorMatrix></filter
>
<filter id="cb-deuteranopia"
><feColorMatrix
type="matrix"
values="0.625 0.375 0 0 0 0.7 0.3 0 0 0 0 0.3 0.7 0 0 0 0 0 1 0"
></feColorMatrix></filter
>
<filter id="cb-tritanopia"
><feColorMatrix
type="matrix"
values="0.95 0.05 0 0 0 0 0.433 0.567 0 0 0 0.475 0.525 0 0 0 0 0 1 0"
></feColorMatrix></filter
>
<filter id="cb-achromatopsia"
><feColorMatrix
type="matrix"
values="0.299 0.587 0.114 0 0 0.299 0.587 0.114 0 0 0.299 0.587 0.114 0 0 0 0 0 1 0"
></feColorMatrix></filter
>
</defs>
</svg>
</div>
</div>
<script type="module" is:inline>
import "/scripts/color-palettes.js";
const ROOT_ID = "{rootId}";
(() => {
const cards = [
{
key: "categorical",
title: "Categorical",
desc: "For <strong>non‑numeric categories</strong>; <strong>visually distinct</strong> colors.",
},
{
key: "sequential",
title: "Sequential",
desc: "For <strong>numeric scales</strong>; gradient from <strong>dark to light</strong>. Ideal for <strong>heatmaps</strong>.",
},
{
key: "diverging",
title: "Diverging",
desc: "For numeric scales with negative and positive; Opposing extremes with smooth contrast around a neutral midpoint.",
},
];
const getPaletteColors = (key, count) => {
const total = Number(count) || 6;
if (
window.ColorPalettes &&
typeof window.ColorPalettes.getColors === "function"
) {
return window.ColorPalettes.getColors(key, total) || [];
}
return [];
};
const render = () => {
const root =
document.getElementById(ROOT_ID) || document.querySelector(".palettes");
if (!root) return;
const grid = root.querySelector(".palettes__grid");
if (!grid) return;
const input = document.getElementById("color-count");
const total = input ? Number(input.value) || 6 : 6;
const html = cards
.map((c) => {
const colors = getPaletteColors(c.key, total);
const swatches = colors
.map(
(col) => `<div class=\"sw\" style=\"background:${col}\"></div>`,
)
.join("");
const baseHex =
window.ColorPalettes &&
typeof window.ColorPalettes.getPrimary === "function"
? window.ColorPalettes.getPrimary()
: colors[0] || "#FF0000";
const hueDeg = (() => {
try {
const s = baseHex.replace("#", "");
const v =
s.length === 3
? s
.split("")
.map((ch) => ch + ch)
.join("")
: s;
const r = parseInt(v.slice(0, 2), 16) / 255,
g = parseInt(v.slice(2, 4), 16) / 255,
b = parseInt(v.slice(4, 6), 16) / 255;
const M = Math.max(r, g, b),
m = Math.min(r, g, b),
d = M - m;
if (d === 0) return 0;
let h = 0;
if (M === r) h = ((g - b) / d) % 6;
else if (M === g) h = (b - r) / d + 2;
else h = (r - g) / d + 4;
h *= 60;
if (h < 0) h += 360;
return h;
} catch {
return 0;
}
})();
const gradient =
c.key === "categorical"
? (() => {
const steps = 60; // smooth hue wheel (fixed orientation)
const wheel = Array.from(
{ length: steps },
(_, i) =>
`hsl(${Math.round((i / steps) * 360)}, 100%, 50%)`,
).join(", ");
return `conic-gradient(${wheel})`;
})()
: colors.length
? `linear-gradient(90deg, ${colors.join(", ")})`
: `linear-gradient(90deg, var(--border-color), var(--border-color))`;
const previewInner = (() => {
if (c.key !== "categorical" || !colors.length) return "";
const ring = 18;
const cx = 24;
const cy = 24;
const offset = (hueDeg / 360) * 2 * Math.PI;
return colors
.map((col, i) => {
const angle = offset + (i / colors.length) * 2 * Math.PI;
const x = cx + ring * Math.cos(angle);
const y = cy + ring * Math.sin(angle);
return `<span class=\"dot\" style=\"left:${x - 2}px; top:${y - 2}px\"></span>`;
})
.join("");
})();
const donutHole =
c.key === "categorical" ? '<span class="donut-hole"></span>' : "";
return `
<div class="palette-card" data-colors="${colors.join(",")}">
<div class="palette-card__content">
<div class=\"palette-card__preview\" aria-hidden=\"true\" style=\"background:${gradient}\">${previewInner}${donutHole}</div>
<div class="palette-card__content__info">
<div class="palette-card__title">${c.title}</div>
<div class="palette-card__desc">${c.desc}</div>
</div>
</div>
<div class="palette-card__swatches" style="grid-template-columns: repeat(${colors.length}, minmax(0, 1fr));">${swatches}</div>
<button class="palette-card__copy button--ghost" type="button" aria-label="Copy palette">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M16 1H4c-1.1 0-2 .9-2 2v12h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
</button>
</div>`;
})
.join("");
grid.innerHTML = html;
};
const MODE_TO_CLASS = {
protanopia: "cb-protanopia",
deuteranopia: "cb-deuteranopia",
tritanopia: "cb-tritanopia",
achromatopsia: "cb-achromatopsia",
};
const CLEAR_CLASSES = Object.values(MODE_TO_CLASS);
const clearCbClasses = () => {
const rootEl = document.documentElement;
CLEAR_CLASSES.forEach((cls) => rootEl.classList.remove(cls));
};
const applyCbClass = (mode) => {
clearCbClasses();
const cls = MODE_TO_CLASS[mode];
if (cls) document.documentElement.classList.add(cls);
};
const currentCbMode = () => {
const rootEl = document.documentElement;
for (const [mode, cls] of Object.entries(MODE_TO_CLASS)) {
if (rootEl.classList.contains(cls)) return mode;
}
return "none";
};
const setupCbSim = () => {
const select = document.getElementById("cb-select");
if (!select) return;
try {
select.value = currentCbMode();
} catch {}
select.addEventListener("change", () => applyCbClass(select.value));
};
const setupCountControl = () => {
const input = document.getElementById("color-count");
const out = document.getElementById("color-count-out");
if (!input) return;
const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
const read = () => clamp(Number(input.value) || 6, 6, 10);
const syncOut = () => {
if (out) out.textContent = String(read());
};
const onChange = () => {
syncOut();
render();
};
syncOut();
input.addEventListener("input", onChange);
document.addEventListener("palettes:updated", () => {
syncOut();
render();
});
};
let copyDelegationSetup = false;
const setupCopyDelegation = () => {
if (copyDelegationSetup) return;
const grid = document.querySelector(".palettes .palettes__grid");
if (!grid) return;
grid.addEventListener("click", async (e) => {
const btn = e.target.closest
? e.target.closest(".palette-card__copy")
: null;
if (!btn) return;
const card = btn.closest(".palette-card");
if (!card) return;
const colors = (card.dataset.colors || "").split(",").filter(Boolean);
const json = JSON.stringify(colors, null, 2);
try {
await navigator.clipboard.writeText(json);
const old = btn.innerHTML;
btn.innerHTML =
'<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 16.2l-3.5-3.5-1.4 1.4L9 19 20.3 7.7l-1.4-1.4z"/></svg>';
setTimeout(() => (btn.innerHTML = old), 900);
} catch {
window.prompt("Copy palette", json);
}
});
copyDelegationSetup = true;
};
const bootstrap = () => {
setupCbSim();
setupCountControl();
setupCopyDelegation();
// Render immediately
render();
// Re-render on palette updates
document.addEventListener("palettes:updated", render);
// Force an immediate notify after listeners are attached (ensures initial render)
try {
if (
window.ColorPalettes &&
typeof window.ColorPalettes.notify === "function"
)
window.ColorPalettes.notify();
else if (
window.ColorPalettes &&
typeof window.ColorPalettes.refresh === "function"
)
window.ColorPalettes.refresh();
} catch {}
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", bootstrap, { once: true });
} else {
bootstrap();
}
})();
</script>