mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-19 07:29:14 +00:00
Compare commits
147 Commits
palette-re
...
custom-pal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a0b2da5a1 | ||
|
|
d64d75b3f4 | ||
|
|
fe2829698a | ||
|
|
fe2be5cbdb | ||
|
|
f9b932042e | ||
|
|
8dee82a44a | ||
|
|
0b883866d1 | ||
|
|
d0a60d2c30 | ||
|
|
398ae15979 | ||
|
|
0780c12adb | ||
|
|
bfafc08761 | ||
|
|
2ac15dcda1 | ||
|
|
67437b719d | ||
|
|
f05c8f7b84 | ||
|
|
2c0ff72f0d | ||
|
|
672fc3a5ad | ||
|
|
ff45ca2232 | ||
|
|
7dcbd7407f | ||
|
|
7c04550753 | ||
|
|
08bf971f91 | ||
|
|
8245d8a40a | ||
|
|
e342f513b7 | ||
|
|
cdaa34e1bc | ||
|
|
0cca25a118 | ||
|
|
e9edc572b5 | ||
|
|
77da38fda3 | ||
|
|
cd4486cc86 | ||
|
|
badc6c9dc2 | ||
|
|
33f3f8d4c0 | ||
|
|
2bdfcae9ba | ||
|
|
f369916f01 | ||
|
|
c9a1e21cdb | ||
|
|
d649d2ee3b | ||
|
|
44469183cb | ||
|
|
d30149e718 | ||
|
|
416aaee672 | ||
|
|
e9ea0b7f1c | ||
|
|
5d97db178a | ||
|
|
45b3a8e76e | ||
|
|
550df496e1 | ||
|
|
cde67b7984 | ||
|
|
fd6e7e19f0 | ||
|
|
c442e52c63 | ||
|
|
9c57646f48 | ||
|
|
344e693c8b | ||
|
|
12b2ab133a | ||
|
|
1b26bee1af | ||
|
|
27c7e56a7e | ||
|
|
22e5850a3f | ||
|
|
f4897dcabe | ||
|
|
3dd5e0e8aa | ||
|
|
515b48f8a5 | ||
|
|
9f141dbc4a | ||
|
|
ca60751cb8 | ||
|
|
7dfa2f6a93 | ||
|
|
31c4dc658f | ||
|
|
82c34a8fe6 | ||
|
|
15ac2d169d | ||
|
|
412670a21d | ||
|
|
c70ea3627c | ||
|
|
0a938d5cf3 | ||
|
|
1a9372839c | ||
|
|
12c5747cd2 | ||
|
|
bb24db30b5 | ||
|
|
48d7e45d30 | ||
|
|
6dd2fbec74 | ||
|
|
d7dbf0f3f9 | ||
|
|
ba9d4c1f21 | ||
|
|
d4131095a8 | ||
|
|
1dd47557c0 | ||
|
|
054058a52c | ||
|
|
9f0d1df974 | ||
|
|
a918c2297d | ||
|
|
96704a2d7e | ||
|
|
3ae89b827f | ||
|
|
6523925eaf | ||
|
|
9d6cf9efb8 | ||
|
|
73892da3a7 | ||
|
|
b1a29ecf69 | ||
|
|
089450c25e | ||
|
|
ed9a1280c1 | ||
|
|
b50b5983d3 | ||
|
|
748fd42d40 | ||
|
|
efe570f7b3 | ||
|
|
110dc7da60 | ||
|
|
d778013667 | ||
|
|
e898179802 | ||
|
|
5c78e3226f | ||
|
|
daa0ccee26 | ||
|
|
890791f94e | ||
|
|
9a03cea920 | ||
|
|
fd9235fe29 | ||
|
|
df108ba346 | ||
|
|
510a6c4eac | ||
|
|
b627c9b7d5 | ||
|
|
29aeb078b7 | ||
|
|
b966f57a83 | ||
|
|
9928f77091 | ||
|
|
baae409bfc | ||
|
|
15abc6d21c | ||
|
|
353c053153 | ||
|
|
ab01fbb5af | ||
|
|
a73b3d5697 | ||
|
|
7b6b570ac9 | ||
|
|
438ddf5ba2 | ||
|
|
5216061c39 | ||
|
|
e466a0aa8d | ||
|
|
08876bbda9 | ||
|
|
a73daf9426 | ||
|
|
af832017d3 | ||
|
|
9244bfbe15 | ||
|
|
1f89043040 | ||
|
|
9865a71499 | ||
|
|
f2e8a71567 | ||
|
|
72d8058259 | ||
|
|
08f652f0dc | ||
|
|
48b37b05bb | ||
|
|
a3e1cebf18 | ||
|
|
9632e57fd0 | ||
|
|
5bfac00428 | ||
|
|
a0069c9783 | ||
|
|
b43a3f736a | ||
|
|
7ed3e5e92b | ||
|
|
01b697d9e6 | ||
|
|
fa2e35a299 | ||
|
|
cb5f8433d5 | ||
|
|
1fa95f66e8 | ||
|
|
f682293c38 | ||
|
|
1993182f43 | ||
|
|
6f39781f1f | ||
|
|
bc170cce15 | ||
|
|
27af62591f | ||
|
|
e07aecb0a7 | ||
|
|
8caeb26957 | ||
|
|
ae6b66a3a4 | ||
|
|
e9389b8bd5 | ||
|
|
668666e1c9 | ||
|
|
a679693128 | ||
|
|
3c02ce245e | ||
|
|
4a5b99c60d | ||
|
|
1a2d9ea4f1 | ||
|
|
91d93d83f2 | ||
|
|
6693cafe8e | ||
|
|
d04e3d860e | ||
|
|
65f89cff84 | ||
|
|
e26af1c293 | ||
|
|
538e132a27 |
@@ -12,7 +12,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
{% for hue in hues -%}
|
||||
<tr data-hue="{{ hue }}">
|
||||
<tr data-hue="{{ hue }}" v-if="'{{hue}}' in paletteScales">
|
||||
<th>{{ hue | capitalize }}</th>
|
||||
{% for tint_bg in tints -%}
|
||||
{% set color_bg = palettes[paletteId][hue][tint_bg] %}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% set hasSidebar = true %}
|
||||
{% set hasOutline = true %}
|
||||
{% set paletteId = page.fileSlug %}
|
||||
{% set paletteId = "default" if page.fileSlug == 'custom' else page.fileSlug %}
|
||||
{% set isCustom = page.fileSlug == 'custom' %}
|
||||
{% set tints = ["95", "90", "80", "70", "60", "50", "40", "30", "20", "10", "05"] %}
|
||||
|
||||
{% extends '../_includes/base.njk' %}
|
||||
@@ -15,6 +16,7 @@
|
||||
<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,
|
||||
@@ -46,10 +48,10 @@
|
||||
</h1>
|
||||
|
||||
<div class="block-info" v-cloak>
|
||||
<code class="class">.wa-palette-<span v-content="slug">{{ page.fileSlug }}</span></code>
|
||||
<code class="class" v-if="saved || !isCustom || step > 0">.wa-palette-<span v-content="slug">{{ page.fileSlug }}</span></code>
|
||||
{% include '../_includes/status.njk' %}
|
||||
{% if not isPro %}
|
||||
<wa-badge class="pro" v-if="tweaked">PRO</wa-badge>
|
||||
<wa-badge class="pro" v-if="tweaked || isCustom">PRO</wa-badge>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if description %}
|
||||
@@ -57,28 +59,32 @@
|
||||
{{ description | inlineMarkdown | safe }}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="hue-wheel">
|
||||
{% raw %}
|
||||
<div class="hue-wheel" v-if="!isCustom || step > 1">
|
||||
<template v-for="color, hue in coreColors">
|
||||
<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 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>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
{% endraw %}
|
||||
</header>
|
||||
{% endblock %}
|
||||
|
||||
{% block afterContent %}
|
||||
|
||||
<wa-callout size="small" class="tweaked-callout" variant="warning">
|
||||
<wa-callout size="small" class="tweaked-callout" variant="warning" v-if="!isCustom">
|
||||
<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)" v-content="tweakHumanReadable"></wa-tag>
|
||||
<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" variant="danger">
|
||||
@@ -103,8 +109,9 @@
|
||||
{%- endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
{% raw %}
|
||||
<tbody v-cloak>
|
||||
<tr v-for="hue of allHues" :data-hue="hue" :key="hue"
|
||||
<tr v-for="hue in paletteScalesList" :data-hue="hue" :key="hue"
|
||||
class="color-scale" :class="{
|
||||
tweaked: hue === 'gray' ? tweaked.grayChroma || tweaked.grayColor : hueShifts[hue],
|
||||
}"
|
||||
@@ -112,9 +119,18 @@
|
||||
'--swatch-text-color': `light-dark(var(--wa-color-${ hue }-10), white)`,
|
||||
'--hue-shift': hueShifts[hue] || ''
|
||||
}">
|
||||
<th v-content="capitalize(hue)"></th>
|
||||
<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>
|
||||
<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]">
|
||||
<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] + '')">
|
||||
<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>
|
||||
@@ -124,7 +140,15 @@
|
||||
<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="baseCoreColors[hue]"
|
||||
<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]"
|
||||
coord="h" type="shift"
|
||||
v-model="hueShifts[hue]"
|
||||
:default-color="baseCoreColors[hue]"
|
||||
@@ -142,30 +166,69 @@
|
||||
: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]">
|
||||
<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})">
|
||||
<div slot="trigger" class="color swatch" :style="{ colorScheme: tint > 60 ? 'light' : 'dark' }">
|
||||
<wa-icon name="copy" variant="regular" class="copy-icon"></wa-icon>
|
||||
<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>
|
||||
</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>
|
||||
</tbody>
|
||||
{% endraw %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<color-slider :class="{ tweaked: chromaScale !== 1 }"
|
||||
<color-slider v-if="!isCustom" :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.001"
|
||||
:min="MAX_CHROMA_BOUNDS.min" :max="MAX_CHROMA_BOUNDS.max" :step="0.01"
|
||||
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">
|
||||
@@ -175,6 +238,7 @@
|
||||
{%- endif -%}
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% markdown %}
|
||||
## Color Contrast
|
||||
@@ -270,7 +334,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 variations</h2>
|
||||
<h2 class="index-category">Saved {{ 'custom palettes' if page.fileSlug == 'custom' else title + ' variations' }}</h2>
|
||||
<a v-for="palette of savedVariations" :href="'/docs/palettes/' + palette.id">
|
||||
<wa-card with-header>
|
||||
<div slot="header">
|
||||
|
||||
@@ -80,7 +80,7 @@ sidebar.palette = {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let index; (index = savedPalettes.findIndex(p => p.uid === palette.uid)) > -1; ) {
|
||||
for (let index; index > -1; index = savedPalettes.findIndex(p => p.uid === palette.uid)) {
|
||||
savedPalettes.splice(index, 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { tints } from '/assets/scripts/tweak/data.js';
|
||||
|
||||
export function generateGrays(colors, { grayColor, grayChroma, grayLevel }) {
|
||||
export function generateGrays(colors, { grayColor, grayChroma }) {
|
||||
let ret = {};
|
||||
let undertoneScale = colors[grayColor];
|
||||
|
||||
// These will be the same, since scaling them won't change the relationship
|
||||
ret.maxChromaTint = grayLevel ?? undertoneScale.maxChromaTint;
|
||||
ret.maxChromaTint = undertoneScale.maxChromaTint;
|
||||
Object.defineProperty(ret, 'core', {
|
||||
enumerable: false,
|
||||
get() {
|
||||
|
||||
@@ -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, colors, tweaked, ...options }) {
|
||||
export function getPaletteCode({ base, slug = base, colors, tweaked, roles, ...options }) {
|
||||
let imports = [];
|
||||
|
||||
if (base && options.imports !== false) {
|
||||
if (base && options.imports !== false && !tweaked.seedColors) {
|
||||
imports.push(urls.palette(base));
|
||||
}
|
||||
|
||||
@@ -18,12 +18,14 @@ export function getPaletteCode({ base, colors, tweaked, ...options }) {
|
||||
|
||||
if (tweaked) {
|
||||
for (let hue in colors) {
|
||||
if (hue === 'gray') {
|
||||
if (!tweaked.grayChroma && !tweaked.grayColor) {
|
||||
if (!tweaked.seedColors) {
|
||||
if (hue === 'gray') {
|
||||
if (!tweaked.grayChroma && !tweaked.grayColor) {
|
||||
continue;
|
||||
}
|
||||
} else if (!tweaked.chromaScale && !tweaked.hue?.[hue]) {
|
||||
continue;
|
||||
}
|
||||
} else if (!tweaked.chromaScale && !tweaked.hue?.[hue]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let scale = colors[hue];
|
||||
@@ -46,8 +48,24 @@ export function getPaletteCode({ base, colors, tweaked, ...options }) {
|
||||
}
|
||||
}
|
||||
|
||||
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(base);
|
||||
let selector = options.selector ?? selectors.palette(slug);
|
||||
css += cssRule(selector, declarations);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,8 +28,7 @@ export function tweakPalette(baseColors, tweaks, tweaked) {
|
||||
if (tweaked.grayChroma || tweaked.grayColor) {
|
||||
let grayColor = tweaks.grayColor ?? this.originalGrayColor;
|
||||
let grayChroma = this.computedGrayChroma;
|
||||
let grayLevel = baseColors.gray?.maxChromaTint;
|
||||
ret.gray = generateGrays(baseColors, { grayColor, grayChroma, grayLevel });
|
||||
ret.gray = generateGrays(baseColors, { grayColor, grayChroma });
|
||||
} else {
|
||||
ret.gray = originalScale;
|
||||
}
|
||||
|
||||
187
docs/docs/palettes/app/custom.css
Normal file
187
docs/docs/palettes/app/custom.css
Normal file
@@ -0,0 +1,187 @@
|
||||
/* 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);
|
||||
}
|
||||
@@ -150,21 +150,8 @@ 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 {
|
||||
@@ -225,6 +212,11 @@ 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;
|
||||
@@ -242,33 +234,20 @@ 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);
|
||||
--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%;
|
||||
}
|
||||
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)
|
||||
);
|
||||
|
||||
&,
|
||||
&::before {
|
||||
@@ -281,11 +260,7 @@ wa-dropdown > .color.swatch {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
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;
|
||||
-webkit-mask: radial-gradient(white, transparent);
|
||||
background: radial-gradient(oklch(var(--avg-l) calc(var(--gray-chroma) * var(--max-c)) 0) 5%, transparent 30%),
|
||||
conic-gradient(
|
||||
in oklch,
|
||||
@@ -315,7 +290,6 @@ wa-dropdown > .color.swatch {
|
||||
--scale: 1.2;
|
||||
--line-color: white;
|
||||
--line-style: solid;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&::before {
|
||||
@@ -361,6 +335,16 @@ 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: '';
|
||||
|
||||
@@ -8,7 +8,9 @@ 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';
|
||||
@@ -30,6 +32,12 @@ 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');
|
||||
@@ -38,10 +46,20 @@ 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: palette.colors,
|
||||
baseColors: { ...palette.colors },
|
||||
originalColors: paletteId === 'custom' ? allPalettes.default.colors : palette.colors,
|
||||
permalink: new Permalink(),
|
||||
hueShifts: Object.fromEntries(hues.map(hue => [hue, 0])),
|
||||
chromaScale: 1,
|
||||
@@ -50,22 +68,13 @@ 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,
|
||||
allHues,
|
||||
tints,
|
||||
MAX_CHROMA_BOUNDS,
|
||||
});
|
||||
Object.assign(this, { moreHue, hueBefore, hueAfter, HUE_RANGES, L_RANGES, hues, tints, MAX_CHROMA_BOUNDS });
|
||||
|
||||
if (location.search) {
|
||||
// Read URL params and apply them. This facilitates permalinks.
|
||||
@@ -89,18 +98,37 @@ 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(() => {
|
||||
if (!this.tweaked || this.saved) {
|
||||
this.unsavedChanges = false;
|
||||
}
|
||||
this.unsavedChanges = false;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -113,16 +141,110 @@ let paletteAppSpec = {
|
||||
* @returns
|
||||
*/
|
||||
step() {
|
||||
return this.tweaked ? 1 : 0;
|
||||
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';
|
||||
},
|
||||
|
||||
slug() {
|
||||
return this.paletteId;
|
||||
if (this.isCustom) {
|
||||
return slugify(this.paletteTitle);
|
||||
} else {
|
||||
// The slug does not change for tweaked palettes
|
||||
return this.paletteId;
|
||||
}
|
||||
},
|
||||
|
||||
/** Default palette title for saving */
|
||||
defaultPaletteTitle() {
|
||||
return this.originalPaletteTitle + ' (tweaked)';
|
||||
if (this.isCustom) {
|
||||
return 'My Palette';
|
||||
} else {
|
||||
return this.originalPaletteTitle + ' (tweaked)';
|
||||
}
|
||||
},
|
||||
|
||||
paletteTitle() {
|
||||
@@ -135,6 +257,148 @@ 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,
|
||||
@@ -149,8 +413,10 @@ let paletteAppSpec = {
|
||||
for (let language of ['html', 'css']) {
|
||||
let code = getPaletteCode({
|
||||
base: this.paletteId,
|
||||
colors: this.colors,
|
||||
slug: this.isCustom ? this.slug : undefined,
|
||||
colors: this.paletteScales,
|
||||
tweaked: this.tweaked,
|
||||
roles: this.isCustom ? this.computedRoles : this.roles,
|
||||
language,
|
||||
cdnUrl,
|
||||
});
|
||||
@@ -163,6 +429,18 @@ 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);
|
||||
},
|
||||
@@ -174,6 +452,7 @@ let paletteAppSpec = {
|
||||
: false;
|
||||
|
||||
let ret = {
|
||||
seedColors: this.seedColors.length > 0,
|
||||
chromaScale: this.chromaScale !== 1,
|
||||
hue,
|
||||
grayChroma: this.grayChroma !== undefined && this.grayChroma !== this.originalGrayChroma,
|
||||
@@ -227,7 +506,7 @@ let paletteAppSpec = {
|
||||
},
|
||||
|
||||
contrasts() {
|
||||
return getContrasts(this.colors, this.originalContrasts);
|
||||
return getContrasts(this.paletteScales, this.originalContrasts);
|
||||
},
|
||||
|
||||
baseCoreColors() {
|
||||
@@ -357,7 +636,7 @@ let paletteAppSpec = {
|
||||
},
|
||||
|
||||
maxGrayChroma() {
|
||||
return MAX_GRAY_CHROMA_SCALE[this.grayColor] ?? 0.35;
|
||||
return MAX_GRAY_CHROMA_SCALE[this.grayColor] ?? 0.3;
|
||||
},
|
||||
|
||||
huesAfter() {
|
||||
@@ -375,6 +654,41 @@ 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: {
|
||||
@@ -399,6 +713,19 @@ 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) {
|
||||
@@ -407,14 +734,37 @@ let paletteAppSpec = {
|
||||
// Update page URL
|
||||
this.permalink.updateLocation();
|
||||
|
||||
this.unsavedChanges = true;
|
||||
if (this.saved || this.isCustom) {
|
||||
this.unsavedChanges = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
saved: {
|
||||
roles: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.unsavedChanges = !this.saved;
|
||||
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;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}, // end watch
|
||||
@@ -425,10 +775,26 @@ let paletteAppSpec = {
|
||||
getMaxChroma,
|
||||
log,
|
||||
|
||||
async save({ title } = {}) {
|
||||
/**
|
||||
* 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 } = {}) {
|
||||
let uid = this.uid;
|
||||
|
||||
this.saved ??= { id: this.paletteId, uid: this.uid };
|
||||
this.saved ??= { id: this.paletteId, uid: this.uid, search: location.search };
|
||||
|
||||
if (title) {
|
||||
// Renaming
|
||||
@@ -437,17 +803,13 @@ let paletteAppSpec = {
|
||||
this.saved.title ??= this.defaultPaletteTitle;
|
||||
}
|
||||
|
||||
this.saved.search = location.search;
|
||||
|
||||
this.saved = sidebar.palette.save(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', this.uid);
|
||||
this.permalink.set('uid', uid);
|
||||
this.permalink.updateLocation();
|
||||
await this.$nextTick();
|
||||
this.save(); // Save again to update the search param to include the UID
|
||||
}
|
||||
|
||||
this.unsavedChanges = false;
|
||||
@@ -500,6 +862,100 @@ 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: {
|
||||
@@ -529,6 +985,8 @@ let paletteAppSpec = {
|
||||
|
||||
components: {
|
||||
ColorPopup,
|
||||
ColorInput,
|
||||
ColorSelect,
|
||||
ColorSlider,
|
||||
ColorSwatchPicker,
|
||||
InfoTip,
|
||||
|
||||
357
docs/docs/palettes/app/vue-components/color-input.js
Normal file
357
docs/docs/palettes/app/vue-components/color-input.js
Normal file
@@ -0,0 +1,357 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export default {
|
||||
maxRelative: Number,
|
||||
step: {
|
||||
type: Number,
|
||||
default: 0.001,
|
||||
default: 1,
|
||||
},
|
||||
|
||||
type: {
|
||||
|
||||
85
docs/docs/palettes/app/vue-components/color-swatch.js
Normal file
85
docs/docs/palettes/app/vue-components/color-swatch.js
Normal file
@@ -0,0 +1,85 @@
|
||||
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-'),
|
||||
},
|
||||
};
|
||||
71
docs/docs/palettes/custom.njk
Normal file
71
docs/docs/palettes/custom.njk
Normal file
@@ -0,0 +1,71 @@
|
||||
---
|
||||
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. We’ll 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 it’s 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>
|
||||
Reference in New Issue
Block a user