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 {