Compare commits

..

13 Commits

Author SHA1 Message Date
Lea Verou
3d6b9d05de Update tweak.css 2025-03-24 14:03:14 -04:00
Lea Verou
e12f3fdc21 step 2025-03-24 13:14:52 -04:00
Lea Verou
eaa2427cec Make hue wheel more discreet unless interacted with 2025-03-24 13:01:02 -04:00
Lea Verou
88abcf4a2d Copy icon 2025-03-24 13:01:02 -04:00
Lea Verou
9d75b72af8 Fix saving 2025-03-24 13:01:02 -04:00
Lea Verou
263d7d16ab Port changes 2025-03-24 13:01:02 -04:00
Lea Verou
fa3b387bf1 Fix gray tweaks: do not change level when tweaking 2025-03-24 12:37:56 -04:00
Lea Verou
827dd9f222 Port changes 2025-03-24 12:37:12 -04:00
Lea Verou
f13511905e Move code fetching palettes to separate file 2025-03-21 19:22:16 -04:00
Lea Verou
fef7eff3f4 Remove dead code 2025-03-21 19:18:00 -04:00
Lea Verou
d910502e49 Use jsdelivr for Vue 2025-03-21 19:17:27 -04:00
Lea Verou
b547289b3b Move files to palettes/app 2025-03-21 19:13:31 -04:00
Lea Verou
597d602549 class="status" for status badges 2025-03-21 19:08:42 -04:00
13 changed files with 114 additions and 1337 deletions

View File

@@ -12,7 +12,7 @@
</tr>
</thead>
{% for hue in hues -%}
<tr data-hue="{{ hue }}" v-if="'{{hue}}' in paletteScales">
<tr data-hue="{{ hue }}">
<th>{{ hue | capitalize }}</th>
{% for tint_bg in tints -%}
{% set color_bg = palettes[paletteId][hue][tint_bg] %}

View File

@@ -1,7 +1,6 @@
{% set hasSidebar = true %}
{% set hasOutline = true %}
{% set paletteId = "default" if page.fileSlug == 'custom' else page.fileSlug %}
{% set isCustom = page.fileSlug == 'custom' %}
{% set paletteId = page.fileSlug %}
{% set tints = ["95", "90", "80", "70", "60", "50", "40", "30", "20", "10", "05"] %}
{% extends '../_includes/base.njk' %}
@@ -16,7 +15,6 @@
<div id="palette-app" data-slug="{{ page.fileSlug }}" data-palette-id="{{ page.fileSlug }}">
<div
:class="{
seeded: isSeeded,
'tweaked-chroma': tweaked?.chroma,
'tweaked-hue': tweaked?.hue,
'tweaked-any': Object.keys(tweaksHumanReadable).length,
@@ -48,10 +46,10 @@
</h1>
<div class="block-info" v-cloak>
<code class="class" v-if="saved || !isCustom || step > 0">.wa-palette-<span v-content="slug">{{ page.fileSlug }}</span></code>
<code class="class">.wa-palette-<span v-content="slug">{{ page.fileSlug }}</span></code>
{% include '../_includes/status.njk' %}
{% if not isPro %}
<wa-badge class="pro" v-if="tweaked || isCustom">PRO</wa-badge>
<wa-badge class="pro" v-if="tweaked">PRO</wa-badge>
{% endif %}
</div>
{% if description %}
@@ -59,32 +57,28 @@
{{ description | inlineMarkdown | safe }}
</p>
{% endif %}
{% raw %}
<div class="hue-wheel" v-if="!isCustom || step > 1">
<div class="hue-wheel">
<template v-for="color, hue in coreColors">
<template v-if="!isCustom || seedHues[hue]">
<div :id="`hue-wheel-${hue}`" class="color"
:style="{
'--color': color,
'--h': color.get('oklch.h'),
'--c': color.get('oklch.c'),
'--l': color.get('oklch.l'),
}"></div>
<wa-tooltip :for="`hue-wheel-${ hue }`" hoist>{{ capitalize(hue) }} {{ coreLevels[hue] }}</wa-tooltip>
</template>
<div :id="`hue-wheel-${hue}`" class="color"
:style="{
'--color': color,
'--h': color.get('oklch.h'),
'--c': color.get('oklch.c'),
'--l': color.get('oklch.l'),
}"></div>
<wa-tooltip :for="`hue-wheel-${ hue }`" hoist v-content="capitalize(hue) + ' ' + coreLevels[hue]"></wa-tooltip>
</template>
</div>
{% endraw %}
</div>
</header>
{% endblock %}
{% block afterContent %}
<wa-callout size="small" class="tweaked-callout" variant="warning" v-if="!isCustom">
<wa-callout size="small" class="tweaked-callout" variant="warning">
<wa-icon name="sliders-simple" slot="icon" variant="regular"></wa-icon>
This palette has been tweaked.
<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>
<wa-tag v-for="tweakHumanReadable, param in tweaksHumanReadable" removable @wa-remove="reset(param)" v-content="tweakHumanReadable"></wa-tag>
</div>
<wa-button @click="reset()" appearance="outlined" variant="danger">
@@ -109,9 +103,8 @@
{%- endfor %}
</tr>
</thead>
{% raw %}
<tbody v-cloak>
<tr v-for="hue in paletteScalesList" :data-hue="hue" :key="hue"
<tr v-for="hue of allHues" :data-hue="hue" :key="hue"
class="color-scale" :class="{
tweaked: hue === 'gray' ? tweaked.grayChroma || tweaked.grayColor : hueShifts[hue],
}"
@@ -119,18 +112,9 @@
'--swatch-text-color': `light-dark(var(--wa-color-${ hue }-10), white)`,
'--hue-shift': hueShifts[hue] || ''
}">
<th>
{{ capitalize(hue) }}
<info-tip v-if="isCustom && !seedHues[hue]">
<wa-icon name="sparkles" style="color: var(--wa-color-gray-50)"></wa-icon>
<template #content>Generated scale</template>
</info-tip>
</th>
<th v-content="capitalize(hue)"></th>
<td class="core-column" :style="{'--original-color': `var(--wa-color-${ hue })`, '--color': colors[hue][coreLevels[hue]]}">
<color-popup :title="capitalize(hue) + ' (core)'" :token="`--wa-color-${ hue }`" :color="coreColors[hue]"
:pinned="!!seedColors[colorToIndex[hue].core]"
:deletable="isCustom" @delete="deleteColor(colorToIndex[hue].core)"
:pinnable="isCustom" @pin="addColor(coreColors[hue] + '')">
<color-popup :title="capitalize(hue) + ' (core)'" :token="`--wa-color-${ hue }`" :color="coreColors[hue]">
<div slot="trigger" :id="`core-${ hue }-swatch`" class="color swatch" :style="{colorScheme: coreLevels[hue] > 60 ? 'light' : 'dark'}">
<span v-content="coreLevels[hue]"></span>
<wa-icon name="sliders-simple" class="tweak-icon"></wa-icon>
@@ -140,15 +124,7 @@
<color-swatch-picker :model-value="computedGrayColor" @update:model-value="grayColor = $event" label="Gray undertone" :colors="coreColors"></color-swatch-picker>
</template>
<template v-else>
<color-slider v-if="isCustom && seedColors[colorToIndex[hue].core]"
coord="h" type="shift"
v-model:color="seedColors[colorToIndex[hue].core].color"
:default-value="seedColors[colorToIndex[hue].core].inputColor.oklch.h"
:min="HUE_RANGES[hue].min + 1" :max="HUE_RANGES[hue].max"
label="Adjust hue" :label-min="moreHue[hueBefore[hue]]" :label-max="moreHue[hueAfter[hue]]"
></color-slider>
<color-slider v-if="!isCustom && baseCoreColors[hue]"
<color-slider v-if="baseCoreColors[hue]"
coord="h" type="shift"
v-model="hueShifts[hue]"
:default-color="baseCoreColors[hue]"
@@ -166,69 +142,30 @@
:min="0" :max-relative="maxGrayChroma" :step="0.00001"
label="Gray colorfulness" label-min="Neutral" :label-max="moreHue[computedGrayColor]"
></color-slider>
<color-slider v-else-if="isCustom" v-model:color="seedColors[colorToIndex[hue].core].color"
:default-value="seedColors[colorToIndex[hue].core].inputColor?.oklch.c"
coord="c"
:min="Math.max(coreColors.gray.oklch.c, ...Object.keys(seedHues[hue]).filter(t => t !== coreLevels[hue]).map(t => seedHues[hue][t].oklch.c))"
:max="getMaxChroma(colors[hue].core?.oklch.l, colors[hue].core?.oklch.h) - 0.001" :step="0.00001"
label="Adjust colorfulness" label-min="More muted" label-max="More vibrant"
label-default="Entered color"
format-type="scale"
></color-slider>
</template>
</color-popup>
</td>
<td v-for="tint in tints.toReversed()" :data-tint="tint" :style="{'--original-color': `var(--wa-color-${ hue }-${tint})`, '--color': colors[hue][tint] }">
<color-popup :title="capitalize(hue) + ' ' + tint" :token="`--wa-color-${ hue }-${ tint }`" :color="colors[hue][tint]"
:pinned="!!seedColors[colorToIndex[hue][tint]]"
:deletable="isCustom" @delete="deleteColor(colorToIndex[hue][tint])"
:pinnable="isCustom" @pin="addColor({hue, pinnedHue: hue, level: tint})">
<color-popup :title="capitalize(hue) + ' ' + tint" :token="`--wa-color-${ hue }-${ tint }`" :color="colors[hue][tint]">
<div slot="trigger" class="color swatch" :style="{ colorScheme: tint > 60 ? 'light' : 'dark' }">
<wa-icon class="pinned-icon" name="thumbtack" variant="regular" v-if="seedColors[colorToIndex[hue][tint]]"></wa-icon>
<wa-icon name="sliders-simple" class="tweak-icon"></wa-icon>
<wa-icon name="copy" variant="regular" class="copy-icon"></wa-icon>
</div>
<template #content v-if="isCustom && seedHues[hue] && (tint == '95' || tint == '05' || seedColors[colorToIndex[hue][tint]]) && tweakBase[hue][tint]">
<color-slider v-if="HUE_RANGES[hue]" v-model:color="colors[hue][tint]"
:default-value="colors[hue][tweakBase[hue][tint]].oklch.h"
@input="!seedColors[colorToIndex[hue][tint]] ? addColor({hue, pinnedHue: hue, level: tint}) : null"
@update:color="seedColors[colorToIndex[hue][tint]] ? seedColors[colorToIndex[hue][tint]].color = $event : null"
coord="h"
:min="HUE_RANGES[hue].mid - 70" :max="HUE_RANGES[hue].mid + 70" :step="1"
label="Hue shift" :label-min="moreHue[hueBefore[hue]]" :label-max="moreHue[hueAfter[hue]]"
:label-default="`${capitalize(hue)} ${tweakBase[hue][tint]}`"
format-type="shift"
></color-slider>
<color-slider v-if="hue != 'gray'" v-model:color="colors[hue][tint]"
:default-value="colors[hue][tweakBase[hue][tint]].oklch.c"
@input="!seedColors[colorToIndex[hue][tint]] ? addColor({hue, pinnedHue: hue, level: tint}) : null"
@update:color="seedColors[colorToIndex[hue][tint]] ? seedColors[colorToIndex[hue][tint]].color = $event : null"
coord="c"
:min="coreColors.gray.oklch.c + 0.001"
:max="tint == coreLevels[hue] ? maxChroma(colors[hue][tweakBase[hue][tint]].oklch.l, colors[hue][tweakBase[hue][tint]].oklch.h) : coreColors[hue].oklch.c - 0.001" :step="0.001"
label="Colorfulness" label-min="More muted" label-max="More vibrant"
format-type="scale"
:label-default="`${capitalize(hue)} ${tweakBase[hue][tint]}`"
></color-slider>
</template>
</color-popup>
</td>
</tr>
{% endraw %}
</tbody>
</tbody>
</table>
<color-slider v-if="!isCustom" :class="{ tweaked: chromaScale !== 1 }"
<color-slider :class="{ tweaked: chromaScale !== 1 }"
type="scale"
v-model="chromaScale"
coord="c"
:default-color="baseMaxChromaColor"
:default-value="baseMaxChroma"
:min="MAX_CHROMA_BOUNDS.min" :max="MAX_CHROMA_BOUNDS.max" :step="0.01"
:min="MAX_CHROMA_BOUNDS.min" :max="MAX_CHROMA_BOUNDS.max" :step="0.001"
label="Overall colorfulness" label-min="More muted" label-max="More vibrant"
></color-slider>
{% if page.fileSlug != 'custom' %}
<h2>Used By</h2>
<section class="index-grid">
@@ -238,7 +175,6 @@
{%- endif -%}
{% endfor %}
</section>
{% endif %}
{% markdown %}
## Color Contrast
@@ -334,7 +270,7 @@ Add the following code at the top of your CSS file:
{% endmarkdown %}
<section id="saved" class="index-grid" v-if="savedVariations?.length">
<h2 class="index-category">Saved {{ 'custom palettes' if page.fileSlug == 'custom' else title + ' variations' }}</h2>
<h2 class="index-category">Saved variations</h2>
<a v-for="palette of savedVariations" :href="'/docs/palettes/' + palette.id">
<wa-card with-header>
<div slot="header">

View File

@@ -80,7 +80,7 @@ sidebar.palette = {
return;
}
for (let index; index > -1; index = savedPalettes.findIndex(p => p.uid === palette.uid)) {
for (let index; (index = savedPalettes.findIndex(p => p.uid === palette.uid)) > -1; ) {
savedPalettes.splice(index, 1);
}

View File

@@ -1,11 +1,11 @@
import { tints } from '/assets/scripts/tweak/data.js';
export function generateGrays(colors, { grayColor, grayChroma }) {
export function generateGrays(colors, { grayColor, grayChroma, grayLevel }) {
let ret = {};
let undertoneScale = colors[grayColor];
// These will be the same, since scaling them won't change the relationship
ret.maxChromaTint = undertoneScale.maxChromaTint;
ret.maxChromaTint = grayLevel ?? undertoneScale.maxChromaTint;
Object.defineProperty(ret, 'core', {
enumerable: false,
get() {

View File

@@ -2,10 +2,10 @@ import { stringifyColor } from './util.js';
import { cssImport, cssLiteral, cssRule } from '/assets/scripts/tweak/code.js';
import { selectors, tints, urls } from '/assets/scripts/tweak/data.js';
export function getPaletteCode({ base, slug = base, colors, tweaked, roles, ...options }) {
export function getPaletteCode({ base, colors, tweaked, ...options }) {
let imports = [];
if (base && options.imports !== false && !tweaked.seedColors) {
if (base && options.imports !== false) {
imports.push(urls.palette(base));
}
@@ -18,14 +18,12 @@ export function getPaletteCode({ base, slug = base, colors, tweaked, roles, ...o
if (tweaked) {
for (let hue in colors) {
if (!tweaked.seedColors) {
if (hue === 'gray') {
if (!tweaked.grayChroma && !tweaked.grayColor) {
continue;
}
} else if (!tweaked.chromaScale && !tweaked.hue?.[hue]) {
if (hue === 'gray') {
if (!tweaked.grayChroma && !tweaked.grayColor) {
continue;
}
} else if (!tweaked.chromaScale && !tweaked.hue?.[hue]) {
continue;
}
let scale = colors[hue];
@@ -48,24 +46,8 @@ export function getPaletteCode({ base, slug = base, colors, tweaked, roles, ...o
}
}
if (roles) {
for (let role in roles) {
let hue = roles[role];
if (!hue) {
continue;
}
for (let suffix of [...tints.map(t => '-' + t), '', '-key']) {
declarations.push(`--${prefix}-${role}${suffix}: var(--${prefix}-${hue}${suffix});`);
}
declarations.push('');
}
}
if (declarations.length > 0) {
let selector = options.selector ?? selectors.palette(slug);
let selector = options.selector ?? selectors.palette(base);
css += cssRule(selector, declarations);
}

View File

@@ -28,7 +28,8 @@ export function tweakPalette(baseColors, tweaks, tweaked) {
if (tweaked.grayChroma || tweaked.grayColor) {
let grayColor = tweaks.grayColor ?? this.originalGrayColor;
let grayChroma = this.computedGrayChroma;
ret.gray = generateGrays(baseColors, { grayColor, grayChroma });
let grayLevel = baseColors.gray?.maxChromaTint;
ret.gray = generateGrays(baseColors, { grayColor, grayChroma, grayLevel });
} else {
ret.gray = originalScale;
}

View File

@@ -1,187 +0,0 @@
/* CSS for custom palettes only */
#seed-colors {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(22ch, 1fr));
gap: var(--wa-space-m);
> .add-button {
flex-flow: column wrap;
height: auto;
min-height: 15ch;
border: var(--wa-panel-border-width) var(--wa-panel-border-style) var(--wa-color-surface-border);
--border-color: var(--wa-color-surface-border);
border-radius: var(--wa-panel-border-radius);
background-color: var(--wa-color-surface-default);
box-shadow: var(--wa-shadow-s);
wa-icon {
font-size: 200%;
margin: 0;
margin-top: 0.35em;
}
}
> wa-card {
--spacing: var(--wa-space-s);
[slot='image'] {
position: relative;
height: 5.5rem;
width: 100%;
border-start-start-radius: var(--inner-border-radius);
border-start-end-radius: var(--inner-border-radius);
background-color: var(--color);
color: canvastext;
.tweak-icon {
position: absolute;
top: var(--wa-space-s);
right: var(--wa-space-s);
--background-color-hover: oklab(from currentColor l a b / 15%);
--text-color-hover: currentColor;
&:not(:hover, :focus, :has(+ :focus-within)) {
opacity: 50%;
}
&:is(.tweaked *) {
&::part(base) {
transition: var(--wa-transition-normal);
transition-property: padding, border, opacity;
background-color: var(--color-original);
padding: var(--wa-space-s);
border: 1px solid hsl(0 0 100 / 60%);
}
}
}
.name {
display: flex;
gap: var(--wa-space-xs);
position: absolute;
bottom: var(--wa-space-xs);
left: var(--wa-space-s);
font-weight: var(--wa-font-weight-semibold);
wa-dropdown.pin-hue {
wa-button {
--outlined-border-color: oklab(from currentColor l a b / 10%);
--outlined-background-color-hover: transparent;
--border-width: 1.5px;
--text-color: currentColor;
--wa-space: var(--wa-space-xs);
--wa-space-smaller: var(--wa-space-2xs);
}
&.pin-hue.pinned {
wa-button {
--outlined-border-color: oklab(from currentColor l a b / 40%);
font-weight: var(--wa-font-weight-bold);
}
}
wa-icon[name='thumbtack'] {
opacity: 60%;
}
}
.level {
font-weight: var(--wa-font-weight-bold);
}
}
}
wa-input {
margin-top: var(--wa-space-xs);
}
wa-icon-button {
color: light-dark(black, white);
transition: opacity var(--wa-transition-slow);
--background-color-hover: oklab(from currentColor l a b / 15%);
--text-color-hover: currentColor;
}
}
.color-to-role {
--border-width: 0;
margin-inline-start: calc(-1 * var(--wa-space-3xs));
&::part(tags) {
margin-inline-start: 0;
}
&::part(combobox) {
padding: var(--wa-space-3xs);
min-height: auto;
}
}
}
wa-icon-button.delete-button {
position: absolute;
top: var(--wa-space-s);
right: var(--wa-space-s);
--text-color-hover: var(--wa-color-danger-on-normal);
}
.pinned-icon {
opacity: 70%;
}
#suggested-colors {
margin-top: var(--wa-space-2xl);
h3 {
margin-bottom: 0;
}
&::part(content) {
padding-block-start: 0;
}
p.wa-caption-m {
margin-block: var(--wa-space-xs) var(--wa-space-m);
text-wrap: pretty;
}
.suggestions {
display: flex;
flex-wrap: wrap;
gap: var(--wa-space-s);
wa-button {
/* --background-color-hover: var(--background-color); */
height: var(--wa-form-control-height);
aspect-ratio: 1.2;
wa-icon {
transition: var(--wa-transition-normal);
}
&:not(:focus, :hover) wa-icon {
opacity: 0;
}
}
}
}
#roles {
margin-block: var(--wa-space-2xl);
> div {
display: flex;
flex-wrap: wrap;
gap: var(--wa-space-m);
> wa-select {
flex: 1;
max-width: 20ch;
}
}
}
.seed-color-tweak .popup {
min-width: clamp(0ch, 50ch, 90vw);
}

View File

@@ -150,8 +150,21 @@ wa-dropdown > .color.swatch {
opacity: var(--tweak-icon-opacity, 0%);
}
.copy-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
opacity: var(--copy-icon-opacity, 0%);
}
.color.swatch:hover {
--tweak-icon-opacity: 40%;
--copy-icon-opacity: 40%;
}
wa-dropdown[open] .copy-icon {
--copy-icon-opacity: 60%;
}
&.tweaked .core-column {
@@ -212,11 +225,6 @@ wa-dropdown > .color.swatch {
}
}
[data-slug='custom'] > :not(.seeded) #seed-colors ~ :not(#saved),
#outline:has(+ main > [data-slug='custom'] > :not(.seeded)) li:nth-child(n + 2) {
display: none;
}
[id='palette-info'] {
display: grid;
grid-template-columns: 1fr auto;
@@ -234,20 +242,33 @@ wa-dropdown > .color.swatch {
position: relative;
width: calc(var(--r) * 2);
aspect-ratio: 1;
border: 2px solid transparent;
border-radius: 50%;
--lc: var(--avg-l) var(--max-c);
--lc2: var(--avg-l) calc(var(--max-c) / 2);
margin-top: calc(var(--r) * -0.05);
background: conic-gradient(
in oklch,
oklch(var(--lc) 0),
oklch(var(--lc) 60),
oklch(var(--lc) 120),
oklch(var(--lc) 180),
oklch(var(--lc) 240),
oklch(var(--lc) 300),
oklch(var(--lc) 360)
);
--cover-size: calc(100% - var(--visible-size, 0%));
background:
radial-gradient(closest-side, var(--wa-color-surface-default) 100%, transparent 0) center / var(--cover-size)
var(--cover-size),
conic-gradient(
in oklch,
oklch(var(--lc) 0),
oklch(var(--lc) 60),
oklch(var(--lc) 120),
oklch(var(--lc) 180),
oklch(var(--lc) 240),
oklch(var(--lc) 300),
oklch(var(--lc) 360)
);
background-origin: border-box;
background-clip: padding-box, border-box;
background-repeat: no-repeat;
transition: var(--wa-transition-slow) ease-in background-size;
&:is(:hover, :focus-within) {
--visible-size: 100%;
}
&,
&::before {
@@ -260,7 +281,11 @@ wa-dropdown > .color.swatch {
display: block;
height: 100%;
border-radius: 50%;
-webkit-mask: radial-gradient(white, transparent);
mask: radial-gradient(white, transparent);
mask-size: var(--visible-size, 0%) var(--visible-size, 0%);
mask-repeat: no-repeat;
transition: inherit;
transition-property: mask-size;
background: radial-gradient(oklch(var(--avg-l) calc(var(--gray-chroma) * var(--max-c)) 0) 5%, transparent 30%),
conic-gradient(
in oklch,
@@ -290,6 +315,7 @@ wa-dropdown > .color.swatch {
--scale: 1.2;
--line-color: white;
--line-style: solid;
z-index: 2;
}
&::before {
@@ -335,16 +361,6 @@ wa-dropdown > .color.swatch {
margin-inline-start: var(--wa-space-xs);
}
.seeded {
wa-badge.status {
display: none;
}
wa-badge.pro {
filter: grayscale(0.95);
}
}
.selected-swatch,
.color-select wa-option::before {
content: '';

View File

@@ -8,9 +8,7 @@ import getPaletteCode from './color/get-palette-code.js';
import allPalettes from './color/palettes.js';
import { tweakColor, tweakPalette } from './color/tweak.js';
import { getContrasts, identifyColor } from './color/util.js';
import ColorInput from './vue-components/color-input.js';
import ColorPopup from './vue-components/color-popup.js';
import ColorSelect from './vue-components/color-select.js';
import ColorSlider from './vue-components/color-slider.js';
import ColorSwatchPicker from './vue-components/color-swatch-picker.js';
import InfoTip from './vue-components/info-tip.js';
@@ -32,12 +30,6 @@ import {
} from '/assets/scripts/tweak/data.js';
import { camelCase, capitalize, log, slugify, subtractAngles } from '/assets/scripts/tweak/util.js';
const firstSeedColor = '#0071ec';
const defaults = {
grayChroma: 0.15,
grayColor: 'indigo',
};
let paletteAppSpec = {
data() {
let appRoot = document.querySelector('#palette-app');
@@ -46,20 +38,10 @@ let paletteAppSpec = {
return {
uid: undefined,
maxSeedUid: 0,
seedColors: [],
seedColorSamples: [
'#0071ec',
'oklch(77% 0.19 70)',
'rgb(95, 59, 255)',
'#f06',
'yellowgreen',
'oklch(82% 0.185 195)',
'oklch(30% 0.18 150)',
],
paletteId,
originalPaletteTitle: palette.title,
originalColors: paletteId === 'custom' ? allPalettes.default.colors : palette.colors,
originalColors: palette.colors,
baseColors: { ...palette.colors },
permalink: new Permalink(),
hueShifts: Object.fromEntries(hues.map(hue => [hue, 0])),
chromaScale: 1,
@@ -68,13 +50,22 @@ let paletteAppSpec = {
saved: null,
unsavedChanges: false,
savedPalettes: sidebar.palettes.saved,
roles: Object.fromEntries(ROLES.map(role => [role, undefined])),
};
},
created() {
// Non-reactive variables to expose
Object.assign(this, { moreHue, hueBefore, hueAfter, HUE_RANGES, L_RANGES, hues, tints, MAX_CHROMA_BOUNDS });
Object.assign(this, {
moreHue,
hueBefore,
hueAfter,
HUE_RANGES,
L_RANGES,
hues,
allHues,
tints,
MAX_CHROMA_BOUNDS,
});
if (location.search) {
// Read URL params and apply them. This facilitates permalinks.
@@ -98,37 +89,18 @@ let paletteAppSpec = {
}
}
if (this.permalink.has('color')) {
this.seedColors = this.permalink.getAll('color').map(value => {
if (value.startsWith('{')) {
try {
return JSON.parse(value);
} catch (e) {
return { value };
}
} else {
return { value };
}
});
}
if (this.permalink.has('uid')) {
this.uid = Number(this.permalink.get('uid'));
this.saved = sidebar.palettes.saved.find(p => p.uid === this.uid);
}
for (let role in this.roles) {
let value = this.permalink.get(`role-${role}`);
if (value) {
this.roles[role] = value;
}
}
}
},
mounted() {
nextTick().then(() => {
this.unsavedChanges = false;
if (!this.tweaked || this.saved) {
this.unsavedChanges = false;
}
});
},
@@ -141,110 +113,16 @@ let paletteAppSpec = {
* @returns
*/
step() {
if (this.isCustom) {
if (this.isSeeded) {
return 2;
} else if (this.seedColors.length > 0) {
return 1;
} else {
return 0;
}
} else {
return this.tweaked ? 1 : 0;
}
},
suggestedForRole() {
let ret = {};
if (!this.seedHues.green) {
ret.success = ['green'];
}
ret.warning = [];
if (!this.seedHues.yellow) {
ret.warning.push('yellow');
}
if (!this.seedHues.orange) {
ret.warning.push('orange');
}
if (!this.seedHues.red) {
ret.danger = ['red'];
}
return ret;
},
defaultRoles() {
let seedHues = new Set(this.seedHueList);
// Arrays define candidates in preference order
let ret = {
brand: ['blue', 'indigo', 'purple', 'cyan', 'pink', 'green', 'orange', 'yellow', 'red', 'gray'],
neutral: 'gray',
success: ['green'],
warning: ['yellow', 'orange'],
danger: ['red'],
};
// Reduce to first candidate in seed hues
for (let role in ret) {
if (Array.isArray(ret[role])) {
ret[role] = ret[role].find(hue => seedHues.has(hue));
}
}
if (this.seedColors.length === 0) {
return ret;
}
// Now apply brand color to anything empty
for (let role in ret) {
if (!ret[role]) {
ret[role] = 'brand';
}
}
return ret;
},
computedRoles() {
return Object.fromEntries(ROLES.map(role => [role, this.roles[role] ?? this.defaultRoles[role]]));
},
suggestedColors() {
let ret = {};
for (let hue in this.coreColors) {
if (!this.seedHues[hue] && hue !== 'gray') {
ret[hue] = this.coreColors[hue];
}
}
return ret;
},
isCustom() {
return this.paletteId === 'custom';
return this.tweaked ? 1 : 0;
},
slug() {
if (this.isCustom) {
return slugify(this.paletteTitle);
} else {
// The slug does not change for tweaked palettes
return this.paletteId;
}
return this.paletteId;
},
/** Default palette title for saving */
defaultPaletteTitle() {
if (this.isCustom) {
return 'My Palette';
} else {
return this.originalPaletteTitle + ' (tweaked)';
}
return this.originalPaletteTitle + ' (tweaked)';
},
paletteTitle() {
@@ -257,148 +135,6 @@ let paletteAppSpec = {
}
},
seedColorValues() {
return this.seedColors.map(c => {
if (c.pinnedHue) {
let { value, pinnedHue } = c;
return { value, pinnedHue };
} else {
return c.value;
}
});
},
seedColorObjectsRaw() {
return this.seedColors.map(c => c.colorRaw);
},
seedColorObjects() {
return this.seedColors.map(c => c.color);
},
isSeeded() {
return this.seedColorObjectsRaw.filter(Boolean).length > 0;
},
seedColorInfo() {
return this.seedColors.map(({ hue, level }) => ({ hue, level }));
},
/**
* Map hue + level to index in seedColors
*/
colorToIndex() {
let ret = {};
for (let hue of allHues) {
ret[hue] = {};
}
if (!this.isSeeded) {
return ret;
}
for (let i = 0; i < this.seedColors.length; i++) {
let { hue, level } = this.seedColors[i];
if (!hue || !level) {
continue;
}
ret[hue][level] = i;
}
for (let hue in this.coreLevels) {
if (ret[hue]) {
ret[hue].core = ret[hue][this.coreLevels[hue]];
}
}
return ret;
},
hueRoles() {
let ret = {};
for (let role in this.computedRoles) {
let value = this.computedRoles[role];
ret[value] ??= {};
ret[value][role] = this.roles[role];
}
return ret;
},
seedColorRoles() {
return this.seedColorInfo.map(info => {
if (!info) {
return [];
}
let { hue } = info;
return this.hueRoles[hue];
});
},
seedHueList() {
return Object.keys(this.seedHues);
},
seedHues() {
// Make sure hues are in the right order
let ret = {};
for (let hue of hues) {
Object.defineProperty(ret, hue, { value: undefined, enumerable: false, writable: true, configurable: true });
}
for (let i = 0; i < this.seedColors.length; i++) {
let seed = this.seedColors[i];
let { hue, level, color } = seed;
if (!hue) {
continue;
}
if (!ret[hue]) {
// First color of this hue
delete ret[hue]; // remove non-enumerable descriptor
ret[hue] = {};
}
ret[hue][level] = color;
}
return ret;
},
paletteScales() {
if (!this.isCustom) {
return this.colors;
}
let ret = Object.fromEntries(
Object.keys(this.colors)
.filter(hue => this.seedHues[hue] || hue === 'gray')
.map(hue => [hue, this.colors[hue]]),
);
// Ensure gray is last
if (ret.gray) {
let grayScale = ret.gray;
delete ret.gray;
ret.gray = grayScale;
}
return ret;
},
paletteScalesList() {
return Object.keys(this.paletteScales);
},
paletteScalesSet() {
return new Set(this.paletteScalesList);
},
tweaks() {
return {
hueShifts: this.hueShifts,
@@ -413,10 +149,8 @@ let paletteAppSpec = {
for (let language of ['html', 'css']) {
let code = getPaletteCode({
base: this.paletteId,
slug: this.isCustom ? this.slug : undefined,
colors: this.paletteScales,
colors: this.colors,
tweaked: this.tweaked,
roles: this.isCustom ? this.computedRoles : this.roles,
language,
cdnUrl,
});
@@ -429,18 +163,6 @@ let paletteAppSpec = {
return ret;
},
baseColors() {
if (!this.isSeeded) {
return this.originalColors;
}
let { huesAfter } = this;
return (
generatePalette(this.seedHues, { huesAfter, grayChroma: defaults.grayChroma, grayColor: defaults.grayColor }) ??
this.originalColors
);
},
colors() {
return tweakPalette.call(this, this.baseColors, this.tweaks, this.tweaked);
},
@@ -452,7 +174,6 @@ let paletteAppSpec = {
: false;
let ret = {
seedColors: this.seedColors.length > 0,
chromaScale: this.chromaScale !== 1,
hue,
grayChroma: this.grayChroma !== undefined && this.grayChroma !== this.originalGrayChroma,
@@ -506,7 +227,7 @@ let paletteAppSpec = {
},
contrasts() {
return getContrasts(this.paletteScales, this.originalContrasts);
return getContrasts(this.colors, this.originalContrasts);
},
baseCoreColors() {
@@ -636,7 +357,7 @@ let paletteAppSpec = {
},
maxGrayChroma() {
return MAX_GRAY_CHROMA_SCALE[this.grayColor] ?? 0.3;
return MAX_GRAY_CHROMA_SCALE[this.grayColor] ?? 0.35;
},
huesAfter() {
@@ -654,41 +375,6 @@ let paletteAppSpec = {
savedVariations() {
return this.savedPalettes.filter(palette => palette.id === this.paletteId && palette.uid !== this.uid);
},
/** When tweaking a non-core tint, which tint are we tweaking relative to? */
tweakBase() {
let ret = {};
for (let hue in this.paletteScales) {
let pinned = Object.keys(this.seedHues[hue] ?? {}).sort((a, b) => a - b);
let core = this.coreLevels[hue];
ret[hue] ??= {};
for (let tint in this.paletteScales[hue]) {
if (tint === core) {
continue;
}
let delta = tint - core;
if (pinned.length <= 1) {
// If nothing is pinned or just the core level is pinned, all other tints are edited relative to that
ret[hue][tint] = core;
} else {
// Find closest pinned tint in the direction of the core color
if (delta < 0) {
// We want the first pinned tint that is larger than tint
ret[hue][tint] = pinned.find(pinnedTint => pinnedTint > tint);
} else {
// We want the last pinned tint that is smaller than tint
ret[hue][tint] = pinned.findLast(pinnedTint => pinnedTint < tint);
}
}
}
}
return ret;
},
}, // end computed
watch: {
@@ -713,19 +399,6 @@ let paletteAppSpec = {
this.permalink.set('gray-chroma', this.grayChroma, this.originalGrayChroma);
},
seedColorValues: {
deep: true,
handler() {
this.permalink.set('color', this.seedColorValues);
this.permalink.updateLocation();
if (this.saved || this.isCustom) {
this.unsavedChanges = true;
}
},
},
tweaks: {
deep: true,
async handler(value, oldValue) {
@@ -734,37 +407,14 @@ let paletteAppSpec = {
// Update page URL
this.permalink.updateLocation();
if (this.saved || this.isCustom) {
this.unsavedChanges = true;
}
this.unsavedChanges = true;
},
},
roles: {
saved: {
deep: true,
handler() {
for (let role in this.roles) {
this.permalink.set(`role-${role}`, this.roles[role]);
}
// Update page URL
this.permalink.updateLocation();
if (this.saved || this.isCustom) {
this.unsavedChanges = true;
}
},
},
paletteScalesSet: {
deep: true,
handler() {
for (let role in this.roles) {
if (this.roles[role] && !this.paletteScalesSet.has(this.roles[role])) {
// Role color is no longer in the palette
this.roles[role] = undefined;
}
}
this.unsavedChanges = !this.saved;
},
},
}, // end watch
@@ -775,26 +425,10 @@ let paletteAppSpec = {
getMaxChroma,
log,
/**
* Testing method. Import all core colors from a given palette.
* @param {string} paletteId
*/
emulate(paletteId) {
this.seedColors = [];
for (let hue in allPalettes[paletteId].colors) {
if (hue !== 'gray') {
let coreTint = allPalettes[paletteId].colors[hue].maxChromaTint;
let coreColor = allPalettes[paletteId].colors[hue][coreTint];
this.addColor(coreColor);
}
}
},
save({ title } = {}) {
async save({ title } = {}) {
let uid = this.uid;
this.saved ??= { id: this.paletteId, uid: this.uid, search: location.search };
this.saved ??= { id: this.paletteId, uid: this.uid };
if (title) {
// Renaming
@@ -803,13 +437,17 @@ let paletteAppSpec = {
this.saved.title ??= this.defaultPaletteTitle;
}
sidebar.palette.save(this.saved);
this.saved.search = location.search;
this.saved = sidebar.palette.save(this.saved);
if (uid !== this.saved.uid) {
// UID changed (most likely from saving a new palette)
this.uid = this.saved.uid;
this.permalink.set('uid', uid);
this.permalink.set('uid', this.uid);
this.permalink.updateLocation();
await this.$nextTick();
this.save(); // Save again to update the search param to include the UID
}
this.unsavedChanges = false;
@@ -862,100 +500,6 @@ let paletteAppSpec = {
return context;
},
/**
* Assign a hue to a role
* @param {string} role - Role we are setting
* @param {string} hue - Hue (literal or semantic)
*/
setRoleColor(role, hue) {
if (!this.seedHues[hue] && hue !== 'gray' && !ROLES.includes(hue)) {
// We're also adding it
this.addColor(this.coreColors[hue]);
}
this.roles[role] = hue;
},
/**
* Set a color's role(s)
* @param {string | number} hueOrIndex
* @param {string | string[]} roles
*/
setColorRole(hueOrIndex, roles) {
let hue = hueOrIndex >= 0 ? this.seedColorInfo[hueOrIndex]?.hue : hueOrIndex;
roles = new Set(Array.isArray(roles) ? roles : [roles]);
for (let role in this.roles) {
if (roles.has(role)) {
this.roles[role] = hue;
} else if (this.roles[role] === hue) {
this.roles[role] = undefined;
}
}
},
addColor(value, options) {
if (!value) {
if (this.seedColors.length === 0) {
value = firstSeedColor;
} else {
// Add suggestions
for (let hue of ['red', 'green', 'yellow', 'blue', 'orange', 'cyan', 'purple', 'pink', 'indigo']) {
if (hue in this.suggestedColors) {
value = { hue };
break;
}
}
}
}
if (value?.hue) {
// Pinning a generated color
let { hue, level, pinnedHue } = value;
level ??= this.coreLevels[hue];
let color = this.colors[hue][level];
value = { value: color + '', color, pinnedHue };
}
if (typeof value === 'string') {
value = { value };
} else if (value instanceof Color || value?.constructor.name === 'Color') {
value = { value: value + '', color: value };
}
if (options) {
Object.assign(value, options);
}
value.uid ??= this.maxSeedUid++;
this.seedColors.push(value);
},
deleteColor(index) {
this.seedColors.splice(index, 1);
},
getColor(ref) {
let color, index;
if (this.isCustom) {
if (ref?.hue) {
let { hue, level } = ref;
color = this.colors[hue][level];
index = this.colorToIndex[hue][level];
} else if (ref > 0) {
index = ref;
color = this.seedColors[index]?.color;
}
} else {
let { hue, level } = ref;
color = this.baseColors[hue][level];
}
return { color, index };
},
}, // end methods
directives: {
@@ -985,8 +529,6 @@ let paletteAppSpec = {
components: {
ColorPopup,
ColorInput,
ColorSelect,
ColorSlider,
ColorSwatchPicker,
InfoTip,

View File

@@ -1,357 +0,0 @@
const template = `
<wa-card size="small" class="color" :class="{tweaked}"
:style="{'--color': value, '--color-original': inputValue}">
<div slot="image" :style="{ colorScheme: level <= 60 ? 'dark' : 'light'}">
<color-popup placement="top-start" class="seed-color-tweak" :pinned=true deletable @delete="$emit('delete')" title="Edit color">
<wa-icon-button name="sliders-simple" class="tweak-icon"></wa-icon-button>
<template #content>
<color-slider label="Hue" label-default="Entered color"
coord="h" :min="0" :max="359" :step="1"
v-model:color="color" :default-value="inputLCH[2]" ></color-slider>
<color-slider label="Colorfulness" label-default="Entered color"
coord="c" :min="0" :max="maxChroma" :step="0.001"
v-model:color="color" :default-value="inputLCH[1]" format-type="scale" :format-base-value="maxChroma" ></color-slider>
<color-slider label="Lightness" label-default="Entered color"
coord="l" :min="0" :max="1" :step="0.01"
v-model:color="color" :default-value="inputLCH[0]" format-type="scale" :format-base-value="1" ></color-slider>
</template>
</color-popup>
<div class="name">
<wa-dropdown class="pin-hue" :class="{pinned: pinnedHue}">
<wa-button slot="trigger" appearance="outlined" caret>
<wa-icon name="thumbtack" v-if="pinnedHue" variant="solid" slot="prefix"></wa-icon>
{{ capitalize(hue) || 'New color' }}
</wa-button>
<wa-menu @wa-select="pinnedHue = $event.detail.item.value">
<wa-menu-item type="checkbox" :checked="pinnedHue ? null : ''">Automatic <em>({{ capitalize(detectedColorInfo.hue) }})</em></wa-menu-item>
<wa-divider></wa-divider>
<wa-menu-label>Pin to…</wa-menu-label>
<wa-menu-item v-for="hue in allHues" type="checkbox" :value="hue" :checked="pinnedHue === hue ? '' : null">{{ capitalize(hue) }}</wa-menu-item>
</wa-menu>
</wa-dropdown>
<span class="level">{{ level }}</span>
</div>
</div>
<wa-select class="color-to-role" multiple appearance="plain" placeholder="(No states)" max-options-visible="2"
ref="roles" :value.attr="Object.keys(roles).join(' ')" :value="Object.keys(roles)"
:getTag="getTag"
@input="$emit('update:roles', $event.target.value)">
<wa-option v-for="role in ROLES" :value="role" :class="{'default': !roles[role]}">{{ capitalize(role) }}</wa-option>
</wa-select>
<wa-input :value="valueRaw" @input="handleInput" @focus="inputFocused = true" @blur="inputFocused = false" ref="input"></wa-input>
</wa-card>
`;
import Color from 'https://colorjs.io/dist/color.js';
// import { nextTick } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';
import { nextTick } from 'https://cdn.jsdelivr.net/npm/vue@3/dist/vue.esm-browser.js';
import getMaxChroma from '../color/get-max-chroma.js';
import { identifyColor } from '../color/util.js';
import ColorPopup from './color-popup.js';
import ColorSlider from './color-slider.js';
import InfoTip from './info-tip.js';
import { ROLES, allHues } from '/assets/scripts/tweak/data.js';
import { capitalize } from '/assets/scripts/tweak/util.js';
await customElements.whenDefined('wa-select');
let maxUid = 0;
const expose = [
'valueRaw',
'value',
'inputValueRaw',
'inputValue',
'colorRaw',
'color',
'inputColorRaw',
'inputColor',
'hue',
'pinnedHue',
'level',
'tweaked',
];
export default {
expose,
props: {
modelValue: {
type: Object,
default(rawProps) {
return { value: '' };
},
},
otherColors: {
type: Array,
},
roles: {
type: Object,
default: {},
},
},
emits: ['update:modelValue', 'update:roles', 'delete'],
data() {
let uid = this.modelValue.uid ?? maxUid++;
if (this.modelValue.uid) {
maxUid = Math.max(maxUid, uid);
}
this.modelValue.uid = uid;
let valueRaw = this.modelValue.value;
let inputValueRaw = this.modelValue.inputValue ?? valueRaw;
let color = tryColor(this.modelValue.value);
let inputColor = tryColor(inputValueRaw);
return {
uid,
initialProps: { ...this.modelValue },
valueRaw,
value: color ? valueRaw : undefined,
color,
inputValueRaw,
inputValue: inputColor ? inputValueRaw : undefined,
inputColor,
pinnedHue: this.modelValue.pinnedHue,
editing: 0,
inputFocused: false,
watching: {},
};
},
created() {
// Non-reactive variables to expose
Object.assign(this, { ROLES, allHues });
},
async mounted() {
if (this.modelValue.editImmediately) {
let input = this.$refs.input;
await input.updateComplete;
input.focus();
input.select();
}
},
computed: {
inputLCH() {
return this.inputColor?.oklch;
},
currentLCH() {
return this.color?.oklch;
},
tweaked() {
if (this.inputFocused || this.editing > 0 || !this.inputLCH || !this.currentLCH) {
return false;
}
return this.inputLCH.some((coord, i) => coord !== this.currentLCH[i]);
},
computedValue() {
let ret = {};
for (let property of expose) {
ret[property] = this[property];
}
return ret;
},
colorRaw() {
let ret = tryColor(this.modelValue.valueRaw);
if (ret) {
this.value = this.modelValue.valueRaw;
}
return ret;
},
colorInfo() {
let ret = { ...this.detectedColorInfo };
if (this.pinnedHue) {
ret.hue = this.pinnedHue;
}
return ret;
},
detectedColorInfo() {
if (!this.color) {
return { hue: undefined, level: undefined };
}
return identifyColor(this.color, this.otherColors);
},
hue() {
return this.colorInfo.hue;
},
level() {
return this.colorInfo.level;
},
stringifiedColor() {
// return stringifyColor(this.colorRaw);
return this.color + '';
},
inputColorRaw() {
let ret = tryColor(this.inputValueRaw);
if (ret) {
this.inputValue = this.inputValueRaw;
}
return ret;
},
maxChroma() {
if (!this.color) {
return 0.4;
}
return getMaxChroma(this.color.oklch.l, this.color.oklch.h);
},
},
methods: {
capitalize,
handleInput(event) {
this.editing++;
let value = event.target.value;
// Editing the input manually also incorporates any tweaks as part of the color itself
// I.e. input color and color are now the same
this.valueRaw = this.inputValueRaw = value;
nextTick().then(() => {
if (this.colorRaw) {
this.color = this.colorRaw;
this.$refs.input.setCustomValidity('');
} else {
this.$refs.input.setCustomValidity('Invalid color');
this.$refs.input.reportValidity();
}
this.editing--;
});
},
mutateModelValue(mutator) {
if (this.watching.modelValue === null) {
// If we're not watching modelValue, it means we're reacting to a change to it
// so no point in updating it again
return;
}
if (this.watching.modelValue) {
this.watching.modelValue();
this.watching.modelValue = null;
}
mutator();
this.watching.modelValue = this.$watch('modelValue', {
deep: true,
handler() {
let computedValue = this.computedValue;
// What changed?
if (this.modelValue.value !== computedValue.value) {
this.valueRaw = this.modelValue.value;
}
if (this.modelValue.color + '' !== computedValue.color + '') {
this.color = this.modelValue.color;
}
},
});
},
getTag(option) {
let isDefault = option.classList.contains('default');
let tag = Object.assign(document.createElement('wa-tag'), {
part: `tag${isDefault ? ' default' : ''}`,
exportparts: `
base:tag__base,
content:tag__content,
remove-button:tag__remove-button,
remove-button__base:tag__remove-button__base`,
size: 'small',
removable: !isDefault,
'data-value': option.value,
id: 'tag-' + option.value,
innerHTML: option.label + ` <wa-tooltip hoist for="tag-${option.value}">Default role</wa-tooltip>`,
});
return tag;
},
},
watch: {
/** colorRaw -> color */
colorRaw: {
deep: true,
handler() {
if (this.colorRaw) {
this.color = this.colorRaw;
}
},
},
/** inputColorRaw -> inputColor */
inputColorRaw: {
deep: true,
handler() {
if (this.inputColorRaw) {
this.inputColor = this.inputColorRaw;
}
},
},
/** color -> value, valueRaw, modelValue.value */
color: {
deep: true,
handler() {
if (this.tweaked && this.color) {
// If tweaked, color is the source of truth
this.value = this.valueRaw = this.color + '';
}
},
},
/** computedValue -> modelValue */
computedValue: {
deep: true,
immediate: true,
handler() {
this.mutateModelValue(() => {
Object.assign(this.modelValue, this.computedValue);
this.$emit('update:modelValue', this.modelValue);
});
},
},
},
template,
components: { InfoTip, ColorSlider, ColorPopup },
compilerOptions: {
isCustomElement: tag => tag.startsWith('wa-'),
},
};
function tryColor(value) {
if (!value) {
return null;
}
if (value instanceof Color) {
return value;
}
try {
return new Color(value);
} catch (e) {
return null;
}
}

View File

@@ -56,7 +56,7 @@ export default {
maxRelative: Number,
step: {
type: Number,
default: 1,
default: 0.001,
},
type: {

View File

@@ -1,85 +0,0 @@
const template = `
<color-popup :title :token="token" :color="modelValue"
:pinned :pinnable @pin="$emit('pin')" :deletable @delete="$emit('delete')">
<div slot="trigger" class="color swatch" :style="{ '--color': modelValue, colorScheme: level > 60 ? 'light' : 'dark' }">
<wa-icon class="pinned-icon" name="thumbtack" variant="regular" v-if="pinned"></wa-icon>
<wa-icon name="sliders-simple" class="tweak-icon"></wa-icon>
</div>
<template #content>
<color-slider v-if="(isEdge || pinned) && tweakBase && HUE_RANGES[hue]"
:color="modelValue" @update:model-value="$emit('update:modelValue', $event)" :default-value="colors[hue][tweakBase].oklch.h"
@input="!pinned ? $emit('pin') : null"
coord="h" :min="HUE_RANGES[hue].min + 1" :max="HUE_RANGES[hue].max" :step="1"
label="Hue shift" :label-min="moreHue[hueBefore[hue]]" :label-max="moreHue[hueAfter[hue]]"
:label-default="\`\${capitalize(hue)} \${tweakBase}\`"
></color-slider>
</template>
</color-popup>
`;
import Color from 'https://colorjs.io/dist/color.js';
import ColorPopup from './color-popup.js';
import ColorSlider from './color-slider.js';
import InfoTip from './info-tip.js';
import { HUE_RANGES, hueAfter, hueBefore, hues, moreHue } from '/assets/scripts/tweak/data.js';
import { capitalize, clamp, promise, roundTo } from '/assets/scripts/tweak/util.js';
export default {
props: {
modelValue: Color,
hue: {
type: String,
required: true,
},
level: {
type: [String, Number],
required: true,
},
coreLevel: {
type: [String, Number],
required: true,
},
pinned: Boolean,
pinnable: Boolean,
deletable: Boolean,
colors: {
type: Object,
required: true,
},
tweakBase: [String, Number],
},
emits: ['update:modelValue', 'pin', 'delete'],
data() {
return {};
},
created() {
// Attach non-reactive data
Object.assign(this, { moreHue, HUE_RANGES });
},
computed: {
title() {
return capitalize(this.hue) + ' ' + this.level;
},
hueBefore() {
return hueBefore[this.hue];
},
hueAfter() {
return hueAfter[this.hue];
},
token() {
return `--wa-color-${this.hue}-${this.level}`;
},
isEdge() {
return this.level == '95' || this.level == '05';
},
isCore() {
return this.level == this.coreLevel;
},
},
methods: { capitalize },
template,
components: { InfoTip, ColorSlider, ColorPopup },
compilerOptions: {
isCustomElement: tag => tag.startsWith('wa-'),
},
};

View File

@@ -1,71 +0,0 @@
---
title: Custom
isPro: true
override:tags: [palettes, pro]
order: 99
description: Create your own color palette from scratch, from one or more seed colors.
status: experimental
---
<link href="{{ page.url }}../app/custom.css" rel="stylesheet">
<h2 v-if="step > 0" v-cloak>My Colors</h2>
<p v-if="step > 0" v-cloak>
Just add your colors, in any order. Well sort them out for you, generate tints, and suggest additional colors.
</p>
<div id="seed-colors">
<template v-for="color, i in seedColors" :key="color.uid ?? maxSeedUid">
<color-input v-model="seedColors[i]"
:other-colors="seedColors.filter((_, j) => j !== i).map(c => c.color)"
:roles="seedColorRoles[i]"
@update:roles="roles => setColorRole(i, roles)"
@delete="deleteColor(i)"></color-input>
</template>
<wa-button class="add-button" appearance="outlined" @click="addColor(undefined, {editImmediately: true})">
<wa-icon slot="prefix" name="plus" variant="regular"></wa-icon>
<span v-content="step > 0 ? 'Add color' : 'New palette'">New palette</span>
</wa-button>
</div>
<wa-details id="suggested-colors" v-if="step > 0" v-cloak open>
<h3 class="wa-heading-m" slot="summary">Suggestions</h3>
<p class="wa-caption-m">
Generated by our fancy-schmancy algorithm to complement your colors.
See a color you like? Grab it before its gone!
</p>
<div class="suggestions wa-cluster wa-align-items-start wa-gap-s">
<template v-for="color, hue in suggestedColors">
<info-tip>
<wa-button :style="{'--background-color': color}" @click="addColor({hue})">
<wa-icon name="plus"></wa-icon>
</wa-button>
<template #content>{% raw %}{{ capitalize(hue) }}{% endraw %}</template>
</info-tip>
</template>
</div>
</wa-details>
<section id="roles" v-if="step > 0" v-cloak>
<h2>Roles</h2>
<div>
<color-select v-for="computedRole, role in computedRoles"
:model-value="computedRoles[role]"
@update:model-value="value => setRoleColor(role, value)"
:class="{'default': !roles[role]}"
:label="capitalize(role) + ':'"
:groups="{
Dynamic: !['brand', 'neutral'].includes(role) ? ['brand', 'neutral'] : undefined,
Colors: Object.keys(paletteScales),
Common: suggestedForRole[role]
}"
:get-label="capitalize"
:get-content="value => capitalize(value) + (seedHues[value] || computedRoles[value] || value === 'gray' ? '' : ' <wa-icon name=square-plus variant=regular></wa-icon>')"
:get-color="value => coreColors[computedRoles[value] ?? value]">
{# <wa-badge class="default-badge" v-if="!roles[role]" slot="suffix" variant="neutral" appearance="outlined">Default</wa-badge> #}
</color-select>
</div>
</section>