| | <div class="palettes" style="width:100%; margin: 10px 0;"> |
| | <style> |
| | .palettes { box-sizing: border-box; overflow-x: hidden; } |
| | .palettes .palettes__grid { display: grid; grid-template-columns: 1fr; gap: 12px; max-width: 100%; } |
| | .palettes .palette-card { position: relative; display: grid; grid-template-columns: auto 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 .18s ease, transform .18s ease, border-color .18s ease; } |
| | |
| | .palettes .palette-card__swatches { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); grid-auto-rows: 1fr; gap: 2px; margin: 0; min-height: 20px; } |
| | .palettes .palette-card__swatches .sw { width: 100%; min-width: 0; min-height: 0; 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: center; gap: 6px; min-width: 0; padding-right: 0; } |
| | .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 .palette-card__actions { display: flex; align-items: center; justify-content: flex-start; justify-self: start; align-self: stretch; } |
| | |
| | |
| | |
| | .palettes .copy-btn svg { width: 18px; height: 18px; fill: currentColor; display: block; } |
| | |
| | .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: 640px) { |
| | .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__actions { justify-self: start; } |
| | |
| | } |
| | </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">6</output> |
| | </div> |
| | <div class="palettes__count"> |
| | <input id="color-count" type="range" min="6" max="10" step="1" value="6" 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"/> |
| | </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"/> |
| | </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"/> |
| | </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"/> |
| | </filter> |
| | </defs> |
| | </svg> |
| | </div> |
| | </div> |
| | <script> |
| | (() => { |
| | const cards = [ |
| | { key: 'categorical', title: 'Categorical', desc: 'For <strong>non‑numeric categories</strong>; <strong>visually distinct</strong> colors. The more you have the more likely they are to look similar.' }, |
| | { 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: 'Opposing extremes via <strong>base → white → complement</strong>; smooth contrast around a neutral midpoint.' } |
| | ]; |
| | |
| | const getCssVar = (name) => { |
| | try { return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); } catch { return ''; } |
| | }; |
| | |
| | const getPaletteColors = (key) => { |
| | const count = Number(getCssVar(`--palette-${key}-count-current`)) || Number(getCssVar('--palette-count')) || 6; |
| | const colors = []; |
| | for (let i = 1; i <= count; i++) { |
| | const v = getCssVar(`--palette-${key}-${i}`); |
| | if (v) colors.push(v); |
| | } |
| | return colors; |
| | }; |
| | |
| | const render = () => { |
| | const mount = document.currentScript ? document.currentScript.previousElementSibling : null; |
| | const root = mount && mount.closest('.palettes') ? mount.closest('.palettes') : document.querySelector('.palettes'); |
| | if (!root) return; |
| | const grid = root.querySelector('.palettes__grid'); |
| | if (!grid) return; |
| | const html = cards.map((c) => { |
| | const colors = getPaletteColors(c.key); |
| | const swatches = colors.map(col => `<div class="sw" style="background:${col}"></div>`).join(''); |
| | return ` |
| | <div class="palette-card" data-colors="${colors.join(',')}"> |
| | <div class="palette-card__content"> |
| | <div class="palette-card__content__info"> |
| | <div class="palette-card__title">${c.title}</div> |
| | <div class="palette-card__desc">${c.desc}</div> |
| | </div> |
| | <button class="copy-btn 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> |
| | <div class="palette-card__actions"></div> |
| | <div class="palette-card__swatches" style="grid-template-columns: repeat(${colors.length}, minmax(0, 1fr));">${swatches}</div> |
| | </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 read = () => { |
| | const v = Number(getCssVar('--palette-count')) || 6; |
| | return Math.max(6, Math.min(10, v)); |
| | }; |
| | const write = (v) => { |
| | document.documentElement.style.setProperty('--palette-count', String(v)); |
| | if (window.ColorPalettes && typeof window.ColorPalettes.refresh === 'function') window.ColorPalettes.refresh(); |
| | }; |
| | const syncOut = () => { if (out) out.textContent = String(read()); }; |
| | const syncInput = () => { try { input.value = String(read()); } catch {} }; |
| | syncInput(); syncOut(); |
| | input.addEventListener('input', () => { write(input.value); syncOut(); }); |
| | document.addEventListener('palettes:updated', () => { syncInput(); syncOut(); render(); }); |
| | }; |
| | |
| | let copyDelegationSetup = false; |
| | const setupCopyDelegation = () => { |
| | if (copyDelegationSetup) return; |
| | const root = document.querySelector('.palettes'); |
| | if (!root) return; |
| | const grid = root.querySelector('.palettes__grid'); |
| | if (!grid) return; |
| | grid.addEventListener('click', async (e) => { |
| | const target = e.target.closest ? e.target.closest('.copy-btn') : null; |
| | if (!target) return; |
| | const card = target.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 = target.innerHTML; |
| | target.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(() => target.innerHTML = old, 900); |
| | } catch { |
| | window.prompt('Copy palette', json); |
| | } |
| | }); |
| | copyDelegationSetup = true; |
| | }; |
| | |
| | const bootstrap = () => { |
| | setupCbSim(); |
| | setupCountControl(); |
| | render(); |
| | setupCopyDelegation(); |
| | |
| | document.addEventListener('palettes:updated', render); |
| | const mo = new MutationObserver(() => render()); |
| | mo.observe(document.documentElement, { attributes: true, attributeFilter: ['style'] }); |
| | |
| | |
| | const waitForPalettes = () => { |
| | if (getCssVar('--palette-categorical-1')) { render(); return; } |
| | setTimeout(waitForPalettes, 50); |
| | }; |
| | waitForPalettes(); |
| | }; |
| | |
| | if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bootstrap, { once: true }); |
| | else bootstrap(); |
| | })(); |
| | </script> |
| |
|
| |
|