mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-19 15:34:15 +00:00
Compare commits
198 Commits
select-css
...
custom-pal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a0b2da5a1 | ||
|
|
59dcaaff83 | ||
|
|
5bad30ec30 | ||
|
|
87c1762146 | ||
|
|
899edd1d5e | ||
|
|
872a110b1e | ||
|
|
d64d75b3f4 | ||
|
|
fe2829698a | ||
|
|
07fe6d598e | ||
|
|
fe2be5cbdb | ||
|
|
f9b932042e | ||
|
|
8dee82a44a | ||
|
|
79bafc513a | ||
|
|
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 | ||
|
|
1d03f7bee0 | ||
|
|
ba9d4c1f21 | ||
|
|
a9bf1bd838 | ||
|
|
d4131095a8 | ||
|
|
1dd47557c0 | ||
|
|
054058a52c | ||
|
|
9f0d1df974 | ||
|
|
a918c2297d | ||
|
|
96704a2d7e | ||
|
|
c0ca739366 | ||
|
|
3ae89b827f | ||
|
|
a6745602d6 | ||
|
|
da4f619d95 | ||
|
|
1283a696a5 | ||
|
|
d12b97b0b0 | ||
|
|
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 | ||
|
|
e5c2884880 | ||
|
|
1d600a77c4 | ||
|
|
353c053153 | ||
|
|
ab01fbb5af | ||
|
|
a73b3d5697 | ||
|
|
7b6b570ac9 | ||
|
|
438ddf5ba2 | ||
|
|
5216061c39 | ||
|
|
e466a0aa8d | ||
|
|
08876bbda9 | ||
|
|
a73daf9426 | ||
|
|
af832017d3 | ||
|
|
9244bfbe15 | ||
|
|
1f89043040 | ||
|
|
9865a71499 | ||
|
|
f2e8a71567 | ||
|
|
72d8058259 | ||
|
|
db3c568ba2 | ||
|
|
4bb9805ba6 | ||
|
|
bd935fa8d5 | ||
|
|
c3e582b47b | ||
|
|
4d094a4e19 | ||
|
|
782c404bdf | ||
|
|
f1438981b2 | ||
|
|
18b88c2f5c | ||
|
|
a2d85f49a3 | ||
|
|
be00026cd3 | ||
|
|
58ed88bc5a | ||
|
|
1d14e186f3 | ||
|
|
5f672aabc2 | ||
|
|
db08e12a32 | ||
|
|
e0fc639226 | ||
|
|
e6c662b543 | ||
|
|
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 | ||
|
|
d1de9a9a73 | ||
|
|
4931de8eb4 | ||
|
|
538e132a27 | ||
|
|
71e7227763 | ||
|
|
dd671e15aa | ||
|
|
2daeea0349 | ||
|
|
3cb6625c1d | ||
|
|
c4b5446d01 | ||
|
|
41affca083 | ||
|
|
132dbfabcc | ||
|
|
4fc6224464 | ||
|
|
4921d1c32e | ||
|
|
54d71d2319 | ||
|
|
c1ecca0169 | ||
|
|
d6a91919e0 | ||
|
|
4621094ea1 | ||
|
|
726dc73e2a | ||
|
|
4bfebf3249 | ||
|
|
99ad0abdd3 | ||
|
|
902e2b6367 |
@@ -13,4 +13,4 @@ package-lock.json
|
|||||||
tsconfig.json
|
tsconfig.json
|
||||||
cdn
|
cdn
|
||||||
_site
|
_site
|
||||||
docs/assets/scripts/prism.js
|
docs/assets/scripts/prism-downloaded.js
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import * as path from 'node:path';
|
||||||
import { anchorHeadingsPlugin } from './_utils/anchor-headings.js';
|
import { anchorHeadingsPlugin } from './_utils/anchor-headings.js';
|
||||||
import { codeExamplesPlugin } from './_utils/code-examples.js';
|
import { codeExamplesPlugin } from './_utils/code-examples.js';
|
||||||
import { copyCodePlugin } from './_utils/copy-code.js';
|
import { copyCodePlugin } from './_utils/copy-code.js';
|
||||||
@@ -8,6 +9,7 @@ import { removeDataAlphaElements } from './_utils/remove-data-alpha-elements.js'
|
|||||||
// import { formatCodePlugin } from './_utils/format-code.js';
|
// import { formatCodePlugin } from './_utils/format-code.js';
|
||||||
import litPlugin from '@lit-labs/eleventy-plugin-lit';
|
import litPlugin from '@lit-labs/eleventy-plugin-lit';
|
||||||
import { readFile } from 'fs/promises';
|
import { readFile } from 'fs/promises';
|
||||||
|
import nunjucks from 'nunjucks';
|
||||||
import componentList from './_data/componentList.js';
|
import componentList from './_data/componentList.js';
|
||||||
import * as filters from './_utils/filters.js';
|
import * as filters from './_utils/filters.js';
|
||||||
import { outlinePlugin } from './_utils/outline.js';
|
import { outlinePlugin } from './_utils/outline.js';
|
||||||
@@ -16,7 +18,10 @@ import { searchPlugin } from './_utils/search.js';
|
|||||||
|
|
||||||
import process from 'process';
|
import process from 'process';
|
||||||
|
|
||||||
const packageData = JSON.parse(await readFile('./package.json', 'utf-8'));
|
import * as url from 'url';
|
||||||
|
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
||||||
|
|
||||||
|
const packageData = JSON.parse(await readFile(path.join(__dirname, '..', 'package.json'), 'utf-8'));
|
||||||
const isAlpha = process.argv.includes('--alpha');
|
const isAlpha = process.argv.includes('--alpha');
|
||||||
const isDev = process.argv.includes('--develop');
|
const isDev = process.argv.includes('--develop');
|
||||||
|
|
||||||
@@ -24,12 +29,23 @@ const globalData = {
|
|||||||
package: packageData,
|
package: packageData,
|
||||||
isAlpha,
|
isAlpha,
|
||||||
layout: 'page.njk',
|
layout: 'page.njk',
|
||||||
|
|
||||||
|
server: {
|
||||||
|
head: '',
|
||||||
|
loginOrAvatar: '',
|
||||||
|
flashes: '',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const passThroughExtensions = ['js', 'css', 'png', 'svg', 'jpg', 'mp4'];
|
const passThroughExtensions = ['js', 'css', 'png', 'svg', 'jpg', 'mp4'];
|
||||||
const passThrough = [...passThroughExtensions.map(ext => 'docs/**/*.' + ext)];
|
const passThrough = [...passThroughExtensions.map(ext => 'docs/**/*.' + ext)];
|
||||||
|
|
||||||
export default function (eleventyConfig) {
|
export default function (eleventyConfig) {
|
||||||
|
/**
|
||||||
|
* This is the guard we use for now to make sure our final built files dont need a 2nd pass by the server. This keeps us able to still deploy the bare HTML files on Vercel until the app is ready.
|
||||||
|
*/
|
||||||
|
const serverBuild = process.env.WEBAWESOME_SERVER === 'true';
|
||||||
|
|
||||||
// NOTE - alpha setting removes certain pages
|
// NOTE - alpha setting removes certain pages
|
||||||
if (isAlpha) {
|
if (isAlpha) {
|
||||||
eleventyConfig.ignores.add('**/experimental/**');
|
eleventyConfig.ignores.add('**/experimental/**');
|
||||||
@@ -55,7 +71,38 @@ export default function (eleventyConfig) {
|
|||||||
|
|
||||||
// Shortcodes - {% shortCode arg1, arg2 %}
|
// Shortcodes - {% shortCode arg1, arg2 %}
|
||||||
eleventyConfig.addShortcode('cdnUrl', location => {
|
eleventyConfig.addShortcode('cdnUrl', location => {
|
||||||
return `https://early.webawesome.com/webawesome@${packageData.version}/dist/` + location.replace(/^\//, '');
|
return `https://early.webawesome.com/webawesome@${packageData.version}/dist/` + (location || '').replace(/^\//, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Turns `{% server "foo" %} into `{{ server.foo | safe }}` when the WEBAWESOME_SERVER variable is set to "true"
|
||||||
|
eleventyConfig.addShortcode('server', function (property) {
|
||||||
|
if (serverBuild) {
|
||||||
|
return `{{ server.${property} | safe }}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
eleventyConfig.addTransform('second-nunjucks-transform', function NunjucksTransform(content) {
|
||||||
|
// For a server build, we expect a server to run the second transform.
|
||||||
|
if (serverBuild) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only run the transform on files nunjucks would transform.
|
||||||
|
if (!this.page.inputPath.match(/.(md|html|njk)$/)) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** This largely mimics what an app would do and just stubs out what we don't care about. */
|
||||||
|
return nunjucks.renderString(content, {
|
||||||
|
// Stub the server EJS shortcodes.
|
||||||
|
server: {
|
||||||
|
head: '',
|
||||||
|
loginOrAvatar: '',
|
||||||
|
flashes: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Paired shortcodes - {% shortCode %}content{% endShortCode %}
|
// Paired shortcodes - {% shortCode %}content{% endShortCode %}
|
||||||
@@ -94,7 +141,7 @@ export default function (eleventyConfig) {
|
|||||||
eleventyConfig.addPlugin(highlightCodePlugin());
|
eleventyConfig.addPlugin(highlightCodePlugin());
|
||||||
|
|
||||||
// Add copy code buttons to code blocks
|
// Add copy code buttons to code blocks
|
||||||
eleventyConfig.addPlugin(copyCodePlugin());
|
eleventyConfig.addPlugin(copyCodePlugin);
|
||||||
|
|
||||||
// Various text replacements
|
// Various text replacements
|
||||||
eleventyConfig.addPlugin(
|
eleventyConfig.addPlugin(
|
||||||
@@ -117,29 +164,6 @@ export default function (eleventyConfig) {
|
|||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
// SSR plugin
|
|
||||||
if (!isDev) {
|
|
||||||
//
|
|
||||||
// Problematic components in SSR land:
|
|
||||||
// - animation (breaks on navigation + ssr with Turbo)
|
|
||||||
// - mutation-observer (why SSR this?)
|
|
||||||
// - resize-observer (why SSR this?)
|
|
||||||
// - tooltip (why SSR this?)
|
|
||||||
//
|
|
||||||
const omittedModules = [];
|
|
||||||
const componentModules = componentList
|
|
||||||
.filter(component => !omittedModules.includes(component.tagName.split(/wa-/)[1]))
|
|
||||||
.map(component => {
|
|
||||||
const name = component.tagName.split(/wa-/)[1];
|
|
||||||
return `./dist/components/${name}/${name}.js`;
|
|
||||||
});
|
|
||||||
|
|
||||||
eleventyConfig.addPlugin(litPlugin, {
|
|
||||||
mode: 'worker',
|
|
||||||
componentModules,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the search index
|
// Build the search index
|
||||||
eleventyConfig.addPlugin(
|
eleventyConfig.addPlugin(
|
||||||
searchPlugin({
|
searchPlugin({
|
||||||
@@ -166,6 +190,31 @@ export default function (eleventyConfig) {
|
|||||||
eleventyConfig.addPassthroughCopy(glob);
|
eleventyConfig.addPassthroughCopy(glob);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SSR plugin
|
||||||
|
// Make sure this is the last thing, we dont want to run the risk of accidentally transforming shadow roots with the nunjucks 2nd transform.
|
||||||
|
if (!isDev) {
|
||||||
|
//
|
||||||
|
// Problematic components in SSR land:
|
||||||
|
// - animation (breaks on navigation + ssr with Turbo)
|
||||||
|
// - mutation-observer (why SSR this?)
|
||||||
|
// - resize-observer (why SSR this?)
|
||||||
|
// - tooltip (why SSR this?)
|
||||||
|
//
|
||||||
|
const omittedModules = [];
|
||||||
|
const componentModules = componentList
|
||||||
|
.filter(component => !omittedModules.includes(component.tagName.split(/wa-/)[1]))
|
||||||
|
.map(component => {
|
||||||
|
const name = component.tagName.split(/wa-/)[1];
|
||||||
|
const componentDirectory = process.env.UNBUNDLED_DIST_DIRECTORY || path.join('.', 'dist');
|
||||||
|
return path.join(componentDirectory, 'components', name, `${name}.js`);
|
||||||
|
});
|
||||||
|
|
||||||
|
eleventyConfig.addPlugin(litPlugin, {
|
||||||
|
mode: 'worker',
|
||||||
|
componentModules,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
markdownTemplateEngine: 'njk',
|
markdownTemplateEngine: 'njk',
|
||||||
dir: {
|
dir: {
|
||||||
|
|||||||
1
docs/_data/hueRanges.js
Normal file
1
docs/_data/hueRanges.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { HUE_RANGES as default } from '../assets/scripts/tweak/data.js';
|
||||||
@@ -1 +1 @@
|
|||||||
["red", "yellow", "green", "cyan", "blue", "indigo", "purple", "pink", "gray"]
|
["red", "orange", "yellow", "green", "cyan", "blue", "indigo", "purple", "pink", "gray"]
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
<meta name="theme-color" content="#f36944">
|
<meta name="theme-color" content="#f36944">
|
||||||
|
|
||||||
<script type="module" src="/assets/scripts/code-examples.js"></script>
|
<script type="module" src="/assets/scripts/code-examples.js"></script>
|
||||||
<script type="module" src="/assets/scripts/copy-code.js"></script>
|
|
||||||
|
|
||||||
<script type="module" src="/assets/scripts/scroll.js"></script>
|
<script type="module" src="/assets/scripts/scroll.js"></script>
|
||||||
<script type="module" src="/assets/scripts/turbo.js"></script>
|
<script type="module" src="/assets/scripts/turbo.js"></script>
|
||||||
<script type="module" src="/assets/scripts/search.js"></script>
|
<script type="module" src="/assets/scripts/search.js"></script>
|
||||||
<script type="module" src="/assets/scripts/outline.js"></script>
|
<script type="module" src="/assets/scripts/outline.js"></script>
|
||||||
|
{% if hasSidebar %}<script type="module" src="/assets/scripts/sidebar-tweaks.js"></script>{% endif %}
|
||||||
<script defer data-domain="backers.webawesome.com" src="https://plausible.io/js/script.js"></script>
|
<script defer data-domain="backers.webawesome.com" src="https://plausible.io/js/script.js"></script>
|
||||||
|
|
||||||
{# Docs styles #}
|
{# Docs styles #}
|
||||||
@@ -50,6 +50,9 @@
|
|||||||
Search
|
Search
|
||||||
<kbd slot="suffix" class="only-desktop">/</kbd>
|
<kbd slot="suffix" class="only-desktop">/</kbd>
|
||||||
</wa-button>
|
</wa-button>
|
||||||
|
|
||||||
|
{# Login #}
|
||||||
|
{% server "loginOrAvatar" %}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -76,14 +79,19 @@
|
|||||||
</aside>
|
</aside>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
{# Main #}
|
{# Main #}
|
||||||
<main id="content">
|
<main id="content">
|
||||||
{# Expandable outline #}
|
{# Expandable outline #}
|
||||||
|
{% if hasOutline %}
|
||||||
<nav id="outline-expandable">
|
<nav id="outline-expandable">
|
||||||
<details class="outline-links">
|
<details class="outline-links">
|
||||||
<summary>On this page</summary>
|
<summary>On this page</summary>
|
||||||
</details>
|
</details>
|
||||||
</nav>
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div id="flashes">{% server "flashes" %}</div>
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
{% include 'breadcrumbs.njk' %}
|
{% include 'breadcrumbs.njk' %}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
{% set breadcrumbs = page.url | breadcrumbs %}
|
{% set ancestors = page.url | ancestors %}
|
||||||
{% if breadcrumbs.length > 0 %}
|
|
||||||
|
{% if ancestors.length > 0 %}
|
||||||
<wa-breadcrumb id="docs-breadcrumbs">
|
<wa-breadcrumb id="docs-breadcrumbs">
|
||||||
{% for crumb in breadcrumbs %}
|
{% for ancestor in ancestors %}
|
||||||
<wa-breadcrumb-item href="{{ crumb.url }}">{{ crumb.title }}</wa-breadcrumb-item>
|
{% if ancestor.page.url != "/" %}
|
||||||
|
<wa-breadcrumb-item href="{{ ancestor.page.url }}">{{ ancestor.data.title }}</wa-breadcrumb-item>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<wa-breadcrumb-item>{# Current page #}</wa-breadcrumb-item>
|
<wa-breadcrumb-item>{# Current page #}</wa-breadcrumb-item>
|
||||||
</wa-breadcrumb>
|
</wa-breadcrumb>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<table class="colors wa-palette-{{ paletteId }}">
|
<table class="colors wa-palette-{{ paletteId }} contrast-table" data-min-contrast="{{ minContrast }}">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
@@ -12,19 +12,31 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
{% for hue in hues -%}
|
{% for hue in hues -%}
|
||||||
<tr>
|
<tr data-hue="{{ hue }}" v-if="'{{hue}}' in paletteScales">
|
||||||
<th>{{ hue | capitalize }}</th>
|
<th>{{ hue | capitalize }}</th>
|
||||||
{% for tint_bg in tints -%}
|
{% for tint_bg in tints -%}
|
||||||
{% set color_bg = palettes[paletteId][hue][tint_bg] %}
|
{% set color_bg = palettes[paletteId][hue][tint_bg] %}
|
||||||
{% for tint_fg in tints | reverse -%}
|
{% for tint_fg in tints | reverse -%}
|
||||||
{% set color_fg = palettes[paletteId][hue][tint_fg] %}
|
{% set color_fg = palettes[paletteId][hue][tint_fg] %}
|
||||||
{% if (tint_fg - tint_bg) | abs == difference %}
|
{% if (tint_fg - tint_bg) | abs == difference %}
|
||||||
<td>
|
{% set contrast_wcag = '' %}
|
||||||
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}-{{ tint_bg }}); color: var(--wa-color-{{ hue }}-{{ tint_fg }})">
|
{% if color_fg and color_bg -%}
|
||||||
{% set contrast_wcag = '' %}
|
{% set contrast_wcag = color_bg.contrast(color_fg, 'WCAG21') %}
|
||||||
{% if color_fg and color_bg %}
|
{%- endif %}
|
||||||
{% set contrast_wcag = color_bg.contrast(color_fg, 'WCAG21') %}
|
<td v-for="contrast of [contrasts.{{ hue }}['{{ tint_bg }}']['{{ tint_fg }}']]"
|
||||||
{% endif %}
|
data-tint-bg="{{ tint_bg }}" data-tint-fg="{{ tint_fg }}" data-original-contrast="{{ contrast_wcag }}">
|
||||||
|
<div v-content:number="contrast.value"
|
||||||
|
class="color swatch" :class="{
|
||||||
|
'value-up': contrast.value - contrast.original > 0.0001,
|
||||||
|
'value-down': contrast.original - contrast.value > 0.0001,
|
||||||
|
'contrast-fail': contrast.value < {{ minContrast }}
|
||||||
|
}"
|
||||||
|
style="--color: var(--wa-color-{{ hue }}-{{ tint_bg }}); color: var(--wa-color-{{ hue }}-{{ tint_fg }})"
|
||||||
|
:style="{
|
||||||
|
'--color': contrast.bgColor,
|
||||||
|
color: contrast.fgColor,
|
||||||
|
}"
|
||||||
|
>
|
||||||
{% if contrast_wcag %}
|
{% if contrast_wcag %}
|
||||||
{{ contrast_wcag | number({maximumSignificantDigits: 2}) }}
|
{{ contrast_wcag | number({maximumSignificantDigits: 2}) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
{# Cards for pages listed by category #}
|
{# Cards for pages listed by category #}
|
||||||
|
|
||||||
<section id="grid" class="index-grid">
|
<section id="grid" class="index-grid">
|
||||||
{% for category, pages in allPages | groupByTags(categories) -%}
|
{% set groupedPages = allPages | groupPages(categories, page) %}
|
||||||
<h2 class="index-category">{{ category | getCategoryTitle(categories) }}</h2>
|
{% for category, pages in groupedPages -%}
|
||||||
{%- for page in pages -%}
|
{% if groupedPages.meta.groupCount > 1 %}
|
||||||
{%- if not page.data.parent or listChildren -%}
|
<h2 class="index-category">
|
||||||
{% include "page-card.njk" %}
|
{% if pages.meta.url %}<a href="{{ pages.meta.url }}">{{ pages.meta.title }}</a>
|
||||||
{%- endif -%}
|
{% else %}
|
||||||
{%- endfor -%}
|
{{ pages.meta.title }}
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
|
{% endif %}
|
||||||
|
{%- for page in pages -%}
|
||||||
|
{% include "page-card.njk" %}
|
||||||
|
{%- endfor -%}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -47,3 +47,7 @@
|
|||||||
<link rel="stylesheet" href="/dist/styles/webawesome.css" />
|
<link rel="stylesheet" href="/dist/styles/webawesome.css" />
|
||||||
<link id="color-stylesheet" rel="stylesheet" href="/dist/styles/utilities.css" />
|
<link id="color-stylesheet" rel="stylesheet" href="/dist/styles/utilities.css" />
|
||||||
<link rel="stylesheet" href="/dist/styles/forms.css" />
|
<link rel="stylesheet" href="/dist/styles/forms.css" />
|
||||||
|
|
||||||
|
|
||||||
|
{# Used by Web Awesome App to inject other assets into the head. #}
|
||||||
|
{% server "head" %}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<a href="{{ page.url }}"{{ page.data.keywords | attr('data-keywords') }}>
|
<a href="{{ page.url }}"{{ page.data.keywords | attr('data-keywords') }}>
|
||||||
<wa-card with-header>
|
<wa-card with-header>
|
||||||
<div slot="header">
|
<div slot="header">
|
||||||
{% include "svgs/" + (page.data.icon or "thumbnail-placeholder") + ".njk" %}
|
{% include "svgs/" + (page.data.icon or "thumbnail-placeholder") + ".njk" ignore missing %}
|
||||||
</div>
|
</div>
|
||||||
<span class="page-name">{{ page.data.title }}</span>
|
<span class="page-name">{{ page.data.title }}</span>
|
||||||
{% if pageSubtitle -%}
|
{% if pageSubtitle -%}
|
||||||
|
|||||||
36
docs/_includes/palette.njk
Normal file
36
docs/_includes/palette.njk
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<table class="colors main wa-palette-{{ paletteId }} static-palette">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th class="core-column">Core tint</th>
|
||||||
|
{% for tint in tints -%}
|
||||||
|
<th>{{ tint }}</th>
|
||||||
|
{%- endfor %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{%- set hueBefore = hues[hues|length - 2] -%}
|
||||||
|
{% for hue in hues -%}
|
||||||
|
{% set scale = palettes[paletteId][hue] %}
|
||||||
|
{% set coreTint = scale.maxChromaTint %}
|
||||||
|
{%- set coreColor = scale[coreTint] -%}
|
||||||
|
{%- set maxChroma = coreColor.c if coreColor.c > maxChroma else maxChroma -%}
|
||||||
|
<tr data-hue="{{ hue }}" class="color-scale" style="--swatch-text-color: light-dark(var(--wa-color-{{ hue }}-10), white)">
|
||||||
|
<th>{{ hue | capitalize }}</th>
|
||||||
|
<td class="core-column" style="--color: var(--wa-color-{{ hue }})">
|
||||||
|
<div class="color swatch" style="color-scheme: {{ 'light' if scale.maxChromaTint > 60 else 'dark' }};">
|
||||||
|
{{ scale.maxChromaTint }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{% for tint in tints -%}
|
||||||
|
{%- set color = scale[tint] -%}
|
||||||
|
<td style="--color: var(--wa-color-{{ hue }}-{{ tint }}); color-scheme: ">
|
||||||
|
<div class="color swatch" style="color-scheme: {{ 'light' if tint > 60 else 'dark' }};">
|
||||||
|
<wa-copy-button value="--wa-color-{{ hue }}-{{ tint }}" copy-label="--wa-color-{{ hue }}-{{ tint }}"></wa-copy-button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{%- endfor -%}
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
{# Some collections (like "patterns") will not have any items in the alpha build for example. So this checks to make sure the collection exists. #}
|
{# Some collections (like "patterns") will not have any items in the alpha build for example. So this checks to make sure the collection exists. #}
|
||||||
{% if collections[tag] -%}
|
{% if collections[tag] -%}
|
||||||
{% set groupUrl %}/docs/{{ tag }}/{% endset %}
|
{% set groupUrl %}/docs/{{ tag }}/{% endset %}
|
||||||
|
{% set groupItem = groupUrl | getCollectionItemFromUrl %}
|
||||||
|
{% set children = groupItem.data.children if groupItem.data.children.length > 0 else (collections[tag] | sort) %}
|
||||||
|
|
||||||
<wa-details {{ ((tag in (tags or [])) or (groupUrl in page.url)) | attr('open') }}>
|
<wa-details {{ ((tag in (tags or [])) or (groupUrl in page.url)) | attr('open') }}>
|
||||||
<h2 slot="summary">
|
<h2 slot="summary">
|
||||||
{% if groupUrl | getCollectionItemFromUrl %}
|
{% if groupItem %}
|
||||||
<a href="{{ groupUrl }}" title="Overview">{{ title or (tag | capitalize) }}
|
<a href="{{ groupUrl }}" title="Overview">{{ title or (tag | capitalize) }}
|
||||||
<wa-icon name="grid-2"></wa-icon>
|
<wa-icon name="grid-2"></wa-icon>
|
||||||
</a>
|
</a>
|
||||||
@@ -12,10 +15,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</h2>
|
</h2>
|
||||||
<ul>
|
<ul>
|
||||||
{% for page in collections[tag] | sort %}
|
{% for page in children %}
|
||||||
{% if not page.data.parent -%}
|
|
||||||
{% include 'sidebar-link.njk' %}
|
{% include 'sidebar-link.njk' %}
|
||||||
{%- endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</wa-details>
|
</wa-details>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{% if not (isAlpha and page.data.noAlpha) and page.fileSlug != tag and not page.data.unlisted -%}
|
{% if not (isAlpha and page.data.noAlpha) and not page.data.unlisted -%}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ page.url }}">{{ page.data.title }}</a>
|
<a href="{{ page.url }}">{{ page.data.title }}</a>
|
||||||
{% if page.data.status == 'experimental' %}<wa-icon name="flask"></wa-icon>{% endif %}
|
{% if page.data.status == 'experimental' %}<wa-icon name="flask"></wa-icon>{% endif %}
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
{% if since -%}
|
{% if since -%}
|
||||||
<wa-badge variant="neutral">Since {{ since }}</wa-badge>
|
<wa-badge variant="neutral" class="since">Since {{ since }}</wa-badge>
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
|
|
||||||
{%- if status %}
|
{%- if status %}
|
||||||
{%- if status == "wip" %}
|
{%- if status == "wip" %}
|
||||||
<wa-badge variant="danger">
|
<wa-badge variant="danger" class="status">
|
||||||
<wa-icon name="pickaxe"></wa-icon>
|
<wa-icon name="pickaxe"></wa-icon>
|
||||||
Work In Progress
|
Work In Progress
|
||||||
</wa-badge>
|
</wa-badge>
|
||||||
{%- elif status == "experimental" %}
|
{%- elif status == "experimental" %}
|
||||||
<wa-badge variant="warning">
|
<wa-badge variant="warning" class="status">
|
||||||
<wa-icon name="flask"></wa-icon>
|
<wa-icon name="flask"></wa-icon>
|
||||||
Experimental
|
Experimental
|
||||||
</wa-badge>
|
</wa-badge>
|
||||||
{%- elif status == "stable" %}
|
{%- elif status == "stable" %}
|
||||||
<wa-badge variant="brand">Stable</wa-badge>
|
<wa-badge variant="brand" class="status">Stable</wa-badge>
|
||||||
{%- else %}
|
{%- else %}
|
||||||
<wa-badge>{{ status}}</wa-badge>
|
<wa-badge class="status">{{ status}}</wa-badge>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
{% set paletteId = palette.fileSlug or page.fileSlug %}
|
{% set paletteId = palette.fileSlug or page.fileSlug %}
|
||||||
{% set tints = [80, 60, 40, 20] %}
|
{% set suffixes = ['-80', '', '-20'] %}
|
||||||
{% set width = 20 %}
|
{% set width = 20 %}
|
||||||
{% set height = 13 %}
|
{% set height = 12 %}
|
||||||
{% set gap_x = 3 %}
|
{% set height_core = 20 %}
|
||||||
{% set gap_y = 3 %}
|
{% set gap_x = 4 %}
|
||||||
|
{% set gap_y = 4 %}
|
||||||
|
|
||||||
<svg viewBox="0 0 {{ (width + gap_x) * hues|length }} {{ (height + gap_y) * tints|length }}" fill="none" xmlns="http://www.w3.org/2000/svg" class="wa-palette-{{ paletteId }} palette-icon">
|
{% set total_width = (width + gap_x) * hues|length %}
|
||||||
|
{% set total_height = (height + gap_y) * suffixes|length + (height_core - height) %}
|
||||||
|
<svg viewBox="0 0 {{ total_width }} {{ total_height }}" fill="none" xmlns="http://www.w3.org/2000/svg" class="wa-palette-{{ paletteId }} palette-icon">
|
||||||
<style>
|
<style>
|
||||||
@import url('/dist/styles/color/{{ paletteId }}.css') layer(palette.{{ paletteId }});
|
@import url('/dist/styles/color/{{ paletteId }}.css') layer(palette.{{ paletteId }});
|
||||||
.palette-icon {
|
.palette-icon {
|
||||||
@@ -15,10 +18,14 @@
|
|||||||
|
|
||||||
{% for hue in hues -%}
|
{% for hue in hues -%}
|
||||||
{% set hueIndex = loop.index0 %}
|
{% set hueIndex = loop.index0 %}
|
||||||
{% for tint in tints -%}
|
{% set y = 0 %}
|
||||||
<rect x="{{ hueIndex * (width + gap_x) }}" y="{{ loop.index0 * (height + gap_y) }}"
|
{% for suffix in suffixes -%}
|
||||||
width="{{ width }}" height="{{ height }}"
|
{% set swatch_height = height if suffix else height_core %}
|
||||||
fill="var(--wa-color-{{ hue }}-{{ tint }})" rx="4" />
|
|
||||||
|
<rect x="{{ hueIndex * (width + gap_x) }}" y="{{ y }}"
|
||||||
|
width="{{ width }}" height="{{ swatch_height }}"
|
||||||
|
fill="var(--wa-color-{{ hue }}{{ suffix }})" rx="2" />
|
||||||
|
{% set y = y + swatch_height + gap_y %}
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
---
|
---
|
||||||
layout: page-outline
|
layout: page-outline
|
||||||
tags: ["overview"]
|
|
||||||
---
|
---
|
||||||
{% set forTag = forTag or (page.url | split('/') | last) %}
|
{% set forTag = forTag or (page.url | split('/') | last) %}
|
||||||
{% if description %}
|
{% if description %}
|
||||||
@@ -13,8 +12,10 @@ tags: ["overview"]
|
|||||||
</wa-input>
|
</wa-input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% set allPages = collections[forTag] %}
|
{% set allPages = allPages or collections[forTag] %}
|
||||||
|
{% if allPages and allPages.length > 0 %}
|
||||||
{% include "grouped-pages.njk" %}
|
{% include "grouped-pages.njk" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<link href="/assets/styles/filter.css" rel="stylesheet">
|
<link href="/assets/styles/filter.css" rel="stylesheet">
|
||||||
<script type="module" src="/assets/scripts/filter.js"></script>
|
<script type="module" src="/assets/scripts/filter.js"></script>
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
{% set hasSidebar = true %}
|
{% if hasSidebar == undefined %}
|
||||||
{% set hasOutline = false %}
|
{% set hasSidebar = true %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if hasOutline == undefined %}
|
||||||
|
{% set hasOutline = false %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% extends "../_includes/base.njk" %}
|
{% extends "../_includes/base.njk" %}
|
||||||
|
|||||||
@@ -1,17 +1,105 @@
|
|||||||
{% set hasSidebar = true %}
|
{% set hasSidebar = true %}
|
||||||
{% set hasOutline = true %}
|
{% set hasOutline = true %}
|
||||||
{# {% set forceTheme = page.fileSlug %} #}
|
{% set paletteId = "default" if page.fileSlug == 'custom' else page.fileSlug %}
|
||||||
|
{% set isCustom = page.fileSlug == 'custom' %}
|
||||||
{% extends '../_layouts/block.njk' %}
|
|
||||||
|
|
||||||
{% set paletteId = page.fileSlug %}
|
|
||||||
|
|
||||||
{% block afterContent %}
|
|
||||||
<style>@import url('/dist/styles/color/{{ paletteId }}.css') layer(palette.{{ paletteId }});</style>
|
|
||||||
|
|
||||||
{% set tints = ["95", "90", "80", "70", "60", "50", "40", "30", "20", "10", "05"] %}
|
{% set tints = ["95", "90", "80", "70", "60", "50", "40", "30", "20", "10", "05"] %}
|
||||||
|
|
||||||
<table class="colors wa-palette-{{ paletteId }}">
|
{% extends '../_includes/base.njk' %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<style>@import url('/dist/styles/color/{{ paletteId }}.css') layer(palette.{{ paletteId }});</style>
|
||||||
|
<link href="{{ page.url }}../app/tweak.css" rel="stylesheet">
|
||||||
|
<script type="module" src="{{ page.url }}../app/tweak.js"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<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,
|
||||||
|
}"
|
||||||
|
:style="{
|
||||||
|
'--chroma-scale': chromaScale,
|
||||||
|
'--gray-chroma': tweaked?.grayChroma ? grayChroma : originalGrayChroma,
|
||||||
|
'--max-c': maxChroma,
|
||||||
|
'--avg-l': L_RANGES[level].mid,
|
||||||
|
}">
|
||||||
|
|
||||||
|
<header id="palette-info">
|
||||||
|
{% include 'breadcrumbs.njk' %}
|
||||||
|
|
||||||
|
<h1 class="title">
|
||||||
|
<span v-content="saved?.title || (step > 0 ? defaultPaletteTitle : paletteTitle)">{{ title }}</span>
|
||||||
|
<template v-if="saved || step > 0">
|
||||||
|
<wa-icon-button name="pencil" label="Rename palette" @click="rename"></wa-icon-button>
|
||||||
|
<wa-icon-button v-if="saved" class="delete" name="trash" label="Delete palette" @click="deleteSaved"></wa-icon-button>
|
||||||
|
<wa-button @click="save()" :disabled="!unsavedChanges"
|
||||||
|
:variant="unsavedChanges ? 'success' : 'neutral'" size="small" :appearance="unsavedChanges ? 'accent' : 'outlined'">
|
||||||
|
<span slot="prefix" class="icon-modifier">
|
||||||
|
<wa-icon name="sidebar" variant="regular"></wa-icon>
|
||||||
|
<wa-icon name="circle-plus" class="modifier" style="color: light-dark(var(--wa-color-green-70), var(--wa-color-green-60));"></wa-icon>
|
||||||
|
</span>
|
||||||
|
<span v-content="unsavedChanges ? 'Save' : 'Saved'">Save</span>
|
||||||
|
</wa-button>
|
||||||
|
</template>
|
||||||
|
</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>
|
||||||
|
{% include '../_includes/status.njk' %}
|
||||||
|
{% if not isPro %}
|
||||||
|
<wa-badge class="pro" v-if="tweaked || isCustom">PRO</wa-badge>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if description %}
|
||||||
|
<p class="summary">
|
||||||
|
{{ description | inlineMarkdown | safe }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% raw %}
|
||||||
|
<div class="hue-wheel" v-if="!isCustom || step > 1">
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
{% endraw %}
|
||||||
|
</header>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block afterContent %}
|
||||||
|
|
||||||
|
<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)">{% raw %}{{ tweakHumanReadable }}{% endraw %}</wa-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<wa-button @click="reset()" appearance="outlined" variant="danger">
|
||||||
|
<span slot="prefix" class="icon-modifier">
|
||||||
|
<wa-icon name="circle-xmark" variant="regular"></wa-icon>
|
||||||
|
</span>
|
||||||
|
Reset
|
||||||
|
</wa-button>
|
||||||
|
</wa-callout>
|
||||||
|
|
||||||
|
<h2>Scales</h2>
|
||||||
|
|
||||||
|
{% include "palette.njk" %}
|
||||||
|
|
||||||
|
<table class="colors main wa-palette-{{ paletteId }}">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
@@ -21,26 +109,126 @@
|
|||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
{% for hue in hues -%}
|
{% raw %}
|
||||||
<tr>
|
<tbody v-cloak>
|
||||||
<th>{{ hue | capitalize }}</th>
|
<tr v-for="hue in paletteScalesList" :data-hue="hue" :key="hue"
|
||||||
<td class="core-column">
|
class="color-scale" :class="{
|
||||||
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}); color: var(--wa-color-{{ hue }}-{{ '05' if palettes[paletteId][hue].maxChromaTint > 60 else '95' }});">
|
tweaked: hue === 'gray' ? tweaked.grayChroma || tweaked.grayColor : hueShifts[hue],
|
||||||
{{ palettes[paletteId][hue].maxChromaTint }}
|
}"
|
||||||
<wa-copy-button value="--wa-color-{{ hue }}" copy-label="--wa-color-{{ hue }}"></wa-copy-button>
|
:style="{
|
||||||
</div>
|
'--swatch-text-color': `light-dark(var(--wa-color-${ hue }-10), white)`,
|
||||||
</td>
|
'--hue-shift': hueShifts[hue] || ''
|
||||||
{% for tint in tints -%}
|
}">
|
||||||
<td>
|
<th>
|
||||||
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}-{{ tint }})">
|
{{ capitalize(hue) }}
|
||||||
<wa-copy-button value="--wa-color-{{ hue }}-{{ tint }}" copy-label="--wa-color-{{ hue }}-{{ tint }}"></wa-copy-button>
|
<info-tip v-if="isCustom && !seedHues[hue]">
|
||||||
</div>
|
<wa-icon name="sparkles" style="color: var(--wa-color-gray-50)"></wa-icon>
|
||||||
</td>
|
<template #content>Generated scale</template>
|
||||||
{%- endfor -%}
|
</info-tip>
|
||||||
</tr>
|
</th>
|
||||||
{%- endfor %}
|
<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] + '')">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<template #content>
|
||||||
|
<template v-if="hue === 'gray'">
|
||||||
|
<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]"
|
||||||
|
coord="h" type="shift"
|
||||||
|
v-model="hueShifts[hue]"
|
||||||
|
:default-color="baseCoreColors[hue]"
|
||||||
|
: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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<color-slider v-if="hue === 'gray'" coord="c" type="scale"
|
||||||
|
:model-value="computedGrayChroma"
|
||||||
|
@update:model-value="grayChroma = $event"
|
||||||
|
:default-color="baseCoreColors[computedGrayColor]"
|
||||||
|
:base-value="baseCoreColors[originalGrayColor].oklch.c"
|
||||||
|
:default-value-relative="originalGrayChroma"
|
||||||
|
: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})">
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<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.01"
|
||||||
|
label="Overall colorfulness" label-min="More muted" label-max="More vibrant"
|
||||||
|
></color-slider>
|
||||||
|
|
||||||
|
{% if page.fileSlug != 'custom' %}
|
||||||
<h2>Used By</h2>
|
<h2>Used By</h2>
|
||||||
|
|
||||||
<section class="index-grid">
|
<section class="index-grid">
|
||||||
@@ -50,6 +238,7 @@
|
|||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</section>
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% markdown %}
|
{% markdown %}
|
||||||
## Color Contrast
|
## Color Contrast
|
||||||
@@ -65,6 +254,7 @@ A difference of `40` ensures a minimum **3:1** contrast ratio, suitable for larg
|
|||||||
{% endmarkdown %}
|
{% endmarkdown %}
|
||||||
|
|
||||||
{% set difference = 40 %}
|
{% set difference = 40 %}
|
||||||
|
{% set minContrast = 3 %}
|
||||||
{% include "contrast-table.njk" %}
|
{% include "contrast-table.njk" %}
|
||||||
|
|
||||||
{% markdown %}
|
{% markdown %}
|
||||||
@@ -84,6 +274,7 @@ A difference of `50` ensures a minimum **4.5:1** contrast ratio, suitable for no
|
|||||||
{% endmarkdown %}
|
{% endmarkdown %}
|
||||||
|
|
||||||
{% set difference = 50 %}
|
{% set difference = 50 %}
|
||||||
|
{% set minContrast = 4.5 %}
|
||||||
{% include "contrast-table.njk" %}
|
{% include "contrast-table.njk" %}
|
||||||
|
|
||||||
{% markdown %}
|
{% markdown %}
|
||||||
@@ -102,6 +293,7 @@ A difference of `60` ensures a minimum **7:1** contrast ratio, suitable for all
|
|||||||
{% endmarkdown %}
|
{% endmarkdown %}
|
||||||
|
|
||||||
{% set difference = 60 %}
|
{% set difference = 60 %}
|
||||||
|
{% set minContrast = 7 %}
|
||||||
{% include "contrast-table.njk" %}
|
{% include "contrast-table.njk" %}
|
||||||
|
|
||||||
{% markdown %}
|
{% markdown %}
|
||||||
@@ -114,13 +306,48 @@ This also goes for a difference of `65`:
|
|||||||
{% include "contrast-table.njk" %}
|
{% include "contrast-table.njk" %}
|
||||||
|
|
||||||
{% markdown %}
|
{% markdown %}
|
||||||
## How to use this palette
|
## How to use this palette { #usage }
|
||||||
|
|
||||||
If you are using a Web Awesome theme that uses this palette, it will already be included.
|
If you are using a Web Awesome theme that uses this palette, it will already be included.
|
||||||
To use a different palette than a theme default, or to use it in a custom theme, you can import this palette directly from the Web Awesome CDN.
|
To use a different palette than a theme default, or to use it in a custom theme, you can import this palette directly from the Web Awesome CDN.
|
||||||
|
|
||||||
{% set stylesheet = 'styles/color/' + page.fileSlug + '.css' %}
|
{% set stylesheet = 'styles/color/' + page.fileSlug + '.css' %}
|
||||||
{% include 'import-stylesheet-code.md.njk' %}
|
<wa-tab-group class="import-stylesheet-code">
|
||||||
|
<wa-tab panel="html">In HTML</wa-tab>
|
||||||
|
<wa-tab panel="css">In CSS</wa-tab>
|
||||||
|
<wa-tab-panel name="html">
|
||||||
|
|
||||||
|
Add the following code to the `<head>` of your page:
|
||||||
|
```html { v-content:html="code.html.highlighted" }
|
||||||
|
<link rel="stylesheet" href="{% cdnUrl stylesheet %}" />
|
||||||
|
```
|
||||||
|
</wa-tab-panel>
|
||||||
|
<wa-tab-panel name="css">
|
||||||
|
|
||||||
|
Add the following code at the top of your CSS file:
|
||||||
|
```css { v-content:html="code.css.highlighted" }
|
||||||
|
@import url('{% cdnUrl stylesheet %}');
|
||||||
|
```
|
||||||
|
</wa-tab-panel>
|
||||||
|
</wa-tab-group>
|
||||||
|
|
||||||
{% endmarkdown %}
|
{% 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>
|
||||||
|
<a v-for="palette of savedVariations" :href="'/docs/palettes/' + palette.id">
|
||||||
|
<wa-card with-header>
|
||||||
|
<div slot="header">
|
||||||
|
{# {% include "svgs/palette.njk" %} #}
|
||||||
|
{% include "svgs/thumbnail-placeholder.njk" %}
|
||||||
|
</div>
|
||||||
|
<span class="page-name" v-text="palette.title"></span>
|
||||||
|
</wa-card>
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
</div></div> {# end palette app #}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ wa_data.palettes = {
|
|||||||
<wa-option label="{{ palette.data.title }}" value="{{ palette.fileSlug if not currentPalette }}" {{ (palette.fileSlug if currentPalette) | attr('data-id') }}>
|
<wa-option label="{{ palette.data.title }}" value="{{ palette.fileSlug if not currentPalette }}" {{ (palette.fileSlug if currentPalette) | attr('data-id') }}>
|
||||||
<wa-card with-header>
|
<wa-card with-header>
|
||||||
<div slot="header">
|
<div slot="header">
|
||||||
{% include "svgs/" + (palette.data.icon or "thumbnail-placeholder") + ".njk" %}
|
{% include "svgs/" + (palette.data.icon or "thumbnail-placeholder") + ".njk" ignore missing %}
|
||||||
</div>
|
</div>
|
||||||
<span class="page-name">
|
<span class="page-name">
|
||||||
{{ palette.data.title }}
|
{{ palette.data.title }}
|
||||||
@@ -81,7 +81,7 @@ wa_data.palettes = {
|
|||||||
{% set palette = defaultPalette %}
|
{% set palette = defaultPalette %}
|
||||||
</wa-select>
|
</wa-select>
|
||||||
|
|
||||||
<wa-select name="brand" label="Brand color" value="" clearable>
|
<wa-select class="color-select" name="brand" label="Brand color" value="" clearable>
|
||||||
<div class="selected-swatch" slot="prefix"></div>
|
<div class="selected-swatch" slot="prefix"></div>
|
||||||
{% for hue in hues %}
|
{% for hue in hues %}
|
||||||
{% set currentBrand = hue == brand %}
|
{% set currentBrand = hue == brand %}
|
||||||
|
|||||||
@@ -3,30 +3,39 @@ import { parse } from 'node-html-parser';
|
|||||||
/**
|
/**
|
||||||
* Eleventy plugin to add copy buttons to code blocks.
|
* Eleventy plugin to add copy buttons to code blocks.
|
||||||
*/
|
*/
|
||||||
export function copyCodePlugin(options = {}) {
|
export function copyCodePlugin(eleventyConfig, options = {}) {
|
||||||
options = {
|
options = {
|
||||||
container: 'body',
|
container: 'body',
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
|
|
||||||
return function (eleventyConfig) {
|
let codeCount = 0;
|
||||||
eleventyConfig.addTransform('copy-code', content => {
|
eleventyConfig.addTransform('copy-code', content => {
|
||||||
const doc = parse(content, { blockTextElements: { code: true } });
|
const doc = parse(content, { blockTextElements: { code: true } });
|
||||||
const container = doc.querySelector(options.container);
|
const container = doc.querySelector(options.container);
|
||||||
|
|
||||||
if (!container) {
|
if (!container) {
|
||||||
return content;
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for code blocks
|
||||||
|
container.querySelectorAll('pre > code').forEach(code => {
|
||||||
|
const pre = code.closest('pre');
|
||||||
|
let preId = pre.getAttribute('id') || `code-block-${++codeCount}`;
|
||||||
|
let codeId = code.getAttribute('id') || `${preId}-inner`;
|
||||||
|
|
||||||
|
if (!code.getAttribute('id')) {
|
||||||
|
code.setAttribute('id', codeId);
|
||||||
|
}
|
||||||
|
if (!pre.getAttribute('id')) {
|
||||||
|
pre.setAttribute('id', preId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look for code blocks
|
// Add a copy button
|
||||||
container.querySelectorAll('pre > code').forEach(code => {
|
pre.innerHTML += `<wa-icon-button href="#${preId}" class="block-link-icon" name="link"></wa-icon-button>
|
||||||
const pre = code.closest('pre');
|
<wa-copy-button from="${codeId}" class="copy-button" hoist></wa-copy-button>`;
|
||||||
|
|
||||||
// Add a copy button (we set the copy data at runtime to reduce page bloat)
|
|
||||||
pre.innerHTML = `<wa-copy-button class="copy-button" hoist></wa-copy-button>` + pre.innerHTML;
|
|
||||||
});
|
|
||||||
|
|
||||||
return doc.toString();
|
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
return doc.toString();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ function getCollection(name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getCollectionItemFromUrl(url, collection) {
|
export function getCollectionItemFromUrl(url, collection) {
|
||||||
|
if (!url) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
collection ??= getCollection.call(this, 'all') || [];
|
collection ??= getCollection.call(this, 'all') || [];
|
||||||
return collection.find(item => item.url === url);
|
return collection.find(item => item.url === url);
|
||||||
}
|
}
|
||||||
@@ -42,35 +45,33 @@ export function split(text, separator) {
|
|||||||
return (text + '').split(separator).filter(Boolean);
|
return (text + '').split(separator).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function breadcrumbs(url, { withCurrent = false } = {}) {
|
export function ancestors(url, { withCurrent = false, withRoot = false } = {}) {
|
||||||
const parts = split(url, '/');
|
let ret = [];
|
||||||
const ret = [];
|
let currentUrl = url;
|
||||||
|
let currentItem = getCollectionItemFromUrl.call(this, url);
|
||||||
|
|
||||||
while (parts.length) {
|
if (!currentItem) {
|
||||||
let partialUrl = '/' + parts.join('/') + '/';
|
// Might have eleventyExcludeFromCollections, jump to parent
|
||||||
let item = getCollectionItemFromUrl.call(this, partialUrl);
|
let parentUrl = this.ctx.parentUrl;
|
||||||
|
if (parentUrl) {
|
||||||
if (item && (partialUrl !== url || withCurrent)) {
|
url = parentUrl;
|
||||||
let title = item.data.title;
|
|
||||||
if (title) {
|
|
||||||
ret.unshift({ url: partialUrl, title });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parts.pop();
|
|
||||||
|
|
||||||
if (item?.data.parent) {
|
|
||||||
let parentURL = item.data.parent;
|
|
||||||
if (!item.data.parent.startsWith('/')) {
|
|
||||||
// Parent is in the same directory
|
|
||||||
parts.push(item.data.parent);
|
|
||||||
parentURL = '/' + parts.join('/') + '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
let parentBreadcrumbs = breadcrumbs.call(this, parentURL, { withCurrent: true });
|
|
||||||
return [...parentBreadcrumbs, ...ret];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (let item; (item = getCollectionItemFromUrl.call(this, url)); url = item.data.parentUrl) {
|
||||||
|
ret.unshift(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!withRoot && ret[0]?.page.url === '/') {
|
||||||
|
// Remove root
|
||||||
|
ret.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!withCurrent && ret.at(-1)?.page.url === currentUrl) {
|
||||||
|
// Remove current page
|
||||||
|
ret.pop();
|
||||||
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,69 +181,178 @@ export function sort(arr, by = { 'data.order': 1, 'data.title': '' }) {
|
|||||||
/**
|
/**
|
||||||
* Group an 11ty collection (or any array of objects with a `data.tags` property) by certain tags.
|
* Group an 11ty collection (or any array of objects with a `data.tags` property) by certain tags.
|
||||||
* @param {object[]} collection
|
* @param {object[]} collection
|
||||||
* @param { Object<string, string> | (string | Object<string, string>)[]} [tags] The tags to group by. If not provided/empty, defaults to grouping by all tags.
|
* @param { Object<string, string> | string[]} [options] Options object or array of tags to group by.
|
||||||
* @returns { Object.<string, object[]> } An object with keys for each tag, and an array of items for each tag.
|
* @param {string[] | true} [options.tags] Tags to group by. If true, groups by all tags.
|
||||||
|
* If not provided/empty, defaults to grouping by page hierarchy, with any pages with more than 1 children becoming groups.
|
||||||
|
* @param {string[]} [options.groups] The groups to use if only a subset or a specific order is desired. Defaults to `options.tags`.
|
||||||
|
* @param {string[]} [options.titles] Any title overrides for groups.
|
||||||
|
* @param {string | false} [options.other="Other"] The title to use for the "Other" group. If `false`, the "Other" group is removed..
|
||||||
|
* @returns { Object.<string, object[]> } An object of group ids to arrays of page objects.
|
||||||
*/
|
*/
|
||||||
export function groupByTags(collection, tags) {
|
export function groupPages(collection, options = {}, page) {
|
||||||
if (!collection) {
|
if (!collection) {
|
||||||
console.error(`Empty collection passed to groupByTags() to group by ${JSON.stringify(tags)}`);
|
console.error(`Empty collection passed to groupPages() to group by ${JSON.stringify(options)}`);
|
||||||
}
|
|
||||||
if (!tags) {
|
|
||||||
// Default to grouping by union of all tags
|
|
||||||
tags = Array.from(new Set(collection.flatMap(item => item.data.tags)));
|
|
||||||
} else if (Array.isArray(tags)) {
|
|
||||||
// May contain objects of one-off tag -> label mappings
|
|
||||||
tags = tags.map(tag => (typeof tag === 'object' ? Object.keys(tag)[0] : tag));
|
|
||||||
} else if (typeof tags === 'object') {
|
|
||||||
// tags is an object of tags to labels, so we just want the keys
|
|
||||||
tags = Object.keys(tags);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let ret = Object.fromEntries(tags.map(tag => [tag, []]));
|
if (Array.isArray(options)) {
|
||||||
ret.other = [];
|
options = { tags: options };
|
||||||
|
}
|
||||||
|
|
||||||
|
let { tags, groups, titles = {}, other = 'Other' } = options;
|
||||||
|
|
||||||
|
if (groups === undefined && Array.isArray(tags)) {
|
||||||
|
groups = tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
let grouping;
|
||||||
|
|
||||||
|
if (tags) {
|
||||||
|
grouping = {
|
||||||
|
isGroup: item => undefined,
|
||||||
|
getCandidateGroups: item => item.data.tags,
|
||||||
|
getGroupMeta: group => ({}),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
grouping = {
|
||||||
|
isGroup: item => (item.data.children.length >= 2 ? item.page.url : undefined),
|
||||||
|
getCandidateGroups: item => {
|
||||||
|
let parentUrl = item.data.parentUrl;
|
||||||
|
if (page?.url === parentUrl) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [parentUrl];
|
||||||
|
},
|
||||||
|
getGroupMeta: group => {
|
||||||
|
let item = byUrl[group] || getCollectionItemFromUrl.call(this, group);
|
||||||
|
return {
|
||||||
|
title: item?.data.title,
|
||||||
|
url: group,
|
||||||
|
item,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
sortGroups: groups => sort(groups.map(url => byUrl[url]).filter(Boolean)).map(item => item.page.url),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let byUrl = {};
|
||||||
|
let byParentUrl = {};
|
||||||
|
|
||||||
for (let item of collection) {
|
for (let item of collection) {
|
||||||
let categorized = false;
|
let url = item.page.url;
|
||||||
|
let parentUrl = item.data.parentUrl;
|
||||||
|
|
||||||
for (let tag of tags) {
|
byUrl[url] = item;
|
||||||
if (item.data.tags.includes(tag)) {
|
|
||||||
ret[tag].push(item);
|
|
||||||
categorized = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!categorized) {
|
if (parentUrl) {
|
||||||
ret.other.push(item);
|
byParentUrl[parentUrl] ??= [];
|
||||||
|
byParentUrl[parentUrl].push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove empty categories
|
let urlToGroups = {};
|
||||||
for (let category in ret) {
|
|
||||||
if (ret[category].length === 0) {
|
for (let item of collection) {
|
||||||
delete ret[category];
|
let url = item.page.url;
|
||||||
|
let parentUrl = item.data.parentUrl;
|
||||||
|
|
||||||
|
if (grouping.isGroup(item)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parentItem = byUrl[parentUrl];
|
||||||
|
if (parentItem && !grouping.isGroup(parentItem)) {
|
||||||
|
// Their parent is also here and is not a group
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let candidateGroups = grouping.getCandidateGroups(item);
|
||||||
|
|
||||||
|
if (groups) {
|
||||||
|
candidateGroups = candidateGroups.filter(group => groups.includes(group));
|
||||||
|
}
|
||||||
|
|
||||||
|
urlToGroups[url] ??= [];
|
||||||
|
|
||||||
|
for (let group of candidateGroups) {
|
||||||
|
urlToGroups[url].push(group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ret = {};
|
||||||
|
|
||||||
|
for (let url in urlToGroups) {
|
||||||
|
let groups = urlToGroups[url];
|
||||||
|
let item = byUrl[url];
|
||||||
|
|
||||||
|
if (groups.length === 0) {
|
||||||
|
// Not filtered out but also not categorized
|
||||||
|
groups = ['other'];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let group of groups) {
|
||||||
|
ret[group] ??= [];
|
||||||
|
ret[group].push(item);
|
||||||
|
|
||||||
|
if (!ret[group].meta) {
|
||||||
|
if (group === 'other') {
|
||||||
|
ret[group].meta = { title: other };
|
||||||
|
} else {
|
||||||
|
ret[group].meta = grouping.getGroupMeta(group);
|
||||||
|
ret[group].meta.title = titles[group] ?? ret[group].meta.title ?? capitalize(group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (other === false) {
|
||||||
|
delete ret.other;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort
|
||||||
|
let sortedGroups = groups ?? grouping.sortGroups?.(Object.keys(ret));
|
||||||
|
|
||||||
|
if (sortedGroups) {
|
||||||
|
ret = sortObject(ret, sortedGroups);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(ret, 'meta', {
|
||||||
|
value: {
|
||||||
|
groupCount: Object.keys(ret).length,
|
||||||
|
},
|
||||||
|
enumerable: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort an object by its keys
|
||||||
|
* @param {*} obj
|
||||||
|
* @param {function | string[]} order
|
||||||
|
*/
|
||||||
|
function sortObject(obj, order) {
|
||||||
|
let ret = {};
|
||||||
|
let sortedKeys = Array.isArray(order) ? order : Object.keys(obj).sort(order);
|
||||||
|
|
||||||
|
for (let key of sortedKeys) {
|
||||||
|
if (key in obj) {
|
||||||
|
ret[key] = obj[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any keys that weren't in the order
|
||||||
|
for (let key in obj) {
|
||||||
|
if (!(key in ret)) {
|
||||||
|
ret[key] = obj[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCategoryTitle(category, categories) {
|
function capitalize(str) {
|
||||||
let title;
|
str += '';
|
||||||
if (Array.isArray(categories)) {
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
// Find relevant entry
|
|
||||||
// [{id: "Title"}, id2, ...]
|
|
||||||
title = categories.find(entry => typeof entry === 'object' && entry?.[category])?.[category];
|
|
||||||
} else if (typeof categories === 'object') {
|
|
||||||
// {id: "Title", id2: "Title 2", ...}
|
|
||||||
title = categories[category];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (title) {
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Capitalized
|
|
||||||
return category.charAt(0).toUpperCase() + category.slice(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const IDENTITY = x => x;
|
const IDENTITY = x => x;
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
function setCopyValue() {
|
|
||||||
document.querySelectorAll('.copy-button').forEach(copyButton => {
|
|
||||||
const pre = copyButton.closest('pre');
|
|
||||||
const code = pre?.querySelector('code');
|
|
||||||
|
|
||||||
if (code) {
|
|
||||||
copyButton.value = code.textContent;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set data for all copy buttons when the page loads
|
|
||||||
setCopyValue();
|
|
||||||
|
|
||||||
document.addEventListener('turbo:load', setCopyValue);
|
|
||||||
8
docs/assets/scripts/prism-downloaded.js
Normal file
8
docs/assets/scripts/prism-downloaded.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,37 +0,0 @@
|
|||||||
/**
|
|
||||||
* Get import code for remixed themes.
|
|
||||||
*/
|
|
||||||
export const urls = {
|
|
||||||
colors: id => `styles/themes/${id}/color.css`,
|
|
||||||
palette: id => `styles/color/${id}.css`,
|
|
||||||
brand: id => `styles/brand/${id}.css`,
|
|
||||||
typography: id => `styles/themes/${id}/typography.css`,
|
|
||||||
};
|
|
||||||
|
|
||||||
function getImport(url, options = {}) {
|
|
||||||
let { language = 'html', cdnUrl = '/dist/', attributes } = options;
|
|
||||||
url = cdnUrl + url;
|
|
||||||
|
|
||||||
if (language === 'css') {
|
|
||||||
return `@import url('${url}');`;
|
|
||||||
} else {
|
|
||||||
attributes = attributes ? ` ${attributes}` : '';
|
|
||||||
return `<link rel="stylesheet" href="${url}"${attributes} />`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCode(base, params, options) {
|
|
||||||
let ret = [];
|
|
||||||
|
|
||||||
if (base) {
|
|
||||||
ret.push(`styles/themes/${base}.css`);
|
|
||||||
}
|
|
||||||
|
|
||||||
ret.push(
|
|
||||||
...Object.entries(params)
|
|
||||||
.filter(([aspect, id]) => Boolean(id))
|
|
||||||
.map(([aspect, id]) => urls[aspect](id)),
|
|
||||||
);
|
|
||||||
|
|
||||||
return ret.map(url => getImport(url, options)).join('\n');
|
|
||||||
}
|
|
||||||
269
docs/assets/scripts/sidebar-tweaks.js
Normal file
269
docs/assets/scripts/sidebar-tweaks.js
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
const sidebar = (globalThis.sidebar = {});
|
||||||
|
|
||||||
|
sidebar.palettes = {
|
||||||
|
render() {
|
||||||
|
if (this.saved.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let palette of this.saved) {
|
||||||
|
sidebar.palette.render(palette);
|
||||||
|
}
|
||||||
|
|
||||||
|
sidebar.updateCurrent();
|
||||||
|
},
|
||||||
|
|
||||||
|
saved: [],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update saved palettes from local storage
|
||||||
|
*/
|
||||||
|
fromLocalStorage() {
|
||||||
|
// Replace contents of array without breaking references
|
||||||
|
let saved = localStorage.savedPalettes ? JSON.parse(localStorage.savedPalettes) : [];
|
||||||
|
this.saved.splice(0, this.saved.length, ...saved);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write palettes to local storage
|
||||||
|
*/
|
||||||
|
toLocalStorage() {
|
||||||
|
if (this.saved.length > 0) {
|
||||||
|
localStorage.savedPalettes = JSON.stringify(this.saved);
|
||||||
|
} else {
|
||||||
|
delete localStorage.savedPalettes;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
sidebar.palettes.fromLocalStorage();
|
||||||
|
|
||||||
|
// Palettes were updated in another tab
|
||||||
|
addEventListener('storage', () => sidebar.palettes.fromLocalStorage());
|
||||||
|
|
||||||
|
sidebar.palette = {
|
||||||
|
getUid() {
|
||||||
|
let savedPalettes = sidebar.palettes.saved;
|
||||||
|
let uids = new Set(savedPalettes.map(p => p.uid));
|
||||||
|
|
||||||
|
if (savedPalettes.length === 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find first available number
|
||||||
|
for (let i = 1; i <= savedPalettes.length + 1; i++) {
|
||||||
|
if (!uids.has(i)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
equals(p1, p2) {
|
||||||
|
if (!p1 || !p2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return p1.id === p2.id && p1.uid === p2.uid;
|
||||||
|
},
|
||||||
|
|
||||||
|
delete(palette) {
|
||||||
|
let savedPalettes = sidebar.palettes.saved;
|
||||||
|
let count = savedPalettes.length;
|
||||||
|
|
||||||
|
if (count === 0 || !palette.uid) {
|
||||||
|
// No stored palettes or this palette has not been saved
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO improve UX of this
|
||||||
|
if (!confirm(`Are you sure you want to delete palette “${palette.title}”?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index; index > -1; index = savedPalettes.findIndex(p => p.uid === palette.uid)) {
|
||||||
|
savedPalettes.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedPalettes.length === count) {
|
||||||
|
// Nothing was removed
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
let pathname = `/docs/palettes/${palette.id}/`;
|
||||||
|
let url = pathname + palette.search;
|
||||||
|
let uls = new Set();
|
||||||
|
|
||||||
|
for (let a of document.querySelectorAll(`#sidebar a[href="${url}"]`)) {
|
||||||
|
let li = a.closest('li');
|
||||||
|
let ul = li.closest('ul');
|
||||||
|
uls.add(ul);
|
||||||
|
li.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove empty lists
|
||||||
|
for (let ul of uls) {
|
||||||
|
if (!ul.children.length) {
|
||||||
|
ul.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sidebar.updateCurrent();
|
||||||
|
|
||||||
|
sidebar.palettes.toLocalStorage();
|
||||||
|
|
||||||
|
if (globalThis.paletteApp?.saved?.uid === palette.uid) {
|
||||||
|
// We deleted the currently active palette
|
||||||
|
paletteApp.postDelete();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render(palette) {
|
||||||
|
// Find existing <a>
|
||||||
|
let { title, id, search, uid } = palette;
|
||||||
|
|
||||||
|
for (let a of document.querySelectorAll(`#sidebar a[href^="/docs/palettes/${id}/"][data-uid="${uid}"]`)) {
|
||||||
|
// Palette already in sidebar, just update it
|
||||||
|
a.textContent = palette.title;
|
||||||
|
a.href = `/docs/palettes/${id}/${search}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pathname = `/docs/palettes/${id}/`;
|
||||||
|
let url = pathname + search;
|
||||||
|
let parentA = document.querySelector(`a[href="${pathname}"]`);
|
||||||
|
let parentLi = parentA?.closest('li');
|
||||||
|
let a;
|
||||||
|
|
||||||
|
if (parentLi) {
|
||||||
|
a = Object.assign(document.createElement('a'), { href: url, textContent: title });
|
||||||
|
a.dataset.uid = uid;
|
||||||
|
let badges = [...parentLi.querySelectorAll('wa-badge')].map(badge => badge.cloneNode(true));
|
||||||
|
let ul = parentLi.querySelector('ul') ?? parentLi.appendChild(document.createElement('ul'));
|
||||||
|
let li = document.createElement('li');
|
||||||
|
let deleteButton = Object.assign(document.createElement('wa-icon-button'), {
|
||||||
|
name: 'trash',
|
||||||
|
label: 'Delete',
|
||||||
|
className: 'delete',
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteButton.addEventListener('click', () => {
|
||||||
|
let palette = { id, uid, title: a.textContent, search: a.search };
|
||||||
|
sidebar.palette.delete(palette);
|
||||||
|
});
|
||||||
|
|
||||||
|
li.append(a, ' ', ...badges, deleteButton);
|
||||||
|
ul.appendChild(li);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a palette, either by updating its existing entry or creating a new one
|
||||||
|
* @param {object} palette
|
||||||
|
*/
|
||||||
|
save(palette) {
|
||||||
|
if (!palette.uid) {
|
||||||
|
// First time saving
|
||||||
|
palette.uid = this.getUid();
|
||||||
|
}
|
||||||
|
|
||||||
|
let savedPalettes = sidebar.palettes.saved;
|
||||||
|
let existingIndex = palette.uid ? sidebar.palettes.saved.findIndex(p => p.uid === palette.uid) : -1;
|
||||||
|
let newIndex = existingIndex > -1 ? existingIndex : savedPalettes.length;
|
||||||
|
|
||||||
|
let [oldValues] = sidebar.palettes.saved.splice(newIndex, 1, palette);
|
||||||
|
|
||||||
|
this.render(palette, oldValues);
|
||||||
|
sidebar.updateCurrent();
|
||||||
|
sidebar.palettes.toLocalStorage();
|
||||||
|
|
||||||
|
return palette;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
sidebar.updateCurrent = function () {
|
||||||
|
// Find the sidebar link with the longest shared prefix with the current URL
|
||||||
|
let pathParts = location.pathname.split('/').filter(Boolean);
|
||||||
|
let prefixes = [];
|
||||||
|
|
||||||
|
if (pathParts.length === 1) {
|
||||||
|
// If at /docs/ we just use that, otherwise we want at least two parts (/docs/xxx/)
|
||||||
|
prefixes.push('/' + pathParts[0] + '/');
|
||||||
|
} else {
|
||||||
|
for (let i = 2; i <= pathParts.length; i++) {
|
||||||
|
prefixes.push('/' + pathParts.slice(0, i).join('/') + '/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last prefix includes the search too (if any)
|
||||||
|
if (location.search) {
|
||||||
|
let params = new URLSearchParams(location.search);
|
||||||
|
params.sort();
|
||||||
|
prefixes.push(prefixes.at(-1) + location.search);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want to start from the longest prefix
|
||||||
|
prefixes.reverse();
|
||||||
|
let candidates;
|
||||||
|
let matchingPrefix;
|
||||||
|
|
||||||
|
for (let prefix of prefixes) {
|
||||||
|
candidates = document.querySelectorAll(`#sidebar a[href^="${prefix}"]`);
|
||||||
|
|
||||||
|
if (candidates.length > 0) {
|
||||||
|
matchingPrefix = prefix;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matchingPrefix) {
|
||||||
|
// Abort mission
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchingPrefix === pathParts.at(-1)) {
|
||||||
|
// Full path matches, check search
|
||||||
|
if (location.search) {
|
||||||
|
candidates = [...candidates];
|
||||||
|
|
||||||
|
let searchParams = new URLSearchParams(location.search);
|
||||||
|
|
||||||
|
if (searchParams.has('uid')) {
|
||||||
|
// Only consider candidates with the same uid
|
||||||
|
candidates = candidates.filter(a => {
|
||||||
|
let params = new URLSearchParams(a.search);
|
||||||
|
return params.get('uid') === searchParams.get('uid');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Sort candidates based on how many params they have in common, in descending order
|
||||||
|
candidates = candidates.sort((a, b) => {
|
||||||
|
return countSharedSearchParams(searchParams, b.search) - countSharedSearchParams(searchParams, a.search);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidates.length > 0) {
|
||||||
|
for (let current of document.querySelectorAll('#sidebar a.current')) {
|
||||||
|
current.classList.remove('current');
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates[0].classList.add('current');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
sidebar.render = function () {
|
||||||
|
this.palettes.render();
|
||||||
|
};
|
||||||
|
|
||||||
|
sidebar.render();
|
||||||
|
window.addEventListener('turbo:render', () => sidebar.render());
|
||||||
|
|
||||||
|
function countSharedSearchParams(searchParams, search) {
|
||||||
|
if (!search || search === '?') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let params = new URLSearchParams(search);
|
||||||
|
return [...searchParams.keys()].filter(k => params.get(k) === searchParams.get(k)).length;
|
||||||
|
}
|
||||||
6
docs/assets/scripts/tweak.js
Normal file
6
docs/assets/scripts/tweak.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Get import code for remixed themes and tweaked palettes.
|
||||||
|
*/
|
||||||
|
export { getThemeCode } from './tweak/code.js';
|
||||||
|
export { HUE_RANGES, cdnUrl, hues, selectors, tints, urls } from './tweak/data.js';
|
||||||
|
export { default as Permalink } from './tweak/permalink.js';
|
||||||
54
docs/assets/scripts/tweak/code.js
Normal file
54
docs/assets/scripts/tweak/code.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* Get import code for remixed themes and tweaked palettes.
|
||||||
|
*/
|
||||||
|
import { urls } from './data.js';
|
||||||
|
|
||||||
|
export function cssImport(url, options = {}) {
|
||||||
|
let { language = 'html', cdnUrl = '/dist/', attributes } = options;
|
||||||
|
url = cdnUrl + url;
|
||||||
|
|
||||||
|
if (language === 'css') {
|
||||||
|
return `@import url('${url}');`;
|
||||||
|
} else {
|
||||||
|
attributes = attributes ? ` ${attributes}` : '';
|
||||||
|
return `<link rel="stylesheet" href="${url}"${attributes} />`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cssLiteral(value, options = {}) {
|
||||||
|
let { language = 'html' } = options;
|
||||||
|
|
||||||
|
if (language === 'css') {
|
||||||
|
return value;
|
||||||
|
} else {
|
||||||
|
return `<style>\n${value}\n</style>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Params in correct order
|
||||||
|
export const themeParams = ['colors', 'palette', 'brand', 'typography'];
|
||||||
|
|
||||||
|
export function getThemeCode(base, params, options) {
|
||||||
|
let ret = [];
|
||||||
|
|
||||||
|
if (base) {
|
||||||
|
ret.push(urls.theme(base));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let aspect of themeParams) {
|
||||||
|
let value = params[aspect];
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
ret.push(urls[aspect](value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret.map(url => cssImport(url, options)).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cssRule(selector, declarations, { indent = ' ' } = {}) {
|
||||||
|
selector = Array.isArray(selector) ? selector.flat().join(',\n') : selector;
|
||||||
|
declarations = Array.isArray(declarations) ? declarations.flat() : declarations;
|
||||||
|
declarations = declarations.map(declaration => indent + declaration.trim()).join('\n');
|
||||||
|
return `${selector} {\n${declarations.trimEnd()}\n}`;
|
||||||
|
}
|
||||||
213
docs/assets/scripts/tweak/data.js
Normal file
213
docs/assets/scripts/tweak/data.js
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
/**
|
||||||
|
* Data related to theme remixing and palette tweaking
|
||||||
|
* Must work in both browser and Node.js
|
||||||
|
*/
|
||||||
|
export const cdnUrl = globalThis.document ? document.documentElement.dataset.cdnUrl : '/dist/';
|
||||||
|
|
||||||
|
export const urls = {
|
||||||
|
theme: id => `styles/themes/${id}.css`,
|
||||||
|
colors: id => `styles/themes/${id}/color.css`,
|
||||||
|
palette: id => `styles/color/${id}.css`,
|
||||||
|
brand: id => `styles/brand/${id}.css`,
|
||||||
|
typography: id => `styles/themes/${id}/typography.css`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const docsURLs = {
|
||||||
|
colors: '/docs/themes/',
|
||||||
|
palette: '/docs/palettes/',
|
||||||
|
typography: '/docs/themes/',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const icons = {
|
||||||
|
colors: 'palette',
|
||||||
|
palette: 'swatchbook',
|
||||||
|
brand: 'droplet',
|
||||||
|
typography: 'font-case',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectors = {
|
||||||
|
palette: id =>
|
||||||
|
[':where(:root)', ':host', ":where([class^='wa-theme-'], [class*=' wa-theme-'])", `.wa-palette-${id}`].join(',\n'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HUE_RANGES = {
|
||||||
|
red: { min: 15, max: 35 }, // 20
|
||||||
|
orange: { min: 35, max: 75 }, // 40
|
||||||
|
yellow: { min: 75, max: 110 }, // 35
|
||||||
|
green: { min: 115, max: 170 }, // 55
|
||||||
|
cyan: { min: 170, max: 220 }, // 50
|
||||||
|
blue: { min: 220, max: 265 }, // 45
|
||||||
|
indigo: { min: 265, max: 290 }, // 25
|
||||||
|
purple: { min: 290, max: 320 }, // 30
|
||||||
|
pink: { min: 320, max: 375 }, // 55
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hues = Object.keys(HUE_RANGES);
|
||||||
|
export const allHues = [...hues, 'gray'];
|
||||||
|
export const tints = ['05', '10', '20', '30', '40', '50', '60', '70', '80', '90', '95'];
|
||||||
|
|
||||||
|
export const L_RANGES = {
|
||||||
|
'05': { min: 0.18, max: 0.2 },
|
||||||
|
10: { min: 0.23, max: 0.25 },
|
||||||
|
20: { min: 0.31, max: 0.35 },
|
||||||
|
30: { min: 0.38, max: 0.43 },
|
||||||
|
40: { min: 0.45, max: 0.5 },
|
||||||
|
50: { min: 0.55, max: 0.6 },
|
||||||
|
60: { min: 0.65, max: 0.7 },
|
||||||
|
70: { min: 0.73, max: 0.78 },
|
||||||
|
80: { min: 0.82, max: 0.85 },
|
||||||
|
90: { min: 0.91, max: 0.93 },
|
||||||
|
95: { min: 0.95, max: 0.97 },
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let range of [HUE_RANGES, L_RANGES]) {
|
||||||
|
for (let key in range) {
|
||||||
|
range[key].mid = (range[key].min + range[key].max) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Most common tint per hue.
|
||||||
|
* Largely the statistical mode, but also informed by the average and median.
|
||||||
|
*/
|
||||||
|
export const HUE_TOP_TINT = {
|
||||||
|
red: 50,
|
||||||
|
orange: 70,
|
||||||
|
yellow: 80,
|
||||||
|
green: 80,
|
||||||
|
cyan: 70,
|
||||||
|
blue: 50,
|
||||||
|
indigo: 40,
|
||||||
|
purple: 50,
|
||||||
|
pink: 50,
|
||||||
|
gray: 40,
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
┌─────────┬──────┬──────┬────────┬──────┬────────┬───────┐
|
||||||
|
│ (index) │ min │ max │ median │ avg │ stddev │ count │
|
||||||
|
├─────────┼──────┼──────┼────────┼──────┼────────┼───────┤
|
||||||
|
│ red │ 0.74 │ 1 │ 0.92 │ 0.88 │ 0.085 │ 9 │
|
||||||
|
│ yellow │ 0.72 │ 1 │ 0.98 │ 0.92 │ 0.11 │ 8 │
|
||||||
|
│ green │ 0.55 │ 0.93 │ 0.75 │ 0.75 │ 0.1 │ 8 │
|
||||||
|
│ cyan │ 0.7 │ 0.88 │ 0.82 │ 0.81 │ 0.053 │ 8 │
|
||||||
|
│ blue │ 0.54 │ 1 │ 0.83 │ 0.82 │ 0.15 │ 9 │
|
||||||
|
│ indigo │ 0.63 │ 1 │ 0.87 │ 0.86 │ 0.13 │ 8 │
|
||||||
|
│ purple │ 0.58 │ 0.99 │ 0.86 │ 0.84 │ 0.11 │ 8 │
|
||||||
|
│ pink │ 0.74 │ 1 │ 0.93 │ 0.89 │ 0.089 │ 8 │
|
||||||
|
└─────────┴──────┴──────┴────────┴──────┴────────┴───────┘
|
||||||
|
*/
|
||||||
|
/** Max(Average, Median) % of max P3 chroma per hue, relative to palette maximum and capped to 0.8 */
|
||||||
|
export const HUE_CHROMA_SCALE = {
|
||||||
|
red: 0.92,
|
||||||
|
orange: 0.96, // interpolated
|
||||||
|
yellow: 1,
|
||||||
|
green: 0.7,
|
||||||
|
cyan: 0.81,
|
||||||
|
blue: 0.83,
|
||||||
|
indigo: 0.87,
|
||||||
|
purple: 0.86,
|
||||||
|
pink: 0.92,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CHROMA_SCALE_LIGHTEST = {
|
||||||
|
95: 1,
|
||||||
|
90: 0.8,
|
||||||
|
80: 0.5,
|
||||||
|
70: 0.2,
|
||||||
|
60: 0.2,
|
||||||
|
50: 0.15,
|
||||||
|
40: 0.1,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MAX_CHROMA_BY_TINT = {
|
||||||
|
95: 0.11,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chroma levels to identify gray.
|
||||||
|
* First number: below this we identify as gray regardless
|
||||||
|
* Second number: below this we identify as gray if it's also in the bottom 25% of colors when sorted by chroma
|
||||||
|
*/
|
||||||
|
export const GRAY_CHROMA_BY_TINT = {
|
||||||
|
'05': [0.03, 0.05],
|
||||||
|
10: [0.035, 0.06],
|
||||||
|
20: [0.045, 0.06],
|
||||||
|
30: [0.05, 0.06],
|
||||||
|
40: [0.05, 0.06],
|
||||||
|
50: [0.04, 0.06],
|
||||||
|
60: [0.03, 0.05],
|
||||||
|
70: [0.02, 0.04],
|
||||||
|
80: [0.015, 0.03],
|
||||||
|
90: [0.007, 0.01],
|
||||||
|
95: [0.004, 0.005],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const moreHue = {
|
||||||
|
red: 'Redder',
|
||||||
|
orange: 'More orange', // https://www.reddit.com/r/grammar/comments/u9n0uo/is_it_oranger_or_more_orange/
|
||||||
|
yellow: 'Yellower',
|
||||||
|
green: 'Greener',
|
||||||
|
cyan: 'More cyan',
|
||||||
|
blue: 'Bluer',
|
||||||
|
indigo: 'More indigo',
|
||||||
|
purple: 'Purpler',
|
||||||
|
pink: 'Pinker',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hueBefore = {};
|
||||||
|
export const hueAfter = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < hues.length; i++) {
|
||||||
|
hueBefore[hues[i]] = hues[i - 1] ?? hues.at(-1);
|
||||||
|
hueAfter[hues[i]] = hues[i + 1] ?? hues[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HUE_SHIFTS = [
|
||||||
|
// Reds
|
||||||
|
{ range: [0, 25], peak: [10, 25], shift: { dark: 15, light: -18 }, maxConsecutive: { dark: 4, light: -2 } },
|
||||||
|
// Yellows
|
||||||
|
{ range: [30, 112], peak: [70, 100], shift: { dark: -48, light: 16 }, maxConsecutive: { dark: -20, light: 4 } },
|
||||||
|
|
||||||
|
// Greens
|
||||||
|
{ range: [140, 160], peak: [145, 155], shift: { dark: 15, light: -5 }, maxConsecutive: { dark: 7, light: -5 } },
|
||||||
|
// Blues
|
||||||
|
{ range: [240, 265], peak: [245, 260], shift: { dark: -3, light: -15 }, maxConsecutive: { dark: -3, light: -4 } },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const CHROMA_CURVES = {
|
||||||
|
50: { dark: 0.9, light: 0.8 },
|
||||||
|
60: { dark: 1, light: 1.2 },
|
||||||
|
70: { light: 1.2 },
|
||||||
|
80: { dark: 1.1, light: 2 },
|
||||||
|
90: { dark: 3, light: 2 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MAX_CHROMA_BOUNDS = { min: 0.08, max: 0.3 };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max gray chroma (% of chroma of undertone) per hue
|
||||||
|
*/
|
||||||
|
export const MAX_GRAY_CHROMA_SCALE = {
|
||||||
|
red: 0.2,
|
||||||
|
orange: 0.2,
|
||||||
|
yellow: 0.25,
|
||||||
|
green: 0.25,
|
||||||
|
cyan: 0.3,
|
||||||
|
blue: 0.35,
|
||||||
|
indigo: 0.35,
|
||||||
|
purple: 0.3,
|
||||||
|
pink: 0.25,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Default accent tint if all chromas are 0, but also the tint accent colors will be nudged towards (see chromaTolerance) */
|
||||||
|
export const DEFAULT_ACCENT = 60;
|
||||||
|
|
||||||
|
/** Min and max allowed tints */
|
||||||
|
export const MIN_ACCENT = 40;
|
||||||
|
export const MAX_ACCENT = 90;
|
||||||
|
|
||||||
|
/** Chroma tolerance: Chroma will need to differ more than this to gravitate away from defaultAccent */
|
||||||
|
export const CHROMA_TOLERANCE = 0.000001;
|
||||||
|
|
||||||
|
export const ROLES = ['brand', 'neutral', 'success', 'warning', 'danger'];
|
||||||
104
docs/assets/scripts/tweak/permalink.js
Normal file
104
docs/assets/scripts/tweak/permalink.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
const IDENTITY = x => x;
|
||||||
|
|
||||||
|
export default class Permalink extends URLSearchParams {
|
||||||
|
/** Params changed since last URL I/O */
|
||||||
|
changed = false;
|
||||||
|
|
||||||
|
constructor(params) {
|
||||||
|
super(location.search);
|
||||||
|
this.params = params;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return Object.fromEntries(this.entries());
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key, value, defaultValue) {
|
||||||
|
if (equals(value, defaultValue) || equals(value, '')) {
|
||||||
|
value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
value ??= null; // undefined -> null
|
||||||
|
|
||||||
|
let oldValue = Array.isArray(value) ? this.getAll(key) : this.get(key);
|
||||||
|
let changed = !equals(value, oldValue);
|
||||||
|
|
||||||
|
if (!changed) {
|
||||||
|
// Nothing to do here
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
super.delete(key);
|
||||||
|
value = value.slice();
|
||||||
|
|
||||||
|
for (let v of value) {
|
||||||
|
if (v || v === 0) {
|
||||||
|
if (typeof v === 'object') {
|
||||||
|
super.append(key, JSON.stringify(v));
|
||||||
|
} else {
|
||||||
|
super.append(key, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (value === null) {
|
||||||
|
super.delete(key);
|
||||||
|
} else {
|
||||||
|
super.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sort();
|
||||||
|
this.changed ||= changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update page URL if it has changed since last time
|
||||||
|
*/
|
||||||
|
updateLocation() {
|
||||||
|
if (this.changed) {
|
||||||
|
// If there’s already a search, replace it.
|
||||||
|
// We don’t want to clog the user’s history while they iterate
|
||||||
|
let search = this.toString();
|
||||||
|
let historyAction = location.search && search ? 'replaceState' : 'pushState';
|
||||||
|
history[historyAction](null, '', `?${search}`);
|
||||||
|
this.changed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function equals(value, oldValue) {
|
||||||
|
if (Array.isArray(value) || Array.isArray(oldValue)) {
|
||||||
|
value = toArray(value);
|
||||||
|
oldValue = toArray(oldValue);
|
||||||
|
|
||||||
|
if (value.length !== oldValue.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.every((v, i) => equals(v, oldValue[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// (value ?? oldValue ?? true) returns true if they're both empty (null or undefined)
|
||||||
|
[value, oldValue] = [value, oldValue].map(v => (!v && v !== false && v !== 0 ? null : v));
|
||||||
|
return value === oldValue || String(value) === String(oldValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a value to an array. `undefined` and `null` values are converted to an empty array.
|
||||||
|
* @param {*} value - The value to convert.
|
||||||
|
* @returns {any[]} The converted array.
|
||||||
|
*/
|
||||||
|
function toArray(value) {
|
||||||
|
value ??= [];
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't convert "foo" into ["f", "o", "o"]
|
||||||
|
if (typeof value !== 'string' && typeof value[Symbol.iterator] === 'function') {
|
||||||
|
return Array.from(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [value];
|
||||||
|
}
|
||||||
304
docs/assets/scripts/tweak/util.js
Normal file
304
docs/assets/scripts/tweak/util.js
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
// https://lea.verou.me/blog/2016/12/resolve-promises-externally-with-this-one-weird-trick/
|
||||||
|
export function promise() {
|
||||||
|
let res, rej;
|
||||||
|
|
||||||
|
let promise = new Promise((resolve, reject) => {
|
||||||
|
res = resolve;
|
||||||
|
rej = reject;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.assign(promise, { resolve: res, reject: rej });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeAngles(angles) {
|
||||||
|
// First, normalize each angle individually
|
||||||
|
let normalizedAngles = angles.map(h => ((h % 360) + 360) % 360);
|
||||||
|
|
||||||
|
for (let i = 1; i < angles.length; i++) {
|
||||||
|
let angle = normalizedAngles[i];
|
||||||
|
let prevAngle = normalizedAngles[i - 1];
|
||||||
|
let delta = angle - prevAngle;
|
||||||
|
|
||||||
|
if (Math.abs(delta) > 180) {
|
||||||
|
let equivalent = [angle + 360, angle - 360];
|
||||||
|
|
||||||
|
// Offset hue to minimize difference in the direction that brings it closer to the previous hue
|
||||||
|
let deltas = equivalent.map(e => Math.abs(e - prevAngle));
|
||||||
|
|
||||||
|
normalizedAngles[i] = equivalent[deltas[0] < deltas[1] ? 0 : 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedAngles;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function subtractAngles(θ1, θ2) {
|
||||||
|
let [a, b] = normalizeAngles([θ1, θ2]);
|
||||||
|
return a - b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an object of keys to ranges, find the closest range.
|
||||||
|
* Ranges are assumed to be mutually exclusive.
|
||||||
|
* @param {Object<string, {min: number, max: number}>} ranges
|
||||||
|
* @param {number} value
|
||||||
|
* @param {object} options
|
||||||
|
* @param {"angle" | undefined} options.type
|
||||||
|
* @param {number} [options.tolerance=Infinity] If value is not within any range, how close can it be?
|
||||||
|
* @param {(range: {min: number, max: number}) => {min: number, max: number}} options.getRange
|
||||||
|
* @returns {{key: string, distance: number}} The key of the closest range. Distance is 0 if the value is within the range, negative if below, positive if above.
|
||||||
|
*/
|
||||||
|
export function getRange(ranges, value, options) {
|
||||||
|
let { type } = options || {};
|
||||||
|
let keys = Object.keys(ranges);
|
||||||
|
let closest = { key: keys[0], distance: Infinity };
|
||||||
|
|
||||||
|
for (let key of keys) {
|
||||||
|
let range = ranges[key];
|
||||||
|
|
||||||
|
if (options?.getRange) {
|
||||||
|
range = options.getRange(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
let { min, max } = range;
|
||||||
|
|
||||||
|
if (Array.isArray(range)) {
|
||||||
|
[min, max] = range;
|
||||||
|
}
|
||||||
|
|
||||||
|
let deltaMin = type === 'angle' ? subtractAngles(value, min) : value - min;
|
||||||
|
let deltaMax = type === 'angle' ? subtractAngles(value, max) : value - max;
|
||||||
|
|
||||||
|
if (deltaMin >= 0 && deltaMax <= 0) {
|
||||||
|
return { key, distance: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.abs(deltaMin) < Math.abs(closest.distance)) {
|
||||||
|
closest = { key, distance: deltaMin };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deltaMax > 0 && Math.abs(deltaMax) < Math.abs(closest.distance)) {
|
||||||
|
closest = { key, distance: deltaMax };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO use angle functions to check tolerance against angles
|
||||||
|
if (options?.tolerance !== undefined && Math.abs(closest.distance) > options.tolerance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return closest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function camelCase(str) {
|
||||||
|
return (str + '').replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function capitalize(str) {
|
||||||
|
if (!str) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
str = str + '';
|
||||||
|
return str[0].toUpperCase() + str.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function arrayNext(array, element) {
|
||||||
|
let index = array.indexOf(element);
|
||||||
|
return array[(index + 1) % array.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function arrayPrevious(array, element) {
|
||||||
|
let index = array.indexOf(element);
|
||||||
|
return array[(index - 1 + array.length) % array.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function levelToIndex(level) {
|
||||||
|
if (level === '05') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return level === '95' ? 10 : +level / 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function indexToLevel(i) {
|
||||||
|
if (i === 0) {
|
||||||
|
return '05';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (i === 10 ? 95 : i * 10) + '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function previousLevel(level) {
|
||||||
|
if (level === '05') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return indexToLevel(levelToIndex(level) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nextLevel(level) {
|
||||||
|
if (level === '95') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return indexToLevel(levelToIndex(level) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function relativeLevel(level, steps) {
|
||||||
|
if (level == 100) {
|
||||||
|
// loose intentional
|
||||||
|
return relativeLevel(95, ++steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level == 95) {
|
||||||
|
// loose intentional
|
||||||
|
return relativeLevel(90, ++steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level == 0) {
|
||||||
|
// loose intentional
|
||||||
|
return relativeLevel(5, --steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level == 5) {
|
||||||
|
// loose intentional
|
||||||
|
return relativeLevel(10, --steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = clamp(0, levelToIndex(level) + steps, 10);
|
||||||
|
|
||||||
|
return indexToLevel(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} p Number from 0-1 where 0 is start and 1 is end
|
||||||
|
* @param {*} start Number for p=0
|
||||||
|
* @param {*} end Number for p=1
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function interpolate(p, range = [0, 1], options) {
|
||||||
|
let [start, end] = range;
|
||||||
|
|
||||||
|
if (p <= 0 || p >= 1 || range.length === 2) {
|
||||||
|
let value = start + p * (end - start);
|
||||||
|
return options?.unclamped ? value : clamp(start, value, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're here, there are more points in the range
|
||||||
|
let interval = 1 / (range.length - 1);
|
||||||
|
let index = Math.floor(p / interval);
|
||||||
|
let intervalProgress = progress(p, [index * interval, (index + 1) * interval]);
|
||||||
|
return interpolate(intervalProgress, range.slice(index, index + 2), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inverse of interpolate: given a value, find the progress between start and end.
|
||||||
|
* @param {*} value
|
||||||
|
* @param {*} range
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function progress(value, range = [0, 1], options) {
|
||||||
|
let [start, end] = range;
|
||||||
|
|
||||||
|
if (value <= start || value >= end || range.length === 2) {
|
||||||
|
let ret = (value - start) / (end - start);
|
||||||
|
|
||||||
|
return options?.unclamped ? ret : clamp(0, ret, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're here, there are more points in the range
|
||||||
|
let index = range.findIndex((v, i) => value > range[i - 1] && value <= v);
|
||||||
|
return (index - 1) / (range.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapRange(value, { from, to, progression }) {
|
||||||
|
let p = progress(value, from);
|
||||||
|
|
||||||
|
if (progression) {
|
||||||
|
p = progression(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
return interpolate(p, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clamp(min, value, max) {
|
||||||
|
if (max < min) {
|
||||||
|
[min, max] = [max, min];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (min !== undefined) {
|
||||||
|
value = Math.max(min, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (max !== undefined) {
|
||||||
|
value = Math.min(max, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clampAngle(min, value, max) {
|
||||||
|
[min, value, max] = normalizeAngles([min, value, max]);
|
||||||
|
return clamp(min, value, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function interpolateAngles(p, range) {
|
||||||
|
range = normalizeAngles(range);
|
||||||
|
return interpolate(p, range, { unclamped: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function progressAngle(angle, range) {
|
||||||
|
[angle, ...range] = normalizeAngles([angle, ...range]);
|
||||||
|
return progress(angle, range, { unclamped: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Round a number to the nearest multiple of `roundTo` or to the closest number in an array of numbers
|
||||||
|
* @param {number} value
|
||||||
|
* @param {number | number[]} roundTo
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function roundTo(value, roundTo = 1) {
|
||||||
|
if (Array.isArray(roundTo)) {
|
||||||
|
let closest = roundTo[0];
|
||||||
|
let closestDistance = Math.abs(value - closest);
|
||||||
|
|
||||||
|
for (let candidate of roundTo) {
|
||||||
|
let distance = Math.abs(value - candidate);
|
||||||
|
|
||||||
|
if (distance < closestDistance) {
|
||||||
|
closest = candidate;
|
||||||
|
closestDistance = distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return closest;
|
||||||
|
}
|
||||||
|
|
||||||
|
let decimals = roundTo.toString().split('.')[1]?.length ?? 0;
|
||||||
|
let ret = Math.round(value / roundTo) * roundTo;
|
||||||
|
|
||||||
|
if (decimals > 0) {
|
||||||
|
// Eliminate IEEE 754 floating point errors
|
||||||
|
ret = +ret.toFixed(decimals);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function slugify(str) {
|
||||||
|
return str
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '') // Convert accented letters to ASCII
|
||||||
|
.replace(/[^\w\s-]/g, '') // Remove remaining non-ASCII characters
|
||||||
|
.trim()
|
||||||
|
.replace(/\s+/g, '-') // Convert whitespace to hyphens
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function log(...args) {
|
||||||
|
console.log(...args);
|
||||||
|
return args[0];
|
||||||
|
}
|
||||||
@@ -27,3 +27,19 @@ wa-copy-button.copy-button {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.block-link-icon {
|
||||||
|
position: absolute;
|
||||||
|
inset-block-start: 0;
|
||||||
|
inset-inline-end: calc(100% + var(--wa-space-s));
|
||||||
|
|
||||||
|
transition: var(--wa-transition-slow);
|
||||||
|
|
||||||
|
&:not(:hover, :focus) {
|
||||||
|
opacity: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:not(:hover, :focus-within) > & {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ wa-page > header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Pro badges */
|
/* Pro badges */
|
||||||
wa-badge.pro::part(base) {
|
wa-badge.pro {
|
||||||
background-color: var(--wa-brand-orange);
|
background-color: var(--wa-brand-orange);
|
||||||
border-color: var(--wa-brand-orange);
|
border-color: var(--wa-brand-orange);
|
||||||
}
|
}
|
||||||
@@ -188,6 +188,29 @@ wa-badge.pro::part(base) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wa-icon-button.delete {
|
||||||
|
vertical-align: -0.2em;
|
||||||
|
margin-inline-start: var(--wa-space-xs);
|
||||||
|
|
||||||
|
&:not(li:hover > *, :focus) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wa-icon-button.delete {
|
||||||
|
&:hover {
|
||||||
|
color: var(--wa-color-danger-on-quiet);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::part(base):hover {
|
||||||
|
background: var(--wa-color-danger-fill-quiet);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:hover, :focus) {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar-close-button {
|
#sidebar-close-button {
|
||||||
@@ -232,16 +255,32 @@ wa-page > main {
|
|||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1.title wa-badge {
|
h1.title {
|
||||||
vertical-align: middle;
|
wa-icon-button {
|
||||||
|
font-size: var(--wa-font-size-l);
|
||||||
|
color: var(--wa-color-text-quiet);
|
||||||
|
|
||||||
&::part(base) {
|
&:not(:hover, :focus) {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wa-badge {
|
||||||
|
vertical-align: middle;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-info {
|
.block-info {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--wa-space-xs);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
margin-block-end: var(--wa-flow-spacing);
|
margin-block-end: var(--wa-flow-spacing);
|
||||||
|
|
||||||
|
code {
|
||||||
|
line-height: var(--wa-line-height-condensed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Current link */
|
/* Current link */
|
||||||
@@ -400,9 +439,23 @@ wa-page > main:has(> .index-grid) {
|
|||||||
|
|
||||||
&.color {
|
&.color {
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
|
transition: background var(--wa-transition-slow);
|
||||||
|
background:
|
||||||
|
linear-gradient(var(--color-top, transparent) 0% 100%) top no-repeat border-box,
|
||||||
|
linear-gradient(var(--color-bottom, transparent) 0% 100%) bottom no-repeat border-box var(--color,);
|
||||||
|
background-position: top, bottom;
|
||||||
|
background-size:
|
||||||
|
var(--color-top-width, 100%) var(--color-top-height, 30%),
|
||||||
|
var(--color-bottom-width, 100%) var(--color-bottom-height, 30%);
|
||||||
|
color: var(--swatch-text-color);
|
||||||
|
|
||||||
|
&.contrast-fail {
|
||||||
|
outline: 1px dashed var(--wa-color-red);
|
||||||
|
outline-offset: calc(-1 * var(--wa-space-2xs));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wa-copy-button {
|
> wa-copy-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -463,6 +516,55 @@ table.colors {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.value-up,
|
||||||
|
.value-down {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: ' ' var(--icon);
|
||||||
|
position: absolute;
|
||||||
|
margin-inline-start: 3em;
|
||||||
|
scale: 1 0.6;
|
||||||
|
color: color-mix(in oklch, oklch(from var(--icon-color) none c h) 0%, oklch(from currentColor l none none));
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-down {
|
||||||
|
--icon: '▼';
|
||||||
|
--icon-color: var(--wa-color-danger-fill-quiet);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
margin-block-end: -0.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-up {
|
||||||
|
--icon: '▲';
|
||||||
|
--icon-color: var(--wa-color-success-fill-quiet);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-modifier {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
|
||||||
|
.modifier {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -0.1em;
|
||||||
|
right: -0.3em;
|
||||||
|
font-size: 60%;
|
||||||
|
|
||||||
|
&::part(svg) {
|
||||||
|
stroke: var(--background-color, var(--wa-color-surface-default));
|
||||||
|
stroke-width: 100px;
|
||||||
|
paint-order: stroke;
|
||||||
|
overflow: visible;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Layout Examples */
|
/* Layout Examples */
|
||||||
.layout-example-boundary {
|
.layout-example-boundary {
|
||||||
border: var(--wa-border-width-s) dashed var(--wa-color-neutral-border-normal);
|
border: var(--wa-border-width-s) dashed var(--wa-color-neutral-border-normal);
|
||||||
@@ -544,3 +646,46 @@ table.colors {
|
|||||||
max-height: 21lh;
|
max-height: 21lh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.color-select {
|
||||||
|
&.default::part(display-input) {
|
||||||
|
opacity: 0.6;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
> small {
|
||||||
|
margin-inline-start: var(--wa-space-xs);
|
||||||
|
padding-block: 0 var(--wa-space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::part(combobox)::before,
|
||||||
|
wa-option::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 1.2em;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
margin-inline-end: var(--wa-space-xs);
|
||||||
|
flex: none;
|
||||||
|
border-radius: var(--wa-border-radius-m);
|
||||||
|
background: var(--color);
|
||||||
|
border: 1px solid var(--wa-color-surface-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
wa-option {
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
width: 1em;
|
||||||
|
margin-inline: var(--wa-space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::part(checked-icon) {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-badge {
|
||||||
|
opacity: 0.6;
|
||||||
|
margin-inline-start: var(--wa-space-xs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ icon: card
|
|||||||
|
|
||||||
<div slot="footer">
|
<div slot="footer">
|
||||||
<wa-button variant="brand" pill>More Info</wa-button>
|
<wa-button variant="brand" pill>More Info</wa-button>
|
||||||
<wa-rating></wa-rating>
|
<wa-rating label="Rating"></wa-rating>
|
||||||
</div>
|
</div>
|
||||||
</wa-card>
|
</wa-card>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.card-overview {
|
.card-overview {
|
||||||
max-width: 300px;
|
width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-overview small {
|
.card-overview small {
|
||||||
|
|||||||
@@ -2,13 +2,10 @@
|
|||||||
title: Components
|
title: Components
|
||||||
description: Components are the essential building blocks to create intuitive, cohesive experiences. Browse the library of customizable, framework-friendly web components included in Web Awesome.
|
description: Components are the essential building blocks to create intuitive, cohesive experiences. Browse the library of customizable, framework-friendly web components included in Web Awesome.
|
||||||
layout: overview
|
layout: overview
|
||||||
categories:
|
|
||||||
- actions
|
|
||||||
- feedback: 'Feedback & Status'
|
|
||||||
- imagery
|
|
||||||
- inputs
|
|
||||||
- navigation
|
|
||||||
- organization
|
|
||||||
- helpers: 'Utilities'
|
|
||||||
override:tags: []
|
override:tags: []
|
||||||
|
categories:
|
||||||
|
tags: [actions, feedback, imagery, inputs, navigation, organization, helpers]
|
||||||
|
titles:
|
||||||
|
feedback: 'Feedback & Status'
|
||||||
|
helpers: 'Utilities'
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,10 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* Global data for all pages
|
||||||
|
*/
|
||||||
|
import { sort } from '../_utils/filters.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
eleventyComputed: {
|
eleventyComputed: {
|
||||||
children(data) {
|
// Default parent. Can be overridden by explicitly setting parent in the data.
|
||||||
let mainTag = data.tags?.[0];
|
// parent can refer to either an ancestor page in the URL or another page in the same directory
|
||||||
let collection = data.collections[mainTag] ?? [];
|
parent(data) {
|
||||||
|
let { parent, page } = data;
|
||||||
|
|
||||||
return collection.filter(item => item.data.parent === data.page.fileSlug);
|
if (parent) {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return page.url.split('/').filter(Boolean).at(-2);
|
||||||
|
},
|
||||||
|
|
||||||
|
parentUrl(data) {
|
||||||
|
let { parent, page } = data;
|
||||||
|
return getParentUrl(page.url, parent);
|
||||||
|
},
|
||||||
|
|
||||||
|
parentItem(data) {
|
||||||
|
let { parentUrl } = data;
|
||||||
|
return data.collections.all.find(item => item.url === parentUrl);
|
||||||
|
},
|
||||||
|
|
||||||
|
children(data) {
|
||||||
|
let { collections, page, parentOf } = data;
|
||||||
|
|
||||||
|
if (parentOf) {
|
||||||
|
return collections[parentOf];
|
||||||
|
}
|
||||||
|
|
||||||
|
let collection = collections.all ?? [];
|
||||||
|
let url = page.url;
|
||||||
|
|
||||||
|
let ret = collection.filter(item => {
|
||||||
|
return item.data.parentUrl === url;
|
||||||
|
});
|
||||||
|
|
||||||
|
sort(ret);
|
||||||
|
|
||||||
|
return ret;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getParentUrl(url, parent) {
|
||||||
|
let parts = url.split('/').filter(Boolean);
|
||||||
|
let ancestorIndex = parts.findLastIndex(part => part === parent);
|
||||||
|
let retParts = parts.slice();
|
||||||
|
|
||||||
|
if (ancestorIndex > -1) {
|
||||||
|
// parent is an ancestor
|
||||||
|
retParts.splice(ancestorIndex + 1);
|
||||||
|
} else {
|
||||||
|
// parent is a sibling in the same directory
|
||||||
|
retParts.splice(-1, 1, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ret = retParts.join('/');
|
||||||
|
|
||||||
|
if (url.startsWith('/')) {
|
||||||
|
ret = '/' + ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!retParts.at(-1).includes('.') && !ret.endsWith('/')) {
|
||||||
|
// If no extension, make sure to end with a slash
|
||||||
|
ret += '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ret === '/docs/') {
|
||||||
|
ret = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: Clamped brand tokens
|
title: Clamped Color Tokens
|
||||||
layout: block
|
layout: block
|
||||||
---
|
---
|
||||||
|
|
||||||
{% set tints = ['40-max', '50-max', '60-max', '40-min', '50-min', '60-min'] %}
|
{% set tints = ['max-50', 'max-60', 'max-70', 'min-50', 'min-60', 'min-70'] %}
|
||||||
|
{% set hues = ['red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'indigo', 'purple', 'pink', 'gray'] %}
|
||||||
{% for hue in hues %}
|
|
||||||
<link href="/dist/styles/brand/{{ hue }}.css" rel="stylesheet">
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<table class="colors">
|
<table class="colors">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -20,18 +17,18 @@ layout: block
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
{% for hue in hues -%}
|
{% for hue in hues -%}
|
||||||
<tr class="wa-brand-{{ hue }}">
|
<tr class="wa-color-{{ hue }}">
|
||||||
<th>{{ hue | capitalize }}</th>
|
<th>{{ hue | capitalize }}</th>
|
||||||
<td class="core-column">
|
<td class="core-column">
|
||||||
<div class="color swatch" style="background-color: var(--wa-color-brand); color: var(--wa-color-brand-on);">
|
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}); color: var(--wa-color-{{ hue }}-on); --key: var(--wa-color-{{ hue }}-key);">
|
||||||
{{ palettes[paletteId][hue].maxChromaTint }}
|
{{ palettes[paletteId][hue].maxChromaTint }}
|
||||||
<wa-copy-button value="--wa-color-brand" copy-label="--wa-color-brand"></wa-copy-button>
|
<wa-copy-button value="--wa-color-{{ hue }}" copy-label="--wa-color-{{ hue }}"></wa-copy-button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
{% for tint in tints -%}
|
{% for tint in tints -%}
|
||||||
<td>
|
<td>
|
||||||
<div class="color swatch" style="background-color: var(--wa-color-brand-{{ tint }})">
|
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}-{{ tint }})">
|
||||||
<wa-copy-button value="--wa-color-brand-{{ tint }}" copy-label="--wa-color-brand-{{ tint }}"></wa-copy-button>
|
<wa-copy-button value="--wa-color-{{ hue }}-{{ tint }}" copy-label="--wa-color-{{ hue }}-{{ tint }}"></wa-copy-button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
@@ -41,7 +38,7 @@ layout: block
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.core-column .color.swatch::before {
|
.core-column .color.swatch::before {
|
||||||
counter-reset: key var(--wa-color-brand-key);
|
counter-reset: key var(--key);
|
||||||
content: counter(key);
|
content: counter(key);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
title: Layout
|
title: Layout
|
||||||
description: Layout components and utility classes help you organize content that can adapt to any device or screen size. See the [installation instructions](#installation) to use Web Awesome's layout tools in your project.
|
description: Layout components and utility classes help you organize content that can adapt to any device or screen size. See the [installation instructions](#installation) to use Web Awesome's layout tools in your project.
|
||||||
layout: overview
|
layout: overview
|
||||||
|
parentOf: layout
|
||||||
categories: ["components", "utilities"]
|
categories: ["components", "utilities"]
|
||||||
override:tags: []
|
override:tags: []
|
||||||
---
|
---
|
||||||
@@ -22,4 +23,4 @@ Or, you can choose to import _only_ the utilities:
|
|||||||
```html
|
```html
|
||||||
<link rel="stylesheet" href="{% cdnUrl 'styles/utilities.css' %}" />
|
<link rel="stylesheet" href="{% cdnUrl 'styles/utilities.css' %}" />
|
||||||
```
|
```
|
||||||
{% endmarkdown %}
|
{% endmarkdown %}
|
||||||
|
|||||||
@@ -42,6 +42,14 @@ wa-code-demo::part(preview) {
|
|||||||
<wa-input label="WA Input (url)" type="url"></wa-input>
|
<wa-input label="WA Input (url)" type="url"></wa-input>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Pill shaped text fields
|
||||||
|
|
||||||
|
Add the `wa-pill` class to an `<input>` to make it pill-shaped.
|
||||||
|
|
||||||
|
```html {.example}
|
||||||
|
<label>Input <input type="text" placeholder="placeholder" class="wa-pill"></label>
|
||||||
|
```
|
||||||
|
|
||||||
## Color Picker
|
## Color Picker
|
||||||
|
|
||||||
Basic:
|
Basic:
|
||||||
|
|||||||
28
docs/docs/palettes/app/color/generate-grays.js
Normal file
28
docs/docs/palettes/app/color/generate-grays.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { tints } from '/assets/scripts/tweak/data.js';
|
||||||
|
|
||||||
|
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 = undertoneScale.maxChromaTint;
|
||||||
|
Object.defineProperty(ret, 'core', {
|
||||||
|
enumerable: false,
|
||||||
|
get() {
|
||||||
|
return this[this.maxChromaTint];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
ret.maxChromaTintRaw = undertoneScale.maxChromaTintRaw;
|
||||||
|
|
||||||
|
for (let tint of tints) {
|
||||||
|
let colorUndertone = undertoneScale[tint].clone().to('oklch');
|
||||||
|
ret[tint] = colorUndertone.set({ c: c => c * grayChroma });
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.maxChroma = ret[ret.maxChromaTint].get('oklch.c');
|
||||||
|
ret.maxChromaRaw = ret[ret.maxChromaTintRaw].get('oklch.c');
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default generateGrays;
|
||||||
162
docs/docs/palettes/app/color/generate-palette.js
Normal file
162
docs/docs/palettes/app/color/generate-palette.js
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
// TODO move these to local imports
|
||||||
|
import Color from 'https://colorjs.io/dist/color.js';
|
||||||
|
import generateGrays from './generate-grays.js';
|
||||||
|
import generateScale from './generate-scale.js';
|
||||||
|
import getMaxChroma from './get-max-chroma.js';
|
||||||
|
import { getCoreTint } from './util.js';
|
||||||
|
import {
|
||||||
|
HUE_CHROMA_SCALE,
|
||||||
|
HUE_RANGES,
|
||||||
|
HUE_TOP_TINT,
|
||||||
|
L_RANGES,
|
||||||
|
MAX_ACCENT,
|
||||||
|
MIN_ACCENT,
|
||||||
|
} from '/assets/scripts/tweak/data.js';
|
||||||
|
import {
|
||||||
|
clamp,
|
||||||
|
clampAngle,
|
||||||
|
interpolate,
|
||||||
|
normalizeAngles,
|
||||||
|
progressAngle,
|
||||||
|
roundTo,
|
||||||
|
subtractAngles,
|
||||||
|
} from '/assets/scripts/tweak/util.js';
|
||||||
|
|
||||||
|
export default function generatePalette(seedHues, { huesAfter: allHuesAfter, ...options } = {}) {
|
||||||
|
let ret = {};
|
||||||
|
|
||||||
|
// Generate scales from seed hues
|
||||||
|
let firstSeedHue;
|
||||||
|
|
||||||
|
let coreLevels = {};
|
||||||
|
let seedMeta = {};
|
||||||
|
|
||||||
|
for (let hue in seedHues) {
|
||||||
|
let seedColors = seedHues[hue];
|
||||||
|
|
||||||
|
if (!seedColors) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
firstSeedHue ??= hue;
|
||||||
|
|
||||||
|
let coreLevel = (coreLevels[hue] = getCoreTint(seedColors));
|
||||||
|
let coreColor = seedColors[coreLevel];
|
||||||
|
let [l, c, h] = coreColor.getAll('oklch');
|
||||||
|
|
||||||
|
let lOffset = l - L_RANGES[coreLevel].mid;
|
||||||
|
let cScale = c / getMaxChroma(l, h);
|
||||||
|
let relativeCScale = cScale / HUE_CHROMA_SCALE[hue];
|
||||||
|
let levelOffset = coreLevel - HUE_TOP_TINT[hue];
|
||||||
|
seedMeta[hue] = { lOffset, cScale, relativeCScale, levelOffset };
|
||||||
|
|
||||||
|
ret[hue] = generateScale(seedColors);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!firstSeedHue) {
|
||||||
|
// No valid seed colors, abort mission
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill in remaining hues
|
||||||
|
let hueBefore = firstSeedHue;
|
||||||
|
|
||||||
|
for (let hue of allHuesAfter[firstSeedHue]) {
|
||||||
|
if (hue in ret) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let huesAfter = allHuesAfter[hue];
|
||||||
|
let seedHuesAfter = huesAfter.filter(hue => seedHues[hue]);
|
||||||
|
let neighboringSeedHues = [seedHuesAfter.at(-1), seedHuesAfter[0]];
|
||||||
|
|
||||||
|
// A number from 0 to 1 indicating how close we are to each neighboring seed hue (0 if only one seed hue)
|
||||||
|
let hueProgress =
|
||||||
|
seedHuesAfter.length === 1
|
||||||
|
? 0
|
||||||
|
: progressAngle(
|
||||||
|
HUE_RANGES[hue].mid,
|
||||||
|
neighboringSeedHues.map(hue => HUE_RANGES[hue].mid),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hue of the core color of the previous seed scale
|
||||||
|
let hBefore = ret[hueBefore][ret[hueBefore].maxChromaTint].get('oklch.h');
|
||||||
|
|
||||||
|
// We start from the midpoint of the hue range
|
||||||
|
let h = HUE_RANGES[hue].mid;
|
||||||
|
|
||||||
|
// Shift if too close to seed hues
|
||||||
|
let hBeforeDelta = subtractAngles(h, hBefore);
|
||||||
|
|
||||||
|
if (Math.abs(hBeforeDelta) < 40) {
|
||||||
|
h = hBefore + 40 * Math.sign(hBeforeDelta);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seedHuesAfter.length > 1) {
|
||||||
|
let hueAfter = seedHuesAfter[0];
|
||||||
|
let hAfter = ret[hueAfter][ret[hueAfter].maxChromaTint].get('oklch.h');
|
||||||
|
[hBefore, h, hAfter] = normalizeAngles([hBefore, h, hAfter]);
|
||||||
|
let hAfterDelta = subtractAngles(hAfter, h);
|
||||||
|
|
||||||
|
if (hAfter - 40 < hBefore + 40) {
|
||||||
|
// It's not possible to have a distance of at least 40deg from both neighboring hues
|
||||||
|
// so at least maximize distance
|
||||||
|
h = (hBefore + hAfter) / 2;
|
||||||
|
} else if (hAfterDelta < 40) {
|
||||||
|
h = hAfter - 40;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure hue is still within range for this scale
|
||||||
|
h = clampAngle(HUE_RANGES[hue].min, h, HUE_RANGES[hue].max);
|
||||||
|
|
||||||
|
let coreLevelOffset = interpolate(
|
||||||
|
hueProgress,
|
||||||
|
neighboringSeedHues.map(hue => seedMeta[hue].levelOffset),
|
||||||
|
);
|
||||||
|
let coreLevel = clamp(MIN_ACCENT, roundTo(HUE_TOP_TINT[hue] + coreLevelOffset, 10), MAX_ACCENT);
|
||||||
|
|
||||||
|
coreLevels[hue] = coreLevel;
|
||||||
|
let lOffsets = neighboringSeedHues.map(hue => seedMeta[hue].lOffset);
|
||||||
|
let lOffset = interpolate(hueProgress, lOffsets);
|
||||||
|
let l = L_RANGES[coreLevel].mid + lOffset;
|
||||||
|
|
||||||
|
let cScale = 1;
|
||||||
|
|
||||||
|
if (hue === 'yellow') {
|
||||||
|
// Yellow tends to be the brighest hue in the palette
|
||||||
|
cScale = Math.max(
|
||||||
|
...Object.values(seedMeta)
|
||||||
|
.map(meta => meta.relativeCScale)
|
||||||
|
.filter(c => c > 0),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
cScale = interpolate(
|
||||||
|
hueProgress,
|
||||||
|
neighboringSeedHues.map(neighboringHue => seedMeta[neighboringHue].relativeCScale),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
cScale *= HUE_CHROMA_SCALE[hue];
|
||||||
|
|
||||||
|
let maxC = getMaxChroma(l, h);
|
||||||
|
let c = cScale * maxC;
|
||||||
|
// let c = interpolate(
|
||||||
|
// hueProgress,
|
||||||
|
// pinnedScale.map(scale => scale.maxChroma),
|
||||||
|
// );
|
||||||
|
|
||||||
|
let coreColor = new Color('oklch', [l, c, h]).toGamut('p3');
|
||||||
|
|
||||||
|
ret[hue] = generateScale(coreColor);
|
||||||
|
hueBefore = hue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('gray' in seedHues) {
|
||||||
|
ret.gray = generateScale(seedHues.gray);
|
||||||
|
} else {
|
||||||
|
ret.gray = generateGrays(ret, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
138
docs/docs/palettes/app/color/generate-scale.js
Normal file
138
docs/docs/palettes/app/color/generate-scale.js
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import { getCoreTint, getHueShift, getLightness, identifyColor } from './util.js';
|
||||||
|
import {
|
||||||
|
CHROMA_CURVES,
|
||||||
|
CHROMA_SCALE_LIGHTEST,
|
||||||
|
L_RANGES,
|
||||||
|
MAX_CHROMA_BY_TINT,
|
||||||
|
tints,
|
||||||
|
} from '/assets/scripts/tweak/data.js';
|
||||||
|
import { clamp, interpolate, progress } from '/assets/scripts/tweak/util.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a scale of tints from one or more key colors
|
||||||
|
* @param {Color | Record<number | string, Color>} seedColors
|
||||||
|
* @returns {Record<number | string, Color>}
|
||||||
|
*/
|
||||||
|
export function generateScale(seedColors) {
|
||||||
|
if (seedColors.constructor.name === 'Color') {
|
||||||
|
// Single color given
|
||||||
|
let { level } = identifyColor(seedColors);
|
||||||
|
seedColors = { [level]: seedColors };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find core color
|
||||||
|
let coreLevel = getCoreTint(seedColors);
|
||||||
|
let coreColor = seedColors[coreLevel];
|
||||||
|
let coreChroma = coreColor.get('oklch.c');
|
||||||
|
|
||||||
|
let scale = {};
|
||||||
|
|
||||||
|
Object.defineProperties(scale, {
|
||||||
|
maxChromaTint: { value: coreLevel, enumerable: false, configurable: true },
|
||||||
|
maxChromaTintRaw: { value: coreLevel, enumerable: false, configurable: true },
|
||||||
|
maxChroma: { value: coreChroma, enumerable: false, configurable: true },
|
||||||
|
maxChromaRaw: { value: coreChroma, enumerable: false, configurable: true },
|
||||||
|
core: {
|
||||||
|
get() {
|
||||||
|
return this[this.maxChromaTint];
|
||||||
|
},
|
||||||
|
enumerable: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// First, add pinned colors
|
||||||
|
for (let tint in seedColors) {
|
||||||
|
scale[tint] = seedColors[tint];
|
||||||
|
}
|
||||||
|
|
||||||
|
// For finding lightest and darkest pinned colors
|
||||||
|
let pinnedTints = Object.keys(seedColors).sort((a, b) => a - b);
|
||||||
|
let chromaCurve = CHROMA_CURVES[clamp(50, coreLevel, 90)];
|
||||||
|
|
||||||
|
// Now generate the rest, starting from the edges
|
||||||
|
if (!('95' in scale)) {
|
||||||
|
let lightestPinnedTint = pinnedTints.at(-1);
|
||||||
|
let lightest = seedColors[lightestPinnedTint];
|
||||||
|
let lOffset = lightest.get('oklch.l') - L_RANGES[lightestPinnedTint].mid;
|
||||||
|
let chromaScale = CHROMA_SCALE_LIGHTEST[lightestPinnedTint];
|
||||||
|
let hueShift = getHueShift(lightest, lightestPinnedTint, '95');
|
||||||
|
|
||||||
|
let color = lightest.clone().to('oklch');
|
||||||
|
color.set({
|
||||||
|
l: getLightness(95, lOffset),
|
||||||
|
c: clamp(0, lightest.get('oklch.c') * chromaScale, MAX_CHROMA_BY_TINT[95]),
|
||||||
|
h: h => h + hueShift,
|
||||||
|
});
|
||||||
|
|
||||||
|
scale[95] = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('05' in scale)) {
|
||||||
|
let darkestPinnedTint = pinnedTints[0];
|
||||||
|
let darkest = seedColors[darkestPinnedTint];
|
||||||
|
let lOffset = darkest.get('oklch.l') - L_RANGES[darkestPinnedTint].mid;
|
||||||
|
let color = darkest.clone().to('oklch');
|
||||||
|
let hueShift = getHueShift(darkest, darkestPinnedTint, '05');
|
||||||
|
|
||||||
|
color.set({
|
||||||
|
l: getLightness('05', lOffset),
|
||||||
|
// TODO c
|
||||||
|
h: h => h + hueShift,
|
||||||
|
});
|
||||||
|
|
||||||
|
scale['05'] = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tintBefore = '05';
|
||||||
|
|
||||||
|
for (let tint of tints) {
|
||||||
|
if (tint in scale) {
|
||||||
|
// Pinned or already generated
|
||||||
|
tintBefore = tint;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generated color
|
||||||
|
// First, find closest pinned colors before and after
|
||||||
|
let tintAfter = pinnedTints.find(level => level > tint) ?? '95';
|
||||||
|
let neighboringTints = [tintBefore, tintAfter];
|
||||||
|
let neighboringColors = neighboringTints.map(t => scale[t]);
|
||||||
|
let tintProgress = progress(tint, neighboringTints);
|
||||||
|
|
||||||
|
let color = coreColor.clone().to('oklch');
|
||||||
|
|
||||||
|
// Lightness
|
||||||
|
let lOffset = interpolate(
|
||||||
|
tintProgress,
|
||||||
|
neighboringTints.map(t => scale[t].get('oklch.l') - L_RANGES[t].mid),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Interpolate hue linearly and chroma with a power curve
|
||||||
|
color.set({
|
||||||
|
l: getLightness(tint, lOffset),
|
||||||
|
c: interpolate(
|
||||||
|
tintProgress,
|
||||||
|
neighboringColors.map(c => c.get('oklch.c')),
|
||||||
|
{
|
||||||
|
progression: tint > coreLevel ? p => p ** chromaCurve.light : undefined,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
h: interpolate(
|
||||||
|
tintProgress,
|
||||||
|
neighboringColors.map(c => c.get('oklch.h')),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
scale[tint] = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let tint in scale) {
|
||||||
|
if (!(tint in seedColors) && scale[tint].toGamut) {
|
||||||
|
scale[tint] = scale[tint].toGamut('p3');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default generateScale;
|
||||||
3
docs/docs/palettes/app/color/generate.js
Normal file
3
docs/docs/palettes/app/color/generate.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { generateGrays, generateGrays as grays } from './generate-grays.js';
|
||||||
|
export { generatePalette, generatePalette as palette } from './generate-palette.js';
|
||||||
|
export { generateScale, generateScale as scale } from './generate-scale.js';
|
||||||
91
docs/docs/palettes/app/color/get-max-chroma.js
Normal file
91
docs/docs/palettes/app/color/get-max-chroma.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* Memoized calculation of OKLCH gamut boundary for a given L and H
|
||||||
|
* Currently unused, but we can use it if existing code becomes too slow.
|
||||||
|
*/
|
||||||
|
import Color from 'https://colorjs.io/dist/color.js';
|
||||||
|
import { interpolate, progress, progressAngle, roundTo } from '/assets/scripts/tweak/util.js';
|
||||||
|
|
||||||
|
/** Max oklch.c per h and l (rounded to 1 significant digit) */
|
||||||
|
const maxChroma = {};
|
||||||
|
const OOG_CHROMA = 0.4; // guaranteed to be OOG for every P3 color
|
||||||
|
const C_THRESHOLD = 0.03;
|
||||||
|
const MIN_H_STEP = 0.1;
|
||||||
|
const MIN_L_STEP = 0.001;
|
||||||
|
|
||||||
|
export default function getMaxChroma(l, h) {
|
||||||
|
let { hStep, lStep, count } = calculateBoundary(l, h);
|
||||||
|
|
||||||
|
let hRounded = roundTo(h, hStep);
|
||||||
|
let lRounded = roundTo(l, lStep);
|
||||||
|
|
||||||
|
// Calculate gamut boundary around this point
|
||||||
|
let hProgress = progressAngle(h - hRounded, [-hStep, 0, hStep]);
|
||||||
|
let lProgress = progress(l - lRounded, [-lStep, 0, lStep]);
|
||||||
|
let maxChromaH = [];
|
||||||
|
|
||||||
|
for (let i of [-1, 0, 1]) {
|
||||||
|
let h = roundTo(hRounded + i * hStep, hStep);
|
||||||
|
|
||||||
|
let cs = [-1, 0, 1].map(j => {
|
||||||
|
let l = roundTo(lRounded + j * lStep, lStep);
|
||||||
|
|
||||||
|
return maxChroma[l][h];
|
||||||
|
});
|
||||||
|
|
||||||
|
maxChromaH.push(interpolate(lProgress, cs));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interpolate between the 9 points using bilinear interpolation
|
||||||
|
let c = interpolate(hProgress, maxChromaH);
|
||||||
|
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateBoundary(pointL, pointH, lStep = 0.1, hStep = 10) {
|
||||||
|
let hRounded = roundTo(pointH, hStep);
|
||||||
|
let lRounded = roundTo(pointL, lStep);
|
||||||
|
let ret = { count: 0, hStep, lStep };
|
||||||
|
|
||||||
|
for (let i of [-1, 0, 1]) {
|
||||||
|
let l = roundTo(lRounded + i * lStep, lStep);
|
||||||
|
maxChroma[l] ??= {};
|
||||||
|
|
||||||
|
for (let j of [-1, 0, 1]) {
|
||||||
|
let h = roundTo(hRounded + j * hStep, hStep);
|
||||||
|
|
||||||
|
if (maxChroma[l][h] !== undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let gamutBoundary = new Color('oklch', [l, OOG_CHROMA, h]).toGamut('p3', { method: 'oklch.c' });
|
||||||
|
let c = gamutBoundary.get('c');
|
||||||
|
maxChroma[l][h] = c;
|
||||||
|
ret.count++;
|
||||||
|
let tooFar = { h: false, l: false };
|
||||||
|
|
||||||
|
if (i > -1) {
|
||||||
|
let lPrev = roundTo(lRounded + (i - 1) * lStep, lStep);
|
||||||
|
let cPrev = maxChroma[lPrev][h];
|
||||||
|
tooFar.l = Math.abs(c - cPrev) > C_THRESHOLD && lStep > MIN_L_STEP;
|
||||||
|
|
||||||
|
if (tooFar.l) {
|
||||||
|
ret.lStep /= 2;
|
||||||
|
ret.count += calculateBoundary(pointL, pointH, ret.lStep, ret.hStep).count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (j > -1) {
|
||||||
|
let hPrev = roundTo(hRounded + (j - 1) * hStep, hStep);
|
||||||
|
let cPrev = maxChroma[l][hPrev];
|
||||||
|
tooFar.h = Math.abs(c - cPrev) > C_THRESHOLD && hStep > MIN_H_STEP;
|
||||||
|
|
||||||
|
if (tooFar.h) {
|
||||||
|
ret.hStep /= 2;
|
||||||
|
ret.count += calculateBoundary(pointL, pointH, ret.lStep, ret.hStep).count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
83
docs/docs/palettes/app/color/get-palette-code.js
Normal file
83
docs/docs/palettes/app/color/get-palette-code.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
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 }) {
|
||||||
|
let imports = [];
|
||||||
|
|
||||||
|
if (base && options.imports !== false && !tweaked.seedColors) {
|
||||||
|
imports.push(urls.palette(base));
|
||||||
|
}
|
||||||
|
|
||||||
|
let ret = imports.map(url => cssImport(url, options)).join('\n');
|
||||||
|
|
||||||
|
let declarations = [];
|
||||||
|
let prefix = options.prefix ?? 'wa-color';
|
||||||
|
|
||||||
|
let css = '';
|
||||||
|
|
||||||
|
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]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let scale = colors[hue];
|
||||||
|
|
||||||
|
for (let tint of tints) {
|
||||||
|
let color = scale[tint];
|
||||||
|
let stringified = stringifyColor(color);
|
||||||
|
declarations.push(`--${prefix}-${hue}-${tint}: ${stringified};`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let coreTint = scale.maxChromaTint;
|
||||||
|
if (coreTint) {
|
||||||
|
declarations.push(
|
||||||
|
`--${prefix}-${hue}: var(--${prefix}-${hue}-${coreTint});`,
|
||||||
|
`--${prefix}-${hue}-key: ${coreTint};`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
declarations.push('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
css += cssRule(selector, declarations);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (css) {
|
||||||
|
if (imports.length) {
|
||||||
|
ret += '\n\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
ret += `${cssLiteral(css, options)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getPaletteCode;
|
||||||
28
docs/docs/palettes/app/color/palettes.js
Normal file
28
docs/docs/palettes/app/color/palettes.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// TODO move these to local imports
|
||||||
|
import Color from 'https://colorjs.io/dist/color.js';
|
||||||
|
import { tints } from '/assets/scripts/tweak/data.js';
|
||||||
|
|
||||||
|
let palettes = await fetch('/docs/palettes/data.json').then(r => r.json());
|
||||||
|
|
||||||
|
for (let palette in palettes) {
|
||||||
|
for (let hue in palettes[palette].colors) {
|
||||||
|
let scale = palettes[palette].colors[hue];
|
||||||
|
for (let tint of tints) {
|
||||||
|
let color = scale[tint];
|
||||||
|
|
||||||
|
if (Array.isArray(color)) {
|
||||||
|
scale[tint] = new Color('oklch', color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(scale, 'core', {
|
||||||
|
get() {
|
||||||
|
return this[this.maxChromaTint];
|
||||||
|
},
|
||||||
|
enumerable: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.allPalettes = palettes;
|
||||||
|
export default palettes;
|
||||||
74
docs/docs/palettes/app/color/tweak.js
Normal file
74
docs/docs/palettes/app/color/tweak.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
// TODO move these to local imports
|
||||||
|
import generateGrays from './generate-grays.js';
|
||||||
|
import { tints } from '/assets/scripts/tweak/data.js';
|
||||||
|
|
||||||
|
export function tweakPalette(baseColors, tweaks, tweaked) {
|
||||||
|
let ret = {};
|
||||||
|
|
||||||
|
if (!tweaked) {
|
||||||
|
return baseColors;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let hue in baseColors) {
|
||||||
|
let originalScale = baseColors[hue];
|
||||||
|
let scale = (ret[hue] = {});
|
||||||
|
let descriptors = Object.getOwnPropertyDescriptors(originalScale);
|
||||||
|
Object.defineProperties(scale, {
|
||||||
|
maxChromaTint: { ...descriptors.maxChromaTint, enumerable: false },
|
||||||
|
maxChromaTintRaw: { ...descriptors.maxChromaTintRaw, enumerable: false },
|
||||||
|
core: {
|
||||||
|
get() {
|
||||||
|
return this[this.maxChromaTint];
|
||||||
|
},
|
||||||
|
enumerable: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hue === 'gray') {
|
||||||
|
if (tweaked.grayChroma || tweaked.grayColor) {
|
||||||
|
let grayColor = tweaks.grayColor ?? this.originalGrayColor;
|
||||||
|
let grayChroma = this.computedGrayChroma;
|
||||||
|
ret.gray = generateGrays(baseColors, { grayColor, grayChroma });
|
||||||
|
} else {
|
||||||
|
ret.gray = originalScale;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let tint of tints) {
|
||||||
|
scale[tint] = tweakColor(hue, originalScale[tint], tweaks, tweaked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tweakColor(hue, originalColor, tweaks, tweaked) {
|
||||||
|
if (!tweaked) {
|
||||||
|
return originalColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
let color = originalColor;
|
||||||
|
let { hueShifts, chromaScale = 1, grayColor, grayChroma } = tweaks;
|
||||||
|
|
||||||
|
let tweak = {};
|
||||||
|
let thisTweaked = false;
|
||||||
|
|
||||||
|
if (tweaked.hue && hueShifts[hue]) {
|
||||||
|
tweak.h = h => h + hueShifts[hue];
|
||||||
|
thisTweaked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tweaked.chromaScale && chromaScale !== 1) {
|
||||||
|
tweak.c = c => c * chromaScale;
|
||||||
|
thisTweaked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thisTweaked) {
|
||||||
|
color = color.clone().to('oklch').set(tweak);
|
||||||
|
}
|
||||||
|
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default tweakPalette;
|
||||||
154
docs/docs/palettes/app/color/util.js
Normal file
154
docs/docs/palettes/app/color/util.js
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import {
|
||||||
|
CHROMA_TOLERANCE,
|
||||||
|
DEFAULT_ACCENT,
|
||||||
|
GRAY_CHROMA_BY_TINT,
|
||||||
|
HUE_RANGES,
|
||||||
|
HUE_SHIFTS,
|
||||||
|
L_RANGES,
|
||||||
|
MAX_ACCENT,
|
||||||
|
MIN_ACCENT,
|
||||||
|
tints,
|
||||||
|
} from '/assets/scripts/tweak/data.js';
|
||||||
|
import { clamp, getRange, mapRange } from '/assets/scripts/tweak/util.js';
|
||||||
|
|
||||||
|
export function identifyColor(color, colors) {
|
||||||
|
let [l, c, h] = color.getAll('oklch');
|
||||||
|
let level = getRange(L_RANGES, l).key;
|
||||||
|
let hue;
|
||||||
|
|
||||||
|
// Identify grays
|
||||||
|
let grayBounds = GRAY_CHROMA_BY_TINT[level];
|
||||||
|
if (c <= grayBounds[1]) {
|
||||||
|
// Possibly gray
|
||||||
|
if (c <= grayBounds[0]) {
|
||||||
|
// Definitely gray
|
||||||
|
hue = 'gray';
|
||||||
|
} else if (colors) {
|
||||||
|
// May or may not be gray, compare to palette max chroma
|
||||||
|
// FIXME this does not take level into account, so is more likely to identify lighter colors as gray
|
||||||
|
let maxChroma = Math.max(...colors.map(color => color.get('oklch.c')));
|
||||||
|
|
||||||
|
if (c / maxChroma < 0.2) {
|
||||||
|
hue = 'gray';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hue ??= getRange(HUE_RANGES, h, { type: 'angle' }).key;
|
||||||
|
|
||||||
|
return { hue, level };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLightness(level, distance) {
|
||||||
|
return clamp(L_RANGES[level].min, L_RANGES[level].mid + distance, L_RANGES[level].max);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How many tints are between two tints?
|
||||||
|
* E.g. `getTintDistance('90', '95')` should return `1`
|
||||||
|
* @param {number | string} tint1
|
||||||
|
* @param {number | string} tint2
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export function getTintDistance(tint1, tint2) {
|
||||||
|
tint1 = String(tint1);
|
||||||
|
tint2 = String(tint2);
|
||||||
|
return tints.indexOf(tint2) - tints.indexOf(tint1);
|
||||||
|
}
|
||||||
|
export function getHueShift(color, fromTint, toTint) {
|
||||||
|
let tintDistance = getTintDistance(fromTint, toTint);
|
||||||
|
let hueShift = getRange(HUE_SHIFTS, color.get('oklch.h'), {
|
||||||
|
getRange: v => v.range,
|
||||||
|
type: 'angle',
|
||||||
|
tolerance: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hueShift) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
hueShift = HUE_SHIFTS[hueShift.key];
|
||||||
|
|
||||||
|
let { peak, range } = hueShift;
|
||||||
|
let h = color.get('oklch.h');
|
||||||
|
let breakpoints = [range[0], ...peak, range[1]];
|
||||||
|
let intensity = mapRange(h, breakpoints, [0, 1, 1, 0]);
|
||||||
|
let type = tintDistance < 0 ? 'dark' : 'light';
|
||||||
|
let shift = hueShift.shift[type];
|
||||||
|
|
||||||
|
let ret = shift * intensity;
|
||||||
|
let maxConsecutive = hueShift.maxConsecutive[type] ?? hueShift.maxConsecutive;
|
||||||
|
let maxShift = Math.sign(shift) * maxConsecutive * Math.abs(tintDistance);
|
||||||
|
|
||||||
|
ret = clamp(undefined, ret, maxShift);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCoreTint(scale) {
|
||||||
|
let tintsInScale = Object.keys(scale);
|
||||||
|
|
||||||
|
if (tintsInScale.length <= 1) {
|
||||||
|
return tintsInScale[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
let ret = DEFAULT_ACCENT in scale ? DEFAULT_ACCENT : tintsInScale[Math.floor(tintsInScale.length / 2)];
|
||||||
|
let maxChroma = 0;
|
||||||
|
|
||||||
|
for (let tint in scale) {
|
||||||
|
let color = scale[tint];
|
||||||
|
let chroma = color.get('oklch.c');
|
||||||
|
|
||||||
|
if (chroma > maxChroma + CHROMA_TOLERANCE && tint >= MIN_ACCENT && tint <= MAX_ACCENT) {
|
||||||
|
ret = tint;
|
||||||
|
maxChroma = chroma;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getContrasts(colors, originalContrasts) {
|
||||||
|
let ret = {};
|
||||||
|
|
||||||
|
for (let hue in colors) {
|
||||||
|
ret[hue] = {};
|
||||||
|
|
||||||
|
for (let tintBg of tints) {
|
||||||
|
ret[hue][tintBg] = {};
|
||||||
|
let bgColor = colors[hue][tintBg];
|
||||||
|
|
||||||
|
if (!bgColor || !bgColor.contrast) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let tintFg of tints) {
|
||||||
|
let fgColor = colors[hue][tintFg];
|
||||||
|
let value = bgColor.contrast(fgColor, 'WCAG21');
|
||||||
|
if (originalContrasts) {
|
||||||
|
let original = originalContrasts[hue][tintBg][tintFg];
|
||||||
|
ret[hue][tintBg][tintFg] = { value, original, bgColor, fgColor };
|
||||||
|
} else {
|
||||||
|
ret[hue][tintBg][tintFg] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return hex code iff a color is within sRGB, otherwise fall back to its default string representation
|
||||||
|
*
|
||||||
|
* @param {Color} color
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function stringifyColor(color) {
|
||||||
|
if (color?.constructor.name !== 'Color') {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
let format = color.inGamut('srgb') ? 'hex' : undefined;
|
||||||
|
return color.toString({ format });
|
||||||
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
390
docs/docs/palettes/app/tweak.css
Normal file
390
docs/docs/palettes/app/tweak.css
Normal file
@@ -0,0 +1,390 @@
|
|||||||
|
/* CSS included both in predefined palettes and custom ones */
|
||||||
|
:root {
|
||||||
|
--fa-sliders-simple: '\f1de';
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-column {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
> wa-dropdown {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wa-dropdown > .color.swatch {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-slider {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
|
||||||
|
wa-slider {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
--track-height: 1em;
|
||||||
|
--track-color-inactive: transparent;
|
||||||
|
--track-color-active: transparent;
|
||||||
|
--thumb-color: var(--color);
|
||||||
|
--thumb-shadow: 0 0 0 var(--thumb-gap) var(--wa-color-surface-default),
|
||||||
|
var(--wa-shadow-offset-x-m) var(--wa-shadow-offset-y-m) var(--wa-shadow-blur-m)
|
||||||
|
calc(var(--wa-shadow-offset-x-m) * -1 + var(--thumb-gap)) var(--wa-color-shadow);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
--thumb-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::part(base) {
|
||||||
|
position: relative;
|
||||||
|
background: linear-gradient(to right in var(--color-interpolation-space, oklab), var(--color-1), var(--color-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tick {
|
||||||
|
--width: 1px;
|
||||||
|
--height: 0.5em;
|
||||||
|
--tick-color: var(--wa-color-neutral-border-normal);
|
||||||
|
width: 4px;
|
||||||
|
height: 2.4em;
|
||||||
|
background: no-repeat;
|
||||||
|
background-image: linear-gradient(var(--tick-color) 0 100%), linear-gradient(var(--tick-color) 0 100%);
|
||||||
|
background-position: top, bottom;
|
||||||
|
background-size: var(--width) var(--height);
|
||||||
|
position: absolute;
|
||||||
|
left: calc(var(--default-value-progress) * 100% - (var(--default-value-progress) - 0.5) * var(--thumb-size));
|
||||||
|
translate: -50% 0;
|
||||||
|
bottom: -0.5em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--tick-color: var(--wa-color-neutral-border-loud);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[slot='label'] {
|
||||||
|
min-height: 1.1lh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-button {
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: var(--wa-font-size-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-min,
|
||||||
|
.label-max {
|
||||||
|
font-size: var(--wa-font-size-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-min {
|
||||||
|
grid-column: 1;
|
||||||
|
margin-inline-start: 0.15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-max {
|
||||||
|
grid-column: 3;
|
||||||
|
margin-inline-end: 0.1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component='h'] {
|
||||||
|
--color-interpolation-space: oklch increasing hue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
gap: var(--wa-space-m);
|
||||||
|
background: var(--wa-color-surface-default);
|
||||||
|
color: var(--wa-color-text-normal);
|
||||||
|
border: 1px solid var(--wa-color-surface-border);
|
||||||
|
padding: var(--wa-space-m) var(--wa-space-l);
|
||||||
|
border-radius: var(--wa-border-radius-m);
|
||||||
|
color-scheme: light;
|
||||||
|
|
||||||
|
.copyable-code {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--wa-space-xs);
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
code {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 20ch;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> legend {
|
||||||
|
/* Force legend to be rendered inside the fieldset */
|
||||||
|
float: left;
|
||||||
|
clear: all;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wa-heading-s {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--wa-gap-xs);
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> :nth-child(1 of .align-end) {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@scope (.wa-dark) to (.wa-light) {
|
||||||
|
.popup {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-scale {
|
||||||
|
th {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tweak-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
right: var(--wa-space-s);
|
||||||
|
opacity: var(--tweak-icon-opacity, 0%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color.swatch:hover {
|
||||||
|
--tweak-icon-opacity: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tweaked .core-column {
|
||||||
|
--tweak-icon-opacity: 80%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tweaked-callout {
|
||||||
|
padding: var(--wa-space-xs);
|
||||||
|
padding-inline-start: var(--wa-space-m);
|
||||||
|
margin-block: var(--wa-space-m);
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:not(.tweaked-any *) {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::part(message) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--wa-space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
wa-button:first-of-type {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Better UI before Vue initializes */
|
||||||
|
[v-if='saved'],
|
||||||
|
[v-if^='tweaked'],
|
||||||
|
[v-cloak] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.static-palette:has(+ .colors:not([v-cloak])) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-color {
|
||||||
|
wa-radio-button::part(base) {
|
||||||
|
width: 2em;
|
||||||
|
height: 2em;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: var(--wa-border-radius-circle);
|
||||||
|
background: var(--color);
|
||||||
|
background-clip: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
wa-radio-button:is([checked], :state(checked))::part(base) {
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 var(--indicator-width) var(--indicator-color),
|
||||||
|
inset 0 0 0 calc(var(--indicator-width) + 1.5px) var(--wa-color-surface-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::part(form-control-input) {
|
||||||
|
gap: var(--wa-space-xs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hue-wheel {
|
||||||
|
--r: clamp(2em, 6rem, 25vmin);
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1 / 5;
|
||||||
|
position: relative;
|
||||||
|
width: calc(var(--r) * 2);
|
||||||
|
aspect-ratio: 1;
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
|
||||||
|
&,
|
||||||
|
&::before {
|
||||||
|
--stops: 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 {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
-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,
|
||||||
|
oklch(var(--lc2) 0),
|
||||||
|
oklch(var(--lc2) 60),
|
||||||
|
oklch(var(--lc2) 120),
|
||||||
|
oklch(var(--lc2) 180),
|
||||||
|
oklch(var(--lc2) 240),
|
||||||
|
oklch(var(--lc2) 300),
|
||||||
|
oklch(var(--lc2) 360)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color {
|
||||||
|
--scale-c: calc(var(--c) / var(--max-c));
|
||||||
|
--distance: calc(var(--r) * var(--scale-c));
|
||||||
|
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%) rotate(calc(var(--h) * 1deg - 90deg)) translateX(var(--distance));
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
width: calc(1.2em + 0.3em * var(--scale-c));
|
||||||
|
aspect-ratio: 1;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--scale: 1.2;
|
||||||
|
--line-color: white;
|
||||||
|
--line-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
border-top: 2px var(--line-style, dashed) var(--line-color, var(--wa-color-gray-80));
|
||||||
|
padding-top: 100%;
|
||||||
|
top: calc(50% - 1px);
|
||||||
|
right: 50%;
|
||||||
|
width: var(--distance);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid white;
|
||||||
|
box-shadow: var(--wa-shadow-l);
|
||||||
|
background: var(--color);
|
||||||
|
transition: var(--wa-transition-fast);
|
||||||
|
scale: var(--scale, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wa-tooltip {
|
||||||
|
/* Prevent flickering */
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scale-filter {
|
||||||
|
wa-tab wa-icon {
|
||||||
|
margin-right: 0.4em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title wa-icon-button[name='pencil'] {
|
||||||
|
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: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 1.2em;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
flex: none;
|
||||||
|
border-radius: var(--wa-border-radius-m);
|
||||||
|
background: var(--color);
|
||||||
|
border: 1px solid var(--wa-color-surface-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-select wa-option {
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
width: 1em;
|
||||||
|
margin-inline: var(--wa-space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::part(checked-icon) {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
wa-icon[name='square-plus'] {
|
||||||
|
vertical-align: -0.15em;
|
||||||
|
color: var(--color-gray);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-popup {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.popup {
|
||||||
|
min-width: 25ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wa-icon[name='thumbtack'],
|
||||||
|
wa-icon-button[name='thumbtack']::part(icon) {
|
||||||
|
rotate: 45deg;
|
||||||
|
}
|
||||||
1012
docs/docs/palettes/app/tweak.js
Normal file
1012
docs/docs/palettes/app/tweak.js
Normal file
File diff suppressed because it is too large
Load Diff
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
82
docs/docs/palettes/app/vue-components/color-popup.js
Normal file
82
docs/docs/palettes/app/vue-components/color-popup.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import Color from 'https://colorjs.io/dist/color.js';
|
||||||
|
import { stringifyColor } from '../color/util.js';
|
||||||
|
import InfoTip from './info-tip.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
title: String,
|
||||||
|
token: String,
|
||||||
|
color: Color,
|
||||||
|
deletable: Boolean,
|
||||||
|
pinnable: Boolean,
|
||||||
|
pinned: Boolean,
|
||||||
|
placement: String,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
emits: ['delete', 'pin'],
|
||||||
|
mounted() {
|
||||||
|
let popup = this.$refs.popup;
|
||||||
|
|
||||||
|
if (popup) {
|
||||||
|
// Find trigger
|
||||||
|
let trigger = popup.previousElementSibling;
|
||||||
|
if (trigger) {
|
||||||
|
trigger.slot ||= 'trigger';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
stringifiedColor() {
|
||||||
|
return stringifyColor(this.color);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<wa-dropdown class="color-popup" :placement>
|
||||||
|
<slot></slot>
|
||||||
|
<component :is="title ? 'fieldset' : 'div'" class="popup" ref="popup">
|
||||||
|
<component :is="title ? 'legend' : 'div'" class="wa-heading-s" v-if="title || token || deletable || pinnable">
|
||||||
|
<span v-if="title">{{ title }}</span>
|
||||||
|
<wa-copy-button v-if="title && token" :value="token" :copy-label="token"></wa-copy-button>
|
||||||
|
|
||||||
|
<info-tip v-if="deletable && pinned">
|
||||||
|
<wa-button size="small" variant="danger" appearance="plain" class="delete-button align-end" @click="$emit('delete')">
|
||||||
|
<wa-icon name="trash" variant="regular"></wa-icon>
|
||||||
|
</wa-button>
|
||||||
|
<template #content>
|
||||||
|
Delete from my colors
|
||||||
|
</template>
|
||||||
|
</info-tip>
|
||||||
|
<info-tip v-if="pinnable && !pinned">
|
||||||
|
<wa-button appearance="outlined" size="small" class="pin-color align-end" @click="$emit('pin')">
|
||||||
|
<wa-icon name="thumbtack" variant="regular" slot="prefix"></wa-icon>
|
||||||
|
Pin
|
||||||
|
</wa-button>
|
||||||
|
<template #content>
|
||||||
|
Prevent this color from changing as others are edited
|
||||||
|
</template>
|
||||||
|
</info-tip>
|
||||||
|
</component>
|
||||||
|
|
||||||
|
<slot name="content"></slot>
|
||||||
|
|
||||||
|
<div class="wa-stack wa-gap-xs">
|
||||||
|
<div class="copyable-code" v-if="token && !title">
|
||||||
|
<code>{{ token }}</code>
|
||||||
|
<wa-copy-button :value="token"></wa-copy-button>
|
||||||
|
</div>
|
||||||
|
<div class="copyable-code" v-if="color">
|
||||||
|
<code>{{ stringifiedColor }}</code>
|
||||||
|
<wa-copy-button :value="stringifiedColor"></wa-copy-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</component>
|
||||||
|
</wa-dropdown>`,
|
||||||
|
compilerOptions: {
|
||||||
|
isCustomElement: tag => tag.startsWith('wa-'),
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
InfoTip,
|
||||||
|
},
|
||||||
|
};
|
||||||
73
docs/docs/palettes/app/vue-components/color-select.js
Normal file
73
docs/docs/palettes/app/vue-components/color-select.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { capitalize } from '/assets/scripts/tweak/util.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
modelValue: String,
|
||||||
|
label: String,
|
||||||
|
getLabel: {
|
||||||
|
type: Function,
|
||||||
|
default: capitalize,
|
||||||
|
},
|
||||||
|
getContent: {
|
||||||
|
type: Function,
|
||||||
|
},
|
||||||
|
getColor: {
|
||||||
|
type: Function,
|
||||||
|
default: value => `var(--wa-color-${value})`,
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
type: Array,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
groups: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue', 'input'],
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
computedGroups() {
|
||||||
|
let ret = {};
|
||||||
|
|
||||||
|
if (this.values?.length) {
|
||||||
|
ret[''] = this.values;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.groups) {
|
||||||
|
for (let group in this.groups) {
|
||||||
|
if (this.groups[group]?.length) {
|
||||||
|
ret[group] = this.groups[group];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
|
||||||
|
firstGroup() {
|
||||||
|
return Object.keys(this.computedGroups)[0];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
capitalize,
|
||||||
|
handleInput(e) {
|
||||||
|
this.$emit('input', this.modelValue);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<wa-select class="color-select" name="brand" :label="label" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)"
|
||||||
|
:style="{'--color': getColor(modelValue)}">
|
||||||
|
<template v-for="values, group in computedGroups">
|
||||||
|
<template v-if="group">
|
||||||
|
<wa-divider v-if="group !== firstGroup"></wa-divider>
|
||||||
|
<small>{{ group }}</small>
|
||||||
|
</template>
|
||||||
|
<wa-option v-if="values?.length" v-for="value of values" :label="getLabel(value)" :value="value" :style="{'--color': getColor(value)}" v-html="getContent?.(value) ?? getLabel(value)"></wa-option>
|
||||||
|
</template>
|
||||||
|
<slot></slot>
|
||||||
|
</wa-select>
|
||||||
|
`,
|
||||||
|
};
|
||||||
343
docs/docs/palettes/app/vue-components/color-slider.js
Normal file
343
docs/docs/palettes/app/vue-components/color-slider.js
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
const template = `
|
||||||
|
<div class="color-slider" :style="{
|
||||||
|
'--color': computedColor, '--color-1': colorMin, '--color-2': colorMax,
|
||||||
|
'--default-value-progress': defaultProgress,
|
||||||
|
}" :data-component="coord || null">
|
||||||
|
<wa-slider ref="slider" :min="computedMin" :max="computedMax" :step="step" :value="value"
|
||||||
|
@input="handleInput($event.target.value);" @change="inputEnd($event.target.value)">
|
||||||
|
<div slot="label">
|
||||||
|
{{ label }}
|
||||||
|
<wa-icon-button v-if="value !== computedDefaultValue" @click="reset" class="clear-button" name="circle-xmark" library="system" variant="regular" label="Reset"></wa-icon-button>
|
||||||
|
<info-tip>
|
||||||
|
<div class="tick"></div>
|
||||||
|
<template #content>{{ computedLabelDefault }}</template>
|
||||||
|
</info-tip>
|
||||||
|
</div>
|
||||||
|
</wa-slider>
|
||||||
|
<div class="label-min">{{ labelMin }}</div>
|
||||||
|
<div class="label-max">{{ labelMax }}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
import Color from 'https://colorjs.io/dist/color.js';
|
||||||
|
import InfoTip from './info-tip.js';
|
||||||
|
import { capitalize, promise, roundTo } from '/assets/scripts/tweak/util.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
coord: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
validator(value) {
|
||||||
|
return ['l', 'c', 'h'].includes(value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
color: Color,
|
||||||
|
defaultColor: Color,
|
||||||
|
|
||||||
|
defaultValue: Number,
|
||||||
|
defaultValueRelative: Number,
|
||||||
|
|
||||||
|
/** Used for relative types. Defaults to defaultValue if not provided. */
|
||||||
|
baseValue: Number,
|
||||||
|
|
||||||
|
/** Used for formatting only. Only specify if different from base value. */
|
||||||
|
formatBaseValue: {
|
||||||
|
type: Number,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
|
||||||
|
modelValue: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
min: Number,
|
||||||
|
max: Number,
|
||||||
|
minRelative: Number,
|
||||||
|
maxRelative: Number,
|
||||||
|
step: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'raw',
|
||||||
|
},
|
||||||
|
formatType: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
label: String,
|
||||||
|
labelMin: String,
|
||||||
|
labelMax: String,
|
||||||
|
labelDefault: String,
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue', 'update:color', 'input'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
mounted: promise(),
|
||||||
|
initialColor: this.color,
|
||||||
|
value: undefined,
|
||||||
|
tweaking: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (!this.color && !this.defaultColor) {
|
||||||
|
console.warn(
|
||||||
|
`[${this.label}]`,
|
||||||
|
'<color-slider> requires at least one of the following props: color, defaultColor',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.modelValue !== undefined) {
|
||||||
|
this.value = this.getAbsoluteValue(this.modelValue);
|
||||||
|
} else if (this.color) {
|
||||||
|
this.value = this.colorCoords[this.coordIndex];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.$refs.slider) {
|
||||||
|
this.$refs.slider.tooltipFormatter = value => this.formatValue(value);
|
||||||
|
this.$refs.slider.colorSliderData = this; // for debugging
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mounted.resolve();
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
delete this.$refs.slider?.colorSliderData;
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
computedMin() {
|
||||||
|
if (this.minRelative !== undefined) {
|
||||||
|
return getAbsoluteValue(this.minRelative);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.min;
|
||||||
|
},
|
||||||
|
|
||||||
|
computedMax() {
|
||||||
|
if (this.maxRelative !== undefined) {
|
||||||
|
return this.getAbsoluteValue(this.maxRelative);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.max;
|
||||||
|
},
|
||||||
|
|
||||||
|
computedColor() {
|
||||||
|
return this.getColorAt(this.value);
|
||||||
|
},
|
||||||
|
|
||||||
|
computedColorCoords() {
|
||||||
|
return this.computedColor.oklch.slice();
|
||||||
|
},
|
||||||
|
|
||||||
|
colorCoords() {
|
||||||
|
let color = this.color ?? this.computedColor;
|
||||||
|
return color?.oklch.slice();
|
||||||
|
},
|
||||||
|
|
||||||
|
computedColorString() {
|
||||||
|
return `oklch(${this.computedColorCoords.join(' ')})`;
|
||||||
|
},
|
||||||
|
|
||||||
|
colorString() {
|
||||||
|
return `oklch(${this.colorCoords.join(' ')})`;
|
||||||
|
},
|
||||||
|
|
||||||
|
defaultCoords() {
|
||||||
|
if (this.defaultColor) {
|
||||||
|
return this.defaultColor.oklch.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
let ret = this.color.oklch.slice();
|
||||||
|
|
||||||
|
if (this.defaultValue !== undefined) {
|
||||||
|
ret[this.coordIndex] = this.defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
|
||||||
|
coordIndex() {
|
||||||
|
return ['l', 'c', 'h'].indexOf(this.coord);
|
||||||
|
},
|
||||||
|
|
||||||
|
colorMin() {
|
||||||
|
return this.getColorAt(this.computedMin);
|
||||||
|
},
|
||||||
|
|
||||||
|
colorMax() {
|
||||||
|
return this.getColorAt(this.computedMax);
|
||||||
|
},
|
||||||
|
|
||||||
|
isRelative() {
|
||||||
|
return this.type && this.type !== 'raw';
|
||||||
|
},
|
||||||
|
|
||||||
|
computedBaseValue() {
|
||||||
|
if (!this.isRelative) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.baseValue !== undefined) {
|
||||||
|
return this.baseValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.computedDefaultValue;
|
||||||
|
},
|
||||||
|
|
||||||
|
computedDefaultValue() {
|
||||||
|
if (this.defaultValue !== undefined) {
|
||||||
|
return this.defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.defaultValueRelative !== undefined) {
|
||||||
|
return this.getAbsoluteValue(this.defaultValueRelative);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.baseValue !== undefined) {
|
||||||
|
return this.baseValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.defaultCoords[this.coordIndex];
|
||||||
|
},
|
||||||
|
|
||||||
|
computedDefaultColor() {
|
||||||
|
return this.defaultColor ?? this.getColorAt(this.computedDefaultValue);
|
||||||
|
},
|
||||||
|
|
||||||
|
computedLabelDefault() {
|
||||||
|
let labelDefault = this.labelDefault || 'Default value';
|
||||||
|
let formattedDefaultValue = this.formatValue(this.computedDefaultValue);
|
||||||
|
return `${labelDefault} (${formattedDefaultValue})`;
|
||||||
|
},
|
||||||
|
|
||||||
|
defaultProgress() {
|
||||||
|
return (this.computedDefaultValue - this.computedMin) / (this.computedMax - this.computedMin);
|
||||||
|
},
|
||||||
|
|
||||||
|
relativeValue() {
|
||||||
|
this.computedBaseValue;
|
||||||
|
return this.getRelativeValue(this.value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
capitalize,
|
||||||
|
|
||||||
|
getAbsoluteValue(relativeValue) {
|
||||||
|
return getAbsoluteValue({
|
||||||
|
type: this.type,
|
||||||
|
relativeValue,
|
||||||
|
baseValue: this.baseValue ?? this.computedBaseValue,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getRelativeValue(absoluteValue) {
|
||||||
|
return getRelativeValue({
|
||||||
|
type: this.type,
|
||||||
|
absoluteValue,
|
||||||
|
baseValue: this.baseValue ?? this.computedBaseValue,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
formatValue(value = this.value) {
|
||||||
|
let formatType = this.formatType ?? this.type;
|
||||||
|
let style = formatType === 'scale' ? 'percent' : undefined;
|
||||||
|
|
||||||
|
if (formatType && formatType !== 'raw') {
|
||||||
|
let baseValue = this.formatBaseValue ?? this.computedBaseValue;
|
||||||
|
value = getRelativeValue({ type: formatType, absoluteValue: value, baseValue });
|
||||||
|
}
|
||||||
|
|
||||||
|
value = roundTo(value, this.step);
|
||||||
|
return value.toLocaleString(undefined, { style });
|
||||||
|
},
|
||||||
|
|
||||||
|
getColorAt(value) {
|
||||||
|
let coords = this.defaultCoords.slice();
|
||||||
|
coords[this.coordIndex] = value;
|
||||||
|
return new Color('oklch', coords);
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Called when value changes due to user interaction */
|
||||||
|
handleInput(value) {
|
||||||
|
this.value = value;
|
||||||
|
this.tweaking = true;
|
||||||
|
this.$emit('input', value);
|
||||||
|
},
|
||||||
|
|
||||||
|
inputEnd() {
|
||||||
|
this.tweaking = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.handleInput(this.computedDefaultValue);
|
||||||
|
this.inputEnd();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
computedColorString() {
|
||||||
|
if (this.color && this.colorString !== this.computedColorString) {
|
||||||
|
// Color changed, communicate to the outside world
|
||||||
|
this.$emit('update:color', this.computedColor);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
colorString() {
|
||||||
|
if (this.color && this.colorString !== this.computedColorString) {
|
||||||
|
// Color changed in the outside world, update our internals
|
||||||
|
if (this.colorCoords[this.coordIndex] !== this.value) {
|
||||||
|
this.value = this.colorCoords[this.coordIndex];
|
||||||
|
|
||||||
|
let modelValue = this.getRelativeValue(this.value);
|
||||||
|
this.$emit('update:modelValue', modelValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
relativeValue() {
|
||||||
|
this.$emit('update:modelValue', this.relativeValue);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template,
|
||||||
|
components: { InfoTip },
|
||||||
|
compilerOptions: {
|
||||||
|
isCustomElement: tag => tag.startsWith('wa-'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getAbsoluteValue({ type, relativeValue, baseValue }) {
|
||||||
|
if (baseValue === undefined) {
|
||||||
|
type = 'raw';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'shift') {
|
||||||
|
return relativeValue + baseValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'scale') {
|
||||||
|
return relativeValue * baseValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return relativeValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRelativeValue({ type, absoluteValue, baseValue }) {
|
||||||
|
if (baseValue === undefined) {
|
||||||
|
type = 'raw';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'shift') {
|
||||||
|
return absoluteValue - baseValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'scale') {
|
||||||
|
if (!absoluteValue) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return absoluteValue / baseValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return absoluteValue;
|
||||||
|
}
|
||||||
56
docs/docs/palettes/app/vue-components/color-swatch-picker.js
Normal file
56
docs/docs/palettes/app/vue-components/color-swatch-picker.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
const template = `
|
||||||
|
<wa-radio-group class="core-color" orientation="horizontal" :value="modelValue" @input="handleInput($event.target.value)">
|
||||||
|
<template v-for="h in hues">
|
||||||
|
<info-tip>
|
||||||
|
<wa-radio-button :label="capitalize(h)" :value="h" :style="{'--color': colors[h]}"></wa-radio-button>
|
||||||
|
<template #content>{{ capitalize(h) }}</template>
|
||||||
|
</info-tip>
|
||||||
|
</template>
|
||||||
|
<div slot="label">{{ label }}</div>
|
||||||
|
</wa-radio-group>
|
||||||
|
`;
|
||||||
|
|
||||||
|
import Color from 'https://colorjs.io/dist/color.js';
|
||||||
|
import InfoTip from './info-tip.js';
|
||||||
|
import { hues } from '/assets/scripts/tweak/data.js';
|
||||||
|
import { capitalize, promise, roundTo } from '/assets/scripts/tweak/util.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
modelValue: String,
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: 'Color',
|
||||||
|
},
|
||||||
|
colors: Object,
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue', 'input'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
defaultValue: this.modelValue,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
Object.assign(this, { hues });
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {
|
||||||
|
capitalize,
|
||||||
|
|
||||||
|
/** Called when value changes due to user interaction */
|
||||||
|
handleInput(value) {
|
||||||
|
this.value = value;
|
||||||
|
this.$emit('input', value);
|
||||||
|
this.$emit('update:modelValue', value);
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.handleInput(this.defaultValue);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template,
|
||||||
|
components: { InfoTip },
|
||||||
|
compilerOptions: {
|
||||||
|
isCustomElement: tag => tag.startsWith('wa-'),
|
||||||
|
},
|
||||||
|
};
|
||||||
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-'),
|
||||||
|
},
|
||||||
|
};
|
||||||
37
docs/docs/palettes/app/vue-components/info-tip.js
Normal file
37
docs/docs/palettes/app/vue-components/info-tip.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import Color from 'https://colorjs.io/dist/color.js';
|
||||||
|
import { stringifyColor } from '../color/util.js';
|
||||||
|
|
||||||
|
let maxUid = 0;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {},
|
||||||
|
data() {
|
||||||
|
let uid = ++maxUid;
|
||||||
|
return { uid, id: 'info-tip-' + uid };
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
let tooltip = this.$refs.tooltip;
|
||||||
|
if (tooltip) {
|
||||||
|
// Find trigger
|
||||||
|
let trigger = tooltip.previousElementSibling;
|
||||||
|
if (trigger) {
|
||||||
|
if (trigger.id) {
|
||||||
|
// Already has id
|
||||||
|
this.id = trigger.id;
|
||||||
|
} else {
|
||||||
|
trigger.id = this.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
template: `
|
||||||
|
<slot>
|
||||||
|
<wa-icon-button :id="id" name="circle-question" variant="regular"></wa-icon-button>
|
||||||
|
</slot>
|
||||||
|
<wa-tooltip :for="id" ref="tooltip"><slot name="content"></slot></wa-tooltip>
|
||||||
|
`,
|
||||||
|
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>
|
||||||
27
docs/docs/palettes/data.json.njk
Normal file
27
docs/docs/palettes/data.json.njk
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
layout: null
|
||||||
|
permalink: "/docs/palettes/data.json"
|
||||||
|
eleventyExcludeFromCollections: true
|
||||||
|
---
|
||||||
|
{
|
||||||
|
{% for palette in collections.palettes %}
|
||||||
|
{% set paletteId = palette.fileSlug -%}
|
||||||
|
{% set colors = palettes[paletteId] -%}
|
||||||
|
"{{ paletteId }}": {
|
||||||
|
"title": "{{ palette.data.title }}",
|
||||||
|
"colors": {
|
||||||
|
{% for hue, tints in colors -%}
|
||||||
|
"{{ hue }}": {
|
||||||
|
{% for tint, value in tints -%}
|
||||||
|
{% if tint != "05" -%}
|
||||||
|
{% set value = value.coords or value -%}
|
||||||
|
{% set key = "05" if tint == "5" else tint -%}
|
||||||
|
"{{ key }}": {{ value | dump | safe }}{{ ', ' if not loop.last }}
|
||||||
|
{%- endif %}
|
||||||
|
{% endfor -%}
|
||||||
|
}{{ ', ' if not loop.last }}
|
||||||
|
{% endfor %} {# end of hue #}
|
||||||
|
}
|
||||||
|
}{{ ', ' if not loop.last }} {# end of palette #}
|
||||||
|
{% endfor %}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: Default
|
title: Default
|
||||||
description: This is the palette used in the default theme.
|
description: This is the palette used in the default theme.
|
||||||
|
order: 0
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ layout: overview
|
|||||||
override:tags: []
|
override:tags: []
|
||||||
forTag: palette
|
forTag: palette
|
||||||
categories:
|
categories:
|
||||||
|
tags: [other, pro]
|
||||||
other: Free
|
other: Free
|
||||||
pro: Pro
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"layout": "palette.njk",
|
"layout": "palette.njk",
|
||||||
"tags": ["palettes", "palette"],
|
"tags": ["palettes", "palette"],
|
||||||
|
"wide": true,
|
||||||
"eleventyComputed": {
|
"eleventyComputed": {
|
||||||
"snippet": ".wa-palette-{{ page.fileSlug }}",
|
"snippet": ".wa-palette-{{ page.fileSlug }}",
|
||||||
"icon": "palette",
|
"icon": "palette",
|
||||||
|
|||||||
@@ -2,7 +2,5 @@
|
|||||||
title: Patterns
|
title: Patterns
|
||||||
description: Patterns are reusable solutions to common design problems.
|
description: Patterns are reusable solutions to common design problems.
|
||||||
layout: overview
|
layout: overview
|
||||||
categories: ["e-commerce"]
|
|
||||||
listChildren: true
|
|
||||||
override:tags: []
|
override:tags: []
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -14,11 +14,20 @@ During the alpha period, things might break! We take breaking changes very serio
|
|||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
|
- Fixed `wa-pill` class for text fields
|
||||||
|
- Fixed `pill` style for `<wa-input>` elements
|
||||||
|
- Fixed a bug in `<wa-color-picker>` that prevented light dismiss from working when clicking immediately above the color picker dropdown
|
||||||
|
- Fixed a bug in `<wa-select multiple>` that sometimes resulted in empty `<div>` elements being output
|
||||||
|
|
||||||
|
## 3.0.0-alpha.11
|
||||||
|
|
||||||
### Color Palettes
|
### Color Palettes
|
||||||
|
|
||||||
|
- Color palette tweaking UI. Tweak hue, grays, overall colorfulness, save or share the results.
|
||||||
- Added a `pink` scale to all color palettes
|
- Added a `pink` scale to all color palettes
|
||||||
- Fixed an incorrect CSS value in `<wa-select>`'s expand icon
|
|
||||||
- Tweaked hues of all color palettes to make them more distinct and make their hues more intentional
|
- Tweaked hues of all color palettes to make them more distinct and make their hues more intentional
|
||||||
|
- Dropped `violet` and `teal`, instead using `purple` and `cyan` (this is not just a renaming, the colors have been adjusted too).
|
||||||
|
- Fixed a bug in `<wa-switch>` that caused tooltips to work incorrectly when toggling the switch
|
||||||
|
|
||||||
### Design Tokens
|
### Design Tokens
|
||||||
|
|
||||||
@@ -27,30 +36,54 @@ You can find them in the first column of each color palette.
|
|||||||
|
|
||||||
### Themes
|
### Themes
|
||||||
|
|
||||||
- You can now override the brand color of any theme with any of the 9 hues supported.
|
- Improved UI for theme remixing:
|
||||||
- Improved UI for theme remixing, with previews and generated copyable code snippets
|
- You can now override the brand color of any theme with any of the 9 hues supported.
|
||||||
|
- Rich previews
|
||||||
|
- Generated copyable code snippets.
|
||||||
|
- Permalinks
|
||||||
|
- Updated Active, Glossy, Playful, and Premium themes so that `--wa-color-brand-fill-loud` uses the core color of the chosen brand color, regardless of tint.
|
||||||
|
|
||||||
### Components
|
### Components
|
||||||
|
|
||||||
- Various `<wa-radio>` improvements:
|
#### `<wa-radio>`
|
||||||
- Dropped the `base` part. It can now be styled by directly applying CSS to the element itself.
|
|
||||||
- Added `hint` attribute and corresponding slot.
|
|
||||||
- Various `<wa-select>` improvements:
|
|
||||||
- Added the `tag` part (and associated exported parts) to `<wa-select>` to allow targeting the tag that shows when more than the max number of visible items have been selected
|
|
||||||
- Fixed a bug that prevented the placeholder color from being customized with the `--wa-form-control-placeholder-color` token
|
|
||||||
- Dropped the `base` part from `<wa-option>` for easier styling. CSS can now be applied directly to the element itself.
|
|
||||||
- Various `<wa-card>` improvements:
|
|
||||||
- Fixed a bug where child elements did not have correct rounding when headers and footers were absent.
|
|
||||||
- Re-introduced `--border-color` so that the card itself can have a different border color than its inner borders.
|
|
||||||
- Fixed a bug that prevented slots from showing automatically without `with-` attributes
|
|
||||||
|
|
||||||
|
- Dropped the `base` part. It can now be styled by directly applying CSS to the element itself.
|
||||||
|
- Added `hint` attribute and corresponding slot.
|
||||||
|
|
||||||
|
#### `<wa-select>`
|
||||||
|
|
||||||
|
- Added the `tag` part (and associated exported parts) to `<wa-select>` to allow targeting the tag that shows when more than the max number of visible items have been selected
|
||||||
|
- Fixed a bug that prevented the placeholder color from being customized with the `--wa-form-control-placeholder-color` token
|
||||||
|
- Fixed an incorrect CSS value in the expand icon
|
||||||
|
- Fixed a bug that prevented the description from being read by screen readers
|
||||||
|
|
||||||
|
#### `<wa-option>`
|
||||||
|
|
||||||
|
- `label` attribute to override the generated label (useful for rich content)
|
||||||
|
- `defaultLabel` property
|
||||||
|
- Dropped `getTextLabel()` method (if you need dynamic labels, just set the `label` attribute dynamically)
|
||||||
|
- Dropped `base` part for easier styling. CSS can now be applied directly to the element itself.
|
||||||
|
|
||||||
|
#### `<wa-menu-item>`
|
||||||
|
|
||||||
|
- `label` attribute to override the generated label (useful for rich content)
|
||||||
|
- `defaultLabel` property
|
||||||
|
- Dropped `getTextLabel()` method (if you need dynamic labels, just set the `label` attribute dynamically)
|
||||||
|
|
||||||
|
#### `<wa-card>`
|
||||||
|
- Fixed a bug where child elements did not have correct rounding when headers and footers were absent.
|
||||||
|
- Re-introduced `--border-color` so that the card itself can have a different border color than its inner borders.
|
||||||
|
- Fixed a bug that prevented slots from showing automatically without `with-` attributes
|
||||||
|
|
||||||
|
#### `<wa-tab>`
|
||||||
|
|
||||||
|
- Fixed a bug that caused `document.createElement('wa-tab')` to fail (which also meant it could not be used in VueJS and other frameworks)
|
||||||
|
|
||||||
### Docs
|
### Docs
|
||||||
|
|
||||||
- Added an orientation example to the native radio docs
|
- Added an orientation example to the native radio docs
|
||||||
- Fixed a number of broken event listeners throughout the docs
|
- Fixed a number of broken event listeners throughout the docs
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-alpha.10
|
## 3.0.0-alpha.10
|
||||||
|
|
||||||
- 🚨 BREAKING: updated all components to use native events instead of `wa-` prefixed events. This will allow components to work more like native elements in your code, frameworks, third-party plugins, etc. To update your code, simply remove the prefix from your event listeners for the following events.
|
- 🚨 BREAKING: updated all components to use native events instead of `wa-` prefixed events. This will allow components to work more like native elements in your code, frameworks, third-party plugins, etc. To update your code, simply remove the prefix from your event listeners for the following events.
|
||||||
|
|||||||
3
docs/docs/themes/creating.md
vendored
3
docs/docs/themes/creating.md
vendored
@@ -31,8 +31,7 @@ If you're customizing the default dark styles, scope your styles to the followin
|
|||||||
|
|
||||||
```css
|
```css
|
||||||
.wa-dark,
|
.wa-dark,
|
||||||
.wa-invert,
|
.wa-invert {
|
||||||
:is(:host-context(.wa-dark)) {
|
|
||||||
/* your custom styles here */
|
/* your custom styles here */
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
52
docs/docs/themes/demo.njk
vendored
52
docs/docs/themes/demo.njk
vendored
@@ -10,15 +10,17 @@ override:tags: []
|
|||||||
eleventyComputed:
|
eleventyComputed:
|
||||||
forceTheme: "{{ theme.fileSlug }}"
|
forceTheme: "{{ theme.fileSlug }}"
|
||||||
---
|
---
|
||||||
|
{% set isPro = theme.data.isPro %}
|
||||||
|
{% set status = theme.data.status %}
|
||||||
|
{% set since = theme.data.since %}
|
||||||
<link rel="stylesheet" href="/docs/themes/showcase.css" />
|
<link rel="stylesheet" href="/docs/themes/showcase.css" />
|
||||||
|
|
||||||
{% set content %}
|
{% set content %}
|
||||||
<header>
|
<header>
|
||||||
{% include 'breadcrumbs.njk' %}
|
{% include 'breadcrumbs.njk' %}
|
||||||
<h1 class="title">{{ theme.data.title }}</h1>
|
<h1 class="title">{{ theme.data.title }}</h1>
|
||||||
<p id="mix_and_match" hidden class="wa-size-s"></p>
|
<p id="mix_and_match" class="wa-size-s"></p>
|
||||||
<p>{% include 'status.njk' %}</p>
|
<p id="theme-status">{% include 'status.njk' %}</p>
|
||||||
<p id="theme-showcase-description">{{ theme.data.description | inlineMarkdown | safe }}</p>
|
<p id="theme-showcase-description">{{ theme.data.description | inlineMarkdown | safe }}</p>
|
||||||
</header>
|
</header>
|
||||||
{% include 'theme-showcase.njk' %}
|
{% include 'theme-showcase.njk' %}
|
||||||
@@ -34,30 +36,19 @@ eleventyComputed:
|
|||||||
</wa-image-comparer>
|
</wa-image-comparer>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { getCode, urls as stylesheetURLs } from "/assets/scripts/remix.js";
|
import { urls as stylesheetURLs, docsURLs, icons } from "/assets/scripts/tweak/data.js";
|
||||||
|
import { getThemeCode } from "/assets/scripts/tweak/code.js";
|
||||||
|
|
||||||
function updateTheme() {
|
function updateTheme() {
|
||||||
let params = new URLSearchParams(window.location.search);
|
let params = new URLSearchParams(window.location.search);
|
||||||
params = Object.fromEntries(params.entries());
|
params = Object.fromEntries(params.entries());
|
||||||
|
|
||||||
const docsURLs = {
|
|
||||||
colors: '/docs/themes/',
|
|
||||||
palette: '/docs/palettes/',
|
|
||||||
typography: '/docs/themes/'
|
|
||||||
};
|
|
||||||
const icons = {
|
|
||||||
colors: 'palette',
|
|
||||||
palette: 'swatchbook',
|
|
||||||
brand: 'droplet',
|
|
||||||
typography: 'font-case'
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let link of document.querySelectorAll('link.mix-and-match')) {
|
for (let link of document.querySelectorAll('link.mix-and-match')) {
|
||||||
link.remove();
|
link.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
let msgs = [];
|
let tweaks = [];
|
||||||
let code = getCode("{{ theme.fileSlug }}", params, {attributes: 'class="mix-and-match"'});
|
let code = getThemeCode("{{ theme.fileSlug }}", params, {attributes: 'class="mix-and-match"'});
|
||||||
document.head.insertAdjacentHTML('beforeend', code);
|
document.head.insertAdjacentHTML('beforeend', code);
|
||||||
|
|
||||||
for (let name in stylesheetURLs) {
|
for (let name in stylesheetURLs) {
|
||||||
@@ -71,18 +62,29 @@ function updateTheme() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let icon = icons[name];
|
let icon = icons[name];
|
||||||
msgs.push(`<wa-icon name="${icon}" variant="regular"></wa-icon> ${ title }`);
|
tweaks.push(`<wa-icon name="${icon}" variant="regular"></wa-icon> ${ title }`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let p of mix_and_match) {
|
let isRemixed = tweaks.length > 0;
|
||||||
p.hidden = msgs.length === 0;
|
document.documentElement.classList.toggle('is-remixed', isRemixed);
|
||||||
if (msgs.length) {
|
|
||||||
let icon =
|
if (isRemixed) {
|
||||||
p.innerHTML = `<strong><wa-icon name="arrows-rotate"></wa-icon> Remixed</strong> ` + msgs.map(msg => `<wa-badge appearance=outlined>
|
for (let p of document.querySelectorAll("#theme-status")) {
|
||||||
${ msg }</wa-badge>`).join(' ');
|
let proBadge = p.querySelector(".pro");
|
||||||
|
if (!proBadge) {
|
||||||
|
p.insertAdjacentHTML('beforeend', '<wa-badge class="pro">PRO</wa-badge>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let p of mix_and_match) {
|
||||||
|
if (tweaks.length) {
|
||||||
|
p.innerHTML = `<strong><wa-icon name="arrows-rotate"></wa-icon> Remixed</strong> ` + tweaks.map(msg => `<wa-badge appearance=outlined>
|
||||||
|
${ msg }</wa-badge>`).join(' ');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
updateTheme();
|
updateTheme();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
9
docs/docs/themes/index.njk
vendored
9
docs/docs/themes/index.njk
vendored
@@ -1,13 +1,13 @@
|
|||||||
---
|
---
|
||||||
title: Themes
|
title: Themes
|
||||||
description: Themes are collections of design tokens that thread through every Web Awesome component and pattern.
|
description: Themes are collections of design tokens that thread through every Web Awesome component and pattern.
|
||||||
Themes play a crucial role in [customizing Web Awesome](/docs/customizing).
|
Themes play a crucial role in [customizing Web Awesome](/docs/customizing).
|
||||||
layout: overview
|
layout: overview
|
||||||
override:tags: []
|
override:tags: []
|
||||||
forTag: theme
|
forTag: theme
|
||||||
categories:
|
categories:
|
||||||
|
tags: [other, pro]
|
||||||
other: Free
|
other: Free
|
||||||
pro: Pro
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="max-line-length">
|
<div class="max-line-length">
|
||||||
@@ -30,7 +30,7 @@ In pre-made themes, we use a light color scheme by default.
|
|||||||
Additionally, styles may be scoped to the `:root` selector to be activated automatically.
|
Additionally, styles may be scoped to the `:root` selector to be activated automatically.
|
||||||
For pre-made themes, *all* custom properties are scoped to `:root`, the theme class, and `wa-light`.
|
For pre-made themes, *all* custom properties are scoped to `:root`, the theme class, and `wa-light`.
|
||||||
|
|
||||||
Finally, we scope themes to `:host` and `:host-context()` to ensure the styles are applied to the shadow roots of custom elements.
|
Finally, we scope themes to `:host` to ensure the styles are applied to the shadow roots of custom elements.
|
||||||
|
|
||||||
For example, the default theme is set up like this:
|
For example, the default theme is set up like this:
|
||||||
|
|
||||||
@@ -44,8 +44,7 @@ For example, the default theme is set up like this:
|
|||||||
}
|
}
|
||||||
|
|
||||||
.wa-dark,
|
.wa-dark,
|
||||||
.wa-invert,
|
.wa-invert {
|
||||||
:host-context(.wa-dark) {
|
|
||||||
/* subset of CSS custom properties for a dark color scheme */
|
/* subset of CSS custom properties for a dark color scheme */
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
25
docs/docs/themes/remix.css
vendored
25
docs/docs/themes/remix.css
vendored
@@ -94,31 +94,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected-swatch,
|
|
||||||
wa-select[name='brand'] wa-option::before {
|
|
||||||
content: '';
|
|
||||||
display: inline-block;
|
|
||||||
width: 1.2em;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
flex: none;
|
|
||||||
border-radius: var(--wa-border-radius-m);
|
|
||||||
background: var(--color);
|
|
||||||
border: 1px solid var(--wa-color-surface-default);
|
|
||||||
}
|
|
||||||
|
|
||||||
wa-select[name='brand'] wa-option {
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
width: 1em;
|
|
||||||
margin-inline: var(--wa-space-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::part(checked-icon) {
|
|
||||||
order: 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#test_select wa-option:state(selected) {
|
#test_select wa-option:state(selected) {
|
||||||
|
|||||||
55
docs/docs/themes/remix.js
vendored
55
docs/docs/themes/remix.js
vendored
@@ -1,12 +1,6 @@
|
|||||||
import { getCode } from '/assets/scripts/remix.js';
|
import Prism from '/assets/scripts/prism.js';
|
||||||
|
import { cdnUrl, getThemeCode, Permalink } from '/assets/scripts/tweak.js';
|
||||||
await Promise.all(['wa-select', 'wa-option', 'wa-details'].map(tag => customElements.whenDefined(tag)));
|
await Promise.all(['wa-select', 'wa-option', 'wa-details'].map(tag => customElements.whenDefined(tag)));
|
||||||
globalThis.Prism = globalThis.Prism || {};
|
|
||||||
globalThis.Prism.manual = true;
|
|
||||||
await import('/assets/scripts/prism.js');
|
|
||||||
Prism.plugins.customClass.prefix('code-');
|
|
||||||
|
|
||||||
const cdnUrl = document.documentElement.dataset.cdnUrl;
|
|
||||||
|
|
||||||
const domChange = document.startViewTransition ? document.startViewTransition.bind(document) : fn => fn();
|
const domChange = document.startViewTransition ? document.startViewTransition.bind(document) : fn => fn();
|
||||||
|
|
||||||
@@ -57,15 +51,13 @@ function init() {
|
|||||||
typography: '',
|
typography: '',
|
||||||
},
|
},
|
||||||
params: { colors: '', palette: '', brand: '', typography: '' },
|
params: { colors: '', palette: '', brand: '', typography: '' },
|
||||||
urlParams: new URLSearchParams(location.search),
|
urlParams: new Permalink(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Read URL params and apply them. This facilitates permalinks.
|
// Apply params from permalink
|
||||||
if (location.search) {
|
for (let key in data.params) {
|
||||||
for (let aspect in data.params) {
|
if (data.urlParams.has(key)) {
|
||||||
if (data.urlParams.has(aspect)) {
|
data.params[key] = data.urlParams.get(key);
|
||||||
data.params[aspect] = data.urlParams.get(aspect);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +111,10 @@ function setDefault(select, value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function render(changedAspect) {
|
function render(changedAspect) {
|
||||||
|
if (!globalThis.demo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let url = new URL(demo.src);
|
let url = new URL(demo.src);
|
||||||
|
|
||||||
if (!changedAspect || changedAspect === 'colors') {
|
if (!changedAspect || changedAspect === 'colors') {
|
||||||
@@ -129,20 +125,23 @@ function render(changedAspect) {
|
|||||||
|
|
||||||
let brand = data.params.brand || data.defaultParams.brand;
|
let brand = data.params.brand || data.defaultParams.brand;
|
||||||
selects.brand.style.setProperty('--color', `var(--wa-color-${brand})`);
|
selects.brand.style.setProperty('--color', `var(--wa-color-${brand})`);
|
||||||
selects.brand.className = `wa-palette-${computed.palette}`;
|
|
||||||
|
// Add current palette class and remove any other palette classes
|
||||||
|
let paletteClass = `wa-palette-${computed.palette}`;
|
||||||
|
selects.brand.className = selects.brand.className.replace(/\bwa-palette-[a-z]+\b/g, paletteClass);
|
||||||
|
selects.brand.classList.add(paletteClass);
|
||||||
|
|
||||||
for (let aspect in data.params) {
|
for (let aspect in data.params) {
|
||||||
let value = data.params[aspect];
|
let value = data.params[aspect];
|
||||||
|
|
||||||
if (value) {
|
|
||||||
data.urlParams.set(aspect, value);
|
|
||||||
} else {
|
|
||||||
data.urlParams.delete(aspect);
|
|
||||||
}
|
|
||||||
|
|
||||||
selects[aspect].value = value;
|
selects[aspect].value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (let key in data.params) {
|
||||||
|
if (data.params[key]) {
|
||||||
|
data.urlParams.set(key, data.params[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update demo URL
|
// Update demo URL
|
||||||
domChange(() => {
|
domChange(() => {
|
||||||
url.search = data.urlParams;
|
url.search = data.urlParams;
|
||||||
@@ -150,18 +149,14 @@ function render(changedAspect) {
|
|||||||
return new Promise(resolve => (demo.onload = resolve));
|
return new Promise(resolve => (demo.onload = resolve));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update page URL. If there’s already a search, replace it.
|
// Update page URL
|
||||||
// We don’t want to clog the user’s history while they iterate
|
data.urlParams.updateLocation();
|
||||||
let historyAction = location.search ? 'replaceState' : 'pushState';
|
|
||||||
history[historyAction](null, '', `?${data.urlParams}`);
|
|
||||||
|
|
||||||
// Update code snippets
|
// Update code snippets
|
||||||
for (let language in codeSnippets) {
|
for (let language in codeSnippets) {
|
||||||
let codeSnippet = codeSnippets[language];
|
let codeSnippet = codeSnippets[language];
|
||||||
let copyButton = codeSnippet.previousElementSibling;
|
let code = getThemeCode(data.baseTheme, data.params, { language, cdnUrl });
|
||||||
let code = getCode(data.baseTheme, data.params, { language, cdnUrl });
|
|
||||||
codeSnippet.textContent = code;
|
codeSnippet.textContent = code;
|
||||||
copyButton.value = code;
|
|
||||||
Prism.highlightElement(codeSnippet);
|
Prism.highlightElement(codeSnippet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
docs/docs/themes/showcase.css
vendored
5
docs/docs/themes/showcase.css
vendored
@@ -12,6 +12,11 @@ body,
|
|||||||
#mix_and_match {
|
#mix_and_match {
|
||||||
font-weight: var(--wa-font-weight-semibold);
|
font-weight: var(--wa-font-weight-semibold);
|
||||||
color: var(--wa-color-text-quiet);
|
color: var(--wa-color-text-quiet);
|
||||||
|
margin-block-end: var(--wa-space-xs);
|
||||||
|
|
||||||
|
html:not(.is-remixed) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
wa-icon {
|
wa-icon {
|
||||||
vertical-align: -0.15em;
|
vertical-align: -0.15em;
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ title: Design Tokens
|
|||||||
description: A theme is a collection of predefined CSS custom properties that control global styles from color to shadows. These custom properties thread through all Web Awesome components for a consistent look and feel.
|
description: A theme is a collection of predefined CSS custom properties that control global styles from color to shadows. These custom properties thread through all Web Awesome components for a consistent look and feel.
|
||||||
layout: overview
|
layout: overview
|
||||||
override:tags: []
|
override:tags: []
|
||||||
|
categories: {tags: true}
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ description: Build better with Web Awesome, the open source library of web compo
|
|||||||
layout: page
|
layout: page
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.title,
|
.title,
|
||||||
.anchor-heading a,
|
.anchor-heading a,
|
||||||
@@ -387,4 +385,4 @@ layout: page
|
|||||||
© Fonticons, Inc.
|
© Fonticons, Inc.
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@shoelace-style/webawesome",
|
"name": "@shoelace-style/webawesome",
|
||||||
"version": "3.0.0-alpha.10",
|
"version": "3.0.0-alpha.11",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@shoelace-style/webawesome",
|
"name": "@shoelace-style/webawesome",
|
||||||
"version": "3.0.0-alpha.10",
|
"version": "3.0.0-alpha.11",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ctrl/tinycolor": "^4.1.0",
|
"@ctrl/tinycolor": "^4.1.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@shoelace-style/webawesome",
|
"name": "@shoelace-style/webawesome",
|
||||||
"description": "A forward-thinking library of web components.",
|
"description": "A forward-thinking library of web components.",
|
||||||
"version": "3.0.0-alpha.10",
|
"version": "3.0.0-alpha.11",
|
||||||
"homepage": "https://webawesome.com/",
|
"homepage": "https://webawesome.com/",
|
||||||
"author": "Web Awesome",
|
"author": "Web Awesome",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { execSync } from 'child_process';
|
|||||||
import { deleteAsync } from 'del';
|
import { deleteAsync } from 'del';
|
||||||
import esbuild from 'esbuild';
|
import esbuild from 'esbuild';
|
||||||
import { replace } from 'esbuild-plugin-replace';
|
import { replace } from 'esbuild-plugin-replace';
|
||||||
|
|
||||||
import { mkdir, readFile } from 'fs/promises';
|
import { mkdir, readFile } from 'fs/promises';
|
||||||
import getPort, { portNumbers } from 'get-port';
|
import getPort, { portNumbers } from 'get-port';
|
||||||
import { globby } from 'globby';
|
import { globby } from 'globby';
|
||||||
@@ -266,6 +267,13 @@ async function regenerateBundle() {
|
|||||||
* Generates the documentation site.
|
* Generates the documentation site.
|
||||||
*/
|
*/
|
||||||
async function generateDocs() {
|
async function generateDocs() {
|
||||||
|
/**
|
||||||
|
* Used by the webawesome-app to skip doc generation since it will do its own.
|
||||||
|
*/
|
||||||
|
if (process.env.SKIP_ELEVENTY === 'true') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
spinner.start('Writing the docs');
|
spinner.start('Writing the docs');
|
||||||
|
|
||||||
const args = [];
|
const args = [];
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import styles from './button.css';
|
|||||||
@customElement('wa-button')
|
@customElement('wa-button')
|
||||||
export default class WaButton extends WebAwesomeFormAssociatedElement {
|
export default class WaButton extends WebAwesomeFormAssociatedElement {
|
||||||
static shadowStyle = [variantStyles, appearanceStyles, sizeStyles, nativeStyles, styles];
|
static shadowStyle = [variantStyles, appearanceStyles, sizeStyles, nativeStyles, styles];
|
||||||
|
static rectProxy = 'button';
|
||||||
|
|
||||||
static get validators() {
|
static get validators() {
|
||||||
return [...super.validators, MirrorValidator()];
|
return [...super.validators, MirrorValidator()];
|
||||||
@@ -108,7 +109,7 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
|
|||||||
@property({ reflect: true }) value: string | null = null;
|
@property({ reflect: true }) value: string | null = null;
|
||||||
|
|
||||||
/** When set, the underlying button will be rendered as an `<a>` with this `href` instead of a `<button>`. */
|
/** When set, the underlying button will be rendered as an `<a>` with this `href` instead of a `<button>`. */
|
||||||
@property() href = '';
|
@property({ reflect: true }) href = null;
|
||||||
|
|
||||||
/** Tells the browser where to open the link. Only used when `href` is present. */
|
/** Tells the browser where to open the link. Only used when `href` is present. */
|
||||||
@property() target: '_blank' | '_parent' | '_self' | '_top';
|
@property() target: '_blank' | '_parent' | '_self' | '_top';
|
||||||
@@ -224,17 +225,6 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
|
|||||||
this.button.blur();
|
this.button.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
getBoundingClientRect(): DOMRect {
|
|
||||||
let rect = super.getBoundingClientRect();
|
|
||||||
let buttonRect = this.button.getBoundingClientRect();
|
|
||||||
|
|
||||||
if (rect.width === 0 && buttonRect.width > 0) {
|
|
||||||
return buttonRect;
|
|
||||||
}
|
|
||||||
|
|
||||||
return rect;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const isLink = this.isLink();
|
const isLink = this.isLink();
|
||||||
const tag = isLink ? literal`a` : literal`button`;
|
const tag = isLink ? literal`a` : literal`button`;
|
||||||
|
|||||||
@@ -278,11 +278,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Color dropdown
|
* Color dropdown
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.color-dropdown {
|
.color-dropdown {
|
||||||
display: flex;
|
display: contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-dropdown::part(panel) {
|
.color-dropdown::part(panel) {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
:host {
|
:host {
|
||||||
--background-color-hover: var(--wa-color-neutral-fill-quiet);
|
--background-color-hover: var(--wa-color-neutral-fill-quiet);
|
||||||
|
--text-color-hover: color-mix(in oklab, currentColor, var(--wa-color-mix-hover));
|
||||||
|
--background-color-active: transparent;
|
||||||
|
--text-color-active: color-mix(in oklab, currentColor, var(--wa-color-mix-active));
|
||||||
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: var(--wa-color-text-quiet);
|
color: var(--wa-color-text-quiet);
|
||||||
@@ -22,12 +25,13 @@
|
|||||||
|
|
||||||
:host(:not([disabled])) .icon-button:hover,
|
:host(:not([disabled])) .icon-button:hover,
|
||||||
:host(:not([disabled])) .icon-button:focus-visible {
|
:host(:not([disabled])) .icon-button:focus-visible {
|
||||||
background-color: var(--wa-color-neutral-fill-quiet);
|
background-color: var(--background-color-hover);
|
||||||
color: color-mix(in oklab, currentColor, var(--wa-color-mix-hover));
|
color: var(--text-color-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
:host(:not([disabled])) .icon-button:active {
|
:host(:not([disabled])) .icon-button:active {
|
||||||
color: color-mix(in oklab, currentColor, var(--wa-color-mix-active));
|
background-color: var(--background-color-active);
|
||||||
|
color: var(--text-color-active);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-button:focus {
|
.icon-button:focus {
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ import styles from './icon-button.css';
|
|||||||
* @event blur - Emitted when the icon button loses focus.
|
* @event blur - Emitted when the icon button loses focus.
|
||||||
* @event focus - Emitted when the icon button gains focus.
|
* @event focus - Emitted when the icon button gains focus.
|
||||||
*
|
*
|
||||||
* @cssproperty --background-color-hover - The color of the button's background on hover.
|
* @cssproperty [--background-color-hover=var(--wa-color-neutral-fill-quiet)] - The color of the button's background on hover.
|
||||||
|
* @cssproperty [--background-color-active=var(--wa-color-neutral-fill-quiet)] - The color of the button's background on `:active`.
|
||||||
|
* @cssproperty --text-color-hover - The color of the button's background on hover.
|
||||||
|
* @cssproperty --text-color-active - The color of the button's background on `:active`.
|
||||||
*
|
*
|
||||||
* @csspart base - The component's base wrapper.
|
* @csspart base - The component's base wrapper.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
--banner-height: 0px;
|
--banner-height: 0px;
|
||||||
--header-height: 0px;
|
--header-height: 0px;
|
||||||
--subheader-height: 0px;
|
--subheader-height: 0px;
|
||||||
--scroll-margin-top: calc(var(--header-height, 0px) + var(--subheader-height, 0px));
|
--scroll-margin-top: calc(var(--header-height, 0px) + var(--subheader-height, 0px) + 0.5em);
|
||||||
}
|
}
|
||||||
|
|
||||||
slot[name]:not([name='skip-to-content'], [name='navigation-toggle'])::slotted(*) {
|
slot[name]:not([name='skip-to-content'], [name='navigation-toggle'])::slotted(*) {
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import styles from './radio-button.css';
|
|||||||
@customElement('wa-radio-button')
|
@customElement('wa-radio-button')
|
||||||
export default class WaRadioButton extends WebAwesomeFormAssociatedElement {
|
export default class WaRadioButton extends WebAwesomeFormAssociatedElement {
|
||||||
static shadowStyle = [variantStyles, appearanceStyles, sizeStyles, nativeStyles, buttonStyles, styles];
|
static shadowStyle = [variantStyles, appearanceStyles, sizeStyles, nativeStyles, buttonStyles, styles];
|
||||||
|
static rectProxy = 'input';
|
||||||
|
|
||||||
private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix');
|
private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix');
|
||||||
|
|
||||||
|
|||||||
@@ -194,7 +194,7 @@
|
|||||||
background: var(--wa-color-surface-raised);
|
background: var(--wa-color-surface-raised);
|
||||||
border-color: var(--wa-color-surface-border);
|
border-color: var(--wa-color-surface-border);
|
||||||
border-radius: var(--wa-border-radius-m);
|
border-radius: var(--wa-border-radius-m);
|
||||||
border-style: var(--border-style);
|
border-style: var(--wa-border-style);
|
||||||
border-width: var(--border-width);
|
border-width: var(--border-width);
|
||||||
padding-block: var(--wa-space-xs);
|
padding-block: var(--wa-space-xs);
|
||||||
padding-inline: 0;
|
padding-inline: 0;
|
||||||
@@ -208,13 +208,13 @@
|
|||||||
&::slotted(wa-divider) {
|
&::slotted(wa-divider) {
|
||||||
--spacing: var(--wa-space-xs);
|
--spacing: var(--wa-space-xs);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
&::slotted(small) {
|
|
||||||
display: block;
|
slot:not([name])::slotted(small) {
|
||||||
font-size: var(--wa-font-size-s);
|
display: block;
|
||||||
font-weight: var(--wa-font-weight-semibold);
|
font-size: var(--wa-font-size-s);
|
||||||
color: var(--wa-color-text-quiet);
|
font-weight: var(--wa-font-weight-semibold);
|
||||||
padding-block: var(--wa-space-xs);
|
color: var(--wa-color-text-quiet);
|
||||||
padding-inline: var(--wa-space-xl);
|
padding-block: var(--wa-space-xs);
|
||||||
}
|
padding-inline: var(--wa-space-xl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -647,6 +647,7 @@ describe('<wa-select>', () => {
|
|||||||
);
|
);
|
||||||
const el = form.querySelector<WaSelect>('wa-select')!;
|
const el = form.querySelector<WaSelect>('wa-select')!;
|
||||||
|
|
||||||
|
expect(el.defaultValue).to.equal('option-1');
|
||||||
expect(el.value).to.equal('');
|
expect(el.value).to.equal('');
|
||||||
expect(new FormData(form).get('select')).equal('');
|
expect(new FormData(form).get('select')).equal('');
|
||||||
|
|
||||||
@@ -657,6 +658,7 @@ describe('<wa-select>', () => {
|
|||||||
|
|
||||||
await aTimeout(10);
|
await aTimeout(10);
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
expect(el.optionValues ? [...el.optionValues] : []).to.have.members(['option-1']);
|
||||||
expect(el.value).to.equal('option-1');
|
expect(el.value).to.equal('option-1');
|
||||||
expect(new FormData(form).get('select')).equal('option-1');
|
expect(new FormData(form).get('select')).equal('option-1');
|
||||||
});
|
});
|
||||||
@@ -745,6 +747,8 @@ describe('<wa-select>', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const el = form.querySelector<WaSelect>('wa-select')!;
|
const el = form.querySelector<WaSelect>('wa-select')!;
|
||||||
|
expect(el.optionValues ? [...el.optionValues] : []).to.have.members(['bar', 'baz']);
|
||||||
|
expect(el.optionValues?.size).to.equal(2);
|
||||||
expect(el.value).to.have.members(['bar', 'baz']);
|
expect(el.value).to.have.members(['bar', 'baz']);
|
||||||
expect(el.value!.length).to.equal(2);
|
expect(el.value!.length).to.equal(2);
|
||||||
expect(new FormData(form).getAll('select')).to.have.members(['bar', 'baz']);
|
expect(new FormData(form).getAll('select')).to.have.members(['bar', 'baz']);
|
||||||
@@ -760,6 +764,36 @@ describe('<wa-select>', () => {
|
|||||||
expect(new FormData(form).getAll('select')).to.have.members(['foo', 'bar', 'baz']);
|
expect(new FormData(form).getAll('select')).to.have.members(['foo', 'bar', 'baz']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('With setting the value via JS', () => {
|
||||||
|
it('Should preserve value even if not returned', async () => {
|
||||||
|
const form = await fixture<HTMLFormElement>(
|
||||||
|
html` <form>
|
||||||
|
<wa-select name="select">
|
||||||
|
<wa-option value="bar">Bar</wa-option>
|
||||||
|
<wa-option value="baz">Baz</wa-option>
|
||||||
|
</wa-select>
|
||||||
|
</form>`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const el = form.querySelector<WaSelect>('wa-select')!;
|
||||||
|
expect(el.value).to.equal('');
|
||||||
|
|
||||||
|
el.value = 'foo';
|
||||||
|
await aTimeout(10);
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.value).to.equal('');
|
||||||
|
|
||||||
|
const option = document.createElement('wa-option');
|
||||||
|
option.value = 'foo';
|
||||||
|
option.innerText = 'Foo';
|
||||||
|
el.append(option);
|
||||||
|
|
||||||
|
await aTimeout(10);
|
||||||
|
await el.updateComplete;
|
||||||
|
expect(el.value).to.equal('foo');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
|||||||
@state() displayLabel = '';
|
@state() displayLabel = '';
|
||||||
@state() currentOption: WaOption;
|
@state() currentOption: WaOption;
|
||||||
@state() selectedOptions: WaOption[] = [];
|
@state() selectedOptions: WaOption[] = [];
|
||||||
|
@state() optionValues: Set<string> | undefined;
|
||||||
|
|
||||||
/** The name of the select, submitted as a name/value pair with form data. */
|
/** The name of the select, submitted as a name/value pair with form data. */
|
||||||
@property() name = '';
|
@property() name = '';
|
||||||
@@ -158,7 +159,47 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
|||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
@property({ attribute: false }) value: string | string[] | null = null;
|
private _value: string[] | undefined;
|
||||||
|
@property({ attribute: false })
|
||||||
|
set value(val: string | string[]) {
|
||||||
|
let oldValue = this.value;
|
||||||
|
|
||||||
|
if (!Array.isArray(val)) {
|
||||||
|
val = val.split(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._value || this._value.join(' ') !== val.join(' ')) {
|
||||||
|
this._value = val;
|
||||||
|
let newValue = this.value;
|
||||||
|
|
||||||
|
if (newValue != oldValue) {
|
||||||
|
this.requestUpdate('value', oldValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get value() {
|
||||||
|
let value = this._value ?? this.defaultValue;
|
||||||
|
value = Array.isArray(value) ? value : [value];
|
||||||
|
let optionsChanged = !this.optionValues;
|
||||||
|
|
||||||
|
if (optionsChanged) {
|
||||||
|
this.optionValues = new Set(
|
||||||
|
this.getAllOptions()
|
||||||
|
.filter(option => !option.disabled)
|
||||||
|
.map(option => option.value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop values not in the DOM
|
||||||
|
let ret: string | string[] = value.filter(v => this.optionValues!.has(v));
|
||||||
|
ret = this.multiple ? ret : (ret[0] ?? '');
|
||||||
|
|
||||||
|
if (optionsChanged) {
|
||||||
|
this.requestUpdate('value');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
/** The select's size. */
|
/** The select's size. */
|
||||||
@property({ reflect: true, initial: 'medium' }) size: 'small' | 'medium' | 'large' | 'inherit' = 'inherit';
|
@property({ reflect: true, initial: 'medium' }) size: 'small' | 'medium' | 'large' | 'inherit' = 'inherit';
|
||||||
@@ -250,7 +291,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
|||||||
?pill=${this.pill}
|
?pill=${this.pill}
|
||||||
size=${this.size}
|
size=${this.size}
|
||||||
removable
|
removable
|
||||||
@wa-remove=${(event: WaRemoveEvent) => this.handleTagRemove(event, option)}
|
|
||||||
>
|
>
|
||||||
${option.label}
|
${option.label}
|
||||||
</wa-tag>
|
</wa-tag>
|
||||||
@@ -538,21 +578,41 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allOptions = this.getAllOptions();
|
const allOptions = this.getAllOptions();
|
||||||
const val = this.valueHasChanged ? this.value : this.defaultValue;
|
this.optionValues = undefined; // dirty the value so it gets recalculated
|
||||||
const value = Array.isArray(val) ? val : [val];
|
|
||||||
const values: string[] = [];
|
|
||||||
|
|
||||||
// Check for duplicate values in menu items
|
const value = this.value;
|
||||||
allOptions.forEach(option => values.push(option.value));
|
|
||||||
|
|
||||||
// Select only the options that match the new value
|
// Select only the options that match the new value
|
||||||
this.setSelectedOptions(allOptions.filter(el => value.includes(el.value)));
|
this.setSelectedOptions(allOptions.filter(el => value.includes(el.value)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleTagRemove(event: WaRemoveEvent, option: WaOption) {
|
private handleTagRemove(event: WaRemoveEvent, directOption?: WaOption) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (!this.disabled) {
|
if (this.disabled) return;
|
||||||
|
|
||||||
|
// Use the directly provided option if available (from getTag method)
|
||||||
|
let option = directOption;
|
||||||
|
|
||||||
|
// If no direct option was provided, find the option from the event path
|
||||||
|
if (!option) {
|
||||||
|
const tagElement = (event.target as Element).closest('wa-tag[part~=tag]');
|
||||||
|
|
||||||
|
if (tagElement) {
|
||||||
|
// Find the index of this tag among all tags
|
||||||
|
const tagsContainer = this.shadowRoot?.querySelector('[part="tags"]');
|
||||||
|
if (tagsContainer) {
|
||||||
|
const allTags = Array.from(tagsContainer.children);
|
||||||
|
const index = allTags.indexOf(tagElement as HTMLElement);
|
||||||
|
|
||||||
|
if (index >= 0 && index < this.selectedOptions.length) {
|
||||||
|
option = this.selectedOptions[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option) {
|
||||||
this.toggleOptionSelection(option, false);
|
this.toggleOptionSelection(option, false);
|
||||||
|
|
||||||
// Emit after updating
|
// Emit after updating
|
||||||
@@ -565,6 +625,9 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
|||||||
|
|
||||||
// Gets an array of all `<wa-option>` elements
|
// Gets an array of all `<wa-option>` elements
|
||||||
private getAllOptions() {
|
private getAllOptions() {
|
||||||
|
if (!this?.querySelectorAll) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
return [...this.querySelectorAll<WaOption>('wa-option')];
|
return [...this.querySelectorAll<WaOption>('wa-option')];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -628,11 +691,24 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
|||||||
|
|
||||||
// Update selected options cache
|
// Update selected options cache
|
||||||
this.selectedOptions = options.filter(el => el.selected);
|
this.selectedOptions = options.filter(el => el.selected);
|
||||||
|
let selectedValues = new Set(this.selectedOptions.map(el => el.value));
|
||||||
|
|
||||||
|
// Toggle values present in the DOM from this.value, while preserving options NOT present in the DOM (for lazy loading)
|
||||||
|
// Note that options NOT present in the DOM will be moved to the end after this
|
||||||
|
if (selectedValues.size > 0 || this._value) {
|
||||||
|
if (!this._value) {
|
||||||
|
// First time it's set
|
||||||
|
let value = this.defaultValue ?? [];
|
||||||
|
this._value = Array.isArray(value) ? value : [value];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out values that are in the DOM
|
||||||
|
this._value = this._value.filter(value => !this.optionValues?.has(value));
|
||||||
|
this._value.unshift(...selectedValues);
|
||||||
|
}
|
||||||
|
|
||||||
// Update the value and display label
|
// Update the value and display label
|
||||||
if (this.multiple) {
|
if (this.multiple) {
|
||||||
this.value = this.selectedOptions.map(el => el.value);
|
|
||||||
|
|
||||||
if (this.placeholder && this.value.length === 0) {
|
if (this.placeholder && this.value.length === 0) {
|
||||||
// When no items are selected, keep the value empty so the placeholder shows
|
// When no items are selected, keep the value empty so the placeholder shows
|
||||||
this.displayLabel = '';
|
this.displayLabel = '';
|
||||||
@@ -641,7 +717,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const selectedOption = this.selectedOptions[0];
|
const selectedOption = this.selectedOptions[0];
|
||||||
this.value = selectedOption?.value ?? '';
|
|
||||||
this.displayLabel = selectedOption?.label ?? '';
|
this.displayLabel = selectedOption?.label ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -654,10 +729,8 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
|||||||
return this.selectedOptions.map((option, index) => {
|
return this.selectedOptions.map((option, index) => {
|
||||||
if (index < this.maxOptionsVisible || this.maxOptionsVisible <= 0) {
|
if (index < this.maxOptionsVisible || this.maxOptionsVisible <= 0) {
|
||||||
const tag = this.getTag(option, index);
|
const tag = this.getTag(option, index);
|
||||||
// Wrap so we can handle the remove
|
if (!tag) return null;
|
||||||
return html`<div @wa-remove=${(e: WaRemoveEvent) => this.handleTagRemove(e, option)}>
|
return typeof tag === 'string' ? unsafeHTML(tag) : tag;
|
||||||
${typeof tag === 'string' ? unsafeHTML(tag) : tag}
|
|
||||||
</div>`;
|
|
||||||
} else if (index === this.maxOptionsVisible) {
|
} else if (index === this.maxOptionsVisible) {
|
||||||
// Hit tag limit
|
// Hit tag limit
|
||||||
return html`
|
return html`
|
||||||
@@ -673,7 +746,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
|||||||
>
|
>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
return html``;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -873,7 +946,9 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Tags need to wait for first hydration before populating otherwise it will create a hydration mismatch. -->
|
<!-- Tags need to wait for first hydration before populating otherwise it will create a hydration mismatch. -->
|
||||||
${this.multiple && this.hasUpdated ? html`<div part="tags" class="tags">${this.tags}</div>` : ''}
|
${this.multiple && this.hasUpdated
|
||||||
|
? html`<div part="tags" class="tags" @wa-remove=${this.handleTagRemove}>${this.tags}</div>`
|
||||||
|
: ''}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
class="value-input"
|
class="value-input"
|
||||||
@@ -927,6 +1002,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<slot
|
<slot
|
||||||
|
id="hint"
|
||||||
name="hint"
|
name="hint"
|
||||||
part="hint"
|
part="hint"
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import styles from './switch.css';
|
|||||||
*/
|
*/
|
||||||
@customElement('wa-switch')
|
@customElement('wa-switch')
|
||||||
export default class WaSwitch extends WebAwesomeFormAssociatedElement {
|
export default class WaSwitch extends WebAwesomeFormAssociatedElement {
|
||||||
|
static shadowRootOptions = { ...WebAwesomeFormAssociatedElement.shadowRootOptions, delegatesFocus: true };
|
||||||
static shadowStyle = [formControlStyles, sizeStyles, styles];
|
static shadowStyle = [formControlStyles, sizeStyles, styles];
|
||||||
|
|
||||||
static get validators() {
|
static get validators() {
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ let id = 0;
|
|||||||
@customElement('wa-tab')
|
@customElement('wa-tab')
|
||||||
export default class WaTab extends WebAwesomeElement {
|
export default class WaTab extends WebAwesomeElement {
|
||||||
static shadowStyle = styles;
|
static shadowStyle = styles;
|
||||||
public slot = 'nav'; // Auto-slot into nav slot
|
|
||||||
|
|
||||||
private readonly attrId = ++id;
|
private readonly attrId = ++id;
|
||||||
private readonly componentId = `wa-tab-${this.attrId}`;
|
private readonly componentId = `wa-tab-${this.attrId}`;
|
||||||
@@ -47,6 +46,9 @@ export default class WaTab extends WebAwesomeElement {
|
|||||||
@property({ type: Number, reflect: true }) tabIndex = 0;
|
@property({ type: Number, reflect: true }) tabIndex = 0;
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
|
// Auto-slot into nav slot
|
||||||
|
this.slot ||= 'nav';
|
||||||
|
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.setAttribute('role', 'tab');
|
this.setAttribute('role', 'tab');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,16 @@
|
|||||||
--max-width: 30ch;
|
--max-width: 30ch;
|
||||||
--padding: var(--wa-space-2xs) var(--wa-space-xs);
|
--padding: var(--wa-space-2xs) var(--wa-space-xs);
|
||||||
|
|
||||||
|
/** These styles are added so we don't interfere in the DOM. */
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
/** These styles are added so we dont interfere in the DOM. */
|
/** Defaults for inherited CSS properties */
|
||||||
|
color: var(--wa-tooltip-content-color);
|
||||||
|
font-size: var(--wa-tooltip-font-size);
|
||||||
|
line-height: var(--wa-tooltip-line-height);
|
||||||
|
text-align: start;
|
||||||
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip {
|
.tooltip {
|
||||||
@@ -41,12 +47,6 @@
|
|||||||
max-width: var(--max-width);
|
max-width: var(--max-width);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
font: inherit;
|
|
||||||
color: var(--wa-tooltip-content-color);
|
|
||||||
font-size: var(--wa-tooltip-font-size);
|
|
||||||
line-height: var(--wa-tooltip-line-height);
|
|
||||||
text-align: start;
|
|
||||||
white-space: normal;
|
|
||||||
padding: var(--padding);
|
padding: var(--padding);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
|
|||||||
@@ -35,11 +35,8 @@ import styles from './tooltip.css';
|
|||||||
*
|
*
|
||||||
* @cssproperty --background-color - The tooltip's background color.
|
* @cssproperty --background-color - The tooltip's background color.
|
||||||
* @cssproperty --border-radius - The radius of the tooltip's corners.
|
* @cssproperty --border-radius - The radius of the tooltip's corners.
|
||||||
* @cssproperty --text-color - The color of the tooltip's content.
|
|
||||||
* @cssproperty --max-width - The maximum width of the tooltip before its content will wrap.
|
* @cssproperty --max-width - The maximum width of the tooltip before its content will wrap.
|
||||||
* @cssproperty --padding - The padding within the tooltip.
|
* @cssproperty --padding - The padding within the tooltip.
|
||||||
* @cssproperty --hide-delay - The amount of time to wait before hiding the tooltip when hovering.
|
|
||||||
* @cssproperty --show-delay - The amount of time to wait before showing the tooltip when hovering.
|
|
||||||
*/
|
*/
|
||||||
@customElement('wa-tooltip')
|
@customElement('wa-tooltip')
|
||||||
export default class WaTooltip extends WebAwesomeElement {
|
export default class WaTooltip extends WebAwesomeElement {
|
||||||
|
|||||||
@@ -204,6 +204,32 @@ export default class WebAwesomeElement extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBoundingClientRect(): DOMRect {
|
||||||
|
let rect = super.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (rect.width === 0 && rect.height === 0) {
|
||||||
|
let Self = this.constructor as typeof WebAwesomeElement;
|
||||||
|
|
||||||
|
if (Self.rectProxy) {
|
||||||
|
let element = this[Self.rectProxy as keyof this];
|
||||||
|
if (element instanceof Element) {
|
||||||
|
let childRect = element.getBoundingClientRect();
|
||||||
|
if (childRect.width > 0 || childRect.height > 0) {
|
||||||
|
return childRect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If getBoundingClientRect() returns an empty rect,
|
||||||
|
* should we check another element?
|
||||||
|
*/
|
||||||
|
static rectProxy: undefined | string;
|
||||||
|
|
||||||
static createProperty(name: PropertyKey, options?: PropertyDeclaration): void {
|
static createProperty(name: PropertyKey, options?: PropertyDeclaration): void {
|
||||||
if (options) {
|
if (options) {
|
||||||
if (options.initial !== undefined && options.default === undefined) {
|
if (options.initial !== undefined && options.default === undefined) {
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
:where(:root),
|
|
||||||
:host,
|
|
||||||
:where([class^='wa-theme-'], [class*=' wa-theme-']),
|
|
||||||
:where([class^='wa-palette-'], [class*=' wa-palette-']),
|
|
||||||
:where([class^='wa-brand-'], [class*=' wa-brand-']) {
|
|
||||||
/**
|
|
||||||
* Conditional tokens for use in color-mix()
|
|
||||||
* --wa-color-brand-if-lt-N ➡️ 100% if key < N, 0% otherwise
|
|
||||||
* --wa-color-brand-if-gte-N ➡️ 100% if key >= N, 0% otherwise
|
|
||||||
*/
|
|
||||||
--wa-color-brand-if-lt-40: calc(clamp(0, 40 - var(--wa-color-brand-key), 1) * 100%);
|
|
||||||
--wa-color-brand-if-lt-50: calc(clamp(0, 50 - var(--wa-color-brand-key), 1) * 100%);
|
|
||||||
--wa-color-brand-if-lt-60: calc(clamp(0, 60 - var(--wa-color-brand-key), 1) * 100%);
|
|
||||||
--wa-color-brand-if-lt-70: calc(clamp(0, 70 - var(--wa-color-brand-key), 1) * 100%);
|
|
||||||
--wa-color-brand-if-lt-80: calc(clamp(0, 80 - var(--wa-color-brand-key), 1) * 100%);
|
|
||||||
|
|
||||||
--wa-color-brand-if-gte-40: calc(100% - var(--wa-color-brand-if-lt-40));
|
|
||||||
--wa-color-brand-if-gte-50: calc(100% - var(--wa-color-brand-if-lt-50));
|
|
||||||
--wa-color-brand-if-gte-60: calc(100% - var(--wa-color-brand-if-lt-60));
|
|
||||||
--wa-color-brand-if-gte-70: calc(100% - var(--wa-color-brand-if-lt-70));
|
|
||||||
--wa-color-brand-if-gte-80: calc(100% - var(--wa-color-brand-if-lt-80));
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Convenience tokens for common tint cutoffs
|
|
||||||
* --wa-color-brand-N-max ➡️ var(--color-brand) if key <= N, var(--color-brand-N) otherwise
|
|
||||||
* --wa-color-brand-N-min ➡️ var(--color-brand) if key >= N, var(--color-brand-N) otherwise
|
|
||||||
*/
|
|
||||||
--wa-color-brand-40-max: color-mix(
|
|
||||||
in oklab,
|
|
||||||
var(--wa-color-brand) var(--wa-color-brand-if-lt-40),
|
|
||||||
var(--wa-color-brand-40)
|
|
||||||
);
|
|
||||||
--wa-color-brand-40-min: color-mix(
|
|
||||||
in oklab,
|
|
||||||
var(--wa-color-brand) var(--wa-color-brand-if-gte-40),
|
|
||||||
var(--wa-color-brand-40)
|
|
||||||
);
|
|
||||||
|
|
||||||
--wa-color-brand-50-max: color-mix(
|
|
||||||
in oklab,
|
|
||||||
var(--wa-color-brand) var(--wa-color-brand-if-lt-50),
|
|
||||||
var(--wa-color-brand-50)
|
|
||||||
);
|
|
||||||
--wa-color-brand-50-min: color-mix(
|
|
||||||
in oklab,
|
|
||||||
var(--wa-color-brand) var(--wa-color-brand-if-gte-50),
|
|
||||||
var(--wa-color-brand-50)
|
|
||||||
);
|
|
||||||
|
|
||||||
--wa-color-brand-60-max: color-mix(
|
|
||||||
in oklab,
|
|
||||||
var(--wa-color-brand) var(--wa-color-brand-if-lt-60),
|
|
||||||
var(--wa-color-brand-60)
|
|
||||||
);
|
|
||||||
--wa-color-brand-60-min: color-mix(
|
|
||||||
in oklab,
|
|
||||||
var(--wa-color-brand) var(--wa-color-brand-if-gte-60),
|
|
||||||
var(--wa-color-brand-60)
|
|
||||||
);
|
|
||||||
|
|
||||||
/* Text color: white if key < 70, brand-10 otherwise */
|
|
||||||
--wa-color-brand-on: color-mix(in oklab, var(--wa-color-brand-10) var(--wa-color-brand-if-gte-60), white);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
@import url('base.css');
|
|
||||||
|
|
||||||
:where(:root),
|
:where(:root),
|
||||||
:host,
|
:host,
|
||||||
:where([class^='wa-theme-'], [class*=' wa-theme-']),
|
:where([class^='wa-theme-'], [class*=' wa-theme-']),
|
||||||
@@ -17,5 +15,22 @@
|
|||||||
--wa-color-brand-10: var(--wa-color-blue-10);
|
--wa-color-brand-10: var(--wa-color-blue-10);
|
||||||
--wa-color-brand-05: var(--wa-color-blue-05);
|
--wa-color-brand-05: var(--wa-color-blue-05);
|
||||||
--wa-color-brand: var(--wa-color-blue);
|
--wa-color-brand: var(--wa-color-blue);
|
||||||
--wa-color-brand-key: var(--wa-color-blue-key);
|
|
||||||
|
--wa-color-brand-lt-50: var(--wa-color-blue-lt-50);
|
||||||
|
--wa-color-brand-gte-50: var(--wa-color-blue-gte-50);
|
||||||
|
|
||||||
|
--wa-color-brand-lt-60: var(--wa-color-blue-lt-60);
|
||||||
|
--wa-color-brand-gte-60: var(--wa-color-blue-gte-60);
|
||||||
|
|
||||||
|
--wa-color-brand-lt-70: var(--wa-color-blue-lt-70);
|
||||||
|
--wa-color-brand-gte-70: var(--wa-color-blue-gte-70);
|
||||||
|
|
||||||
|
--wa-color-brand-max-50: var(--wa-color-blue-max-50);
|
||||||
|
--wa-color-brand-min-50: var(--wa-color-blue-min-50);
|
||||||
|
--wa-color-brand-max-60: var(--wa-color-blue-max-60);
|
||||||
|
--wa-color-brand-min-60: var(--wa-color-blue-min-60);
|
||||||
|
--wa-color-brand-max-70: var(--wa-color-blue-max-70);
|
||||||
|
--wa-color-brand-min-70: var(--wa-color-blue-min-70);
|
||||||
|
|
||||||
|
--wa-color-brand-on: var(--wa-color-blue-on);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
@import url('base.css');
|
|
||||||
|
|
||||||
:where(:root),
|
:where(:root),
|
||||||
:host,
|
:host,
|
||||||
:where([class^='wa-theme-'], [class*=' wa-theme-']),
|
:where([class^='wa-theme-'], [class*=' wa-theme-']),
|
||||||
@@ -17,5 +15,22 @@
|
|||||||
--wa-color-brand-10: var(--wa-color-cyan-10);
|
--wa-color-brand-10: var(--wa-color-cyan-10);
|
||||||
--wa-color-brand-05: var(--wa-color-cyan-05);
|
--wa-color-brand-05: var(--wa-color-cyan-05);
|
||||||
--wa-color-brand: var(--wa-color-cyan);
|
--wa-color-brand: var(--wa-color-cyan);
|
||||||
--wa-color-brand-key: var(--wa-color-cyan-key);
|
|
||||||
|
--wa-color-brand-lt-50: var(--wa-color-cyan-lt-50);
|
||||||
|
--wa-color-brand-gte-50: var(--wa-color-cyan-gte-50);
|
||||||
|
|
||||||
|
--wa-color-brand-lt-60: var(--wa-color-cyan-lt-60);
|
||||||
|
--wa-color-brand-gte-60: var(--wa-color-cyan-gte-60);
|
||||||
|
|
||||||
|
--wa-color-brand-lt-70: var(--wa-color-cyan-lt-70);
|
||||||
|
--wa-color-brand-gte-70: var(--wa-color-cyan-gte-70);
|
||||||
|
|
||||||
|
--wa-color-brand-max-50: var(--wa-color-cyan-max-50);
|
||||||
|
--wa-color-brand-min-50: var(--wa-color-cyan-min-50);
|
||||||
|
--wa-color-brand-max-60: var(--wa-color-cyan-max-60);
|
||||||
|
--wa-color-brand-min-60: var(--wa-color-cyan-min-60);
|
||||||
|
--wa-color-brand-max-70: var(--wa-color-cyan-max-70);
|
||||||
|
--wa-color-brand-min-70: var(--wa-color-cyan-min-70);
|
||||||
|
|
||||||
|
--wa-color-brand-on: var(--wa-color-cyan-on);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
@import url('base.css');
|
|
||||||
|
|
||||||
:where(:root),
|
:where(:root),
|
||||||
:host,
|
:host,
|
||||||
:where([class^='wa-theme-'], [class*=' wa-theme-']),
|
:where([class^='wa-theme-'], [class*=' wa-theme-']),
|
||||||
@@ -17,5 +15,22 @@
|
|||||||
--wa-color-brand-10: var(--wa-color-gray-10);
|
--wa-color-brand-10: var(--wa-color-gray-10);
|
||||||
--wa-color-brand-05: var(--wa-color-gray-05);
|
--wa-color-brand-05: var(--wa-color-gray-05);
|
||||||
--wa-color-brand: var(--wa-color-gray);
|
--wa-color-brand: var(--wa-color-gray);
|
||||||
--wa-color-brand-key: var(--wa-color-gray-key);
|
|
||||||
|
--wa-color-brand-lt-50: var(--wa-color-gray-lt-50);
|
||||||
|
--wa-color-brand-gte-50: var(--wa-color-gray-gte-50);
|
||||||
|
|
||||||
|
--wa-color-brand-lt-60: var(--wa-color-gray-lt-60);
|
||||||
|
--wa-color-brand-gte-60: var(--wa-color-gray-gte-60);
|
||||||
|
|
||||||
|
--wa-color-brand-lt-70: var(--wa-color-gray-lt-70);
|
||||||
|
--wa-color-brand-gte-70: var(--wa-color-gray-gte-70);
|
||||||
|
|
||||||
|
--wa-color-brand-max-50: var(--wa-color-gray-max-50);
|
||||||
|
--wa-color-brand-min-50: var(--wa-color-gray-min-50);
|
||||||
|
--wa-color-brand-max-60: var(--wa-color-gray-max-60);
|
||||||
|
--wa-color-brand-min-60: var(--wa-color-gray-min-60);
|
||||||
|
--wa-color-brand-max-70: var(--wa-color-gray-max-70);
|
||||||
|
--wa-color-brand-min-70: var(--wa-color-gray-min-70);
|
||||||
|
|
||||||
|
--wa-color-brand-on: var(--wa-color-gray-on);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
@import url('base.css');
|
|
||||||
|
|
||||||
:where(:root),
|
:where(:root),
|
||||||
:host,
|
:host,
|
||||||
:where([class^='wa-theme-'], [class*=' wa-theme-']),
|
:where([class^='wa-theme-'], [class*=' wa-theme-']),
|
||||||
@@ -17,5 +15,22 @@
|
|||||||
--wa-color-brand-10: var(--wa-color-green-10);
|
--wa-color-brand-10: var(--wa-color-green-10);
|
||||||
--wa-color-brand-05: var(--wa-color-green-05);
|
--wa-color-brand-05: var(--wa-color-green-05);
|
||||||
--wa-color-brand: var(--wa-color-green);
|
--wa-color-brand: var(--wa-color-green);
|
||||||
--wa-color-brand-key: var(--wa-color-green-key);
|
|
||||||
|
--wa-color-brand-lt-50: var(--wa-color-green-lt-50);
|
||||||
|
--wa-color-brand-gte-50: var(--wa-color-green-gte-50);
|
||||||
|
|
||||||
|
--wa-color-brand-lt-60: var(--wa-color-green-lt-60);
|
||||||
|
--wa-color-brand-gte-60: var(--wa-color-green-gte-60);
|
||||||
|
|
||||||
|
--wa-color-brand-lt-70: var(--wa-color-green-lt-70);
|
||||||
|
--wa-color-brand-gte-70: var(--wa-color-green-gte-70);
|
||||||
|
|
||||||
|
--wa-color-brand-max-50: var(--wa-color-green-max-50);
|
||||||
|
--wa-color-brand-min-50: var(--wa-color-green-min-50);
|
||||||
|
--wa-color-brand-max-60: var(--wa-color-green-max-60);
|
||||||
|
--wa-color-brand-min-60: var(--wa-color-green-min-60);
|
||||||
|
--wa-color-brand-max-70: var(--wa-color-green-max-70);
|
||||||
|
--wa-color-brand-min-70: var(--wa-color-green-min-70);
|
||||||
|
|
||||||
|
--wa-color-brand-on: var(--wa-color-green-on);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
@import url('base.css');
|
|
||||||
|
|
||||||
:where(:root),
|
:where(:root),
|
||||||
:host,
|
:host,
|
||||||
:where([class^='wa-theme-'], [class*=' wa-theme-']),
|
:where([class^='wa-theme-'], [class*=' wa-theme-']),
|
||||||
@@ -17,5 +15,22 @@
|
|||||||
--wa-color-brand-10: var(--wa-color-indigo-10);
|
--wa-color-brand-10: var(--wa-color-indigo-10);
|
||||||
--wa-color-brand-05: var(--wa-color-indigo-05);
|
--wa-color-brand-05: var(--wa-color-indigo-05);
|
||||||
--wa-color-brand: var(--wa-color-indigo);
|
--wa-color-brand: var(--wa-color-indigo);
|
||||||
--wa-color-brand-key: var(--wa-color-indigo-key);
|
|
||||||
|
--wa-color-brand-lt-50: var(--wa-color-indigo-lt-50);
|
||||||
|
--wa-color-brand-gte-50: var(--wa-color-indigo-gte-50);
|
||||||
|
|
||||||
|
--wa-color-brand-lt-60: var(--wa-color-indigo-lt-60);
|
||||||
|
--wa-color-brand-gte-60: var(--wa-color-indigo-gte-60);
|
||||||
|
|
||||||
|
--wa-color-brand-lt-70: var(--wa-color-indigo-lt-70);
|
||||||
|
--wa-color-brand-gte-70: var(--wa-color-indigo-gte-70);
|
||||||
|
|
||||||
|
--wa-color-brand-max-50: var(--wa-color-indigo-max-50);
|
||||||
|
--wa-color-brand-min-50: var(--wa-color-indigo-min-50);
|
||||||
|
--wa-color-brand-max-60: var(--wa-color-indigo-max-60);
|
||||||
|
--wa-color-brand-min-60: var(--wa-color-indigo-min-60);
|
||||||
|
--wa-color-brand-max-70: var(--wa-color-indigo-max-70);
|
||||||
|
--wa-color-brand-min-70: var(--wa-color-indigo-min-70);
|
||||||
|
|
||||||
|
--wa-color-brand-on: var(--wa-color-indigo-on);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
@import url('base.css');
|
|
||||||
|
|
||||||
:where(:root),
|
:where(:root),
|
||||||
:host,
|
:host,
|
||||||
:where([class^='wa-theme-'], [class*=' wa-theme-']),
|
:where([class^='wa-theme-'], [class*=' wa-theme-']),
|
||||||
@@ -17,5 +15,22 @@
|
|||||||
--wa-color-brand-10: var(--wa-color-orange-10);
|
--wa-color-brand-10: var(--wa-color-orange-10);
|
||||||
--wa-color-brand-05: var(--wa-color-orange-05);
|
--wa-color-brand-05: var(--wa-color-orange-05);
|
||||||
--wa-color-brand: var(--wa-color-orange);
|
--wa-color-brand: var(--wa-color-orange);
|
||||||
--wa-color-brand-key: var(--wa-color-orange-key);
|
|
||||||
|
--wa-color-brand-lt-50: var(--wa-color-orange-lt-50);
|
||||||
|
--wa-color-brand-gte-50: var(--wa-color-orange-gte-50);
|
||||||
|
|
||||||
|
--wa-color-brand-lt-60: var(--wa-color-orange-lt-60);
|
||||||
|
--wa-color-brand-gte-60: var(--wa-color-orange-gte-60);
|
||||||
|
|
||||||
|
--wa-color-brand-lt-70: var(--wa-color-orange-lt-70);
|
||||||
|
--wa-color-brand-gte-70: var(--wa-color-orange-gte-70);
|
||||||
|
|
||||||
|
--wa-color-brand-max-50: var(--wa-color-orange-max-50);
|
||||||
|
--wa-color-brand-min-50: var(--wa-color-orange-min-50);
|
||||||
|
--wa-color-brand-max-60: var(--wa-color-orange-max-60);
|
||||||
|
--wa-color-brand-min-60: var(--wa-color-orange-min-60);
|
||||||
|
--wa-color-brand-max-70: var(--wa-color-orange-max-70);
|
||||||
|
--wa-color-brand-min-70: var(--wa-color-orange-min-70);
|
||||||
|
|
||||||
|
--wa-color-brand-on: var(--wa-color-orange-on);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
@import url('base.css');
|
|
||||||
|
|
||||||
:where(:root),
|
:where(:root),
|
||||||
:host,
|
:host,
|
||||||
:where([class^='wa-theme-'], [class*=' wa-theme-']),
|
:where([class^='wa-theme-'], [class*=' wa-theme-']),
|
||||||
@@ -17,5 +15,22 @@
|
|||||||
--wa-color-brand-10: var(--wa-color-pink-10);
|
--wa-color-brand-10: var(--wa-color-pink-10);
|
||||||
--wa-color-brand-05: var(--wa-color-pink-05);
|
--wa-color-brand-05: var(--wa-color-pink-05);
|
||||||
--wa-color-brand: var(--wa-color-pink);
|
--wa-color-brand: var(--wa-color-pink);
|
||||||
--wa-color-brand-key: var(--wa-color-pink-key);
|
|
||||||
|
--wa-color-brand-lt-50: var(--wa-color-pink-lt-50);
|
||||||
|
--wa-color-brand-gte-50: var(--wa-color-pink-gte-50);
|
||||||
|
|
||||||
|
--wa-color-brand-lt-60: var(--wa-color-pink-lt-60);
|
||||||
|
--wa-color-brand-gte-60: var(--wa-color-pink-gte-60);
|
||||||
|
|
||||||
|
--wa-color-brand-lt-70: var(--wa-color-pink-lt-70);
|
||||||
|
--wa-color-brand-gte-70: var(--wa-color-pink-gte-70);
|
||||||
|
|
||||||
|
--wa-color-brand-max-50: var(--wa-color-pink-max-50);
|
||||||
|
--wa-color-brand-min-50: var(--wa-color-pink-min-50);
|
||||||
|
--wa-color-brand-max-60: var(--wa-color-pink-max-60);
|
||||||
|
--wa-color-brand-min-60: var(--wa-color-pink-min-60);
|
||||||
|
--wa-color-brand-max-70: var(--wa-color-pink-max-70);
|
||||||
|
--wa-color-brand-min-70: var(--wa-color-pink-min-70);
|
||||||
|
|
||||||
|
--wa-color-brand-on: var(--wa-color-pink-on);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
@import url('base.css');
|
|
||||||
|
|
||||||
:where(:root),
|
:where(:root),
|
||||||
:host,
|
:host,
|
||||||
:where([class^='wa-theme-'], [class*=' wa-theme-']),
|
:where([class^='wa-theme-'], [class*=' wa-theme-']),
|
||||||
@@ -17,5 +15,22 @@
|
|||||||
--wa-color-brand-10: var(--wa-color-purple-10);
|
--wa-color-brand-10: var(--wa-color-purple-10);
|
||||||
--wa-color-brand-05: var(--wa-color-purple-05);
|
--wa-color-brand-05: var(--wa-color-purple-05);
|
||||||
--wa-color-brand: var(--wa-color-purple);
|
--wa-color-brand: var(--wa-color-purple);
|
||||||
--wa-color-brand-key: var(--wa-color-purple-key);
|
|
||||||
|
--wa-color-brand-lt-50: var(--wa-color-purple-lt-50);
|
||||||
|
--wa-color-brand-gte-50: var(--wa-color-purple-gte-50);
|
||||||
|
|
||||||
|
--wa-color-brand-lt-60: var(--wa-color-purple-lt-60);
|
||||||
|
--wa-color-brand-gte-60: var(--wa-color-purple-gte-60);
|
||||||
|
|
||||||
|
--wa-color-brand-lt-70: var(--wa-color-purple-lt-70);
|
||||||
|
--wa-color-brand-gte-70: var(--wa-color-purple-gte-70);
|
||||||
|
|
||||||
|
--wa-color-brand-max-50: var(--wa-color-purple-max-50);
|
||||||
|
--wa-color-brand-min-50: var(--wa-color-purple-min-50);
|
||||||
|
--wa-color-brand-max-60: var(--wa-color-purple-max-60);
|
||||||
|
--wa-color-brand-min-60: var(--wa-color-purple-min-60);
|
||||||
|
--wa-color-brand-max-70: var(--wa-color-purple-max-70);
|
||||||
|
--wa-color-brand-min-70: var(--wa-color-purple-min-70);
|
||||||
|
|
||||||
|
--wa-color-brand-on: var(--wa-color-purple-on);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
@import url('base.css');
|
|
||||||
|
|
||||||
:where(:root),
|
:where(:root),
|
||||||
:host,
|
:host,
|
||||||
:where([class^='wa-theme-'], [class*=' wa-theme-']),
|
:where([class^='wa-theme-'], [class*=' wa-theme-']),
|
||||||
@@ -17,5 +15,22 @@
|
|||||||
--wa-color-brand-10: var(--wa-color-red-10);
|
--wa-color-brand-10: var(--wa-color-red-10);
|
||||||
--wa-color-brand-05: var(--wa-color-red-05);
|
--wa-color-brand-05: var(--wa-color-red-05);
|
||||||
--wa-color-brand: var(--wa-color-red);
|
--wa-color-brand: var(--wa-color-red);
|
||||||
--wa-color-brand-key: var(--wa-color-red-key);
|
|
||||||
|
--wa-color-brand-lt-50: var(--wa-color-red-lt-50);
|
||||||
|
--wa-color-brand-gte-50: var(--wa-color-red-gte-50);
|
||||||
|
|
||||||
|
--wa-color-brand-lt-60: var(--wa-color-red-lt-60);
|
||||||
|
--wa-color-brand-gte-60: var(--wa-color-red-gte-60);
|
||||||
|
|
||||||
|
--wa-color-brand-lt-70: var(--wa-color-red-lt-70);
|
||||||
|
--wa-color-brand-gte-70: var(--wa-color-red-gte-70);
|
||||||
|
|
||||||
|
--wa-color-brand-max-50: var(--wa-color-red-max-50);
|
||||||
|
--wa-color-brand-min-50: var(--wa-color-red-min-50);
|
||||||
|
--wa-color-brand-max-60: var(--wa-color-red-max-60);
|
||||||
|
--wa-color-brand-min-60: var(--wa-color-red-min-60);
|
||||||
|
--wa-color-brand-max-70: var(--wa-color-red-max-70);
|
||||||
|
--wa-color-brand-min-70: var(--wa-color-red-min-70);
|
||||||
|
|
||||||
|
--wa-color-brand-on: var(--wa-color-red-on);
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user