Compare commits

..

30 Commits

Author SHA1 Message Date
Cory LaViska
ac376afbaa prevent card example from overflowing 2025-03-12 14:44:05 -04:00
Cory LaViska
1283a696a5 fix switch + tooltip behavior (#793) 2025-03-12 18:22:23 +00:00
Cory LaViska
d12b97b0b0 fix wa-pill and wa-input[pill] styles (#791) 2025-03-12 16:19:50 +00:00
Lea Verou
e5c2884880 [Tooltip] Specify inherited CSS properties on host, fixes #773 (#774)
* [Tooltip] Specify inherited CSS properties on host, fixes #773

* Remove unused `--show-delay` and `--hide-delay`
2025-03-10 15:08:27 -04:00
Lea Verou
1d600a77c4 Fix #566 2025-03-10 14:15:06 -04:00
lindsaym-fa
db3c568ba2 Add generated orange to Anodized palette 2025-03-05 22:27:11 -05:00
lindsaym-fa
4bb9805ba6 Add generated orange to Bright palette 2025-03-05 22:27:11 -05:00
lindsaym-fa
bd935fa8d5 Add generated orange to Classic palette 2025-03-05 22:27:11 -05:00
lindsaym-fa
c3e582b47b Add generated orange to Natural palette 2025-03-05 22:27:11 -05:00
lindsaym-fa
4d094a4e19 Add generated orange to Rudimentary palette 2025-03-05 22:27:11 -05:00
lindsaym-fa
782c404bdf Add generated orange to Default palette 2025-03-05 22:27:11 -05:00
lindsaym-fa
f1438981b2 Add generated orange to Elegant palette 2025-03-05 22:27:11 -05:00
lindsaym-fa
18b88c2f5c Add generated orange to Mild palette 2025-03-05 22:27:11 -05:00
lindsaym-fa
a2d85f49a3 Add generated orange to Vogue palette 2025-03-05 22:27:11 -05:00
Lea Verou
be00026cd3 Update postprocess.js 2025-03-05 22:27:11 -05:00
Lea Verou
58ed88bc5a Add orange to list of hues 2025-03-05 22:27:11 -05:00
Lea Verou
1d14e186f3 Generate missing hues from neighboring hues 2025-03-05 22:27:11 -05:00
Lea Verou
5f672aabc2 Refactor: variable rename for consistency 2025-03-05 22:27:11 -05:00
Lea Verou
db08e12a32 Pave the way for being able to have core colors that are not mapped to any tint 2025-03-05 22:27:11 -05:00
Lea Verou
e0fc639226 Only use hex when color is within sRGB 2025-03-05 22:27:11 -05:00
Lea Verou
e6c662b543 tintless.js -> postprocess.js 2025-03-05 22:27:11 -05:00
lindsaym-fa
d1de9a9a73 Fix incorrect sizing tokens in size utilities 2025-02-26 01:01:39 -05:00
lindsaym-fa
4931de8eb4 Fix text color for filled appearance 2025-02-26 01:01:39 -05:00
Lea Verou
71e7227763 Theme remixing fix: Order of params should not matter (#772)
Also renamed the `theme` export to `getThemeCode` since it was being renamed everywhere it was imported.
2025-02-21 14:03:55 -05:00
Lea Verou
dd671e15aa Changelog (#770) 2025-02-21 13:14:19 -05:00
Cory LaViska
2daeea0349 3.0.0-alpha.11 2025-02-21 12:53:05 -05:00
Cory LaViska
3cb6625c1d update changelog 2025-02-21 12:52:51 -05:00
Lea Verou
c4b5446d01 Fix boundingClientRect issue for elements whose host is display: contents 2025-02-21 12:02:20 -05:00
Lindsay M
41affca083 Allow color tweak tags to wrap (#769) 2025-02-21 11:50:13 -05:00
Lea Verou
132dbfabcc Gray tweaks prototype (#761)
Co-authored-by: Lindsay M <126139086+lindsaym-fa@users.noreply.github.com>
2025-02-20 12:10:43 -05:00
59 changed files with 1125 additions and 575 deletions

View File

@@ -1 +1 @@
["red", "yellow", "green", "cyan", "blue", "indigo", "purple", "pink", "gray"] ["red", "orange", "yellow", "green", "cyan", "blue", "indigo", "purple", "pink", "gray"]

View File

@@ -18,11 +18,15 @@
tweaking: tweaking.chroma, tweaking: tweaking.chroma,
'tweaking-chroma': tweaking.chroma, 'tweaking-chroma': tweaking.chroma,
'tweaking-hue': tweaking.chroma, 'tweaking-hue': tweaking.chroma,
'tweaked-chroma': tweaked.chroma, 'tweaking-gray-chroma': tweaking.grayChroma,
'tweaked-hue': tweaked.hue, 'tweaked-chroma': tweaked?.chroma,
'tweaked-any': tweaked.chroma || tweaked.hue 'tweaked-hue': tweaked?.hue,
'tweaked-any': tweaked
}" }"
:style="{ '--chroma-scale': chromaScale }"> :style="{
'--chroma-scale': chromaScale,
'--gray-chroma': tweaked?.grayChroma ? grayChroma : '',
}">
{% include 'breadcrumbs.njk' %} {% include 'breadcrumbs.njk' %}
@@ -36,6 +40,9 @@
<div class="block-info"> <div class="block-info">
<code class="class">.wa-palette-{{ paletteId }}</code> <code class="class">.wa-palette-{{ paletteId }}</code>
{% include '../_includes/status.njk' %} {% include '../_includes/status.njk' %}
{% if not isPro %}
<wa-badge class="pro" v-if="tweaked">PRO</wa-badge>
{% endif %}
</div> </div>
{% if description %} {% if description %}
<p class="summary"> <p class="summary">
@@ -48,18 +55,20 @@
{% set maxChroma = 0 %} {% set maxChroma = 0 %}
<wa-callout size="small" class="tweaked-callout" variant="brand"> <wa-callout size="small" class="tweaked-callout" variant="warning">
<wa-icon name="sliders-simple" slot="icon" variant="regular"></wa-icon> <wa-icon name="sliders-simple" slot="icon" variant="regular"></wa-icon>
This palette has been tweaked. This palette has been tweaked.
<wa-tag v-for="tweakHumanReadable, param in tweaksHumanReadable" removable @wa-remove="removeTweak(param)">{% raw %}{{ tweakHumanReadable }}{% endraw %}</wa-tag> <div class="wa-cluster wa-gap-xs">
<wa-tag v-for="tweakHumanReadable, param in tweaksHumanReadable" removable @wa-remove="reset(param)">{% raw %}{{ tweakHumanReadable }}{% endraw %}</wa-tag>
</div>
<wa-button @click="reset" appearance="outlined"> <wa-button @click="reset()" appearance="outlined" variant="danger">
<span slot="prefix" class="icon-modifier"> <span slot="prefix" class="icon-modifier">
<wa-icon name="circle-xmark" variant="regular"></wa-icon> <wa-icon name="circle-xmark" variant="regular"></wa-icon>
</span> </span>
Reset Reset
</wa-button> </wa-button>
<wa-button v-if="!saved" @click="save"> <wa-button v-if="!saved" @click="save" variant="success">
<span slot="prefix" class="icon-modifier"> <span slot="prefix" class="icon-modifier">
<wa-icon name="sidebar" variant="regular"></wa-icon> <wa-icon name="sidebar" variant="regular"></wa-icon>
<wa-icon name="circle-plus" class="modifier" style="color: light-dark(var(--wa-color-green-70), var(--wa-color-green-60));"></wa-icon> <wa-icon name="circle-plus" class="modifier" style="color: light-dark(var(--wa-color-green-70), var(--wa-color-green-60));"></wa-icon>
@@ -81,25 +90,67 @@
{# Initialize to last hue before gray #} {# Initialize to last hue before gray #}
{%- set hueBefore = hues[hues|length - 2] -%} {%- set hueBefore = hues[hues|length - 2] -%}
{% for hue in hues -%} {% 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 -%} {%- set maxChroma = coreColor.c if coreColor.c > maxChroma else maxChroma -%}
<tr data-hue="{{ hue }}" class="color-scale" :class="{tweaking: tweaking.{{ hue }}, tweaked: hueShifts.{{ hue }} }" {% if hue === 'gray' %}
<tr data-hue="{{ hue }}" class="color-scale"
:class="{tweaking: tweaking.grayChroma, tweaked: tweaked.grayChroma || tweaked.grayColor }">
{% else %}
<tr data-hue="{{ hue }}" class="color-scale"
:class="{tweaking: tweaking.{{ hue }}, tweaked: hueShifts.{{ hue }} }"
:style="{ '--hue-shift': hueShifts.{{ hue }} || '' }"> :style="{ '--hue-shift': hueShifts.{{ hue }} || '' }">
{% endif %}
<th> <th>
{{ hue | capitalize }} {{ hue | capitalize }}
</th> </th>
<td class="core-column" style="--color: var(--wa-color-{{ hue }})"> <td class="core-column"
{% if hue !== 'gray' %} style="--color: var(--wa-color-{{ hue }})"
{%- set hueAfter = hues[loop.index0 + 1] -%} :style="{
{%- set hueAfter = hues[0] if hueAfter == 'gray' else hueAfter -%} '--color-tweaked': colors.{{ hue }}[{{ coreTint }}],
{%- set minShift = hueRanges[hue].min - coreColor.h | round -%} '--color-gray-undertone': colors[grayColor][{{coreTint}}],
{%- set maxShift = hueRanges[hue].max - coreColor.h | round -%} '--color-tweaked-no-gray-chroma': colorsMinusGrayChroma.{{ hue }}[{{ coreTint }}],
}">
<wa-dropdown> <wa-dropdown>
<div slot="trigger" id="core-{{ hue }}-swatch" data-tint="core" class="color swatch" style="background-color: var(--wa-color-{{ hue }}); color: var(--wa-color-{{ hue }}-{{ '05' if palettes[paletteId][hue].maxChromaTint > 60 else '95' }});"> <div slot="trigger" id="core-{{ hue }}-swatch" data-tint="core" class="color swatch"
{{ palettes[paletteId][hue].maxChromaTint }} style="background-color: var(--wa-color-{{ hue }}); color: var(--wa-color-{{ hue }}-{{ '05' if palettes[paletteId][hue].maxChromaTint > 60 else '95' }});"
<wa-icon name="sliders-simple" class="tweak-icon"></wa-icon> >
</div> {{ palettes[paletteId][hue].maxChromaTint }}
<div class="popup"> <wa-icon name="sliders-simple" class="tweak-icon"></wa-icon>
</div>
<div class="popup">
{% if hue === 'gray' %}
<wa-radio-group class="core-color" orientation="horizontal" v-model="grayColor">
{% for h in hues -%}
{%- if h !== 'gray' -%}
<wa-radio-button id="gray-undertone-{{ h }}" value="{{ h }}" label="{{ h | capitalize }}" style="--color: var(--wa-color-{{ h }})"></wa-radio-button>
<wa-tooltip for="gray-undertone-{{ h }}" hoist>
{{ h | capitalize }}
</wa-tooltip>
{%- endif -%}
{%- endfor -%}
<div slot="label">
Gray undertone
</div>
</wa-radio-group>
<div class="decorated-slider gray-chroma-slider" :style="{'--max': maxGrayChroma}">
<wa-slider name="gray-chroma" v-model="grayChroma" ref="grayChromaSlider"
value="0" min="0" :max="maxGrayChroma" step="0.01"
@input="tweaking.grayChroma = true" @change="tweaking.grayChroma = false">
<div slot="label">
Gray colorfulness
<wa-icon-button @click="grayChroma = originalGrayChroma" class="clear-button" name="circle-xmark" library="system" variant="regular" label="Reset"></wa-icon-button>
</div>
</wa-slider>
<div class="label-min">Neutral</div>
<div class="label-max" v-content="moreHue[grayColor]">Warmer/Cooler</div>
</div>
{% else %}
{%- 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 -%}
<div class="decorated-slider hue-shift-slider" style="--min: {{ minShift }}; --max: {{ maxShift }};"> <div class="decorated-slider hue-shift-slider" style="--min: {{ minShift }}; --max: {{ maxShift }};">
<wa-slider name="{{ hue }}-shift" v-model="hueShifts.{{ hue }}" value="0" <wa-slider name="{{ hue }}-shift" v-model="hueShifts.{{ hue }}" value="0"
min="{{ minShift }}" max="{{ maxShift }}" step="1" min="{{ minShift }}" max="{{ maxShift }}" step="1"
@@ -113,23 +164,23 @@
<div class="label-min">More {{hueBefore}}</div> <div class="label-min">More {{hueBefore}}</div>
<div class="label-max">More {{hueAfter}}</div> <div class="label-max">More {{hueAfter}}</div>
</div> </div>
{%- set hueBefore = hue -%}
{% endif %}
<div class="wa-gap-s"> <div class="wa-gap-s">
<code>--wa-color-{{ hue }}</code> <code>--wa-color-{{ hue }}</code>
<wa-copy-button value="--wa-color-{{ hue }}" copy-label="--wa-color-{{ hue }}"></wa-copy-button> <wa-copy-button value="--wa-color-{{ hue }}" copy-label="--wa-color-{{ hue }}"></wa-copy-button>
</div> </div>
</div>` </div>`
</wa-dropdown> </wa-dropdown>
{%- set hueBefore = hue -%}
{% else %}
<div id="core-{{ hue }}-swatch" class="color swatch" data-tint="core" style="background-color: var(--wa-color-{{ hue }}); color: var(--wa-color-{{ hue }}-{{ '05' if palettes[paletteId][hue].maxChromaTint > 60 else '95' }});">
{{ palettes[paletteId][hue].maxChromaTint }}
</div>
{% endif %}
</td> </td>
{% for tint in tints -%} {% for tint in tints -%}
{%- set color = palettes[paletteId][hue][tint] -%} {%- set color = palettes[paletteId][hue][tint] -%}
<td data-tint="{{ tint }}" style="--color: var(--wa-color-{{ hue }}-{{ tint }})"> <td data-tint="{{ tint }}" style="--color: var(--wa-color-{{ hue }}-{{ tint }})"
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}-{{ tint }})"> :style="{
'--color-tweaked': colors.{{ hue }}[{{ tint }}],
'--color-tweaked-no-gray-chroma': colorsMinusGrayChroma.{{ hue }}[{{ tint }}],
}">
<div class="color swatch" style="--color: var(--wa-color-{{ hue }}-{{ tint }})">
<wa-copy-button value="--wa-color-{{ hue }}-{{ tint }}" copy-label="--wa-color-{{ hue }}-{{ tint }}"></wa-copy-button> <wa-copy-button value="--wa-color-{{ hue }}-{{ tint }}" copy-label="--wa-color-{{ hue }}-{{ tint }}"></wa-copy-button>
</div> </div>
</td> </td>
@@ -144,7 +195,8 @@
<div class="decorated-slider chroma-scale-slider wa-palette-{{ paletteId }}" <div class="decorated-slider chroma-scale-slider wa-palette-{{ paletteId }}"
:class="{ tweaked: chromaScale !== 1 }" :class="{ tweaked: chromaScale !== 1 }"
style="--min: {{ chromaScaleBounds[0] }}; --max: {{ chromaScaleBounds[1] }};"> style="--min: {{ chromaScaleBounds[0] }}; --max: {{ chromaScaleBounds[1] }};">
<wa-slider name="chroma-scale" v-model="chromaScale" value="1" step="0.01" <wa-slider name="chroma-scale" ref="chromaScaleSlider"
v-model="chromaScale" value="1" step="0.01"
min="{{ chromaScaleBounds[0] }}" max="{{ chromaScaleBounds[1] }}" min="{{ chromaScaleBounds[0] }}" max="{{ chromaScaleBounds[1] }}"
@input="tweaking.chroma = true" @input="tweaking.chroma = true"
@change="tweaking.chroma = false"> @change="tweaking.chroma = false">

View File

@@ -13,7 +13,9 @@ sidebar.palettes = {
sidebar.updateCurrent(); sidebar.updateCurrent();
}, },
saved: localStorage.savedPalettes ? JSON.parse(localStorage.savedPalettes) : [], updateSaved() {
this.saved = localStorage.savedPalettes ? JSON.parse(localStorage.savedPalettes) : [];
},
save(saved = this.saved) { save(saved = this.saved) {
this.saved = saved ?? []; this.saved = saved ?? [];
@@ -26,6 +28,9 @@ sidebar.palettes = {
}, },
}; };
sidebar.palettes.updateSaved();
addEventListener('storage', event => sidebar.palettes.updateSaved());
sidebar.palette = { sidebar.palette = {
getUid() { getUid() {
let savedPalettes = sidebar.palettes.saved; let savedPalettes = sidebar.palettes.saved;
@@ -36,7 +41,7 @@ sidebar.palette = {
} }
// Find first available number // 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)) { if (!uids.has(i)) {
return i; return i;
} }
@@ -94,7 +99,7 @@ sidebar.palette = {
sidebar.palettes.save(savedPalettes); sidebar.palettes.save(savedPalettes);
if (sidebar.palette.equals(globalThis.paletteApp?.saved, palette)) { 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 // We want to start from the longest prefix
prefixes.reverse(); prefixes.reverse();
let candidates;
let matchingPrefix;
for (let prefix of prefixes) { for (let prefix of prefixes) {
let a = document.querySelector(`#sidebar a[href^="${prefix}"]`); candidates = document.querySelectorAll(`#sidebar a[href^="${prefix}"]`);
if (a) { if (candidates.length > 0) {
for (let current of document.querySelectorAll('#sidebar a.current')) { matchingPrefix = prefix;
current.classList.remove('current');
}
a.classList.add('current');
break; 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 () { sidebar.render = function () {
@@ -204,3 +243,12 @@ sidebar.render = function () {
sidebar.render(); sidebar.render();
window.addEventListener('turbo:render', () => 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;
}

View File

@@ -1,6 +1,6 @@
/** /**
* Get import code for remixed themes and tweaked palettes. * Get import code for remixed themes and tweaked palettes.
*/ */
export { theme as getThemeCode } from './tweak/code.js'; export { getThemeCode } from './tweak/code.js';
export { cdnUrl, hueRanges, hues, selectors, tints, urls } from './tweak/data.js'; export { cdnUrl, hueRanges, hues, selectors, tints, urls } from './tweak/data.js';
export { default as Permalink } from './tweak/permalink.js'; export { default as Permalink } from './tweak/permalink.js';

View File

@@ -25,18 +25,23 @@ export function cssLiteral(value, options = {}) {
} }
} }
export function theme(base, params, options) { // Params in correct order
export const themeParams = ['colors', 'palette', 'brand', 'typography'];
export function getThemeCode(base, params, options) {
let ret = []; let ret = [];
if (base) { if (base) {
ret.push(urls.theme(base)); ret.push(urls.theme(base));
} }
ret.push( for (let aspect of themeParams) {
...Object.entries(params) let value = params[aspect];
.filter(([aspect, id]) => Boolean(id))
.map(([aspect, id]) => urls[aspect](id)), if (value) {
); ret.push(urls[aspect](value));
}
}
return ret.map(url => cssImport(url, options)).join('\n'); return ret.map(url => cssImport(url, options)).join('\n');
} }

View File

@@ -29,6 +29,45 @@ export const hueRanges = {
pink: { min: 320, max: 365 }, // 45 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 hues = Object.keys(hueRanges);
export const tints = ['05', '10', '20', '30', '40', '50', '60', '70', '80', '90', '95']; export const tints = ['05', '10', '20', '30', '40', '50', '60', '70', '80', '90', '95'];

View File

@@ -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;
}

View File

@@ -19,13 +19,13 @@ icon: card
<div slot="footer"> <div slot="footer">
<wa-button variant="brand" pill>More Info</wa-button> <wa-button variant="brand" pill>More Info</wa-button>
<wa-rating></wa-rating> <wa-rating label="Rating"></wa-rating>
</div> </div>
</wa-card> </wa-card>
<style> <style>
.card-overview { .card-overview {
max-width: 300px; width: 300px;
} }
.card-overview small { .card-overview small {

View File

@@ -42,6 +42,14 @@ wa-code-demo::part(preview) {
<wa-input label="WA Input (url)" type="url"></wa-input> <wa-input label="WA Input (url)" type="url"></wa-input>
``` ```
## Pill shaped text fields
Add the `wa-pill` class to an `<input>` to make it pill-shaped.
```html {.example}
<label>Input <input type="text" placeholder="placeholder" class="wa-pill"></label>
```
## Color Picker ## Color Picker
Basic: Basic:

View File

@@ -25,9 +25,16 @@ wa-dropdown > .color.swatch {
--track-color-inactive: transparent; --track-color-inactive: transparent;
--track-color-active: transparent; --track-color-active: transparent;
--thumb-color: var(--color-tweaked, var(--color)); --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) { &::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 { .hue-shift-slider {
--color-1: oklch(from var(--color) l c calc(h + var(--min, 0))); --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-2: oklch(from var(--color) l c calc(h + var(--max, 0)));
--color-interpolation-space: oklch;
} }
.chroma-scale-slider { .chroma-scale-slider {
--color: var(--wa-color-brand); --color: var(--wa-color-brand);
--color-1: oklch(from var(--color) l calc(c * var(--min)) h); --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-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 { .popup {
@@ -91,13 +105,13 @@ wa-dropdown > .color.swatch {
td:not([data-hue='gray'] *) { td:not([data-hue='gray'] *) {
--tweak-c: calc(c * var(--chroma-scale, 1)); --tweak-c: calc(c * var(--chroma-scale, 1));
--tweak-h: calc(h + var(--hue-shift, 0)); --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-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); --color-tweaked-no-hue-shift: oklch(from var(--color) l var(--tweak-c) h);
&:is([data-tint='90'], [data-tint='95']) { &:is([data-tint='90'], [data-tint='95']) {
/* Work around https://bugs.webkit.org/show_bug.cgi?id=287637 */ /* 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-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); --color-tweaked-no-hue-shift: lch(from var(--color) l var(--tweak-c) h);
@@ -111,14 +125,18 @@ wa-dropdown > .color.swatch {
&:is(.tweaking *) { &:is(.tweaking *) {
--color-2-height: 70%; --color-2-height: 70%;
}
&:is(.tweaking-chroma *) { &:is(.tweaking-chroma *) {
--color: var(--color-tweaked-no-chroma-scale); --color: var(--color-tweaked-no-chroma-scale);
} }
&:is(.tweaking-hue *) { &:is(.tweaking-hue *) {
--color: var(--color-tweaked-no-hue-shift); --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; 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);
}
}

View File

@@ -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 { 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 { cdnUrl, hueRanges, hues, Permalink, tints } from '../../assets/scripts/tweak.js';
import { cssImport, cssLiteral, cssRule } from '../../assets/scripts/tweak/code.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'; import Prism from '/assets/scripts/prism.js';
await Promise.all(['wa-slider'].map(tag => customElements.whenDefined(tag))); 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 = { let paletteAppSpec = {
data() { data() {
let appRoot = document.querySelector('#palette-app'); let appRoot = document.querySelector('#palette-app');
@@ -49,12 +52,17 @@ let paletteAppSpec = {
hueRanges, hueRanges,
hueShifts: Object.fromEntries(hues.map(hue => [hue, 0])), hueShifts: Object.fromEntries(hues.map(hue => [hue, 0])),
chromaScale: 1, chromaScale: 1,
grayChroma: undefined,
grayColor: undefined,
tweaking: {}, tweaking: {},
saved: null, saved: null,
}; };
}, },
created() { created() {
// Non-reactive variables to expose
Object.assign(this, { moreHue });
// Read URL params and apply them. This facilitates permalinks. // Read URL params and apply them. This facilitates permalinks.
this.permalink.mapObject(this.hueShifts, { this.permalink.mapObject(this.hueShifts, {
keyTo: key => key.replace(/-shift$/, ''), keyTo: key => key.replace(/-shift$/, ''),
@@ -63,30 +71,49 @@ let paletteAppSpec = {
valueTo: value => (!value ? 0 : Number(value)), valueTo: value => (!value ? 0 : Number(value)),
}); });
this.grayChroma = this.originalGrayChroma;
this.grayColor = this.originalGrayColor;
if (location.search) { if (location.search) {
// Update from URL // Update from URL
this.permalink.writeTo(this.hueShifts); this.permalink.writeTo(this.hueShifts);
if (this.permalink.has('chroma-scale')) { for (let param of ['chroma-scale', 'gray-color', 'gray-chroma']) {
this.chromaScale = Number(this.permalink.get('chroma-scale') || 1); 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')) { if (this.permalink.has('uid')) {
this.uid = Number(this.permalink.get('uid')); this.uid = Number(this.permalink.get('uid'));
} }
let palette = { id: this.paletteId, uid: this.uid, search: location.search }; this.saved = sidebar.palette.getSaved(this.getPalette());
this.saved = sidebar.palette.getSaved(palette); }
},
mounted() {
for (let ref in this.$refs) {
this.$refs[ref].tooltipFormatter = percentFormatter;
} }
}, },
computed: { computed: {
global() {
return globalThis;
},
tweaks() { tweaks() {
return { hueShifts: this.hueShifts, chromaScale: this.chromaScale }; return {
hueShifts: this.hueShifts,
chromaScale: this.chromaScale,
grayColor: this.grayColor,
grayChroma: this.grayChroma,
};
}, },
isTweaked() { isTweaked() {
@@ -96,7 +123,7 @@ let paletteAppSpec = {
code() { code() {
let ret = {}; let ret = {};
for (let language of ['html', 'css']) { 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] = { ret[language] = {
raw: code, raw: code,
highlighted: Prism.highlight(code, Prism.languages[language], language), highlighted: Prism.highlight(code, Prism.languages[language], language),
@@ -107,47 +134,46 @@ let paletteAppSpec = {
}, },
colors() { colors() {
let ret = {}; return applyTweaks.call(this, this.originalColors, this.tweaks, this.tweaked);
},
for (let hue in this.originalColors) { colorsMinusChromaScale() {
let originalScale = this.originalColors[hue]; let tweaked = { ...this.tweaked, chromaScale: false };
let scale = (ret[hue] = {}); return applyTweaks.call(this, this.originalColors, this.tweaks, tweaked);
let descriptors = Object.getOwnPropertyDescriptors(originalScale); },
Object.defineProperties(scale, {
maxChromaTint: { ...descriptors.maxChromaTint, enumerable: false },
maxChromaTintRaw: { ...descriptors.maxChromaTintRaw, enumerable: false },
});
for (let tint of tints) { colorsMinusHueShifts() {
let oklch = originalScale[tint].coords.slice(); let tweaked = { ...this.tweaked, hue: false };
return applyTweaks.call(this, this.originalColors, this.tweaks, tweaked);
},
if (this.hueShifts[hue]) { colorsMinusGrayChroma() {
oklch[2] += this.hueShifts[hue]; let tweaked = { ...this.tweaked, grayChroma: false };
} return applyTweaks.call(this, this.originalColors, this.tweaks, tweaked);
if (this.chromaScale !== 1) {
oklch[1] *= this.chromaScale;
}
scale[tint] = new Color('oklch', oklch);
}
}
return ret;
}, },
tweaked() { tweaked() {
return { let anyHueTweaked = Object.values(this.hueShifts).some(Boolean);
chroma: this.chromaScale !== 1, let hue = anyHueTweaked
hue: Object.values(this.hueShifts).some(Boolean), ? 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() { tweaksHumanReadable() {
let ret = {}; let ret = {};
if (this.chromaScale !== 1) { 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) { for (let hue in this.hueShifts) {
@@ -158,63 +184,99 @@ let paletteAppSpec = {
} }
let relHue = shift < 0 ? arrayPrevious(hues, hue) : arrayNext(hues, hue); let relHue = shift < 0 ? arrayPrevious(hues, hue) : arrayNext(hues, hue);
let hueTweak = let hueTweak = moreHue[relHue] ?? relHue + 'er';
{
red: 'redder',
orange: 'oranger',
indigo: 'more indigo',
}[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; return ret;
}, },
originalContrasts() { originalContrasts() {
return getContrasts(this.originalColors);
},
contrasts() {
return getContrasts(this.colors, this.originalContrasts);
},
originalCoreColors() {
let ret = {}; let ret = {};
for (let hue in this.originalColors) { 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) { coreColors() {
ret[hue][tintBg] = {}; let ret = {};
let bgColor = this.originalColors[hue][tintBg]; for (let hue in this.colors) {
let maxChromaTintRaw = this.colors[hue].maxChromaTintRaw;
if (!bgColor || !bgColor.contrast) { ret[hue] = this.colors[hue][maxChromaTintRaw];
continue;
}
for (let tintFg of tints) {
let contrast = bgColor.contrast(this.originalColors[hue][tintFg], 'WCAG21');
ret[hue][tintBg][tintFg] = contrast;
}
}
} }
return ret; return ret;
}, },
contrasts() { originalGrayColor() {
let ret = {}; let grayHue = this.originalCoreColors.gray.get('h');
let minDistance = Infinity;
let closestHue = null;
for (let hue in this.colors) { for (let name in this.originalCoreColors) {
ret[hue] = {}; if (name === 'gray') {
continue;
}
for (let tintBg in this.colors[hue]) { let hue = this.originalCoreColors[name].get('h');
ret[hue][tintBg] = {}; let distance = Math.abs(subtractAngles(hue, grayHue));
let bgColor = this.colors[hue][tintBg]; if (distance < minDistance) {
minDistance = distance;
for (let tintFg in this.colors[hue]) { closestHue = name;
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 };
}
} }
} }
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); 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: { tweaks: {
deep: true, deep: true,
async handler(value, oldValue) { async handler(value, oldValue) {
@@ -246,6 +316,10 @@ let paletteAppSpec = {
}, },
methods: { methods: {
getPalette() {
return { id: this.paletteId, uid: this.uid, search: location.search };
},
save({ silent } = {}) { save({ silent } = {}) {
let title = silent let title = silent
? (this.saved?.title ?? this.paletteTitle) ? (this.saved?.title ?? this.paletteTitle)
@@ -258,13 +332,15 @@ let paletteAppSpec = {
let uid = this.uid; let uid = this.uid;
if (!uid) { if (!uid) {
// First time saving
this.uid = uid = sidebar.palette.getUid(); this.uid = uid = sidebar.palette.getUid();
this.permalink.set('uid', uid); this.permalink.set('uid', uid);
this.permalink.updateLocation(); 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); sidebar.palette.save(palette, this.saved);
this.saved = palette; this.saved = palette;
}, },
@@ -286,21 +362,38 @@ let paletteAppSpec = {
deleteSaved() { deleteSaved() {
sidebar.palette.delete(this.saved); sidebar.palette.delete(this.saved);
},
postDelete() {
this.saved = null; this.saved = null;
this.permalink.delete('uid');
this.uid = undefined;
this.permalink.updateLocation();
}, },
reset() { /**
for (let hue in this.hueShifts) { * Remove a specific tweak or all tweaks
this.hueShifts[hue] = 0; * @param {string} [param] - The tweak to remove. If not provided, all tweaks are removed.
} */
this.chromaScale = 1; reset(param) {
}, if (!param || param === 'chromaScale') {
removeTweak(param) {
if (param === 'chromaScale') {
this.chromaScale = 1; this.chromaScale = 1;
} else { }
if (param in this.hueShifts) {
this.hueShifts[param] = 0; 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() { function init() {
let paletteAppContainer = document.querySelector('#palette-app');
globalThis.paletteApp?.unmount?.(); globalThis.paletteApp?.unmount?.();
globalThis.paletteApp = createApp(paletteAppSpec).mount('#palette-app');
if (!paletteAppContainer) {
return;
}
globalThis.paletteApp = createApp(paletteAppSpec).mount(paletteAppContainer);
} }
init(); init();
addEventListener('turbo:render', init); addEventListener('turbo:render', init);
export function getPaletteCode(paletteId, tweaks, options) { export function getPaletteCode(paletteId, colors, tweaked, options) {
let palette = allPalettes[paletteId].colors;
let imports = []; let imports = [];
if (paletteId) { if (paletteId) {
@@ -353,37 +450,27 @@ export function getPaletteCode(paletteId, tweaks, options) {
} }
let css = ''; let css = '';
let declarations = [];
if (tweaks) { if (tweaked) {
let { hueShifts, chromaScale = 1 } = tweaks; for (let hue in colors) {
let declarations = []; if (hue === 'orange') {
continue;
if (hueShifts || chromaScale !== 1) { } else if (hue === 'gray') {
for (let hue in hueShifts) { if (!tweaked.grayChroma && !tweaked.grayColor) {
let shift = hueShifts[hue];
if ((!shift && chromaScale === 1) || hue === 'orange') {
continue; continue;
} }
} else if (!tweaked.chromaScale && !tweaked.hue?.[hue]) {
let scale = palette[hue]; continue;
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('');
} }
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) { if (declarations.length > 0) {
@@ -409,3 +496,85 @@ function arrayPrevious(array, element) {
let index = array.indexOf(element); let index = array.indexOf(element);
return array[(index - 1 + array.length) % array.length]; 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;
}

View File

@@ -14,11 +14,18 @@ During the alpha period, things might break! We take breaking changes very serio
## Next ## Next
- Fixed `wa-pill` class for text fields
- Fixed `pill` style for `<wa-input>` elements
## 3.0.0-alpha.11
### Color Palettes ### Color Palettes
- Color palette tweaking UI. Tweak hue, grays, overall colorfulness, save or share the results.
- Added a `pink` scale to all color palettes - Added a `pink` scale to all color palettes
- Fixed an incorrect CSS value in `<wa-select>`'s expand icon
- Tweaked hues of all color palettes to make them more distinct and make their hues more intentional - Tweaked hues of all color palettes to make them more distinct and make their hues more intentional
- Dropped `violet` and `teal`, instead using `purple` and `cyan` (this is not just a renaming, the colors have been adjusted too).
- Fixed a bug in `<wa-switch>` that caused tooltips to work incorrectly when toggling the switch
### Design Tokens ### Design Tokens
@@ -27,32 +34,54 @@ You can find them in the first column of each color palette.
### Themes ### Themes
- Improved UI for theme remixing:
- You can now override the brand color of any theme with any of the 9 hues supported.
- Rich previews
- Generated copyable code snippets.
- Permalinks
- Updated Active, Glossy, Playful, and Premium themes so that `--wa-color-brand-fill-loud` uses the core color of the chosen brand color, regardless of tint. - Updated Active, Glossy, Playful, and Premium themes so that `--wa-color-brand-fill-loud` uses the core color of the chosen brand color, regardless of tint.
- You can now override the brand color of any theme with any of the 9 hues supported.
- Improved UI for theme remixing, with previews and generated copyable code snippets.
### Components ### Components
- Various `<wa-radio>` improvements: #### `<wa-radio>`
- Dropped the `base` part. It can now be styled by directly applying CSS to the element itself.
- Added `hint` attribute and corresponding slot.
- Various `<wa-select>` improvements:
- Added the `tag` part (and associated exported parts) to `<wa-select>` to allow targeting the tag that shows when more than the max number of visible items have been selected
- Fixed a bug that prevented the placeholder color from being customized with the `--wa-form-control-placeholder-color` token
- Dropped the `base` part from `<wa-option>` for easier styling. CSS can now be applied directly to the element itself.
- Various `<wa-card>` improvements:
- Fixed a bug where child elements did not have correct rounding when headers and footers were absent.
- Re-introduced `--border-color` so that the card itself can have a different border color than its inner borders.
- Fixed a bug that prevented slots from showing automatically without `with-` attributes
- Fixed a bug in `<wa-select>` that prevented the description from being read by screen readers
- Dropped the `base` part. It can now be styled by directly applying CSS to the element itself.
- Added `hint` attribute and corresponding slot.
#### `<wa-select>`
- Added the `tag` part (and associated exported parts) to `<wa-select>` to allow targeting the tag that shows when more than the max number of visible items have been selected
- Fixed a bug that prevented the placeholder color from being customized with the `--wa-form-control-placeholder-color` token
- Fixed an incorrect CSS value in the expand icon
- Fixed a bug that prevented the description from being read by screen readers
#### `<wa-option>`
- `label` attribute to override the generated label (useful for rich content)
- `defaultLabel` property
- Dropped `getTextLabel()` method (if you need dynamic labels, just set the `label` attribute dynamically)
- Dropped `base` part for easier styling. CSS can now be applied directly to the element itself.
#### `<wa-menu-item>`
- `label` attribute to override the generated label (useful for rich content)
- `defaultLabel` property
- Dropped `getTextLabel()` method (if you need dynamic labels, just set the `label` attribute dynamically)
#### `<wa-card>`
- Fixed a bug where child elements did not have correct rounding when headers and footers were absent.
- Re-introduced `--border-color` so that the card itself can have a different border color than its inner borders.
- Fixed a bug that prevented slots from showing automatically without `with-` attributes
#### `<wa-tab>`
- Fixed a bug that caused `document.createElement('wa-tab')` to fail (which also meant it could not be used in VueJS and other frameworks)
### Docs ### Docs
- Added an orientation example to the native radio docs - Added an orientation example to the native radio docs
- Fixed a number of broken event listeners throughout the docs - Fixed a number of broken event listeners throughout the docs
## 3.0.0-alpha.10 ## 3.0.0-alpha.10
- 🚨 BREAKING: updated all components to use native events instead of `wa-` prefixed events. This will allow components to work more like native elements in your code, frameworks, third-party plugins, etc. To update your code, simply remove the prefix from your event listeners for the following events. - 🚨 BREAKING: updated all components to use native events instead of `wa-` prefixed events. This will allow components to work more like native elements in your code, frameworks, third-party plugins, etc. To update your code, simply remove the prefix from your event listeners for the following events.

View File

@@ -10,15 +10,17 @@ override:tags: []
eleventyComputed: eleventyComputed:
forceTheme: "{{ theme.fileSlug }}" forceTheme: "{{ theme.fileSlug }}"
--- ---
{% set isPro = theme.data.isPro %}
{% set status = theme.data.status %}
{% set since = theme.data.since %}
<link rel="stylesheet" href="/docs/themes/showcase.css" /> <link rel="stylesheet" href="/docs/themes/showcase.css" />
{% set content %} {% set content %}
<header> <header>
{% include 'breadcrumbs.njk' %} {% include 'breadcrumbs.njk' %}
<h1 class="title">{{ theme.data.title }}</h1> <h1 class="title">{{ theme.data.title }}</h1>
<p id="mix_and_match" hidden class="wa-size-s"></p> <p id="mix_and_match" class="wa-size-s"></p>
<p>{% include 'status.njk' %}</p> <p id="theme-status">{% include 'status.njk' %}</p>
<p id="theme-showcase-description">{{ theme.data.description | inlineMarkdown | safe }}</p> <p id="theme-showcase-description">{{ theme.data.description | inlineMarkdown | safe }}</p>
</header> </header>
{% include 'theme-showcase.njk' %} {% include 'theme-showcase.njk' %}
@@ -34,30 +36,18 @@ eleventyComputed:
</wa-image-comparer> </wa-image-comparer>
<script type="module"> <script type="module">
import { urls as stylesheetURLs } from "/assets/scripts/tweak/data.js"; import { urls as stylesheetURLs, docsURLs, icons } from "/assets/scripts/tweak/data.js";
import { theme as getThemeCode } from "/assets/scripts/tweak/code.js"; import { getThemeCode } from "/assets/scripts/tweak/code.js";
function updateTheme() { function updateTheme() {
let params = new URLSearchParams(window.location.search); let params = new URLSearchParams(window.location.search);
params = Object.fromEntries(params.entries()); params = Object.fromEntries(params.entries());
const docsURLs = {
colors: '/docs/themes/',
palette: '/docs/palettes/',
typography: '/docs/themes/'
};
const icons = {
colors: 'palette',
palette: 'swatchbook',
brand: 'droplet',
typography: 'font-case'
};
for (let link of document.querySelectorAll('link.mix-and-match')) { for (let link of document.querySelectorAll('link.mix-and-match')) {
link.remove(); link.remove();
} }
let msgs = []; let tweaks = [];
let code = getThemeCode("{{ theme.fileSlug }}", params, {attributes: 'class="mix-and-match"'}); let code = getThemeCode("{{ theme.fileSlug }}", params, {attributes: 'class="mix-and-match"'});
document.head.insertAdjacentHTML('beforeend', code); document.head.insertAdjacentHTML('beforeend', code);
@@ -72,18 +62,29 @@ function updateTheme() {
} }
let icon = icons[name]; let icon = icons[name];
msgs.push(`<wa-icon name="${icon}" variant="regular"></wa-icon> ${ title }`); tweaks.push(`<wa-icon name="${icon}" variant="regular"></wa-icon> ${ title }`);
} }
} }
for (let p of mix_and_match) { let isRemixed = tweaks.length > 0;
p.hidden = msgs.length === 0; document.documentElement.classList.toggle('is-remixed', isRemixed);
if (msgs.length) {
let icon = if (isRemixed) {
p.innerHTML = `<strong><wa-icon name="arrows-rotate"></wa-icon> Remixed</strong> ` + msgs.map(msg => `<wa-badge appearance=outlined> for (let p of document.querySelectorAll("#theme-status")) {
${ msg }</wa-badge>`).join(' '); let proBadge = p.querySelector(".pro");
if (!proBadge) {
p.insertAdjacentHTML('beforeend', '<wa-badge class="pro">PRO</wa-badge>');
}
}
for (let p of mix_and_match) {
if (tweaks.length) {
p.innerHTML = `<strong><wa-icon name="arrows-rotate"></wa-icon> Remixed</strong> ` + tweaks.map(msg => `<wa-badge appearance=outlined>
${ msg }</wa-badge>`).join(' ');
}
} }
} }
} }
updateTheme(); updateTheme();
</script> </script>

View File

@@ -107,6 +107,10 @@ function setDefault(select, value) {
} }
function render(changedAspect) { function render(changedAspect) {
if (!globalThis.demo) {
return;
}
let url = new URL(demo.src); let url = new URL(demo.src);
if (!changedAspect || changedAspect === 'colors') { if (!changedAspect || changedAspect === 'colors') {

View File

@@ -12,6 +12,11 @@ body,
#mix_and_match { #mix_and_match {
font-weight: var(--wa-font-weight-semibold); font-weight: var(--wa-font-weight-semibold);
color: var(--wa-color-text-quiet); color: var(--wa-color-text-quiet);
margin-block-end: var(--wa-space-xs);
html:not(.is-remixed) {
display: none;
}
wa-icon { wa-icon {
vertical-align: -0.15em; vertical-align: -0.15em;

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "@shoelace-style/webawesome", "name": "@shoelace-style/webawesome",
"version": "3.0.0-alpha.10", "version": "3.0.0-alpha.11",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@shoelace-style/webawesome", "name": "@shoelace-style/webawesome",
"version": "3.0.0-alpha.10", "version": "3.0.0-alpha.11",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ctrl/tinycolor": "^4.1.0", "@ctrl/tinycolor": "^4.1.0",

View File

@@ -1,7 +1,7 @@
{ {
"name": "@shoelace-style/webawesome", "name": "@shoelace-style/webawesome",
"description": "A forward-thinking library of web components.", "description": "A forward-thinking library of web components.",
"version": "3.0.0-alpha.10", "version": "3.0.0-alpha.11",
"homepage": "https://webawesome.com/", "homepage": "https://webawesome.com/",
"author": "Web Awesome", "author": "Web Awesome",
"license": "MIT", "license": "MIT",

View File

@@ -12,7 +12,6 @@ import { dirname, join, relative } from 'path';
import process from 'process'; import process from 'process';
import copy from 'recursive-copy'; import copy from 'recursive-copy';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { preprocessStylesheet } from './preprocess-css.js';
import { cdnDir, distDir, docsDir, rootDir, runScript, siteDir } from './utils.js'; import { cdnDir, distDir, docsDir, rootDir, runScript, siteDir } from './utils.js';
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -37,7 +36,6 @@ async function buildAll() {
await generateManifest(); await generateManifest();
await generateReactWrappers(); await generateReactWrappers();
await generateTypes(); await generateTypes();
await preprocessStyles();
await generateStyles(); await generateStyles();
// copy everything to unbundled before we generate bundles. // copy everything to unbundled before we generate bundles.
@@ -107,23 +105,6 @@ function generateReactWrappers() {
return Promise.resolve(); return Promise.resolve();
} }
/**
* Generate preprocessed CSS
*/
async function preprocessStyles() {
const preprocessedCSSFiles = await globby(join(rootDir, 'src/styles/**/*.css.njk'));
if (preprocessedCSSFiles.length > 0) {
spinner.start('Preprocessing stylesheets');
for (let filePath of preprocessedCSSFiles) {
await preprocessStylesheet(filePath);
}
spinner.succeed();
}
}
/** /**
* Copies theme stylesheets to the dist. * Copies theme stylesheets to the dist.
*/ */
@@ -390,7 +371,6 @@ if (isDeveloping) {
try { try {
const isTestFile = filename.includes('.test.ts'); const isTestFile = filename.includes('.test.ts');
const isCssStylesheet = filename.includes('.css'); const isCssStylesheet = filename.includes('.css');
const isPreprocessedStylesheet = filename.endsWith('.css.njk');
const isComponent = const isComponent =
filename.includes('components/') && filename.includes('.ts') && !isCssStylesheet && !isTestFile; filename.includes('components/') && filename.includes('.ts') && !isCssStylesheet && !isTestFile;
@@ -401,10 +381,6 @@ if (isDeveloping) {
await regenerateBundle(); await regenerateBundle();
if (isPreprocessedStylesheet || filename.endsWith('src/styles/data.js')) {
await preprocessStyles();
}
// Copy stylesheets when CSS files change // Copy stylesheets when CSS files change
if (isCssStylesheet) { if (isCssStylesheet) {
await generateStyles(); await generateStyles();

View File

@@ -1,61 +0,0 @@
import { readFile, writeFile } from 'fs/promises';
import nunjucks from 'nunjucks';
import * as prettier from 'prettier';
import prettierConfig from '../prettier.config.js';
import * as globalData from '../src/styles/data.js';
const prelude = inputFilename => `/* DO NOT EDIT THIS FILE. It is generated from "${inputFilename}" */`;
let env = nunjucks.configure({ autoescape: false });
let filenameVariables = Object.keys(globalData)
.filter(key => key.endsWith('s'))
.map(key => key.slice(0, -1));
let filenameVariablesRegex = RegExp(`\\{\\{\\s*(${filenameVariables.join('|')})\\s*\\}\\}`, 'g');
export async function preprocessStylesheet(inputPath) {
const content = await readFile(inputPath, 'utf-8');
let outputPath = inputPath.replace(/\.njk$/, '');
let filename = outputPath.split('/').pop();
if (filenameVariablesRegex.test(filename)) {
// NOTE only supports a single variable right now
filenameVariablesRegex.lastIndex = 0;
let ret = [];
for (let match of filename.matchAll(filenameVariablesRegex)) {
let variable = match[1];
let values = globalData[variable + 's'];
for (let value of values) {
let data = { [variable]: value };
let css = await renderCSS(content, { data, inputPath, outputPath });
// Now use Nunjucks *again*, to render the actual filename
let localOutputFilePath = env.renderString(outputPath, data);
ret.push(writeFile(localOutputFilePath, css, 'utf-8'));
}
}
return Promise.all(ret);
} else {
let css = await renderCSS(content, { inputPath, outputPath });
return writeFile(outputPath, css, 'utf-8');
}
// TODO add generated files to .gitignore?
}
export async function renderCSS(content, { data, inputPath, outputPath } = {}) {
let inputFilename = inputPath.split('/').pop();
data = data ? { ...globalData, ...data } : globalData;
let css = env.renderString(content, data);
if (prelude) {
css = prelude(inputFilename) + '\n' + css;
}
css = await prettier.format(css, { ...prettierConfig, filepath: outputPath });
return css;
}

View File

@@ -52,6 +52,7 @@ import styles from './button.css';
@customElement('wa-button') @customElement('wa-button')
export default class WaButton extends WebAwesomeFormAssociatedElement { export default class WaButton extends WebAwesomeFormAssociatedElement {
static shadowStyle = [variantStyles, appearanceStyles, sizeStyles, nativeStyles, styles]; static shadowStyle = [variantStyles, appearanceStyles, sizeStyles, nativeStyles, styles];
static rectProxy = 'button';
static get validators() { static get validators() {
return [...super.validators, MirrorValidator()]; return [...super.validators, MirrorValidator()];
@@ -224,17 +225,6 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
this.button.blur(); this.button.blur();
} }
getBoundingClientRect(): DOMRect {
let rect = super.getBoundingClientRect();
let buttonRect = this.button.getBoundingClientRect();
if (rect.width === 0 && buttonRect.width > 0) {
return buttonRect;
}
return rect;
}
render() { render() {
const isLink = this.isLink(); const isLink = this.isLink();
const tag = isLink ? literal`a` : literal`button`; const tag = isLink ? literal`a` : literal`button`;

View File

@@ -50,6 +50,7 @@ import styles from './radio-button.css';
@customElement('wa-radio-button') @customElement('wa-radio-button')
export default class WaRadioButton extends WebAwesomeFormAssociatedElement { export default class WaRadioButton extends WebAwesomeFormAssociatedElement {
static shadowStyle = [variantStyles, appearanceStyles, sizeStyles, nativeStyles, buttonStyles, styles]; static shadowStyle = [variantStyles, appearanceStyles, sizeStyles, nativeStyles, buttonStyles, styles];
static rectProxy = 'input';
private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix'); private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix');

View File

@@ -208,13 +208,13 @@
&::slotted(wa-divider) { &::slotted(wa-divider) {
--spacing: var(--wa-space-xs); --spacing: var(--wa-space-xs);
} }
}
&::slotted(small) {
display: block; slot:not([name])::slotted(small) {
font-size: var(--wa-font-size-s); display: block;
font-weight: var(--wa-font-weight-semibold); font-size: var(--wa-font-size-s);
color: var(--wa-color-text-quiet); font-weight: var(--wa-font-weight-semibold);
padding-block: var(--wa-space-xs); color: var(--wa-color-text-quiet);
padding-inline: var(--wa-space-xl); padding-block: var(--wa-space-xs);
} padding-inline: var(--wa-space-xl);
} }

View File

@@ -49,6 +49,7 @@ import styles from './switch.css';
*/ */
@customElement('wa-switch') @customElement('wa-switch')
export default class WaSwitch extends WebAwesomeFormAssociatedElement { export default class WaSwitch extends WebAwesomeFormAssociatedElement {
static shadowRootOptions = { ...WebAwesomeFormAssociatedElement.shadowRootOptions, delegatesFocus: true };
static shadowStyle = [formControlStyles, sizeStyles, styles]; static shadowStyle = [formControlStyles, sizeStyles, styles];
static get validators() { static get validators() {

View File

@@ -4,10 +4,16 @@
--max-width: 30ch; --max-width: 30ch;
--padding: var(--wa-space-2xs) var(--wa-space-xs); --padding: var(--wa-space-2xs) var(--wa-space-xs);
/** These styles are added so we don't interfere in the DOM. */
display: inline-block; display: inline-block;
position: absolute; position: absolute;
/** These styles are added so we dont interfere in the DOM. */ /** Defaults for inherited CSS properties */
color: var(--wa-tooltip-content-color);
font-size: var(--wa-tooltip-font-size);
line-height: var(--wa-tooltip-line-height);
text-align: start;
white-space: normal;
} }
.tooltip { .tooltip {
@@ -41,12 +47,6 @@
max-width: var(--max-width); max-width: var(--max-width);
border-radius: var(--border-radius); border-radius: var(--border-radius);
background-color: var(--background-color); background-color: var(--background-color);
font: inherit;
color: var(--wa-tooltip-content-color);
font-size: var(--wa-tooltip-font-size);
line-height: var(--wa-tooltip-line-height);
text-align: start;
white-space: normal;
padding: var(--padding); padding: var(--padding);
user-select: none; user-select: none;
-webkit-user-select: none; -webkit-user-select: none;

View File

@@ -35,11 +35,8 @@ import styles from './tooltip.css';
* *
* @cssproperty --background-color - The tooltip's background color. * @cssproperty --background-color - The tooltip's background color.
* @cssproperty --border-radius - The radius of the tooltip's corners. * @cssproperty --border-radius - The radius of the tooltip's corners.
* @cssproperty --text-color - The color of the tooltip's content.
* @cssproperty --max-width - The maximum width of the tooltip before its content will wrap. * @cssproperty --max-width - The maximum width of the tooltip before its content will wrap.
* @cssproperty --padding - The padding within the tooltip. * @cssproperty --padding - The padding within the tooltip.
* @cssproperty --hide-delay - The amount of time to wait before hiding the tooltip when hovering.
* @cssproperty --show-delay - The amount of time to wait before showing the tooltip when hovering.
*/ */
@customElement('wa-tooltip') @customElement('wa-tooltip')
export default class WaTooltip extends WebAwesomeElement { export default class WaTooltip extends WebAwesomeElement {

View File

@@ -204,6 +204,32 @@ export default class WebAwesomeElement extends LitElement {
); );
} }
getBoundingClientRect(): DOMRect {
let rect = super.getBoundingClientRect();
if (rect.width === 0 && rect.height === 0) {
let Self = this.constructor as typeof WebAwesomeElement;
if (Self.rectProxy) {
let element = this[Self.rectProxy as keyof this];
if (element instanceof Element) {
let childRect = element.getBoundingClientRect();
if (childRect.width > 0 || childRect.height > 0) {
return childRect;
}
}
}
}
return rect;
}
/**
* If getBoundingClientRect() returns an empty rect,
* should we check another element?
*/
static rectProxy: undefined | string;
static createProperty(name: PropertyKey, options?: PropertyDeclaration): void { static createProperty(name: PropertyKey, options?: PropertyDeclaration): void {
if (options) { if (options) {
if (options.initial !== undefined && options.default === undefined) { if (options.initial !== undefined && options.default === undefined) {

View File

@@ -1,4 +1,3 @@
/* DO NOT EDIT THIS FILE. It is generated from "base.css.njk" */
:where(:root), :where(:root),
:host, :host,
:where([class^='wa-theme-'], [class*=' wa-theme-']), :where([class^='wa-theme-'], [class*=' wa-theme-']),
@@ -9,20 +8,16 @@
* --wa-color-brand-if-lt-N ➡️ 100% if key < N, 0% otherwise * --wa-color-brand-if-lt-N ➡️ 100% if key < N, 0% otherwise
* --wa-color-brand-if-gte-N ➡️ 100% if key >= N, 0% otherwise * --wa-color-brand-if-gte-N ➡️ 100% if key >= N, 0% otherwise
*/ */
--wa-color-brand-if-lt-40: calc(clamp(0, 40 - var(--wa-color-brand-key), 1) * 100%); --wa-color-brand-if-lt-40: calc(clamp(0, 40 - var(--wa-color-brand-key), 1) * 100%);
--wa-color-brand-if-gte-40: calc(100% - var(--wa-color-brand-if-lt-40));
--wa-color-brand-if-lt-50: calc(clamp(0, 50 - var(--wa-color-brand-key), 1) * 100%); --wa-color-brand-if-lt-50: calc(clamp(0, 50 - var(--wa-color-brand-key), 1) * 100%);
--wa-color-brand-if-gte-50: calc(100% - var(--wa-color-brand-if-lt-50));
--wa-color-brand-if-lt-60: calc(clamp(0, 60 - var(--wa-color-brand-key), 1) * 100%); --wa-color-brand-if-lt-60: calc(clamp(0, 60 - var(--wa-color-brand-key), 1) * 100%);
--wa-color-brand-if-gte-60: calc(100% - var(--wa-color-brand-if-lt-60));
--wa-color-brand-if-lt-70: calc(clamp(0, 70 - var(--wa-color-brand-key), 1) * 100%); --wa-color-brand-if-lt-70: calc(clamp(0, 70 - var(--wa-color-brand-key), 1) * 100%);
--wa-color-brand-if-gte-70: calc(100% - var(--wa-color-brand-if-lt-70));
--wa-color-brand-if-lt-80: calc(clamp(0, 80 - var(--wa-color-brand-key), 1) * 100%); --wa-color-brand-if-lt-80: calc(clamp(0, 80 - var(--wa-color-brand-key), 1) * 100%);
--wa-color-brand-if-gte-40: calc(100% - var(--wa-color-brand-if-lt-40));
--wa-color-brand-if-gte-50: calc(100% - var(--wa-color-brand-if-lt-50));
--wa-color-brand-if-gte-60: calc(100% - var(--wa-color-brand-if-lt-60));
--wa-color-brand-if-gte-70: calc(100% - var(--wa-color-brand-if-lt-70));
--wa-color-brand-if-gte-80: calc(100% - var(--wa-color-brand-if-lt-80)); --wa-color-brand-if-gte-80: calc(100% - var(--wa-color-brand-if-lt-80));
/* /*
@@ -30,7 +25,6 @@
* --wa-color-brand-N-max ➡️ var(--color-brand) if key <= N, var(--color-brand-N) otherwise * --wa-color-brand-N-max ➡️ var(--color-brand) if key <= N, var(--color-brand-N) otherwise
* --wa-color-brand-N-min ➡️ var(--color-brand) if key >= N, var(--color-brand-N) otherwise * --wa-color-brand-N-min ➡️ var(--color-brand) if key >= N, var(--color-brand-N) otherwise
*/ */
--wa-color-brand-40-max: color-mix( --wa-color-brand-40-max: color-mix(
in oklab, in oklab,
var(--wa-color-brand) var(--wa-color-brand-if-lt-40), var(--wa-color-brand) var(--wa-color-brand-if-lt-40),

View File

@@ -1,36 +0,0 @@
:where(:root),
:host,
:where([class^='wa-theme-'], [class*=' wa-theme-']),
:where([class^='wa-palette-'], [class*=' wa-palette-']),
:where([class^='wa-brand-'], [class*=' wa-brand-']) {
/**
* Conditional tokens for use in color-mix()
* --wa-color-brand-if-lt-N ➡️ 100% if key < N, 0% otherwise
* --wa-color-brand-if-gte-N ➡️ 100% if key >= N, 0% otherwise
*/
{% for tint in ['40', '50', '60', '70', '80'] %}
--wa-color-brand-if-lt-{{ tint }}: calc(clamp(0, {{ tint }} - var(--wa-color-brand-key), 1) * 100%);
--wa-color-brand-if-gte-{{ tint }}: calc(100% - var(--wa-color-brand-if-lt-{{ tint }}));
{% endfor %}
/*
* Convenience tokens for common tint cutoffs
* --wa-color-brand-N-max ➡️ var(--color-brand) if key <= N, var(--color-brand-N) otherwise
* --wa-color-brand-N-min ➡️ var(--color-brand) if key >= N, var(--color-brand-N) otherwise
*/
{% for tint in ['40', '50', '60', '70'] %}
--wa-color-brand-{{ tint }}-max: color-mix(
in oklab,
var(--wa-color-brand) var(--wa-color-brand-if-lt-{{ tint }}),
var(--wa-color-brand-{{ tint }})
);
--wa-color-brand-{{ tint }}-min: color-mix(
in oklab,
var(--wa-color-brand) var(--wa-color-brand-if-gte-{{ tint }}),
var(--wa-color-brand-{{ tint }})
);
{% endfor %}
/* Text color: white if key < 60, brand-10 otherwise */
--wa-color-brand-on: color-mix(in oklab, var(--wa-color-brand-10) var(--wa-color-brand-if-gte-60), white);
}

View File

@@ -1,4 +1,3 @@
/* DO NOT EDIT THIS FILE. It is generated from "{{ hue }}.css.njk" */
@import url('base.css'); @import url('base.css');
:where(:root), :where(:root),

View File

@@ -1,4 +1,3 @@
/* DO NOT EDIT THIS FILE. It is generated from "{{ hue }}.css.njk" */
@import url('base.css'); @import url('base.css');
:where(:root), :where(:root),

View File

@@ -1,4 +1,3 @@
/* DO NOT EDIT THIS FILE. It is generated from "{{ hue }}.css.njk" */
@import url('base.css'); @import url('base.css');
:where(:root), :where(:root),

View File

@@ -1,4 +1,3 @@
/* DO NOT EDIT THIS FILE. It is generated from "{{ hue }}.css.njk" */
@import url('base.css'); @import url('base.css');
:where(:root), :where(:root),

View File

@@ -1,4 +1,3 @@
/* DO NOT EDIT THIS FILE. It is generated from "{{ hue }}.css.njk" */
@import url('base.css'); @import url('base.css');
:where(:root), :where(:root),

View File

@@ -1,4 +1,3 @@
/* DO NOT EDIT THIS FILE. It is generated from "{{ hue }}.css.njk" */
@import url('base.css'); @import url('base.css');
:where(:root), :where(:root),

View File

@@ -1,4 +1,3 @@
/* DO NOT EDIT THIS FILE. It is generated from "{{ hue }}.css.njk" */
@import url('base.css'); @import url('base.css');
:where(:root), :where(:root),

View File

@@ -1,4 +1,3 @@
/* DO NOT EDIT THIS FILE. It is generated from "{{ hue }}.css.njk" */
@import url('base.css'); @import url('base.css');
:where(:root), :where(:root),

View File

@@ -1,4 +1,3 @@
/* DO NOT EDIT THIS FILE. It is generated from "{{ hue }}.css.njk" */
@import url('base.css'); @import url('base.css');
:where(:root), :where(:root),

View File

@@ -1,13 +0,0 @@
@import url('base.css');
:where(:root),
:host,
:where([class^='wa-theme-'], [class*=' wa-theme-']),
:where([class^='wa-palette-'], [class*=' wa-palette-']),
.wa-brand-{{ hue }} {
{%- for tint in tints | reverse %}
--wa-color-brand-{{ tint }}: var(--wa-color-{{ hue }}-{{ tint }});
{%- endfor %}
--wa-color-brand: var(--wa-color-{{ hue }});
--wa-color-brand-key: var(--wa-color-{{ hue }}-key);
}

View File

@@ -16,6 +16,20 @@
--wa-color-red: var(--wa-color-red-70); --wa-color-red: var(--wa-color-red-70);
--wa-color-red-key: 70; --wa-color-red-key: 70;
--wa-color-orange-95: oklch(96.462% 0.02077 52.138);
--wa-color-orange-90: oklch(92.556% 0.04363 51.242);
--wa-color-orange-80: #fdbb96 /* oklch(84.396% 0.09052 50.397) */;
--wa-color-orange-70: #eb9c74 /* oklch(76.151% 0.10953 47.299) */;
--wa-color-orange-60: #cf8162 /* oklch(67.86% 0.10698 42.148) */;
--wa-color-orange-50: #aa6248 /* oklch(57.281% 0.1014 40.415) */;
--wa-color-orange-40: #864834 /* oklch(47.233% 0.08958 39.166) */;
--wa-color-orange-30: #6b3727 /* oklch(39.974% 0.07776 38.975) */;
--wa-color-orange-20: #50271a /* oklch(32.519% 0.0649 38.022) */;
--wa-color-orange-10: #32160e /* oklch(23.873% 0.04713 38.423) */;
--wa-color-orange-05: #210c06 /* oklch(18.614% 0.03797 38.589) */;
--wa-color-orange: var(--wa-color-orange-70);
--wa-color-orange-key: 70;
--wa-color-yellow-95: #faf3e1 /* oklch(96.479% 0.02487 89.211) */; --wa-color-yellow-95: #faf3e1 /* oklch(96.479% 0.02487 89.211) */;
--wa-color-yellow-90: #f4e5be /* oklch(92.412% 0.0535 89.488) */; --wa-color-yellow-90: #f4e5be /* oklch(92.412% 0.0535 89.488) */;
--wa-color-yellow-80: #eac673 /* oklch(84.03% 0.1101 86.581) */; --wa-color-yellow-80: #eac673 /* oklch(84.03% 0.1101 86.581) */;

View File

@@ -16,6 +16,20 @@
--wa-color-red: var(--wa-color-red-60); --wa-color-red: var(--wa-color-red-60);
--wa-color-red-key: 60; --wa-color-red-key: 60;
--wa-color-orange-95: oklch(96.406% 0.04001 53.476);
--wa-color-orange-90: oklch(92.395% 0.07984 53.06);
--wa-color-orange-80: oklch(84.389% 0.12224 47.981);
--wa-color-orange-70: oklch(76.55% 0.16521 42.512);
--wa-color-orange-60: #ea7237 /* oklch(68.444% 0.16501 44.349) */;
--wa-color-orange-50: #c0561a /* oklch(57.844% 0.15254 45.085) */;
--wa-color-orange-40: #963e05 /* oklch(47.639% 0.13153 45.898) */;
--wa-color-orange-30: oklch(40.376% 0.11554 45.517);
--wa-color-orange-20: oklch(32.94% 0.09927 45.913);
--wa-color-orange-10: oklch(24.083% 0.07743 46.027);
--wa-color-orange-05: oklch(18.817% 0.06098 48.455);
--wa-color-orange: var(--wa-color-orange-70);
--wa-color-orange-key: 70;
--wa-color-yellow-95: #fff4c0 /* oklch(96.32% 0.0677 97.497) */; --wa-color-yellow-95: #fff4c0 /* oklch(96.32% 0.0677 97.497) */;
--wa-color-yellow-90: #ffe579 /* oklch(92.176% 0.13122 96.089) */; --wa-color-yellow-90: #ffe579 /* oklch(92.176% 0.13122 96.089) */;
--wa-color-yellow-80: #ffbf18 /* oklch(84.069% 0.16897 83.446) */; --wa-color-yellow-80: #ffbf18 /* oklch(84.069% 0.16897 83.446) */;

View File

@@ -16,6 +16,20 @@
--wa-color-red: var(--wa-color-red-50); --wa-color-red: var(--wa-color-red-50);
--wa-color-red-key: 50; --wa-color-red-key: 50;
--wa-color-orange-95: oklch(96.245% 0.04153 59.224);
--wa-color-orange-90: oklch(92.468% 0.07656 58.647);
--wa-color-orange-80: oklch(84.375% 0.11283 54.179);
--wa-color-orange-70: #fb945a /* oklch(76.369% 0.14454 48.621) */;
--wa-color-orange-60: #f26b31 /* oklch(68.509% 0.18046 41.503) */;
--wa-color-orange-50: #cf4812 /* oklch(58.288% 0.18026 38.576) */;
--wa-color-orange-40: oklch(48.175% 0.16316 38.526);
--wa-color-orange-30: oklch(40.779% 0.13925 37.899);
--wa-color-orange-20: oklch(33.129% 0.11288 38.58);
--wa-color-orange-10: oklch(24.259% 0.0831 38.502);
--wa-color-orange-05: oklch(18.969% 0.06527 38.137);
--wa-color-orange: var(--wa-color-orange-60);
--wa-color-orange-key: 60;
--wa-color-yellow-95: #fef2bf /* oklch(95.823% 0.06674 96.369) */; --wa-color-yellow-95: #fef2bf /* oklch(95.823% 0.06674 96.369) */;
--wa-color-yellow-90: #fde588 /* oklch(92.2% 0.11633 95.327) */; --wa-color-yellow-90: #fde588 /* oklch(92.2% 0.11633 95.327) */;
--wa-color-yellow-80: #f4c34e /* oklch(83.998% 0.14252 85.76) */; --wa-color-yellow-80: #f4c34e /* oklch(83.998% 0.14252 85.76) */;

View File

@@ -16,6 +16,20 @@
--wa-color-red: var(--wa-color-red-50); --wa-color-red: var(--wa-color-red-50);
--wa-color-red-key: 50; --wa-color-red-key: 50;
--wa-color-orange-95: oklch(96.494% 0.0335 57.914);
--wa-color-orange-90: oklch(92.556% 0.06963 56.631);
--wa-color-orange-80: oklch(84.494% 0.12276 53.381);
--wa-color-orange-70: oklch(76.375% 0.17194 46.091);
--wa-color-orange-60: #eb713f /* oklch(68.398% 0.16422 41.446) */;
--wa-color-orange-50: #cb4b1d /* oklch(58.153% 0.17174 38.404) */;
--wa-color-orange-40: #a2310c /* oklch(48.028% 0.15488 36.538) */;
--wa-color-orange-30: #7f2810 /* oklch(40.591% 0.12506 35.663) */;
--wa-color-orange-20: #5d1d0e /* oklch(32.908% 0.09683 34.387) */;
--wa-color-orange-10: #3a1005 /* oklch(24.088% 0.06954 35.613) */;
--wa-color-orange-05: #270803 /* oklch(18.801% 0.05509 34.149) */;
--wa-color-orange: var(--wa-color-orange-70);
--wa-color-orange-key: 70;
--wa-color-yellow-95: #fef3cd /* oklch(96.322% 0.05069 93.748) */; --wa-color-yellow-95: #fef3cd /* oklch(96.322% 0.05069 93.748) */;
--wa-color-yellow-90: #ffe495 /* oklch(92.377% 0.10246 91.296) */; --wa-color-yellow-90: #ffe495 /* oklch(92.377% 0.10246 91.296) */;
--wa-color-yellow-80: #fac22b /* oklch(84.185% 0.16263 85.991) */; --wa-color-yellow-80: #fac22b /* oklch(84.185% 0.16263 85.991) */;

View File

@@ -16,6 +16,20 @@
--wa-color-red: var(--wa-color-red-40); --wa-color-red: var(--wa-color-red-40);
--wa-color-red-key: 40; --wa-color-red-key: 40;
--wa-color-orange-95: oklch(96.238% 0.02664 61.788);
--wa-color-orange-90: #fbe1cc /* oklch(92.512% 0.04019 60.45) */;
--wa-color-orange-80: #edc1a0 /* oklch(84.097% 0.06661 58.236) */;
--wa-color-orange-70: #dda178 /* oklch(75.734% 0.09064 55.123) */;
--wa-color-orange-60: #cd8351 /* oklch(67.646% 0.1134 53.172) */;
--wa-color-orange-50: #b65d22 /* oklch(57.437% 0.13446 49.881) */;
--wa-color-orange-40: oklch(47.576% 0.13426 46.452);
--wa-color-orange-30: oklch(40.382% 0.12087 47.003);
--wa-color-orange-20: oklch(32.846% 0.0965 46.227);
--wa-color-orange-10: oklch(24.06% 0.06873 45.849);
--wa-color-orange-05: #260900 /* oklch(18.727% 0.05359 44.791) */;
--wa-color-orange: var(--wa-color-orange-50);
--wa-color-orange-key: 50;
--wa-color-yellow-95: #f7f4da /* oklch(96.266% 0.03422 101.63) */; --wa-color-yellow-95: #f7f4da /* oklch(96.266% 0.03422 101.63) */;
--wa-color-yellow-90: #ede7c3 /* oklch(92.349% 0.04769 99.435) */; --wa-color-yellow-90: #ede7c3 /* oklch(92.349% 0.04769 99.435) */;
--wa-color-yellow-80: #d8ca96 /* oklch(83.793% 0.06999 94.829) */; --wa-color-yellow-80: #d8ca96 /* oklch(83.793% 0.06999 94.829) */;

View File

@@ -16,6 +16,20 @@
--wa-color-red: var(--wa-color-red-40); --wa-color-red: var(--wa-color-red-40);
--wa-color-red-key: 40; --wa-color-red-key: 40;
--wa-color-orange-95: oklch(96.126% 0.05417 66.333);
--wa-color-orange-90: oklch(92.413% 0.07898 62.545);
--wa-color-orange-80: #f9bd86 /* oklch(84.088% 0.09891 63.847) */;
--wa-color-orange-70: #e2a05f /* oklch(75.707% 0.11352 64.057) */;
--wa-color-orange-60: #d18228 /* oklch(67.572% 0.13809 64.146) */;
--wa-color-orange-50: oklch(57.202% 0.13583 64.309);
--wa-color-orange-40: oklch(47.462% 0.13789 64.939);
--wa-color-orange-30: oklch(40.049% 0.12025 65.207);
--wa-color-orange-20: oklch(32.552% 0.09777 64.859);
--wa-color-orange-10: oklch(23.884% 0.07141 64.246);
--wa-color-orange-05: oklch(18.698% 0.05597 65.589);
--wa-color-orange: var(--wa-color-orange-60);
--wa-color-orange-key: 60;
--wa-color-yellow-95: #fff4b3 /* oklch(96.064% 0.08319 99.657) */; --wa-color-yellow-95: #fff4b3 /* oklch(96.064% 0.08319 99.657) */;
--wa-color-yellow-90: #fee58c /* oklch(92.346% 0.11242 94.205) */; --wa-color-yellow-90: #fee58c /* oklch(92.346% 0.11242 94.205) */;
--wa-color-yellow-80: #e3c868 /* oklch(83.63% 0.12003 93.943) */; --wa-color-yellow-80: #e3c868 /* oklch(83.63% 0.12003 93.943) */;

View File

@@ -16,6 +16,20 @@
--wa-color-red: var(--wa-color-red-50); --wa-color-red: var(--wa-color-red-50);
--wa-color-red-key: 50; --wa-color-red-key: 50;
--wa-color-orange-95: #f8f0ec /* oklch(96.084% 0.01043 54.557) */;
--wa-color-orange-90: #f2e3d8 /* oklch(92.485% 0.02211 56.694) */;
--wa-color-orange-80: #e4c4ad /* oklch(84.166% 0.04799 57.553) */;
--wa-color-orange-70: #d3a583 /* oklch(75.674% 0.07148 57.481) */;
--wa-color-orange-60: #bc8a65 /* oklch(67.319% 0.08062 57.054) */;
--wa-color-orange-50: #9e6940 /* oklch(56.757% 0.08845 56.746) */;
--wa-color-orange-40: #7e4d27 /* oklch(46.949% 0.08447 57.382) */;
--wa-color-orange-30: #673a17 /* oklch(39.774% 0.0793 55.768) */;
--wa-color-orange-20: #4f2906 /* oklch(32.45% 0.0725 57.629) */;
--wa-color-orange-10: #311702 /* oklch(23.759% 0.05361 57.126) */;
--wa-color-orange-05: #200d01 /* oklch(18.517% 0.04211 57.178) */;
--wa-color-orange: var(--wa-color-orange-50);
--wa-color-orange-key: 50;
--wa-color-yellow-95: #f6f1ea /* oklch(96.009% 0.01076 76.598) */; --wa-color-yellow-95: #f6f1ea /* oklch(96.009% 0.01076 76.598) */;
--wa-color-yellow-90: #eee5d7 /* oklch(92.521% 0.02111 79.091) */; --wa-color-yellow-90: #eee5d7 /* oklch(92.521% 0.02111 79.091) */;
--wa-color-yellow-80: #dbc8a8 /* oklch(84.033% 0.04791 80.753) */; --wa-color-yellow-80: #dbc8a8 /* oklch(84.033% 0.04791 80.753) */;

View File

@@ -16,6 +16,20 @@
--wa-color-red: var(--wa-color-red-50); --wa-color-red: var(--wa-color-red-50);
--wa-color-red-key: 50; --wa-color-red-key: 50;
--wa-color-orange-95: oklch(96.488% 0.04965 58.92);
--wa-color-orange-90: oklch(92.244% 0.08759 57.789);
--wa-color-orange-80: oklch(84.32% 0.12702 56.232);
--wa-color-orange-70: oklch(76.31% 0.17967 50.95);
--wa-color-orange-60: #e87431 /* oklch(68.352% 0.16354 47.083) */;
--wa-color-orange-50: oklch(58.23% 0.17947 43.3);
--wa-color-orange-40: oklch(48.089% 0.16071 40.798);
--wa-color-orange-30: oklch(40.708% 0.13699 39.616);
--wa-color-orange-20: oklch(33.124% 0.1121 39.599);
--wa-color-orange-10: oklch(24.257% 0.08204 39.602);
--wa-color-orange-05: oklch(18.966% 0.06414 39.606);
--wa-color-orange: var(--wa-color-orange-70);
--wa-color-orange-key: 70;
--wa-color-yellow-95: #fff5b4 /* oklch(96.281% 0.08306 100.4) */; --wa-color-yellow-95: #fff5b4 /* oklch(96.281% 0.08306 100.4) */;
--wa-color-yellow-90: #fde572 /* oklch(91.915% 0.13738 97.724) */; --wa-color-yellow-90: #fde572 /* oklch(91.915% 0.13738 97.724) */;
--wa-color-yellow-80: #f4c403 /* oklch(83.862% 0.17104 90.777) */; --wa-color-yellow-80: #f4c403 /* oklch(83.862% 0.17104 90.777) */;

View File

@@ -24,15 +24,22 @@ for (let paletteId in palettes) {
palettes[paletteId] = tokens; palettes[paletteId] = tokens;
for (let hue in tokens) { for (let hue in tokens) {
let tints = Object.assign({}, tokens[hue]); let scale = Object.assign({}, tokens[hue]);
tokens[hue] = tints; tokens[hue] = scale;
let maxChromaTint = DEFAULT_ACCENT; let maxChromaTint = DEFAULT_ACCENT; // TODO handle scale.core
let maxChroma = tints[DEFAULT_ACCENT].c || 0; let maxChroma = scale.core?.get('oklch.c') ?? (scale[DEFAULT_ACCENT].c || 0);
for (let tint in tints) { for (let tint in scale) {
let color = tints[tint].to('oklch'); let color = scale[tint];
tints[tint] = color;
if (!color || color.constructor.name !== 'Color') {
// Not a color
continue;
}
color = color.to('oklch');
scale[tint] = color;
if (tint === '05') { if (tint === '05') {
// The object has both '5' and '05' keys, but '05' is out of order // The object has both '5' and '05' keys, but '05' is out of order
@@ -47,14 +54,14 @@ for (let paletteId in palettes) {
} }
} }
tints['05'] = tints['5']; scale['05'] = scale['5'];
tints.maxChroma = tints.maxChromaRaw = maxChroma; scale.maxChroma = scale.maxChromaRaw = maxChroma;
tints.maxChromaTint = tints.maxChromaTintRaw = maxChromaTint; scale.maxChromaTint = scale.maxChromaTintRaw = maxChromaTint;
if (maxChromaTint < MIN_ACCENT || maxChromaTint > MAX_ACCENT) { if (maxChromaTint < MIN_ACCENT || maxChromaTint > MAX_ACCENT) {
tints.maxChromaTint = clamp(MIN_ACCENT, maxChromaTint, MAX_ACCENT); scale.maxChromaTint = clamp(MIN_ACCENT, maxChromaTint, MAX_ACCENT);
tints.maxChroma = tints[maxChromaTint].c; scale.maxChroma = scale[maxChromaTint].c;
} }
} }
} }

View File

@@ -10,7 +10,7 @@ import { PALETTE_DIR } from './util.js';
export const paletteFiles = fs.readdirSync(PALETTE_DIR + '/').filter(file => file.endsWith('.css')); export const paletteFiles = fs.readdirSync(PALETTE_DIR + '/').filter(file => file.endsWith('.css'));
export const declarationRegex = export const declarationRegex =
/^\s*--wa-color-(?<hue>[a-z]+)-(?<level>[0-9]+):\s*(?<color>.+?)\s*(\/\*.+?\*\/)?\s*;$/gm; /^\s*--wa-color-(?<hue>[a-z]+)(?:-(?<level>[0-9]+|key))?:\s*(?<color>.+?)\s*(\/\*.+?\*\/)?\s*;$/gm;
export const rawCSS = {}; export const rawCSS = {};
function parse(contents, file) { function parse(contents, file) {
@@ -24,8 +24,24 @@ function parse(contents, file) {
const ret = {}; const ret = {};
for (let match of matches) { for (let match of matches) {
let { hue, level, color } = match.groups; let { hue, level = '', color } = match.groups;
ret[hue] ??= {}; ret[hue] ??= {};
let scale = ret[hue];
if (level === 'key') {
scale.maxChromaTint = color;
continue;
}
if (!level) {
if (color.startsWith('var(')) {
// Core color aliased to another color, ignore
continue;
} else {
// Custom core color
level = 'core';
}
}
// Attempt to convert color to Color object, fall back to string if this fails // Attempt to convert color to Color object, fall back to string if this fails
// This will happen for e.g. colors defined via color-mix() // This will happen for e.g. colors defined via color-mix()
@@ -38,13 +54,13 @@ function parse(contents, file) {
if (level.startsWith('0')) { if (level.startsWith('0')) {
// Leading zeroes throw off sorting, add both properties // Leading zeroes throw off sorting, add both properties
// NOTE: Ideally one of the two would be added as non-enumerable, but then we cannot access it via 11ty data // NOTE: Ideally one of the two would be added as non-enumerable, but then we cannot access it via 11ty data
ret[hue][level] = color; scale[level] = color;
// Drop leading zeroes // Drop leading zeroes
level = level.replace(/^0+/, ''); level = level.replace(/^0+/, '');
} }
ret[hue][level] = color; scale[level] = color;
} }
return ret; return ret;

View File

@@ -0,0 +1,162 @@
/**
* Post-process palette CSS files to add core colors, oklch coordinates etc.
* Run via node postprocess.js
* Warning: Will overwrite existing files. Check the diff before committing!
*/
import chalk from 'chalk';
import fs from 'fs';
import path from 'path';
import palettes from './palettes-analyzed.js';
import { PALETTE_DIR, formatComparison, hueToChalk } from './util.js';
// TODO import from global data file instead of duplicating this data
const hues = ['red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'indigo', 'purple', 'pink', 'gray'];
const huesChromatic = hues.slice(0, -1);
/** If a hue is missing, how should it be generated from the neighboring hues? */
const mixPercentage = { orange: 0.6 };
const selector = paletteId =>
[':where(:root)', ':host', ":where([class^='wa-theme-'], [class*=' wa-theme-'])", `.wa-palette-${paletteId}`].join(
',\n',
);
// Used for formatting warnings
const paletteIdMaxChars = Object.keys(palettes).reduce((max, id) => Math.max(max, id.length), 0);
const hueMaxChars = Object.keys(palettes.default).reduce((max, id) => Math.max(max, id.length), 0);
const indent = ' ';
const paletteIssues = { total: 0 };
for (let paletteId in palettes) {
const palette = palettes[paletteId];
let paletteCSS = '';
let hueCSS = Object.fromEntries(hues.map(hue => [hue, '']));
for (let hue in palette) {
let scale = palette[hue];
if (scale.maxChromaTint != scale.maxChromaTintRaw) {
reportPaletteIssue(
`Clamping accent color to ${chalk.bold(scale.maxChromaTint)}, but peak chroma is in ${chalk.bold(scale.maxChromaTintRaw)} (${formatComparison(scale[scale.maxChromaTintRaw].c, scale[scale.maxChromaTint].c)})`,
{ paletteId, hue: hue },
);
}
hueCSS[hue] += scaleCSS(hue, scale);
}
// Generate missing hues
for (let i = 0; i < hues.length; i++) {
let hue = hues[i];
if (hueCSS[hue]) {
continue;
}
// Find previous and next hue to interpolate
// We assume gaps will always be at most 1 hue wide
let prevHue = huesChromatic[i - 1] ?? huesChromatic.at(-1);
let nextHue = huesChromatic[i + 1] ?? huesChromatic[0];
reportPaletteIssue(`Missing hue. Generating from ${prevHue} and ${nextHue}`, { paletteId, hue });
let prevScale = palette[prevHue];
let nextScale = palette[nextHue];
let progress = mixPercentage[hue] ?? 0.5;
let scale = (palette[hue] = {});
scale.maxChromaTint = (1 - progress) * prevScale.maxChromaTint + progress * nextScale.maxChromaTint;
scale.maxChromaTint = Math.round(scale.maxChromaTint / 10) * 10;
for (let tint in prevScale) {
if (tint === '05' || !(tint > 0)) {
continue;
}
let prevColor = palette[prevHue][tint];
let nextColor = palette[nextHue][tint];
let color = prevColor.mix(nextColor, progress, { space: 'oklch' });
scale[tint] = color;
}
// Ensure core color has the max chroma
let coreColor = scale[scale.maxChromaTint];
coreColor.c = Math.max(...Object.values(scale).map(color => color.c || 0)) + 0.0002;
hueCSS[hue] += scaleCSS(hue, scale);
}
hueCSS = Object.values(hueCSS).filter(Boolean).join('\n\n');
// TODO apply Prettier instead of faking it
paletteCSS = `${selector(paletteId)} {\n${hueCSS.trimEnd().replace(/^(?=\S)/gm, indent)}\n}\n`;
fs.writeFileSync(path.join(PALETTE_DIR, paletteId + '.css'), paletteCSS, 'utf8');
}
let issuePaletteCount = Object.keys(paletteIssues).length - 1;
console.info(
`🎨 Wrote ${Object.keys(palettes).length} palette files.` +
(paletteIssues.total > 0
? ` ${chalk.bold(paletteIssues.total)} issues found across ${chalk.bold(issuePaletteCount)} palettes.`
: ''),
);
function reportPaletteIssue(issue, { paletteId, hue }) {
let palettePrefix = `[${paletteId}]`.padEnd(paletteIdMaxChars + 2);
if (!paletteIssues[paletteId]) {
// First time encountering an issue with this palette
paletteIssues[paletteId] = { count: 0 };
} else {
// Don't print palette id multiple times
palettePrefix = ' '.repeat(paletteIdMaxChars + 2);
}
paletteIssues[paletteId].count++;
paletteIssues.total++;
let msg = palettePrefix;
let huePrefix = '';
if (hue) {
huePrefix = hueToChalk(hue)(hue.padEnd(hueMaxChars + 2));
}
console.warn(`${msg}${huePrefix}${issue}`);
}
function declareColor(color, hue, tint) {
tint = tint.padStart(2, '0');
let ret = `--wa-color-${hue}-${tint}: `;
if (color.inGamut('srgb')) {
ret += `${color.toString({ format: 'hex' })} /* ${color.toString()} */;`;
} else {
ret += `${color.toString()};`;
}
return ret;
}
function scaleCSS(hue, scale) {
let ret = [];
for (let tint in scale) {
if (tint === '05' || !(tint > 0)) {
// The object has both '5' and '05' keys, but '05' is out of order
// Also ignore non-tints
continue;
}
let color = scale[tint];
ret.push(declareColor(color, hue, tint));
}
ret.reverse();
ret.push(`--wa-color-${hue}: var(--wa-color-${hue}-${scale.maxChromaTint});`);
ret.push(`--wa-color-${hue}-key: ${scale.maxChromaTint};`);
return ret.join('\n');
}

View File

@@ -1,80 +0,0 @@
/**
* Add tintless variables and OKLCH coords as comments to palette CSS files.
* Run via node tintless.js
* Warning: Will overwrite existing files. Check the diff before committing!
*/
import chalk from 'chalk';
import fs from 'fs';
import path from 'path';
import palettes, { rawPalettes } from './palettes-analyzed.js';
import { PALETTE_DIR, formatComparison, hueToChalk } from './util.js';
const selector = paletteId =>
[':where(:root)', ':host', ":where([class^='wa-theme-'], [class*=' wa-theme-'])", `.wa-palette-${paletteId}`].join(
',\n',
);
// Used for formatting warnings
const paletteIdMaxChars = Object.keys(palettes).reduce((max, id) => Math.max(max, id.length), 0);
const hueMaxChars = Object.keys(palettes.default).reduce((max, id) => Math.max(max, id.length), 0);
let issueCount = 0;
let issuePaletteCount = 0;
for (let paletteId in palettes) {
const tokens = palettes[paletteId];
let css = '';
let prefix = `[${paletteId}]`.padEnd(paletteIdMaxChars + 2);
for (let hue in tokens) {
let tints = tokens[hue];
let tintCSS = '';
for (let tint in tints) {
if (tint === '05' || !(tint > 0)) {
// The object has both '5' and '05' keys, but '05' is out of order
// Also ignore non-tints
continue;
}
let color = tints[tint];
tint = tint.padStart(2, '0');
tintCSS =
`--wa-color-${hue}-${tint}: ${color.toString({ format: 'hex' })} /* ${color.toString()} */;\n` + tintCSS;
}
if (tints.maxChromaTint != tints.maxChromaTintRaw) {
let huePrefix = hueToChalk(hue)(hue.padEnd(hueMaxChars + 2));
console.warn(
`${prefix} ${huePrefix}: Clamping accent color to ${chalk.bold(tints.maxChromaTint)}, but peak chroma is in ${chalk.bold(tints.maxChromaTintRaw)} (${formatComparison(tints[tints.maxChromaTintRaw].c, tints[tints.maxChromaTint].c)})`,
);
issueCount++;
if (prefix.trim()) {
// First time encountering an issue with this palette
issuePaletteCount++;
// Don't print palette id multiple times
prefix = ' '.repeat(paletteIdMaxChars + 2);
}
}
tintCSS += `--wa-color-${hue}: var(--wa-color-${hue}-${tints.maxChromaTint});\n`;
tintCSS += `--wa-color-${hue}-key: ${tints.maxChromaTint};\n`;
css += tintCSS + '\n';
}
let indent = ' ';
css = `${selector(paletteId)} {\n${css.trimEnd().replace(/^(?=\S)/gm, indent)}\n}\n`;
fs.writeFileSync(path.join(PALETTE_DIR, paletteId + '.css'), css, 'utf8');
}
console.info(
`🎨 Wrote ${Object.keys(palettes).length} palette files.` +
(issueCount > 0 ? ` ${chalk.bold(issueCount)} issues found across ${chalk.bold(issuePaletteCount)} palettes.` : ''),
);

View File

@@ -16,6 +16,20 @@
--wa-color-red: var(--wa-color-red-50); --wa-color-red: var(--wa-color-red-50);
--wa-color-red-key: 50; --wa-color-red-key: 50;
--wa-color-orange-95: oklch(96.355% 0.05982 62.448);
--wa-color-orange-90: oklch(92.371% 0.10134 60.314);
--wa-color-orange-80: oklch(84.228% 0.13101 54.157);
--wa-color-orange-70: oklch(76.275% 0.16839 51.143);
--wa-color-orange-60: #e67530 /* oklch(68.22% 0.16179 47.997) */;
--wa-color-orange-50: oklch(58.011% 0.16819 44.953);
--wa-color-orange-40: #9b390d /* oklch(47.739% 0.14004 40.585) */;
--wa-color-orange-30: #7a2b17 /* oklch(40.323% 0.11475 35.602) */;
--wa-color-orange-20: #5c1e0f /* oklch(32.963% 0.09552 34.666) */;
--wa-color-orange-10: #3a0f06 /* oklch(24.042% 0.07066 34.715) */;
--wa-color-orange-05: #270803 /* oklch(18.867% 0.05444 34.696) */;
--wa-color-orange: var(--wa-color-orange-70);
--wa-color-orange-key: 70;
--wa-color-yellow-95: #fef6ab /* oklch(96.234% 0.09455 102.83) */; --wa-color-yellow-95: #fef6ab /* oklch(96.234% 0.09455 102.83) */;
--wa-color-yellow-90: #fde761 /* oklch(92.138% 0.15325 99.997) */; --wa-color-yellow-90: #fde761 /* oklch(92.138% 0.15325 99.997) */;
--wa-color-yellow-80: #f5c308 /* oklch(83.774% 0.17019 89.81) */; --wa-color-yellow-80: #f5c308 /* oklch(83.774% 0.17019 89.81) */;

View File

@@ -1,6 +0,0 @@
/**
* Any data relating to the design system.
*/
export const hues = ['red', 'yellow', 'green', 'cyan', 'blue', 'indigo', 'purple', 'pink', 'gray'];
export const tints = ['05', '10', '20', '30', '40', '50', '60', '70', '80', '90', '95'];
export const variants = ['neutral', 'brand', 'success', 'warning', 'danger'];

View File

@@ -121,7 +121,8 @@ input:where(:not(
font: inherit; font: inherit;
} }
.wa-pill, input.wa-pill,
:host([pill]) { textarea.wa-pill,
border-radius: var(--wa-border-radius-pill); :host([pill]) .wa-text-field {
border-radius: var(--wa-border-radius-pill) !important;
} }

View File

@@ -55,6 +55,8 @@ input[type='range'] {
0 0 0 var(--thumb-gap) var(--wa-color-surface-default); 0 0 0 var(--thumb-gap) var(--wa-color-surface-default);
-webkit-appearance: none; -webkit-appearance: none;
margin-top: calc(var(--thumb-size) / -2 + var(--track-height) / 2); margin-top: calc(var(--thumb-size) / -2 + var(--track-height) / 2);
transition: var(--wa-transition-fast);
transition-property: width, height;
} }
&:enabled { &:enabled {

View File

@@ -45,7 +45,8 @@
--outlined-border-color: var(--wa-color-border-normal); --outlined-border-color: var(--wa-color-border-normal);
--text-color: var(--wa-color-on-normal, var(--wa-color-neutral-on-normal)); --text-color: var(--wa-color-on-quiet, var(--wa-color-neutral-on-quiet));
--text-color-hover: var(--wa-color-on-normal, var(--wa-color-neutral-on-normal));
} }
.wa-plain, .wa-plain,

View File

@@ -9,10 +9,10 @@
:host { :host {
/* Components are meant to override these */ /* Components are meant to override these */
--size-xs: var(--wa-space-xs); --size-xs: var(--wa-font-size-xs);
--size-s: var(--wa-space-s); --size-s: var(--wa-font-size-s);
--size-m: var(--wa-space-m); --size-m: var(--wa-font-size-m);
--size-l: var(--wa-space-l); --size-l: var(--wa-font-size-l);
--space-xs: var(--wa-space-xs); --space-xs: var(--wa-space-xs);
--space-s: var(--wa-space-s); --space-s: var(--wa-space-s);

View File

@@ -1,4 +1,7 @@
/* DO NOT EDIT THIS FILE. It is generated from "variants.css.njk" */ /**
* Do not edit this file directly. It is generated via variants.css.js
*/
/** /**
* Register color properties so that the space toggle hack can work. * Register color properties so that the space toggle hack can work.
*/ */
@@ -61,6 +64,7 @@
* Element defaults. * Element defaults.
We want these to resolve to inherit when inside a variant, and only be applied when not inside an explicit variant. We want these to resolve to inherit when inside a variant, and only be applied when not inside an explicit variant.
*/ */
:host(wa-button), :host(wa-button),
.wa-button, .wa-button,
button, button,

View File

@@ -0,0 +1,68 @@
/**
* Generate variants.css
* To use: node variants.css.js > variants.css
*/
const variants = ['neutral', 'brand', 'success', 'warning', 'danger'];
const roles = ['fill', 'border', 'on'];
const noise = ['loud', 'normal', 'quiet'];
const defaults = {
neutral: [
[':host(wa-button)', '.wa-button', 'button', "input[type='button']", "input[type='submit']"],
[':host(wa-tag)', '.wa-tag'],
],
brand: [
[':host(wa-callout)', '.wa-callout'],
[':host(wa-badge)', '.wa-badge'],
],
};
const types = roles.map(layer => noise.map(priority => layer + '-' + priority)).flat();
let ret = comment('Do not edit this file directly. It is generated via variants.css.js').trimStart();
ret += comment('Register color properties so that the space toggle hack can work.');
for (let type of types) {
ret += cssRule(`@property --wa-color-${type}`, [
`syntax: '<color>';`,
'inherits: true;',
'initial-value: transparent;',
]);
}
ret += comment(`Element defaults.
We want these to resolve to inherit when inside a variant, and only be applied when not inside an explicit variant.`);
for (let variant in defaults) {
let selector = defaults[variant];
let declarations = types.map(type => `--wa-color-${type}: var(--wa-no-variant, var(--wa-color-${variant}-${type}));`);
ret += cssRule(selector, declarations);
}
ret += comment('Variants');
for (let variant of variants) {
let selector = [`.wa-${variant}`, `:host([variant='${variant}'])`];
if (variant === 'neutral') {
selector.unshift(':root');
}
let declarations = types.map(type => `--wa-color-${type}: var(--wa-color-${variant}-${type});`);
ret += cssRule(selector, declarations);
}
ret += cssRule([variants.map(variant => `.wa-${variant}`), ':host([variant])'], '--wa-no-variant: /* space toggle */;');
console.log(ret.trimEnd());
function cssRule(selector, declarations) {
selector = Array.isArray(selector) ? selector.flat().join(',\n') : selector;
declarations = Array.isArray(declarations) ? declarations.flat().join('\n') : declarations;
declarations = declarations.replace(/^/gm, ' ');
return `\n${selector} {\n${declarations}\n}\n`;
}
function comment(text) {
return ['\n/**', ` * ${text}`, ' */\n'].join('\n');
}

View File

@@ -1,54 +0,0 @@
{% set suffixes = [
'fill-loud', 'fill-normal', 'fill-quiet',
'border-loud', 'border-normal', 'border-quiet',
'on-loud', 'on-normal', 'on-quiet'
] -%}
/**
* Register color properties so that the space toggle hack can work.
*/
{% for suffix in suffixes %}
@property --wa-color-{{ suffix }} {
syntax: '<color>';
inherits: true;
initial-value: transparent;
}
{% endfor %}
/**
* Element defaults.
We want these to resolve to inherit when inside a variant, and only be applied when not inside an explicit variant.
*/
:host(wa-button), .wa-button, button, input[type='button'], input[type='submit'],
:host(wa-tag), .wa-tag {
{%- for suffix in suffixes %}
--wa-color-{{ suffix }}: var(--wa-no-variant, var(--wa-color-neutral-{{ suffix }}));
{%- endfor %}
}
:host(wa-callout), .wa-callout,
:host(wa-badge), .wa-badge {
{%- for suffix in suffixes %}
--wa-color-{{ suffix }}: var(--wa-no-variant, var(--wa-color-brand-{{ suffix }}));
{%- endfor %}
}
/**
* Variants
*/
{% for variant in variants %}
{{ ':root,' if variant === 'neutral' }}
.wa-{{ variant }},
:host([variant='{{ variant }}']) {
{%- for suffix in suffixes %}
--wa-color-{{ suffix }}: var(--wa-color-{{ variant }}-{{ suffix }});
{%- endfor %}
}
{%- endfor %}
{% for variant in variants %}
.wa-{{ variant }},
{%- endfor %}
:host([variant]) {
--wa-no-variant: /* space toggle */;
}