diff --git a/docs/_layouts/palette.njk b/docs/_layouts/palette.njk
index 882d823ac..d06ea3bd9 100644
--- a/docs/_layouts/palette.njk
+++ b/docs/_layouts/palette.njk
@@ -18,11 +18,15 @@
tweaking: tweaking.chroma,
'tweaking-chroma': tweaking.chroma,
'tweaking-hue': tweaking.chroma,
- 'tweaked-chroma': tweaked.chroma,
- 'tweaked-hue': tweaked.hue,
- 'tweaked-any': tweaked.chroma || tweaked.hue
+ 'tweaking-gray-chroma': tweaking.grayChroma,
+ 'tweaked-chroma': tweaked?.chroma,
+ 'tweaked-hue': tweaked?.hue,
+ 'tweaked-any': tweaked
}"
- :style="{ '--chroma-scale': chromaScale }">
+ :style="{
+ '--chroma-scale': chromaScale,
+ '--gray-chroma': tweaked?.grayChroma ? grayChroma : '',
+ }">
{% include 'breadcrumbs.njk' %}
@@ -36,6 +40,9 @@
.wa-palette-{{ paletteId }}
{% include '../_includes/status.njk' %}
+ {% if not isPro %}
+ PRO
+ {% endif %}
{% if description %}
@@ -48,18 +55,18 @@
{% set maxChroma = 0 %}
-
+
This palette has been tweaked.
- {% raw %}{{ tweakHumanReadable }}{% endraw %}
+ {% raw %}{{ tweakHumanReadable }}{% endraw %}
-
+
Reset
-
+
@@ -81,25 +88,67 @@
{# Initialize to last hue before gray #}
{%- set hueBefore = hues[hues|length - 2] -%}
{% for hue in hues -%}
-{%- set coreColor = palettes[paletteId][hue][palettes[paletteId][hue].maxChromaTint] -%}
+{% set coreTint = palettes[paletteId][hue].maxChromaTint %}
+{%- set coreColor = palettes[paletteId][hue][coreTint] -%}
{%- set maxChroma = coreColor.c if coreColor.c > maxChroma else maxChroma -%}
-
+{% else %}
+
+{% endif %}
|
{{ hue | capitalize }}
|
-
- {% if hue !== 'gray' %}
- {%- set hueAfter = hues[loop.index0 + 1] -%}
- {%- set hueAfter = hues[0] if hueAfter == 'gray' else hueAfter -%}
- {%- set minShift = hueRanges[hue].min - coreColor.h | round -%}
- {%- set maxShift = hueRanges[hue].max - coreColor.h | round -%}
+ |
-
- {{ palettes[paletteId][hue].maxChromaTint }}
-
-
- `
+ --wa-color-{{ hue }}
+
+
+ `
- {%- set hueBefore = hue -%}
- {% else %}
-
- {{ palettes[paletteId][hue].maxChromaTint }}
-
- {% endif %}
|
{% for tint in tints -%}
{%- set color = palettes[paletteId][hue][tint] -%}
-
-
+
+
|
@@ -144,7 +193,8 @@
-
diff --git a/docs/assets/scripts/sidebar-tweaks.js b/docs/assets/scripts/sidebar-tweaks.js
index 2ff4b0fc4..1c245717d 100644
--- a/docs/assets/scripts/sidebar-tweaks.js
+++ b/docs/assets/scripts/sidebar-tweaks.js
@@ -13,7 +13,9 @@ sidebar.palettes = {
sidebar.updateCurrent();
},
- saved: localStorage.savedPalettes ? JSON.parse(localStorage.savedPalettes) : [],
+ updateSaved() {
+ this.saved = localStorage.savedPalettes ? JSON.parse(localStorage.savedPalettes) : [];
+ },
save(saved = this.saved) {
this.saved = saved ?? [];
@@ -26,6 +28,9 @@ sidebar.palettes = {
},
};
+sidebar.palettes.updateSaved();
+addEventListener('storage', event => sidebar.palettes.updateSaved());
+
sidebar.palette = {
getUid() {
let savedPalettes = sidebar.palettes.saved;
@@ -36,7 +41,7 @@ sidebar.palette = {
}
// Find first available number
- for (let i = 1; i < savedPalettes.length + 1; i++) {
+ for (let i = 1; i <= savedPalettes.length + 1; i++) {
if (!uids.has(i)) {
return i;
}
@@ -94,7 +99,7 @@ sidebar.palette = {
sidebar.palettes.save(savedPalettes);
if (sidebar.palette.equals(globalThis.paletteApp?.saved, palette)) {
- paletteApp.saved = null;
+ paletteApp.postDelete();
}
},
@@ -184,18 +189,52 @@ sidebar.updateCurrent = function () {
// We want to start from the longest prefix
prefixes.reverse();
+ let candidates;
+ let matchingPrefix;
for (let prefix of prefixes) {
- let a = document.querySelector(`#sidebar a[href^="${prefix}"]`);
+ candidates = document.querySelectorAll(`#sidebar a[href^="${prefix}"]`);
- if (a) {
- for (let current of document.querySelectorAll('#sidebar a.current')) {
- current.classList.remove('current');
- }
- a.classList.add('current');
+ if (candidates.length > 0) {
+ matchingPrefix = prefix;
break;
}
}
+
+ if (!matchingPrefix) {
+ // Abort mission
+ return;
+ }
+
+ if (matchingPrefix === pathParts.at(-1)) {
+ // Full path matches, check search
+ if (location.search) {
+ candidates = [...candidates];
+
+ let searchParams = new URLSearchParams(location.search);
+
+ if (searchParams.has('uid')) {
+ // Only consider candidates with the same uid
+ candidates = candidates.filter(a => {
+ let params = new URLSearchParams(a.search);
+ return params.get('uid') === searchParams.get('uid');
+ });
+ } else {
+ // Sort candidates based on how many params they have in common, in descending order
+ candidates = candidates.sort((a, b) => {
+ return countSharedSearchParams(searchParams, b.search) - countSharedSearchParams(searchParams, a.search);
+ });
+ }
+ }
+ }
+
+ if (candidates.length > 0) {
+ for (let current of document.querySelectorAll('#sidebar a.current')) {
+ current.classList.remove('current');
+ }
+
+ candidates[0].classList.add('current');
+ }
};
sidebar.render = function () {
@@ -204,3 +243,12 @@ sidebar.render = function () {
sidebar.render();
window.addEventListener('turbo:render', () => sidebar.render());
+
+function countSharedSearchParams(searchParams, search) {
+ if (!search || search === '?') {
+ return 0;
+ }
+
+ let params = new URLSearchParams(search);
+ return [...searchParams.keys()].filter(k => params.get(k) === searchParams.get(k)).length;
+}
diff --git a/docs/assets/scripts/tweak/data.js b/docs/assets/scripts/tweak/data.js
index 41ef8efe3..24c768d6a 100644
--- a/docs/assets/scripts/tweak/data.js
+++ b/docs/assets/scripts/tweak/data.js
@@ -29,6 +29,45 @@ export const hueRanges = {
pink: { min: 320, max: 365 }, // 45
};
+export const moreHue = {
+ red: 'Redder',
+ orange: 'More orange', // https://www.reddit.com/r/grammar/comments/u9n0uo/is_it_oranger_or_more_orange/
+ yellow: 'Yellower',
+ green: 'Greener',
+ cyan: 'More cyan',
+ blue: 'Bluer',
+ indigo: 'More indigo',
+ pink: 'Pinker',
+};
+
+/**
+ * Max gray chroma (% of chroma of undertone) per hue
+ */
+export const maxGrayChroma = {
+ red: 0.2,
+ orange: 0.2,
+ yellow: 0.25,
+ green: 0.25,
+ cyan: 0.3,
+ blue: 0.35,
+ indigo: 0.35,
+ purple: 0.3,
+ pink: 0.25,
+};
+
+export const docsURLs = {
+ colors: '/docs/themes/',
+ palette: '/docs/palettes/',
+ typography: '/docs/themes/',
+};
+
+export const icons = {
+ colors: 'palette',
+ palette: 'swatchbook',
+ brand: 'droplet',
+ typography: 'font-case',
+};
+
export const hues = Object.keys(hueRanges);
export const tints = ['05', '10', '20', '30', '40', '50', '60', '70', '80', '90', '95'];
diff --git a/docs/assets/scripts/tweak/util.js b/docs/assets/scripts/tweak/util.js
new file mode 100644
index 000000000..12ee8dd55
--- /dev/null
+++ b/docs/assets/scripts/tweak/util.js
@@ -0,0 +1,36 @@
+export function normalizeAngles(angles) {
+ // First, normalize
+ angles = angles.map(h => ((h % 360) + 360) % 360);
+
+ // Remove top and bottom 25% and find average
+ let averageHue =
+ angles
+ .toSorted((a, b) => a - b)
+ .slice(angles.length / 4, -angles.length / 4)
+ .reduce((a, b) => a + b, 0) / angles.length;
+
+ for (let i = 0; i < angles.length; i++) {
+ let h = angles[i];
+ let prevHue = angles[i - 1];
+ let delta = h - prevHue;
+
+ if (Math.abs(delta) > 180) {
+ let equivalent = [h + 360, h - 360];
+ // Offset hue to minimize difference in the direction that brings it closer to the average
+ let delta = h - averageHue;
+
+ if (Math.abs(equivalent[0] - prevHue) <= Math.abs(equivalent[1] - prevHue)) {
+ angles[i] = equivalent[0];
+ } else {
+ angles[i] = equivalent[1];
+ }
+ }
+ }
+
+ return angles;
+}
+
+export function subtractAngles(θ1, θ2) {
+ let [a, b] = normalizeAngles([θ1, θ2]);
+ return a - b;
+}
diff --git a/docs/docs/palettes/tweak.css b/docs/docs/palettes/tweak.css
index 548253ba0..ea2971986 100644
--- a/docs/docs/palettes/tweak.css
+++ b/docs/docs/palettes/tweak.css
@@ -25,9 +25,16 @@ wa-dropdown > .color.swatch {
--track-color-inactive: transparent;
--track-color-active: transparent;
--thumb-color: var(--color-tweaked, var(--color));
+ --thumb-shadow: 0 0 0 var(--thumb-gap) var(--wa-color-surface-default),
+ var(--wa-shadow-offset-x-m) var(--wa-shadow-offset-y-m) var(--wa-shadow-blur-m)
+ calc(var(--wa-shadow-offset-x-m) * -1 + var(--thumb-gap)) var(--wa-color-shadow);
+
+ &:active {
+ --thumb-size: 2em;
+ }
&::part(base) {
- background: linear-gradient(to right in oklch, var(--color-1), var(--color-2));
+ background: linear-gradient(to right in var(--color-interpolation-space, oklab), var(--color-1), var(--color-2));
}
}
@@ -63,13 +70,20 @@ wa-dropdown > .color.swatch {
.hue-shift-slider {
--color-1: oklch(from var(--color) l c calc(h + var(--min, 0)));
--color-2: oklch(from var(--color) l c calc(h + var(--max, 0)));
+ --color-interpolation-space: oklch;
}
.chroma-scale-slider {
--color: var(--wa-color-brand);
--color-1: oklch(from var(--color) l calc(c * var(--min)) h);
--color-2: oklch(from var(--color) l calc(c * var(--max)) h);
- --color-tweaked: oklch(from var(--color) l calc(c * var(--chroma-scale)) h);
+}
+
+.gray-chroma-slider {
+ --color: var(--wa-color-gray);
+ --color-1: oklch(from var(--wa-color-gray) l 0 none);
+ --color-2: oklch(from var(--color-gray-undertone) l calc(c * var(--max)) h);
+ margin-top: var(--wa-space-m);
}
.popup {
@@ -91,13 +105,13 @@ wa-dropdown > .color.swatch {
td:not([data-hue='gray'] *) {
--tweak-c: calc(c * var(--chroma-scale, 1));
--tweak-h: calc(h + var(--hue-shift, 0));
- --color-tweaked: oklch(from var(--color) l var(--tweak-c) var(--tweak-h));
+
--color-tweaked-no-chroma-scale: oklch(from var(--color) l c var(--tweak-h));
--color-tweaked-no-hue-shift: oklch(from var(--color) l var(--tweak-c) h);
&:is([data-tint='90'], [data-tint='95']) {
/* Work around https://bugs.webkit.org/show_bug.cgi?id=287637 */
- --color-tweaked: lch(from var(--color) l var(--tweak-c) var(--tweak-h));
+
--color-tweaked-no-chroma-scale: lch(from var(--color) l c var(--tweak-h));
--color-tweaked-no-hue-shift: lch(from var(--color) l var(--tweak-c) h);
@@ -111,14 +125,18 @@ wa-dropdown > .color.swatch {
&:is(.tweaking *) {
--color-2-height: 70%;
+ }
- &:is(.tweaking-chroma *) {
- --color: var(--color-tweaked-no-chroma-scale);
- }
+ &:is(.tweaking-chroma *) {
+ --color: var(--color-tweaked-no-chroma-scale);
+ }
- &:is(.tweaking-hue *) {
- --color: var(--color-tweaked-no-hue-shift);
- }
+ &:is(.tweaking-hue *) {
+ --color: var(--color-tweaked-no-hue-shift);
+ }
+
+ &:is(.tweaking-gray-chroma *) {
+ --color: var(--color-tweaked-no-gray-chroma);
}
}
@@ -159,6 +177,29 @@ wa-dropdown > .color.swatch {
}
}
-[v-if='saved'] {
+/* Better UI before Vue initializes */
+[v-if='saved'],
+[v-if^='tweaked'] {
display: none;
}
+
+.core-color {
+ wa-radio-button::part(base) {
+ width: 2em;
+ height: 2em;
+ padding: 0;
+ border-radius: var(--wa-border-radius-circle);
+ background: var(--color);
+ background-clip: border-box;
+ }
+
+ wa-radio-button:is([checked], :state(checked))::part(base) {
+ box-shadow:
+ inset 0 0 0 var(--indicator-width) var(--indicator-color),
+ inset 0 0 0 calc(var(--indicator-width) + 1.5px) var(--wa-color-surface-default);
+ }
+
+ &::part(form-control-input) {
+ gap: var(--wa-space-xs);
+ }
+}
diff --git a/docs/docs/palettes/tweak.js b/docs/docs/palettes/tweak.js
index e4c4ae08a..28b131210 100644
--- a/docs/docs/palettes/tweak.js
+++ b/docs/docs/palettes/tweak.js
@@ -3,7 +3,8 @@ import Color from 'https://colorjs.io/dist/color.js';
import { createApp, nextTick } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';
import { cdnUrl, hueRanges, hues, Permalink, tints } from '../../assets/scripts/tweak.js';
import { cssImport, cssLiteral, cssRule } from '../../assets/scripts/tweak/code.js';
-import { selectors, urls } from '../../assets/scripts/tweak/data.js';
+import { maxGrayChroma, moreHue, selectors, urls } from '../../assets/scripts/tweak/data.js';
+import { subtractAngles } from '../../assets/scripts/tweak/util.js';
import Prism from '/assets/scripts/prism.js';
await Promise.all(['wa-slider'].map(tag => customElements.whenDefined(tag)));
@@ -34,6 +35,8 @@ for (let palette in allPalettes) {
}
}
+const percentFormatter = value => value.toLocaleString(undefined, { style: 'percent' });
+
let paletteAppSpec = {
data() {
let appRoot = document.querySelector('#palette-app');
@@ -49,12 +52,17 @@ let paletteAppSpec = {
hueRanges,
hueShifts: Object.fromEntries(hues.map(hue => [hue, 0])),
chromaScale: 1,
+ grayChroma: undefined,
+ grayColor: undefined,
tweaking: {},
saved: null,
};
},
created() {
+ // Non-reactive variables to expose
+ Object.assign(this, { moreHue });
+
// Read URL params and apply them. This facilitates permalinks.
this.permalink.mapObject(this.hueShifts, {
keyTo: key => key.replace(/-shift$/, ''),
@@ -63,30 +71,49 @@ let paletteAppSpec = {
valueTo: value => (!value ? 0 : Number(value)),
});
+ this.grayChroma = this.originalGrayChroma;
+ this.grayColor = this.originalGrayColor;
+
if (location.search) {
// Update from URL
this.permalink.writeTo(this.hueShifts);
- if (this.permalink.has('chroma-scale')) {
- this.chromaScale = Number(this.permalink.get('chroma-scale') || 1);
+ for (let param of ['chroma-scale', 'gray-color', 'gray-chroma']) {
+ if (this.permalink.has(param)) {
+ let value = this.permalink.get(param);
+
+ if (!isNaN(value)) {
+ // Convert numeric values to numbers
+ value = Number(value);
+ }
+
+ let prop = camelCase(param);
+ this[prop] = value;
+ }
}
if (this.permalink.has('uid')) {
this.uid = Number(this.permalink.get('uid'));
}
- let palette = { id: this.paletteId, uid: this.uid, search: location.search };
- this.saved = sidebar.palette.getSaved(palette);
+ this.saved = sidebar.palette.getSaved(this.getPalette());
+ }
+ },
+
+ mounted() {
+ for (let ref in this.$refs) {
+ this.$refs[ref].tooltipFormatter = percentFormatter;
}
},
computed: {
- global() {
- return globalThis;
- },
-
tweaks() {
- return { hueShifts: this.hueShifts, chromaScale: this.chromaScale };
+ return {
+ hueShifts: this.hueShifts,
+ chromaScale: this.chromaScale,
+ grayColor: this.grayColor,
+ grayChroma: this.grayChroma,
+ };
},
isTweaked() {
@@ -96,7 +123,7 @@ let paletteAppSpec = {
code() {
let ret = {};
for (let language of ['html', 'css']) {
- let code = getPaletteCode(this.paletteId, this.tweaks, { language, cdnUrl });
+ let code = getPaletteCode(this.paletteId, this.colors, this.tweaked, { language, cdnUrl });
ret[language] = {
raw: code,
highlighted: Prism.highlight(code, Prism.languages[language], language),
@@ -107,47 +134,46 @@ let paletteAppSpec = {
},
colors() {
- let ret = {};
+ return applyTweaks.call(this, this.originalColors, this.tweaks, this.tweaked);
+ },
- for (let hue in this.originalColors) {
- let originalScale = this.originalColors[hue];
- let scale = (ret[hue] = {});
- let descriptors = Object.getOwnPropertyDescriptors(originalScale);
- Object.defineProperties(scale, {
- maxChromaTint: { ...descriptors.maxChromaTint, enumerable: false },
- maxChromaTintRaw: { ...descriptors.maxChromaTintRaw, enumerable: false },
- });
+ colorsMinusChromaScale() {
+ let tweaked = { ...this.tweaked, chromaScale: false };
+ return applyTweaks.call(this, this.originalColors, this.tweaks, tweaked);
+ },
- for (let tint of tints) {
- let oklch = originalScale[tint].coords.slice();
+ colorsMinusHueShifts() {
+ let tweaked = { ...this.tweaked, hue: false };
+ return applyTweaks.call(this, this.originalColors, this.tweaks, tweaked);
+ },
- if (this.hueShifts[hue]) {
- oklch[2] += this.hueShifts[hue];
- }
-
- if (this.chromaScale !== 1) {
- oklch[1] *= this.chromaScale;
- }
-
- scale[tint] = new Color('oklch', oklch);
- }
- }
-
- return ret;
+ colorsMinusGrayChroma() {
+ let tweaked = { ...this.tweaked, grayChroma: false };
+ return applyTweaks.call(this, this.originalColors, this.tweaks, tweaked);
},
tweaked() {
- return {
- chroma: this.chromaScale !== 1,
- hue: Object.values(this.hueShifts).some(Boolean),
+ let anyHueTweaked = Object.values(this.hueShifts).some(Boolean);
+ let hue = anyHueTweaked
+ ? Object.fromEntries(Object.entries(this.hueShifts).map(([hue, shift]) => [hue, shift !== 0]))
+ : false;
+
+ let ret = {
+ chromaScale: this.chromaScale !== 1,
+ hue,
+ grayChroma: this.grayChroma !== this.originalGrayChroma,
+ grayColor: this.grayColor !== this.originalGrayColor,
};
+
+ let anyTweaked = Object.values(ret).some(Boolean);
+ return anyTweaked ? ret : false;
},
tweaksHumanReadable() {
let ret = {};
if (this.chromaScale !== 1) {
- ret.chromaScale = 'more ' + (this.chromaScale > 1 ? 'vibrant' : 'muted');
+ ret.chromaScale = 'More ' + (this.chromaScale > 1 ? 'vibrant' : 'muted');
}
for (let hue in this.hueShifts) {
@@ -158,63 +184,99 @@ let paletteAppSpec = {
}
let relHue = shift < 0 ? arrayPrevious(hues, hue) : arrayNext(hues, hue);
- let hueTweak =
- {
- red: 'redder',
- orange: 'oranger',
- indigo: 'more indigo',
- }[relHue] ?? relHue + 'er';
+ let hueTweak = moreHue[relHue] ?? relHue + 'er';
- ret[hue] = hueTweak + ' ' + hue + 's';
+ ret[hue] = capitalize(hueTweak + ' ' + hue + 's');
+ }
+
+ if (this.tweaked.grayChroma || this.tweaked.grayColor) {
+ if (this.tweaked.grayChroma === 0) {
+ ret.grayChroma = 'Achromatic grays';
+ } else {
+ if (this.tweaked.grayColor) {
+ ret.grayColor = capitalize(this.grayColor) + ' gray undertone';
+ }
+
+ if (this.tweaked.grayChroma) {
+ let more = this.tweaked.grayChroma > this.originalGrayChroma;
+ ret.grayChroma = `More ${more ? 'colorful' : 'neutral'} grays`;
+ }
+ }
}
return ret;
},
originalContrasts() {
+ return getContrasts(this.originalColors);
+ },
+
+ contrasts() {
+ return getContrasts(this.colors, this.originalContrasts);
+ },
+
+ originalCoreColors() {
let ret = {};
-
for (let hue in this.originalColors) {
- ret[hue] = {};
+ let maxChromaTintRaw = this.originalColors[hue].maxChromaTintRaw;
+ ret[hue] = this.originalColors[hue][maxChromaTintRaw];
+ }
+ return ret;
+ },
- for (let tintBg of tints) {
- ret[hue][tintBg] = {};
- let bgColor = this.originalColors[hue][tintBg];
-
- if (!bgColor || !bgColor.contrast) {
- continue;
- }
-
- for (let tintFg of tints) {
- let contrast = bgColor.contrast(this.originalColors[hue][tintFg], 'WCAG21');
- ret[hue][tintBg][tintFg] = contrast;
- }
- }
+ coreColors() {
+ let ret = {};
+ for (let hue in this.colors) {
+ let maxChromaTintRaw = this.colors[hue].maxChromaTintRaw;
+ ret[hue] = this.colors[hue][maxChromaTintRaw];
}
return ret;
},
- contrasts() {
- let ret = {};
+ originalGrayColor() {
+ let grayHue = this.originalCoreColors.gray.get('h');
+ let minDistance = Infinity;
+ let closestHue = null;
- for (let hue in this.colors) {
- ret[hue] = {};
+ for (let name in this.originalCoreColors) {
+ if (name === 'gray') {
+ continue;
+ }
- for (let tintBg in this.colors[hue]) {
- ret[hue][tintBg] = {};
- let bgColor = this.colors[hue][tintBg];
-
- for (let tintFg in this.colors[hue]) {
- let fgColor = this.colors[hue][tintFg];
- let value = bgColor.contrast(fgColor, 'WCAG21');
- let original = this.originalContrasts[hue][tintBg][tintFg];
- ret[hue][tintBg][tintFg] = { value, original, bgColor, fgColor };
- }
+ let hue = this.originalCoreColors[name].get('h');
+ let distance = Math.abs(subtractAngles(hue, grayHue));
+ if (distance < minDistance) {
+ minDistance = distance;
+ closestHue = name;
}
}
- return ret;
+ return closestHue ?? 'indigo';
+ },
+
+ originalGrayChroma() {
+ let coreTint = this.originalColors.gray.maxChromaTint;
+ let grayChroma = this.originalColors.gray[coreTint].get('c');
+ if (grayChroma === 0 || grayChroma === null) {
+ return 0;
+ }
+
+ let grayColorChroma = this.originalColors[this.originalGrayColor][coreTint].get('c');
+ return grayChroma / grayColorChroma;
+ },
+
+ /**
+ * We want to preserve the original grayChroma selection so that when the user switches to another undertone
+ * that supports higher chromas, their selection will be there.
+ * This property is the gray chroma % that is actually applied.
+ */
+ computedGrayChroma() {
+ return Math.min(this.grayChroma, this.maxGrayChroma);
+ },
+
+ maxGrayChroma() {
+ return maxGrayChroma[this.grayColor] ?? 0.3;
},
},
@@ -230,6 +292,14 @@ let paletteAppSpec = {
this.permalink.set('chroma-scale', this.chromaScale, 1);
},
+ grayColor() {
+ this.permalink.set('gray-color', this.grayColor, this.originalGrayColor);
+ },
+
+ grayChroma() {
+ this.permalink.set('gray-chroma', this.grayChroma, this.originalGrayChroma);
+ },
+
tweaks: {
deep: true,
async handler(value, oldValue) {
@@ -246,6 +316,10 @@ let paletteAppSpec = {
},
methods: {
+ getPalette() {
+ return { id: this.paletteId, uid: this.uid, search: location.search };
+ },
+
save({ silent } = {}) {
let title = silent
? (this.saved?.title ?? this.paletteTitle)
@@ -258,13 +332,15 @@ let paletteAppSpec = {
let uid = this.uid;
if (!uid) {
+ // First time saving
this.uid = uid = sidebar.palette.getUid();
this.permalink.set('uid', uid);
this.permalink.updateLocation();
}
- let palette = { title, id: this.paletteId, uid, search: location.search };
+ let palette = { ...this.getPalette(), uid, title };
+
sidebar.palette.save(palette, this.saved);
this.saved = palette;
},
@@ -286,21 +362,38 @@ let paletteAppSpec = {
deleteSaved() {
sidebar.palette.delete(this.saved);
+ },
+
+ postDelete() {
this.saved = null;
+ this.permalink.delete('uid');
+ this.uid = undefined;
+ this.permalink.updateLocation();
},
- reset() {
- for (let hue in this.hueShifts) {
- this.hueShifts[hue] = 0;
- }
- this.chromaScale = 1;
- },
-
- removeTweak(param) {
- if (param === 'chromaScale') {
+ /**
+ * Remove a specific tweak or all tweaks
+ * @param {string} [param] - The tweak to remove. If not provided, all tweaks are removed.
+ */
+ reset(param) {
+ if (!param || param === 'chromaScale') {
this.chromaScale = 1;
- } else {
+ }
+
+ if (param in this.hueShifts) {
this.hueShifts[param] = 0;
+ } else if (!param) {
+ for (let hue in this.hueShifts) {
+ this.hueShifts[hue] = 0;
+ }
+ }
+
+ if (!param || param === 'grayColor') {
+ this.grayColor = this.originalGrayColor;
+ }
+
+ if (!param || param === 'grayChroma') {
+ this.grayChroma = this.originalGrayChroma;
}
},
},
@@ -336,16 +429,20 @@ let paletteAppSpec = {
};
function init() {
+ let paletteAppContainer = document.querySelector('#palette-app');
globalThis.paletteApp?.unmount?.();
- globalThis.paletteApp = createApp(paletteAppSpec).mount('#palette-app');
+
+ if (!paletteAppContainer) {
+ return;
+ }
+
+ globalThis.paletteApp = createApp(paletteAppSpec).mount(paletteAppContainer);
}
init();
addEventListener('turbo:render', init);
-export function getPaletteCode(paletteId, tweaks, options) {
- let palette = allPalettes[paletteId].colors;
-
+export function getPaletteCode(paletteId, colors, tweaked, options) {
let imports = [];
if (paletteId) {
@@ -353,37 +450,27 @@ export function getPaletteCode(paletteId, tweaks, options) {
}
let css = '';
+ let declarations = [];
- if (tweaks) {
- let { hueShifts, chromaScale = 1 } = tweaks;
- let declarations = [];
-
- if (hueShifts || chromaScale !== 1) {
- for (let hue in hueShifts) {
- let shift = hueShifts[hue];
-
- if ((!shift && chromaScale === 1) || hue === 'orange') {
+ if (tweaked) {
+ for (let hue in colors) {
+ if (hue === 'orange') {
+ continue;
+ } else if (hue === 'gray') {
+ if (!tweaked.grayChroma && !tweaked.grayColor) {
continue;
}
-
- let scale = palette[hue];
-
- for (let tint of ['05', '10', '20', '30', '40', '50', '60', '70', '80', '90', '95']) {
- let color = scale[tint];
-
- if (Array.isArray(color)) {
- color = new Color('oklch', coords);
- } else {
- color = color.clone();
- }
- color.set({ h: h => h + shift, c: c => c * chromaScale });
- let stringified = color.toString({ format: color.inGamut('srgb') ? 'hex' : undefined });
-
- declarations.push(`--wa-color-${hue}-${tint}: ${stringified};`);
- }
-
- declarations.push('');
+ } else if (!tweaked.chromaScale && !tweaked.hue?.[hue]) {
+ continue;
}
+
+ for (let tint of tints) {
+ let color = colors[hue][tint];
+ let stringified = color.toString({ format: color.inGamut('srgb') ? 'hex' : undefined });
+ declarations.push(`--wa-color-${hue}-${tint}: ${stringified};`);
+ }
+
+ declarations.push('');
}
if (declarations.length > 0) {
@@ -409,3 +496,85 @@ function arrayPrevious(array, element) {
let index = array.indexOf(element);
return array[(index - 1 + array.length) % array.length];
}
+
+function applyTweaks(originalColors, tweaks, tweaked) {
+ let ret = {};
+ let { hueShifts, chromaScale = 1, grayColor, grayChroma } = tweaks;
+
+ if (!tweaked) {
+ return originalColors;
+ }
+
+ if (tweaked.grayChroma) {
+ grayChroma = this.computedGrayChroma;
+ }
+
+ for (let hue in originalColors) {
+ let originalScale = originalColors[hue];
+ let scale = (ret[hue] = {});
+ let descriptors = Object.getOwnPropertyDescriptors(originalScale);
+ Object.defineProperties(scale, {
+ maxChromaTint: { ...descriptors.maxChromaTint, enumerable: false },
+ maxChromaTintRaw: { ...descriptors.maxChromaTintRaw, enumerable: false },
+ });
+
+ for (let tint of tints) {
+ let color = originalScale[tint].clone();
+
+ if (tweaked.hue && hueShifts[hue]) {
+ color.set({ h: h => h + hueShifts[hue] });
+ }
+
+ if (tweaked.chromaScale && chromaScale !== 1) {
+ color.set({ c: c => c * chromaScale });
+ }
+
+ if (hue === 'gray' && (tweaked.grayChroma || tweaked.grayColor)) {
+ let colorUndertone = originalColors[grayColor][tint].clone();
+ color = colorUndertone.set({ c: c => c * grayChroma });
+ }
+
+ scale[tint] = color;
+ }
+ }
+
+ return ret;
+}
+
+function camelCase(str) {
+ return (str + '').replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
+}
+
+function capitalize(str) {
+ return str[0].toUpperCase() + str.slice(1);
+}
+
+function getContrasts(colors, originalContrasts) {
+ let ret = {};
+
+ for (let hue in colors) {
+ ret[hue] = {};
+
+ for (let tintBg of tints) {
+ ret[hue][tintBg] = {};
+ let bgColor = colors[hue][tintBg];
+
+ if (!bgColor || !bgColor.contrast) {
+ continue;
+ }
+
+ for (let tintFg of tints) {
+ let fgColor = colors[hue][tintFg];
+ let value = bgColor.contrast(fgColor, 'WCAG21');
+ if (originalContrasts) {
+ let original = originalContrasts[hue][tintBg][tintFg];
+ ret[hue][tintBg][tintFg] = { value, original, bgColor, fgColor };
+ } else {
+ ret[hue][tintBg][tintFg] = value;
+ }
+ }
+ }
+ }
+
+ return ret;
+}
diff --git a/docs/docs/themes/demo.njk b/docs/docs/themes/demo.njk
index 6744c81de..cb8a4ef8e 100644
--- a/docs/docs/themes/demo.njk
+++ b/docs/docs/themes/demo.njk
@@ -10,15 +10,17 @@ override:tags: []
eleventyComputed:
forceTheme: "{{ theme.fileSlug }}"
---
-
+{% set isPro = theme.data.isPro %}
+{% set status = theme.data.status %}
+{% set since = theme.data.since %}
{% set content %}
{% include 'breadcrumbs.njk' %}
{{ theme.data.title }}
-
- {% include 'status.njk' %}
+
+ {% include 'status.njk' %}
{{ theme.data.description | inlineMarkdown | safe }}
{% include 'theme-showcase.njk' %}
@@ -34,30 +36,18 @@ eleventyComputed:
diff --git a/docs/docs/themes/remix.js b/docs/docs/themes/remix.js
index c531a3a75..f997541f0 100644
--- a/docs/docs/themes/remix.js
+++ b/docs/docs/themes/remix.js
@@ -107,6 +107,10 @@ function setDefault(select, value) {
}
function render(changedAspect) {
+ if (!globalThis.demo) {
+ return;
+ }
+
let url = new URL(demo.src);
if (!changedAspect || changedAspect === 'colors') {
diff --git a/docs/docs/themes/showcase.css b/docs/docs/themes/showcase.css
index b4693e0fe..57757fabf 100644
--- a/docs/docs/themes/showcase.css
+++ b/docs/docs/themes/showcase.css
@@ -12,6 +12,11 @@ body,
#mix_and_match {
font-weight: var(--wa-font-weight-semibold);
color: var(--wa-color-text-quiet);
+ margin-block-end: var(--wa-space-xs);
+
+ html:not(.is-remixed) {
+ display: none;
+ }
wa-icon {
vertical-align: -0.15em;
diff --git a/src/styles/native/slider.css b/src/styles/native/slider.css
index 40b9ee4db..c1465e6bd 100644
--- a/src/styles/native/slider.css
+++ b/src/styles/native/slider.css
@@ -55,6 +55,8 @@ input[type='range'] {
0 0 0 var(--thumb-gap) var(--wa-color-surface-default);
-webkit-appearance: none;
margin-top: calc(var(--thumb-size) / -2 + var(--track-height) / 2);
+ transition: var(--wa-transition-fast);
+ transition-property: width, height;
}
&:enabled {
|