| <div class="palettes" style="width:100%; margin: 10px 0;"> |
| <style> |
| .palettes .palettes__grid { display: grid; grid-template-columns: 1fr; gap: 12px; } |
| .palettes .palette-card { position: relative; display: grid; grid-template-columns: auto 1fr 260px; align-items: stretch; gap: 14px; 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: 8px; margin: 0; } |
| .palettes .palette-card__swatches .sw { width: 100%; min-width: 0; min-height: 0; border-radius: 8px; border: 1px solid var(--border-color); } |
| .palettes .palette-card__content { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; justify-content: center; min-width: 0; padding-right: 12px; border-right: 1px solid var(--border-color); } |
| .palettes .palette-card__actions { display: flex; align-items: center; justify-content: flex-start; justify-self: start; } |
| .palettes .palette-card__actions { align-self: stretch; } |
| |
| |
| |
| .palettes .copy-btn svg { width: 18px; height: 18px; fill: currentColor; display: block; } |
| @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__grid"></div> |
| </div> |
| <script> |
| (() => { |
| const loadScript = (id, src, onload, onerror) => { |
| let s = document.getElementById(id); |
| if (s) { return onload && onload(); } |
| s = document.createElement('script'); s.id = id; s.src = src; s.async = true; |
| if (onload) s.addEventListener('load', onload, { once: true }); |
| if (onerror) s.addEventListener('error', onerror, { once: true }); |
| document.head.appendChild(s); |
| }; |
| const ensureChroma = (next) => { |
| if (window.chroma) return next(); |
| const tryReady = () => { if (window.chroma) next(); else setTimeout(tryReady, 25); }; |
| const existing = document.getElementById('chroma-cdn') || document.getElementById('chroma-cdn-fallback'); |
| if (existing) { tryReady(); return; } |
| loadScript('chroma-cdn', 'https://unpkg.com/chroma-js@2.4.2/dist/chroma.min.js', tryReady, () => { |
| loadScript('chroma-cdn-fallback', 'https://cdnjs.cloudflare.com/ajax/libs/chroma-js/2.4.2/chroma.min.js', tryReady); |
| }); |
| }; |
| |
| const cards = [ |
| { key: 'categorical', title: 'Categorical', desc: 'For <strong>non‑numeric categories</strong>; <strong>visually distinct</strong> colors. <strong>Up to 6</strong>.', generator: (baseHex) => { |
| const base = chroma(baseHex); |
| const lc = base.lch(); |
| const baseH = base.get('hsl.h') || 0; |
| const L0 = Math.max(40, Math.min(85, lc[0] || 70)); |
| const C0 = Math.max(45, Math.min(75, lc[1] || 70)); |
| const MIN_DELTA = 18; |
| |
| const seen = new Set(); |
| const results = []; |
| |
| const makeSafe = (h, L, C) => { |
| let c = C; |
| let col = chroma.lch(L, c, h); |
| let guard = 0; |
| while (col.clipped && typeof col.clipped === 'function' && col.clipped() && c > 30 && guard < 8) { |
| c -= 5; |
| col = chroma.lch(L, c, h); |
| guard++; |
| } |
| return col; |
| }; |
| |
| const isFarEnough = (hex) => results.every(prev => chroma.distance(hex, prev, 'lab') >= MIN_DELTA); |
| const pushHex = (col) => { |
| const hex = col.hex(); |
| if (!seen.has(hex.toLowerCase())) { results.push(hex); seen.add(hex.toLowerCase()); } |
| }; |
| |
| |
| pushHex(base); |
| |
| |
| const angles = [60, 120, 180, 240, 300]; |
| const hueOffsets = [0, 20, -20, 40, -40, 60, -60, 80, -80]; |
| const lVariants = [L0, Math.max(40, L0 - 6), Math.min(85, L0 + 6)]; |
| |
| angles.forEach(step => { |
| let accepted = false; |
| for (let li = 0; li < lVariants.length && !accepted; li++) { |
| for (let oi = 0; oi < hueOffsets.length && !accepted; oi++) { |
| let h = (baseH + step + hueOffsets[oi] + 360) % 360; |
| let col = makeSafe(h, lVariants[li], C0); |
| const hex = col.hex(); |
| if (!seen.has(hex.toLowerCase()) && isFarEnough(hex)) { |
| pushHex(col); |
| accepted = true; |
| } |
| } |
| } |
| if (!accepted) { |
| |
| let cTry = C0 - 10; |
| let h = (baseH + step + 360) % 360; |
| let trials = 0; |
| while (!accepted && cTry >= 30 && trials < 6) { |
| const col = makeSafe(h, L0, cTry); |
| const hex = col.hex(); |
| if (!seen.has(hex.toLowerCase()) && isFarEnough(hex)) { |
| pushHex(col); |
| accepted = true; |
| break; |
| } |
| cTry -= 5; |
| trials++; |
| } |
| |
| if (!accepted) { |
| let bestHex = null; let bestMin = -1; |
| hueOffsets.forEach(off => { |
| const hh = (baseH + step + off + 360) % 360; |
| const cand = makeSafe(hh, L0, C0).hex(); |
| const minD = results.reduce((m, prev) => Math.min(m, chroma.distance(cand, prev, 'lab')), Infinity); |
| if (minD > bestMin && !seen.has(cand.toLowerCase())) { bestMin = minD; bestHex = cand; } |
| }); |
| if (bestHex) { seen.add(bestHex.toLowerCase()); results.push(bestHex); } |
| } |
| } |
| }); |
| |
| return results.slice(0, 6); |
| }}, |
| { key: 'sequential', title: 'Sequential', desc: 'For <strong>numeric scales</strong>; gradient from <strong>dark to light</strong>. Ideal for <strong>heatmaps</strong>.', generator: (baseHex) => { |
| const c = chroma(baseHex).saturate(0.3); |
| return chroma.scale([c.darken(2), c, c.brighten(2)]).mode('lab').correctLightness(true).colors(6); |
| }}, |
| { key: 'diverging', title: 'Diverging', desc: 'For <strong>centered ranges</strong> with <strong>two extremes</strong> around a <strong>baseline</strong>. (e.g., negatives/positives)', generator: (baseHex) => { |
| const baseH = chroma(baseHex).get('hsl.h'); |
| const compH = (baseH + 180) % 360; |
| const left = chroma.hsl(baseH, 0.75, 0.55); |
| const right = chroma.hsl(compH, 0.75, 0.55); |
| const center = '#ffffff'; |
| const leftRamp = chroma.scale([left, center]).mode('lch').correctLightness(true).colors(4); |
| const rightRamp = chroma.scale([center, right]).mode('lch').correctLightness(true).colors(4); |
| return [leftRamp[0], leftRamp[1], leftRamp[2], rightRamp[1], rightRamp[2], rightRamp[3]]; |
| }} |
| ]; |
| |
| const getCssPrimary = () => { |
| try { return getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim(); } catch { return ''; } |
| }; |
| |
| 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; |
| grid.innerHTML = ''; |
| const css = getCssPrimary(); |
| const baseHex = css && /^#|rgb|hsl/i.test(css) ? chroma(css).hex() : '#E889AB'; |
| |
| cards.forEach((c) => { |
| const card = document.createElement('div'); card.className = 'palette-card'; |
| const sw = document.createElement('div'); sw.className = 'palette-card__swatches'; |
| const colors = c.generator(baseHex).slice(0, 6); |
| colors.forEach(col => { const d = document.createElement('div'); d.className = 'sw'; d.style.background = col; sw.appendChild(d); }); |
| |
| const content = document.createElement('div'); content.className = 'palette-card__content'; |
| const title = document.createElement('div'); title.className = 'palette-card__title'; title.style.textAlign = 'left'; title.style.fontWeight = '800'; title.style.fontSize = '15px'; title.textContent = c.title; |
| const desc = document.createElement('div'); desc.className = 'palette-card__desc'; desc.style.textAlign = 'left'; desc.style.color = 'var(--muted-color)'; desc.style.lineHeight = '1.5'; desc.style.fontSize = '12px'; desc.innerHTML = c.desc; |
| const actions = document.createElement('div'); actions.className = 'palette-card__actions'; |
| const btn = document.createElement('button'); btn.className = 'copy-btn button--ghost'; |
| btn.innerHTML = '<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>'; |
| btn.addEventListener('click', async () => { |
| 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); |
| } |
| }); |
| |
| content.appendChild(title); content.appendChild(desc); |
| actions.appendChild(btn); |
| card.appendChild(actions); card.appendChild(content); card.appendChild(sw); |
| grid.appendChild(card); |
| }); |
| }; |
| |
| const bootstrap = () => { |
| render(); |
| const mo = new MutationObserver(() => render()); |
| mo.observe(document.documentElement, { attributes: true, attributeFilter: ['style'] }); |
| }; |
| |
| if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => ensureChroma(bootstrap), { once: true }); |
| else ensureChroma(bootstrap); |
| })(); |
| </script> |
|
|
|
|