mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 20:19:13 +00:00
Compare commits
233 Commits
card-ssr-s
...
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 | ||
|
|
7f3210b12d | ||
|
|
eee63bdecd | ||
|
|
a91fd07ed7 | ||
|
|
34aa8917a6 | ||
|
|
715c2c0def | ||
|
|
13b5385633 | ||
|
|
d25f3748c4 | ||
|
|
b6620ddf7e | ||
|
|
d70d4a91b1 | ||
|
|
bb1f7b2b7a | ||
|
|
9921c17d63 | ||
|
|
7f964f9b56 | ||
|
|
31eeea1630 | ||
|
|
ffda52a7cf | ||
|
|
9be7919c60 | ||
|
|
c30f3c4b09 | ||
|
|
7e8f13b5cb | ||
|
|
97ab986425 | ||
|
|
f1fa7f713e | ||
|
|
3394a95057 | ||
|
|
eed9ddb9b3 | ||
|
|
daf5e7734d | ||
|
|
63133d0e33 | ||
|
|
b2d74145b8 | ||
|
|
17eeccbc5d | ||
|
|
68f645e776 | ||
|
|
b3e4c59197 | ||
|
|
6080a85035 | ||
|
|
f7e088c8df | ||
|
|
80e1c05b37 | ||
|
|
99f0273783 | ||
|
|
8e13683a30 | ||
|
|
fd73542d2c | ||
|
|
bde0ed7403 | ||
|
|
8dc49f7119 |
@@ -13,3 +13,4 @@ package-lock.json
|
||||
tsconfig.json
|
||||
cdn
|
||||
_site
|
||||
docs/assets/scripts/prism-downloaded.js
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as path from 'node:path';
|
||||
import { anchorHeadingsPlugin } from './_utils/anchor-headings.js';
|
||||
import { codeExamplesPlugin } from './_utils/code-examples.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 litPlugin from '@lit-labs/eleventy-plugin-lit';
|
||||
import { readFile } from 'fs/promises';
|
||||
import nunjucks from 'nunjucks';
|
||||
import componentList from './_data/componentList.js';
|
||||
import * as filters from './_utils/filters.js';
|
||||
import { outlinePlugin } from './_utils/outline.js';
|
||||
@@ -16,7 +18,10 @@ import { searchPlugin } from './_utils/search.js';
|
||||
|
||||
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 isDev = process.argv.includes('--develop');
|
||||
|
||||
@@ -24,12 +29,23 @@ const globalData = {
|
||||
package: packageData,
|
||||
isAlpha,
|
||||
layout: 'page.njk',
|
||||
|
||||
server: {
|
||||
head: '',
|
||||
loginOrAvatar: '',
|
||||
flashes: '',
|
||||
},
|
||||
};
|
||||
|
||||
const passThroughExtensions = ['js', 'css', 'png', 'svg', 'jpg', 'mp4'];
|
||||
const passThrough = [...passThroughExtensions.map(ext => 'docs/**/*.' + ext)];
|
||||
|
||||
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
|
||||
if (isAlpha) {
|
||||
eleventyConfig.ignores.add('**/experimental/**');
|
||||
@@ -55,7 +71,38 @@ export default function (eleventyConfig) {
|
||||
|
||||
// Shortcodes - {% shortCode arg1, arg2 %}
|
||||
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 %}
|
||||
@@ -94,7 +141,7 @@ export default function (eleventyConfig) {
|
||||
eleventyConfig.addPlugin(highlightCodePlugin());
|
||||
|
||||
// Add copy code buttons to code blocks
|
||||
eleventyConfig.addPlugin(copyCodePlugin());
|
||||
eleventyConfig.addPlugin(copyCodePlugin);
|
||||
|
||||
// Various text replacements
|
||||
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
|
||||
eleventyConfig.addPlugin(
|
||||
searchPlugin({
|
||||
@@ -166,6 +190,31 @@ export default function (eleventyConfig) {
|
||||
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 {
|
||||
markdownTemplateEngine: 'njk',
|
||||
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", "teal", "blue", "indigo", "violet", "gray"]
|
||||
["red", "orange", "yellow", "green", "cyan", "blue", "indigo", "purple", "pink", "gray"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default as default } from '../../src/styles/color/palettes.js';
|
||||
export { default as default } from '../../src/styles/color/scripts/palettes-analyzed.js';
|
||||
|
||||
@@ -5,16 +5,18 @@
|
||||
<meta name="theme-color" content="#f36944">
|
||||
|
||||
<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/turbo.js"></script>
|
||||
<script type="module" src="/assets/scripts/search.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>
|
||||
|
||||
{# Docs styles #}
|
||||
<link rel="stylesheet" href="/assets/styles/docs.css" />
|
||||
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body class="layout-{{ layout | stripExtension }}{{ ' page-wide' if wide }}">
|
||||
<!-- use view="desktop" as default to reduce layout jank on desktop site. -->
|
||||
@@ -48,6 +50,9 @@
|
||||
Search
|
||||
<kbd slot="suffix" class="only-desktop">/</kbd>
|
||||
</wa-button>
|
||||
|
||||
{# Login #}
|
||||
{% server "loginOrAvatar" %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -74,14 +79,19 @@
|
||||
</aside>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{# Main #}
|
||||
<main id="content">
|
||||
{# Expandable outline #}
|
||||
{% if hasOutline %}
|
||||
<nav id="outline-expandable">
|
||||
<details class="outline-links">
|
||||
<summary>On this page</summary>
|
||||
</details>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
<div id="flashes">{% server "flashes" %}</div>
|
||||
|
||||
{% block header %}
|
||||
{% include 'breadcrumbs.njk' %}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
{% set breadcrumbs = page.url | breadcrumbs %}
|
||||
{% if breadcrumbs.length > 0 %}
|
||||
{% set ancestors = page.url | ancestors %}
|
||||
|
||||
{% if ancestors.length > 0 %}
|
||||
<wa-breadcrumb id="docs-breadcrumbs">
|
||||
{% for crumb in breadcrumbs %}
|
||||
<wa-breadcrumb-item href="{{ crumb.url }}">{{ crumb.title }}</wa-breadcrumb-item>
|
||||
{% for ancestor in ancestors %}
|
||||
{% if ancestor.page.url != "/" %}
|
||||
<wa-breadcrumb-item href="{{ ancestor.page.url }}">{{ ancestor.data.title }}</wa-breadcrumb-item>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<wa-breadcrumb-item>{# Current page #}</wa-breadcrumb-item>
|
||||
</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>
|
||||
<tr>
|
||||
<th></th>
|
||||
@@ -12,19 +12,31 @@
|
||||
</tr>
|
||||
</thead>
|
||||
{% for hue in hues -%}
|
||||
<tr>
|
||||
<tr data-hue="{{ hue }}" v-if="'{{hue}}' in paletteScales">
|
||||
<th>{{ hue | capitalize }}</th>
|
||||
{% for tint_bg in tints -%}
|
||||
{% set color_bg = palettes[paletteId][hue][tint_bg] %}
|
||||
{% for tint_fg in tints | reverse -%}
|
||||
{% set color_fg = palettes[paletteId][hue][tint_fg] %}
|
||||
{% if (tint_fg - tint_bg) | abs == difference %}
|
||||
<td>
|
||||
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}-{{ tint_bg }}); color: var(--wa-color-{{ hue }}-{{ tint_fg }})">
|
||||
{% set contrast_wcag = '' %}
|
||||
{% if color_fg and color_bg %}
|
||||
{% set contrast_wcag = color_bg.contrast(color_fg, 'WCAG21') %}
|
||||
{% endif %}
|
||||
{% set contrast_wcag = '' %}
|
||||
{% if color_fg and color_bg -%}
|
||||
{% set contrast_wcag = color_bg.contrast(color_fg, 'WCAG21') %}
|
||||
{%- endif %}
|
||||
<td v-for="contrast of [contrasts.{{ hue }}['{{ tint_bg }}']['{{ tint_fg }}']]"
|
||||
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 %}
|
||||
{{ contrast_wcag | number({maximumSignificantDigits: 2}) }}
|
||||
{% else %}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
{# Cards for pages listed by category #}
|
||||
|
||||
<section id="grid" class="index-grid">
|
||||
{% for category, pages in allPages | groupByTags(categories) -%}
|
||||
<h2 class="index-category">{{ category | getCategoryTitle(categories) }}</h2>
|
||||
{%- for page in pages -%}
|
||||
{%- if not page.data.parent or listChildren -%}
|
||||
{% include "page-card.njk" %}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{% set groupedPages = allPages | groupPages(categories, page) %}
|
||||
{% for category, pages in groupedPages -%}
|
||||
{% if groupedPages.meta.groupCount > 1 %}
|
||||
<h2 class="index-category">
|
||||
{% if pages.meta.url %}<a href="{{ pages.meta.url }}">{{ pages.meta.title }}</a>
|
||||
{% else %}
|
||||
{{ pages.meta.title }}
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% endif %}
|
||||
{%- for page in pages -%}
|
||||
{% include "page-card.njk" %}
|
||||
{%- endfor -%}
|
||||
{%- endfor -%}
|
||||
</section>
|
||||
|
||||
@@ -47,3 +47,7 @@
|
||||
<link rel="stylesheet" href="/dist/styles/webawesome.css" />
|
||||
<link id="color-stylesheet" rel="stylesheet" href="/dist/styles/utilities.css" />
|
||||
<link rel="stylesheet" href="/dist/styles/forms.css" />
|
||||
|
||||
|
||||
{# Used by Web Awesome App to inject other assets into the head. #}
|
||||
{% server "head" %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<wa-tab-group>
|
||||
<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">
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
<a href="{{ page.url }}"{{ page.data.keywords | attr('data-keywords') }}>
|
||||
<wa-card with-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>
|
||||
<span class="page-name">{{ page.data.title }}</span>
|
||||
{% if pageSubtitle -%}
|
||||
<div class="wa-caption-s">{{ pageSubtitle }}</div>
|
||||
{%- endif %}
|
||||
</wa-card>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
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. #}
|
||||
{% if collections[tag] -%}
|
||||
{% 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') }}>
|
||||
<h2 slot="summary">
|
||||
{% if groupUrl | getCollectionItemFromUrl %}
|
||||
{% if groupItem %}
|
||||
<a href="{{ groupUrl }}" title="Overview">{{ title or (tag | capitalize) }}
|
||||
<wa-icon name="grid-2"></wa-icon>
|
||||
</a>
|
||||
@@ -12,10 +15,8 @@
|
||||
{% endif %}
|
||||
</h2>
|
||||
<ul>
|
||||
{% for page in collections[tag] | sort %}
|
||||
{% if not page.data.parent -%}
|
||||
{% for page in children %}
|
||||
{% include 'sidebar-link.njk' %}
|
||||
{%- endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</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>
|
||||
<a href="{{ page.url }}">{{ page.data.title }}</a>
|
||||
{% if page.data.status == 'experimental' %}<wa-icon name="flask"></wa-icon>{% endif %}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
{% if since -%}
|
||||
<wa-badge variant="neutral">Since {{ since }}</wa-badge>
|
||||
<wa-badge variant="neutral" class="since">Since {{ since }}</wa-badge>
|
||||
{% endif -%}
|
||||
|
||||
{%- if status %}
|
||||
{%- if status == "wip" %}
|
||||
<wa-badge variant="danger">
|
||||
<wa-badge variant="danger" class="status">
|
||||
<wa-icon name="pickaxe"></wa-icon>
|
||||
Work In Progress
|
||||
</wa-badge>
|
||||
{%- elif status == "experimental" %}
|
||||
<wa-badge variant="warning">
|
||||
<wa-badge variant="warning" class="status">
|
||||
<wa-icon name="flask"></wa-icon>
|
||||
Experimental
|
||||
</wa-badge>
|
||||
{%- elif status == "stable" %}
|
||||
<wa-badge variant="brand">Stable</wa-badge>
|
||||
<wa-badge variant="brand" class="status">Stable</wa-badge>
|
||||
{%- else %}
|
||||
<wa-badge>{{ status}}</wa-badge>
|
||||
<wa-badge class="status">{{ status}}</wa-badge>
|
||||
{%- endif -%}
|
||||
{%- endif %}
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
{% set paletteId = page.fileSlug %}
|
||||
{% set tints = [80, 60, 40, 20] %}
|
||||
{% set paletteId = palette.fileSlug or page.fileSlug %}
|
||||
{% set suffixes = ['-80', '', '-20'] %}
|
||||
{% set width = 20 %}
|
||||
{% set height = 13 %}
|
||||
{% set gap_x = 3 %}
|
||||
{% set gap_y = 3 %}
|
||||
{% set height = 12 %}
|
||||
{% set height_core = 20 %}
|
||||
{% 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>
|
||||
@import url('/dist/styles/color/{{ paletteId }}.css') layer(palette.{{ paletteId }});
|
||||
.palette-icon {
|
||||
@@ -15,10 +18,14 @@
|
||||
|
||||
{% for hue in hues -%}
|
||||
{% set hueIndex = loop.index0 %}
|
||||
{% for tint in tints -%}
|
||||
<rect x="{{ hueIndex * (width + gap_x) }}" y="{{ loop.index0 * (height + gap_y) }}"
|
||||
width="{{ width }}" height="{{ height }}"
|
||||
fill="var(--wa-color-{{ hue }}-{{ tint }})" rx="4" />
|
||||
{% set y = 0 %}
|
||||
{% for suffix in suffixes -%}
|
||||
{% set swatch_height = height if suffix else height_core %}
|
||||
|
||||
<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 %}
|
||||
</svg>
|
||||
|
||||
24
docs/_includes/svgs/theme-color.njk
Normal file
24
docs/_includes/svgs/theme-color.njk
Normal file
@@ -0,0 +1,24 @@
|
||||
{% set themeId = theme.fileSlug %}
|
||||
|
||||
<div>
|
||||
<template shadowrootmode="open">
|
||||
<link rel="stylesheet" href="/dist/styles/utilities.css">
|
||||
<link rel="stylesheet" href="/dist/styles/themes/{{ page.fileSlug or 'default' }}.css">
|
||||
<link rel="stylesheet" href="/dist/styles/themes/{{ themeId }}/color.css">
|
||||
<link rel="stylesheet" href="/assets/styles/theme-icons.css">
|
||||
|
||||
<div class="theme-color-icon wa-theme-{{ themeId }}">
|
||||
<div class="wa-brand wa-accent">A</div>
|
||||
<div class="wa-brand wa-outlined">A</div>
|
||||
<div class="wa-brand wa-filled">A</div>
|
||||
<div class="wa-brand wa-plain">A</div>
|
||||
{# <div class="wa-danger wa-outlined wa-filled"><wa-icon slot="icon" name="circle-exclamation" variant="regular"></wa-icon></div> #}
|
||||
|
||||
<div class="wa-neutral wa-accent">A</div>
|
||||
<div class="wa-neutral wa-outlined">A</div>
|
||||
<div class="wa-neutral wa-filled">A</div>
|
||||
<div class="wa-neutral wa-plain">A</div>
|
||||
{# <div class="wa-warning wa-outlined wa-filled"><wa-icon slot="icon" name="triangle-exclamation" variant="regular"></wa-icon></div> #}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
16
docs/_includes/svgs/theme-typography.njk
Normal file
16
docs/_includes/svgs/theme-typography.njk
Normal file
@@ -0,0 +1,16 @@
|
||||
{% set themeId = theme.fileSlug %}
|
||||
|
||||
<div>
|
||||
<template shadowrootmode="open">
|
||||
<link rel="stylesheet" href="/dist/styles/native/content.css">
|
||||
<link rel="stylesheet" href="/dist/styles/native/blockquote.css">
|
||||
<link rel="stylesheet" href="/dist/styles/themes/{{ page.fileSlug or 'default' }}.css">
|
||||
<link rel="stylesheet" href="/dist/styles/themes/{{ themeId }}/typography.css">
|
||||
<link rel="stylesheet" href="/assets/styles/theme-icons.css">
|
||||
|
||||
<div class="theme-typography-icon wa-theme-{{ themeId }}" data-no-outline data-no-anchor role="presentation">
|
||||
<h3>Title</h3>
|
||||
<p>Body text</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -2,7 +2,7 @@
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M48.8665 59.1993L33.7329 33H64C63.8352 38.2705 62.3694 43.4244 59.7264 48C57.0833 52.5756 53.3507 56.4213 48.8665 59.1993Z" fill="var(--wa-color-green-70)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32 64C26.714 64 21.5155 62.6919 16.8665 60.1993L32 34L47.1336 60.1993C42.4845 62.6919 37.286 64 32 64Z" fill="var(--wa-color-blue-60)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.27365 48C1.63063 43.4244 0.164839 38.2706 0 33L30.2671 33L15.1336 59.1993C10.6493 56.4213 6.91666 52.5756 4.27365 48Z" fill="var(--wa-color-indigo-60)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.27365 16C6.91667 11.4244 10.6493 7.57868 15.1336 4.80072L30.2671 31L0 31C0.16484 25.7295 1.63063 20.5756 4.27365 16Z" fill="var(--wa-color-violet-60)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.27365 16C6.91667 11.4244 10.6493 7.57868 15.1336 4.80072L30.2671 31L0 31C0.16484 25.7295 1.63063 20.5756 4.27365 16Z" fill="var(--wa-color-purple-60)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32 30L16.8665 3.80072C21.5155 1.30813 26.714 0 32 0C37.286 0 42.4845 1.30813 47.1336 3.80072L32 30Z" fill="var(--wa-color-red-60)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M33.7329 31L48.8665 4.80072C53.3507 7.57868 57.0833 11.4244 59.7264 16C62.3694 20.5756 63.8352 25.7294 64 31L33.7329 31Z" fill="var(--wa-color-yellow-80)"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
<wa-divider></wa-divider>
|
||||
<div class="wa-flank">
|
||||
<wa-avatar shape="rounded" style="--size: 3em; --background-color: var(--wa-color-teal-60); --text-color: var(--wa-color-teal-95);">
|
||||
<wa-avatar shape="rounded" style="--size: 3em; --background-color: var(--wa-color-cyan-60); --text-color: var(--wa-color-cyan-95);">
|
||||
<wa-icon slot="icon" name="robot-astromech" family="duotone" style="font-size: 1.5em;"></wa-icon>
|
||||
</wa-avatar>
|
||||
<div class="wa-stack wa-gap-2xs">
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="test-failure"><code>outlined</code></th>
|
||||
<th><code>outlined</code></th>
|
||||
<td>
|
||||
<div class="wa-cluster wa-gap-2xs">
|
||||
<wa-badge variant="brand" appearance="outlined">Brand</wa-badge>
|
||||
@@ -495,7 +495,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="test-failure"><code>outlined</code></th>
|
||||
<th><code>outlined</code></th>
|
||||
<td>
|
||||
<div class="wa-grid wa-gap-2xs">
|
||||
<wa-callout variant="brand" appearance="outlined">
|
||||
@@ -696,7 +696,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="test-failure"><code>outlined</code></th>
|
||||
<th><code>outlined</code></th>
|
||||
<td>
|
||||
<div class="wa-cluster wa-gap-2xs">
|
||||
<wa-tag variant="brand" appearance="outlined">Brand</wa-tag>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
layout: page-outline
|
||||
tags: ["overview"]
|
||||
---
|
||||
{% set forTag = forTag or (page.url | split('/') | last) %}
|
||||
{% if description %}
|
||||
@@ -13,8 +12,10 @@ tags: ["overview"]
|
||||
</wa-input>
|
||||
</div>
|
||||
|
||||
{% set allPages = collections[forTag] %}
|
||||
{% set allPages = allPages or collections[forTag] %}
|
||||
{% if allPages and allPages.length > 0 %}
|
||||
{% include "grouped-pages.njk" %}
|
||||
{% endif %}
|
||||
|
||||
<link href="/assets/styles/filter.css" rel="stylesheet">
|
||||
<script type="module" src="/assets/scripts/filter.js"></script>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{% set hasSidebar = true %}
|
||||
{% set hasOutline = false %}
|
||||
{% if hasSidebar == undefined %}
|
||||
{% set hasSidebar = true %}
|
||||
{% endif %}
|
||||
|
||||
{% if hasOutline == undefined %}
|
||||
{% set hasOutline = false %}
|
||||
{% endif %}
|
||||
|
||||
{% extends "../_includes/base.njk" %}
|
||||
|
||||
@@ -1,39 +1,234 @@
|
||||
{% set hasSidebar = true %}
|
||||
{% set hasOutline = true %}
|
||||
{# {% set forceTheme = page.fileSlug %} #}
|
||||
|
||||
{% extends '../_layouts/block.njk' %}
|
||||
|
||||
{% set paletteId = page.fileSlug %}
|
||||
|
||||
{% block afterContent %}
|
||||
<style>@import url('/dist/styles/color/{{ paletteId }}.css') layer(palette.{{ paletteId }});</style>
|
||||
|
||||
{% set paletteId = "default" if page.fileSlug == 'custom' else page.fileSlug %}
|
||||
{% set isCustom = page.fileSlug == 'custom' %}
|
||||
{% set tints = ["95", "90", "80", "70", "60", "50", "40", "30", "20", "10", "05"] %}
|
||||
|
||||
<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>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="core-column">Core tint</th>
|
||||
{% for tint in tints -%}
|
||||
<th>{{ tint }}</th>
|
||||
{%- endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
{% for hue in hues -%}
|
||||
<tr>
|
||||
<th>{{ hue | capitalize }}</th>
|
||||
{% for tint in tints -%}
|
||||
<td>
|
||||
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}-{{ tint }})">
|
||||
<wa-copy-button value="--wa-color-{{ hue }}-{{ tint }}" copy-label="--wa-color-{{ hue }}-{{ tint }}"></wa-copy-button>
|
||||
</div>
|
||||
</td>
|
||||
{%- endfor -%}
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
{% raw %}
|
||||
<tbody v-cloak>
|
||||
<tr v-for="hue in paletteScalesList" :data-hue="hue" :key="hue"
|
||||
class="color-scale" :class="{
|
||||
tweaked: hue === 'gray' ? tweaked.grayChroma || tweaked.grayColor : hueShifts[hue],
|
||||
}"
|
||||
:style="{
|
||||
'--swatch-text-color': `light-dark(var(--wa-color-${ hue }-10), white)`,
|
||||
'--hue-shift': hueShifts[hue] || ''
|
||||
}">
|
||||
<th>
|
||||
{{ capitalize(hue) }}
|
||||
<info-tip v-if="isCustom && !seedHues[hue]">
|
||||
<wa-icon name="sparkles" style="color: var(--wa-color-gray-50)"></wa-icon>
|
||||
<template #content>Generated scale</template>
|
||||
</info-tip>
|
||||
</th>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<section class="index-grid">
|
||||
@@ -43,6 +238,7 @@
|
||||
{%- endif -%}
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% markdown %}
|
||||
## Color Contrast
|
||||
@@ -58,6 +254,7 @@ A difference of `40` ensures a minimum **3:1** contrast ratio, suitable for larg
|
||||
{% endmarkdown %}
|
||||
|
||||
{% set difference = 40 %}
|
||||
{% set minContrast = 3 %}
|
||||
{% include "contrast-table.njk" %}
|
||||
|
||||
{% markdown %}
|
||||
@@ -77,6 +274,7 @@ A difference of `50` ensures a minimum **4.5:1** contrast ratio, suitable for no
|
||||
{% endmarkdown %}
|
||||
|
||||
{% set difference = 50 %}
|
||||
{% set minContrast = 4.5 %}
|
||||
{% include "contrast-table.njk" %}
|
||||
|
||||
{% markdown %}
|
||||
@@ -95,6 +293,7 @@ A difference of `60` ensures a minimum **7:1** contrast ratio, suitable for all
|
||||
{% endmarkdown %}
|
||||
|
||||
{% set difference = 60 %}
|
||||
{% set minContrast = 7 %}
|
||||
{% include "contrast-table.njk" %}
|
||||
|
||||
{% markdown %}
|
||||
@@ -107,13 +306,48 @@ This also goes for a difference of `65`:
|
||||
{% include "contrast-table.njk" %}
|
||||
|
||||
{% 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.
|
||||
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' %}
|
||||
{% 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 %}
|
||||
|
||||
<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 %}
|
||||
|
||||
|
||||
|
||||
@@ -4,100 +4,143 @@
|
||||
|
||||
{% extends '../_includes/base.njk' %}
|
||||
|
||||
{% block head %}
|
||||
<script>
|
||||
globalThis.wa_data ??= {};
|
||||
wa_data.baseTheme = "{{ page.fileSlug }}";
|
||||
wa_data.themes = {
|
||||
{% for theme in collections.theme -%}
|
||||
"{{ theme.fileSlug }}": {
|
||||
"title": "{{ theme.data.title }}",
|
||||
"palette": "{{ theme.data.palette }}",
|
||||
"brand": "{{ theme.data.brand }}"
|
||||
},
|
||||
{% endfor %}
|
||||
};
|
||||
wa_data.palettes = {
|
||||
{% for palette in collections.palette -%}
|
||||
"{{ palette.fileSlug }}": {
|
||||
"title": "{{ palette.data.title }}",
|
||||
},
|
||||
{% endfor %}
|
||||
};
|
||||
</script>
|
||||
<link href="{{ page.url }}../remix.css" rel="stylesheet">
|
||||
<script src="{{ page.url }}../remix.js" type="module"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
|
||||
<iframe src='{{ page.url }}demo.html' id="demo"></iframe>
|
||||
|
||||
<p id="mix_and_match" class="wa-gap-m">
|
||||
<strong>
|
||||
<wa-icon name="merge" slot="prefix"></wa-icon>
|
||||
Remix
|
||||
<wa-icon-button href="#remixing" name="circle-question" slot="suffix" variant="regular" label="How to use?"></wa-icon-button>
|
||||
</strong>
|
||||
<wa-select name="colors" label="Colors from…" size="small">
|
||||
<wa-details id="mix_and_match" class="wa-gap-m" >
|
||||
<h4 slot="summary" data-no-anchor data-no-outline id="remix">
|
||||
<wa-icon name="arrows-rotate"></wa-icon>
|
||||
Remix this theme
|
||||
<wa-icon id="what-is-remixing" href="#remixing" name="circle-question" slot="suffix" variant="regular"></wa-icon>
|
||||
<wa-tooltip for="what-is-remixing">Customize this theme by changing its colors and/or remixing it with design elements from other themes!</wa-tooltip>
|
||||
</h4>
|
||||
|
||||
<wa-select name="colors" label="Colors from…" value="" clearable>
|
||||
<wa-icon name="palette" slot="prefix" variant="regular"></wa-icon>
|
||||
<wa-option value="">(Theme default)</wa-option>
|
||||
<wa-divider></wa-divider>
|
||||
{% for theme in collections.theme | sort %}
|
||||
{% if theme.fileSlug !== page.fileSlug %}
|
||||
<wa-option value="{{ theme.fileSlug }}">{{ theme.data.title }}</wa-option>
|
||||
{% endif %}
|
||||
{% set currentTheme = theme.fileSlug == page.fileSlug %}
|
||||
<wa-option label="{{ theme.data.title }}" value="{{ theme.fileSlug if not currentTheme }}" {{ (theme.fileSlug if currentTheme) | attr('data-id') }}>
|
||||
<wa-card with-header>
|
||||
<div slot="header">
|
||||
{% include "svgs/theme-color.njk" %}
|
||||
</div>
|
||||
<span class="page-name">
|
||||
{{ theme.data.title }}
|
||||
{% if theme.data.isPro %}<wa-badge class="pro">PRO</wa-badge>{% endif %}
|
||||
{% if currentTheme %}<wa-badge variant="neutral" appearance="outlined">This theme</wa-badge>{% endif %}
|
||||
</span>
|
||||
</wa-card>
|
||||
</wa-option>
|
||||
{% endfor %}
|
||||
</wa-select>
|
||||
|
||||
<wa-select name="palette" label="Palette" size="small">
|
||||
<wa-select name="palette" label="Palette" clearable>
|
||||
<wa-icon name="swatchbook" slot="prefix" variant="regular"></wa-icon>
|
||||
<wa-option value="">(Theme default)</wa-option>
|
||||
<wa-divider></wa-divider>
|
||||
{% for p in collections.palette | sort %}
|
||||
{% if p.fileSlug !== palette %}
|
||||
<wa-option value="{{ p.fileSlug }}">{{ p.data.title }}</wa-option>
|
||||
{% endif %}
|
||||
{% set defaultPalette = palette %}
|
||||
{% for palette in collections.palette | sort %}
|
||||
{% set currentPalette = palette.fileSlug == defaultPalette %}
|
||||
<wa-option label="{{ palette.data.title }}" value="{{ palette.fileSlug if not currentPalette }}" {{ (palette.fileSlug if currentPalette) | attr('data-id') }}>
|
||||
<wa-card with-header>
|
||||
<div slot="header">
|
||||
{% include "svgs/" + (palette.data.icon or "thumbnail-placeholder") + ".njk" ignore missing %}
|
||||
</div>
|
||||
<span class="page-name">
|
||||
{{ palette.data.title }}
|
||||
{% if palette.data.isPro %}<wa-badge class="pro">PRO</wa-badge>{% endif %}
|
||||
{% if currentPalette %}<wa-badge variant="neutral" appearance="outlined">Theme default</wa-badge>{% endif %}
|
||||
</span>
|
||||
</wa-card>
|
||||
</wa-option>
|
||||
{% endfor %}
|
||||
{% set palette = defaultPalette %}
|
||||
</wa-select>
|
||||
|
||||
<wa-select class="color-select" name="brand" label="Brand color" value="" clearable>
|
||||
<div class="selected-swatch" slot="prefix"></div>
|
||||
{% for hue in hues %}
|
||||
{% set currentBrand = hue == brand %}
|
||||
<wa-option label="{{ hue | capitalize }}" value="{{ hue if not currentBrand }}" {{ (hue if currentBrand) | attr('data-id') }} style="--color: var(--wa-color-{{ hue }})">
|
||||
{{ hue | capitalize }}
|
||||
{% if currentBrand %}<wa-badge variant="neutral" appearance="outlined">Theme default</wa-badge>{% endif %}
|
||||
</wa-option>
|
||||
{% endfor %}
|
||||
</wa-select>
|
||||
|
||||
<wa-select name="typography" label="Typography from…" size="small">
|
||||
<wa-select name="typography" label="Typography from…" clearable>
|
||||
<wa-icon name="font-case" slot="prefix"></wa-icon>
|
||||
<wa-option value="">(Theme default)</wa-option>
|
||||
<wa-divider></wa-divider>
|
||||
{% for theme in collections.theme | sort %}
|
||||
{% if theme.fileSlug !== page.fileSlug %}
|
||||
<wa-option value="{{ theme.fileSlug }}">{{ theme.data.title }}</wa-option>
|
||||
{% endif %}
|
||||
{% set currentTheme = theme.fileSlug == page.fileSlug %}
|
||||
<wa-option label="{{ theme.data.title }}" value="{{ theme.fileSlug if not currentTheme }}" {{ (theme.fileSlug if currentTheme) | attr('data-id') }}>
|
||||
<wa-card with-header>
|
||||
<div slot="header">
|
||||
{% include "svgs/theme-typography.njk" %}
|
||||
</div>
|
||||
<span class="page-name">
|
||||
{{ theme.data.title }}
|
||||
{% if theme.data.isPro %}<wa-badge class="pro">PRO</wa-badge>{% endif %}
|
||||
{% if currentTheme %}<wa-badge variant="neutral" appearance="outlined">This theme</wa-badge>{% endif %}
|
||||
</span>
|
||||
</wa-card>
|
||||
</wa-option>
|
||||
{% endfor %}
|
||||
</wa-select>
|
||||
</wa-details>
|
||||
|
||||
|
||||
</p>
|
||||
<script>
|
||||
document.querySelector('#mix_and_match').addEventListener('change', function(event) {
|
||||
let selects = document.querySelectorAll('#mix_and_match wa-select');
|
||||
let url = new URL(demo.src);
|
||||
|
||||
for (let select of selects) {
|
||||
url.searchParams.set(select.name, select.value);
|
||||
}
|
||||
|
||||
demo.src = url;
|
||||
});
|
||||
</script>
|
||||
|
||||
<h2>Default Color Palette</h2>
|
||||
<h2>Color</h2>
|
||||
{% set paletteURL = '/docs/palettes/' + palette + '/' %}
|
||||
{% set themePage = page %}
|
||||
{% set page = paletteURL | getCollectionItemFromUrl %}
|
||||
<div class="index-grid">
|
||||
{% include 'page-card.njk' %}
|
||||
</div>
|
||||
{% set page = themePage %}
|
||||
|
||||
|
||||
<div class="index-grid">
|
||||
{% set themePage = page %}
|
||||
{% set page = paletteURL | getCollectionItemFromUrl %}
|
||||
{% set pageSubtitle = "Default color palette" %}
|
||||
{% include 'page-card.njk' %}
|
||||
{% set page = themePage %}
|
||||
|
||||
<wa-card style="--header-background: var(--wa-color-{{ brand }})" class="wa-palette-{{ palette }}">
|
||||
<div slot="header"></div>
|
||||
<div class="page-name">{{ brand | capitalize }}</div>
|
||||
<div class="wa-caption-s">Default brand color</div>
|
||||
</wa-card>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block afterContent %}
|
||||
{% markdown %}
|
||||
## How to use this theme
|
||||
|
||||
<h2 id="usage">How to use this theme</h2>
|
||||
|
||||
{% markdown %}
|
||||
You can import this theme from the Web Awesome CDN.
|
||||
|
||||
{% set stylesheet = 'styles/themes/' + page.fileSlug + '.css' %}
|
||||
{% include 'import-stylesheet-code.md.njk' %}
|
||||
|
||||
### Remixing { #remixing }
|
||||
|
||||
If you want to combine the **colors** from this theme with another theme, you can import this CSS file *after* the other theme’s CSS file:
|
||||
|
||||
{% set stylesheet = 'styles/themes/' + page.fileSlug + '/color.css' %}
|
||||
{% include 'import-stylesheet-code.md.njk' %}
|
||||
|
||||
To use the **typography** from this theme with another theme, you can import this CSS file *after* the other theme’s CSS file:
|
||||
|
||||
{% set stylesheet = 'styles/themes/' + page.fileSlug + '/typography.css' %}
|
||||
{% include 'import-stylesheet-code.md.njk' %}
|
||||
|
||||
<wa-callout variant="warning">
|
||||
<wa-icon slot="icon" name="triangle-exclamation" variant="regular"></wa-icon>
|
||||
Please note that not all combinations will look good — once you’re mixing and matching, you’re on your own!
|
||||
</wa-callout>
|
||||
|
||||
## Dark mode
|
||||
|
||||
To activate the dark color scheme of the theme on any element and its contents, apply the class `wa-dark` to it.
|
||||
|
||||
@@ -37,7 +37,8 @@ export function anchorHeadingsPlugin(options = {}) {
|
||||
}
|
||||
|
||||
// Look for headings
|
||||
container.querySelectorAll(options.headingSelector).forEach(heading => {
|
||||
let selector = `:is(${options.headingSelector}):not([data-no-anchor], [data-no-anchor] *)`;
|
||||
container.querySelectorAll(selector).forEach(heading => {
|
||||
const hasAnchor = heading.querySelector('a');
|
||||
const existingId = heading.getAttribute('id');
|
||||
const clone = parse(heading.outerHTML);
|
||||
|
||||
@@ -3,30 +3,39 @@ import { parse } from 'node-html-parser';
|
||||
/**
|
||||
* Eleventy plugin to add copy buttons to code blocks.
|
||||
*/
|
||||
export function copyCodePlugin(options = {}) {
|
||||
export function copyCodePlugin(eleventyConfig, options = {}) {
|
||||
options = {
|
||||
container: 'body',
|
||||
...options,
|
||||
};
|
||||
|
||||
return function (eleventyConfig) {
|
||||
eleventyConfig.addTransform('copy-code', content => {
|
||||
const doc = parse(content, { blockTextElements: { code: true } });
|
||||
const container = doc.querySelector(options.container);
|
||||
let codeCount = 0;
|
||||
eleventyConfig.addTransform('copy-code', content => {
|
||||
const doc = parse(content, { blockTextElements: { code: true } });
|
||||
const container = doc.querySelector(options.container);
|
||||
|
||||
if (!container) {
|
||||
return content;
|
||||
if (!container) {
|
||||
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
|
||||
container.querySelectorAll('pre > code').forEach(code => {
|
||||
const pre = code.closest('pre');
|
||||
|
||||
// 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();
|
||||
// Add a copy button
|
||||
pre.innerHTML += `<wa-icon-button href="#${preId}" class="block-link-icon" name="link"></wa-icon-button>
|
||||
<wa-copy-button from="${codeId}" class="copy-button" hoist></wa-copy-button>`;
|
||||
});
|
||||
};
|
||||
|
||||
return doc.toString();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ function getCollection(name) {
|
||||
}
|
||||
|
||||
export function getCollectionItemFromUrl(url, collection) {
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
collection ??= getCollection.call(this, 'all') || [];
|
||||
return collection.find(item => item.url === url);
|
||||
}
|
||||
@@ -42,35 +45,33 @@ export function split(text, separator) {
|
||||
return (text + '').split(separator).filter(Boolean);
|
||||
}
|
||||
|
||||
export function breadcrumbs(url, { withCurrent = false } = {}) {
|
||||
const parts = split(url, '/');
|
||||
const ret = [];
|
||||
export function ancestors(url, { withCurrent = false, withRoot = false } = {}) {
|
||||
let ret = [];
|
||||
let currentUrl = url;
|
||||
let currentItem = getCollectionItemFromUrl.call(this, url);
|
||||
|
||||
while (parts.length) {
|
||||
let partialUrl = '/' + parts.join('/') + '/';
|
||||
let item = getCollectionItemFromUrl.call(this, partialUrl);
|
||||
|
||||
if (item && (partialUrl !== url || withCurrent)) {
|
||||
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];
|
||||
if (!currentItem) {
|
||||
// Might have eleventyExcludeFromCollections, jump to parent
|
||||
let parentUrl = this.ctx.parentUrl;
|
||||
if (parentUrl) {
|
||||
url = parentUrl;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* @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.
|
||||
* @returns { Object.<string, object[]> } An object with keys for each tag, and an array of items for each tag.
|
||||
* @param { Object<string, string> | string[]} [options] Options object or array of tags to group by.
|
||||
* @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) {
|
||||
console.error(`Empty collection passed to groupByTags() to group by ${JSON.stringify(tags)}`);
|
||||
}
|
||||
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);
|
||||
console.error(`Empty collection passed to groupPages() to group by ${JSON.stringify(options)}`);
|
||||
}
|
||||
|
||||
let ret = Object.fromEntries(tags.map(tag => [tag, []]));
|
||||
ret.other = [];
|
||||
if (Array.isArray(options)) {
|
||||
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) {
|
||||
let categorized = false;
|
||||
let url = item.page.url;
|
||||
let parentUrl = item.data.parentUrl;
|
||||
|
||||
for (let tag of tags) {
|
||||
if (item.data.tags.includes(tag)) {
|
||||
ret[tag].push(item);
|
||||
categorized = true;
|
||||
}
|
||||
}
|
||||
byUrl[url] = item;
|
||||
|
||||
if (!categorized) {
|
||||
ret.other.push(item);
|
||||
if (parentUrl) {
|
||||
byParentUrl[parentUrl] ??= [];
|
||||
byParentUrl[parentUrl].push(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove empty categories
|
||||
for (let category in ret) {
|
||||
if (ret[category].length === 0) {
|
||||
delete ret[category];
|
||||
let urlToGroups = {};
|
||||
|
||||
for (let item of collection) {
|
||||
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;
|
||||
}
|
||||
|
||||
export function getCategoryTitle(category, categories) {
|
||||
let title;
|
||||
if (Array.isArray(categories)) {
|
||||
// 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);
|
||||
function capitalize(str) {
|
||||
str += '';
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
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
8
docs/assets/scripts/prism.js
Normal file
8
docs/assets/scripts/prism.js
Normal file
@@ -0,0 +1,8 @@
|
||||
globalThis.Prism = globalThis.Prism || {};
|
||||
globalThis.Prism.manual = true;
|
||||
|
||||
await import('./prism-downloaded.js');
|
||||
|
||||
Prism.plugins.customClass.prefix('code-');
|
||||
|
||||
export default Prism;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.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 */
|
||||
wa-badge.pro::part(base) {
|
||||
wa-badge.pro {
|
||||
background-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 {
|
||||
@@ -232,16 +255,32 @@ wa-page > main {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
h1.title wa-badge {
|
||||
vertical-align: middle;
|
||||
h1.title {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.block-info {
|
||||
display: flex;
|
||||
gap: var(--wa-space-xs);
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-block-end: var(--wa-flow-spacing);
|
||||
|
||||
code {
|
||||
line-height: var(--wa-line-height-condensed);
|
||||
}
|
||||
}
|
||||
|
||||
/* Current link */
|
||||
@@ -360,7 +399,7 @@ wa-page > main:has(> .index-grid) {
|
||||
}
|
||||
|
||||
&::part(header) {
|
||||
background-color: var(--wa-color-neutral-fill-quiet);
|
||||
background-color: var(--header-background, var(--wa-color-neutral-fill-quiet));
|
||||
border-bottom: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -400,9 +439,23 @@ wa-page > main:has(> .index-grid) {
|
||||
|
||||
&.color {
|
||||
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;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -437,6 +490,7 @@ table.colors {
|
||||
padding-block: 0;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
border: none;
|
||||
@@ -456,6 +510,59 @@ table.colors {
|
||||
padding-block: var(--wa-space-s);
|
||||
}
|
||||
}
|
||||
|
||||
.core-column {
|
||||
padding-inline-end: var(--wa-space-xl);
|
||||
}
|
||||
}
|
||||
|
||||
.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 */
|
||||
@@ -538,23 +645,47 @@ table.colors {
|
||||
height: 65vh;
|
||||
max-height: 21lh;
|
||||
}
|
||||
}
|
||||
|
||||
#mix_and_match {
|
||||
strong {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--wa-space-2xs);
|
||||
margin-top: 1.2em;
|
||||
.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);
|
||||
}
|
||||
|
||||
wa-select::part(label) {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
wa-select[value='']::part(display-input),
|
||||
wa-option[value=''] {
|
||||
font-style: italic;
|
||||
color: var(--wa-color-text-quiet);
|
||||
&::part(checked-icon) {
|
||||
order: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.default-badge {
|
||||
opacity: 0.6;
|
||||
margin-inline-start: var(--wa-space-xs);
|
||||
}
|
||||
}
|
||||
|
||||
34
docs/assets/styles/theme-icons.css
Normal file
34
docs/assets/styles/theme-icons.css
Normal file
@@ -0,0 +1,34 @@
|
||||
.theme-color-icon {
|
||||
display: grid;
|
||||
gap: var(--wa-space-xs);
|
||||
grid-template-columns: repeat(4, auto);
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--wa-border-radius-m);
|
||||
background-color: var(--background-color);
|
||||
border: var(--wa-border-width-s) var(--wa-border-style) var(--border-color);
|
||||
padding: var(--wa-space-2xs) var(--wa-space-xs);
|
||||
color: var(--text-color);
|
||||
font-weight: var(--wa-font-weight-semibold);
|
||||
|
||||
&.plain {
|
||||
font-weight: var(--wa-font-weight-bold);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.theme-typography-icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--wa-space-xs);
|
||||
|
||||
h3,
|
||||
p {
|
||||
margin-block: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
@@ -19,13 +19,13 @@ icon: card
|
||||
|
||||
<div slot="footer">
|
||||
<wa-button variant="brand" pill>More Info</wa-button>
|
||||
<wa-rating></wa-rating>
|
||||
<wa-rating label="Rating"></wa-rating>
|
||||
</div>
|
||||
</wa-card>
|
||||
|
||||
<style>
|
||||
.card-overview {
|
||||
max-width: 300px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.card-overview small {
|
||||
|
||||
@@ -450,7 +450,7 @@ Use the `--aspect-ratio` custom property to customize the size of the carousel's
|
||||
const carousel = document.querySelector('wa-carousel.aspect-ratio');
|
||||
const aspectRatio = document.querySelector('wa-select[name="aspect"]');
|
||||
|
||||
aspectRatio.addEventlistener('change', () => {
|
||||
aspectRatio.addEventListener('change', () => {
|
||||
carousel.style.setProperty('--aspect-ratio', aspectRatio.value);
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -82,7 +82,7 @@ Use the `setCustomValidity()` method to set a custom validation message. This wi
|
||||
});
|
||||
|
||||
// Update validity on change
|
||||
checkbox.addEventlistener('change', () => {
|
||||
checkbox.addEventListener('change', () => {
|
||||
checkbox.setCustomValidity(checkbox.checked ? '' : errorMessage);
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@ icon: copy-button
|
||||
<wa-copy-button value="Web Awesome rocks!"></wa-copy-button>
|
||||
```
|
||||
|
||||
:::info
|
||||
Copy buttons use the browser's [`clipboard.writeText()`](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText) method, which requires a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts) (HTTPS) in most browsers.
|
||||
:::
|
||||
|
||||
## Examples
|
||||
|
||||
### Custom Labels
|
||||
|
||||
@@ -2,13 +2,10 @@
|
||||
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.
|
||||
layout: overview
|
||||
categories:
|
||||
- actions
|
||||
- feedback: 'Feedback & Status'
|
||||
- imagery
|
||||
- inputs
|
||||
- navigation
|
||||
- organization
|
||||
- helpers: 'Utilities'
|
||||
override:tags: []
|
||||
categories:
|
||||
tags: [actions, feedback, imagery, inputs, navigation, organization, helpers]
|
||||
titles:
|
||||
feedback: 'Feedback & Status'
|
||||
helpers: 'Utilities'
|
||||
---
|
||||
|
||||
@@ -52,8 +52,8 @@ wa-page {
|
||||
}
|
||||
|
||||
&[slot^='navigation'] {
|
||||
background: var(--wa-color-violet-80);
|
||||
color: var(--wa-color-violet-20);
|
||||
background: var(--wa-color-purple-80);
|
||||
color: var(--wa-color-purple-20);
|
||||
}
|
||||
|
||||
strong {
|
||||
|
||||
@@ -74,3 +74,15 @@ Add the `size` attribute to the [Radio Group](/docs/components/radio-group) to c
|
||||
<wa-radio value="3">Large 3</wa-radio>
|
||||
</wa-radio-group>
|
||||
```
|
||||
|
||||
### Hint
|
||||
|
||||
Add descriptive hint to a switch with the `hint` attribute. For hints that contain HTML, use the `hint` slot instead.
|
||||
|
||||
```html {.example}
|
||||
<wa-radio-group label="Select an option" name="a" value="1">
|
||||
<wa-radio value="1" hint="What should the user know about radio 1?">Option 1</wa-radio>
|
||||
<wa-radio value="2" hint="What should the user know about radio 2?">Option 2</wa-radio>
|
||||
<wa-radio value="3" hint="What should the user know about radio 3?">Option 3</wa-radio>
|
||||
</wa-radio-group>
|
||||
```
|
||||
|
||||
@@ -130,6 +130,15 @@ Note that multi-select options may wrap, causing the control to expand verticall
|
||||
|
||||
Use the `value` attribute to set the initial selection.
|
||||
|
||||
```html {.example}
|
||||
<wa-select value="option-1">
|
||||
<wa-option value="option-1">Option 1</wa-option>
|
||||
<wa-option value="option-2">Option 2</wa-option>
|
||||
<wa-option value="option-3">Option 3</wa-option>
|
||||
<wa-option value="option-4">Option 4</wa-option>
|
||||
</wa-select>
|
||||
```
|
||||
|
||||
When using `multiple`, the `value` _attribute_ uses space-delimited values to select more than one option. Because of this, `<wa-option>` values cannot contain spaces. If you're accessing the `value` _property_ through Javascript, it will be an array.
|
||||
|
||||
```html {.example}
|
||||
@@ -294,7 +303,7 @@ Remember that custom tags are rendered in a shadow root. To style them, you can
|
||||
return `
|
||||
<wa-tag removable>
|
||||
<wa-icon name="${name}" style="padding-inline-end: .5rem;"></wa-icon>
|
||||
${option.getTextLabel()}
|
||||
${option.label}
|
||||
</wa-tag>
|
||||
`;
|
||||
};
|
||||
|
||||
@@ -208,7 +208,7 @@ Try resizing the example below with each option and notice how the panels respon
|
||||
const splitPanel = container.querySelector('wa-split-panel');
|
||||
const select = container.querySelector('wa-select');
|
||||
|
||||
select.addEventlistener('change', () => (splitPanel.primary = select.value));
|
||||
select.addEventListener('change', () => (splitPanel.primary = select.value));
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ The `selection` attribute lets you change the selection behavior of the tree.
|
||||
const selectionMode = document.querySelector('#selection-mode');
|
||||
const tree = document.querySelector('.tree-selectable');
|
||||
|
||||
selectionMode.addEventlistener('change', () => {
|
||||
selectionMode.addEventListener('change', () => {
|
||||
tree.querySelectorAll('wa-tree-item').forEach(item => (item.selected = false));
|
||||
tree.selection = selectionMode.value;
|
||||
});
|
||||
|
||||
@@ -16,21 +16,21 @@ To use a theme, simply add a link to the theme's stylesheet to the `<head>` of y
|
||||
<link rel="stylesheet" href="{% cdnUrl 'styles/themes/awesome.css' %}" />
|
||||
```
|
||||
|
||||
You can [customize any theme](/docs/themes/creating) just with CSS — no preprocessor required. All design tokens are prefixed with `--wa-` to avoid collisions with other libraries or your own custom properties. Simply override any design token in your own stylesheet by scoping your styles to `:where(:root)`, `:host`, the class for the specific theme you want to override (if needed), and the class for the relevant color scheme (if needed). Here's an example that changes the default brand color to violet in light mode:
|
||||
You can [customize any theme](/docs/themes/creating) just with CSS — no preprocessor required. All design tokens are prefixed with `--wa-` to avoid collisions with other libraries or your own custom properties. Simply override any design token in your own stylesheet by scoping your styles to `:where(:root)`, `:host`, the class for the specific theme you want to override (if needed), and the class for the relevant color scheme (if needed). Here's an example that changes the default brand color to purple in light mode:
|
||||
|
||||
```css
|
||||
:where(:root),
|
||||
:host,
|
||||
.wa-light,
|
||||
.wa-dark .wa-invert {
|
||||
--wa-color-brand-fill-quiet: var(--wa-color-violet-95);
|
||||
--wa-color-brand-fill-normal: var(--wa-color-violet-90);
|
||||
--wa-color-brand-fill-loud: var(--wa-color-violet-50);
|
||||
--wa-color-brand-border-quiet: var(--wa-color-violet-90);
|
||||
--wa-color-brand-border-normal: var(--wa-color-violet-80);
|
||||
--wa-color-brand-border-loud: var(--wa-color-violet-60);
|
||||
--wa-color-brand-on-quiet: var(--wa-color-violet-40);
|
||||
--wa-color-brand-on-normal: var(--wa-color-violet-30);
|
||||
--wa-color-brand-fill-quiet: var(--wa-color-purple-95);
|
||||
--wa-color-brand-fill-normal: var(--wa-color-purple-90);
|
||||
--wa-color-brand-fill-loud: var(--wa-color-purple-50);
|
||||
--wa-color-brand-border-quiet: var(--wa-color-purple-90);
|
||||
--wa-color-brand-border-normal: var(--wa-color-purple-80);
|
||||
--wa-color-brand-border-loud: var(--wa-color-purple-60);
|
||||
--wa-color-brand-on-quiet: var(--wa-color-purple-40);
|
||||
--wa-color-brand-on-normal: var(--wa-color-purple-30);
|
||||
--wa-color-brand-on-loud: white;
|
||||
}
|
||||
```
|
||||
@@ -99,8 +99,8 @@ Parts allow you to style *any* standard CSS property, not just those exposed thr
|
||||
|
||||
<style>
|
||||
.gradient-button::part(base) {
|
||||
background: linear-gradient(217deg, var(--wa-color-indigo-50), var(--wa-color-violet-50), var(--wa-color-red-50));
|
||||
border: solid 1px var(--wa-color-violet-50);
|
||||
background: linear-gradient(217deg, var(--wa-color-indigo-50), var(--wa-color-purple-50), var(--wa-color-red-50));
|
||||
border: solid 1px var(--wa-color-purple-50);
|
||||
transition: transform 100ms, box-shadow 100ms;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,80 @@
|
||||
/**
|
||||
* Global data for all pages
|
||||
*/
|
||||
import { sort } from '../_utils/filters.js';
|
||||
|
||||
export default {
|
||||
eleventyComputed: {
|
||||
children(data) {
|
||||
let mainTag = data.tags?.[0];
|
||||
let collection = data.collections[mainTag] ?? [];
|
||||
// Default parent. Can be overridden by explicitly setting parent in the data.
|
||||
// parent can refer to either an ancestor page in the URL or another page in the same directory
|
||||
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;
|
||||
}
|
||||
|
||||
44
docs/docs/experimental/clamped-colors.njk
Normal file
44
docs/docs/experimental/clamped-colors.njk
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: Clamped Color Tokens
|
||||
layout: block
|
||||
---
|
||||
|
||||
{% 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'] %}
|
||||
|
||||
<table class="colors">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="core-column">Core tint</th>
|
||||
{% for tint in tints -%}
|
||||
<th>{{ tint }}</th>
|
||||
{%- endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
{% for hue in hues -%}
|
||||
<tr class="wa-color-{{ hue }}">
|
||||
<th>{{ hue | capitalize }}</th>
|
||||
<td class="core-column">
|
||||
<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 }}
|
||||
<wa-copy-button value="--wa-color-{{ hue }}" copy-label="--wa-color-{{ hue }}"></wa-copy-button>
|
||||
</div>
|
||||
</td>
|
||||
{% for tint in tints -%}
|
||||
<td>
|
||||
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}-{{ tint }})">
|
||||
<wa-copy-button value="--wa-color-{{ hue }}-{{ tint }}" copy-label="--wa-color-{{ hue }}-{{ tint }}"></wa-copy-button>
|
||||
</div>
|
||||
</td>
|
||||
{%- endfor -%}
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
</table>
|
||||
|
||||
<style>
|
||||
.core-column .color.swatch::before {
|
||||
counter-reset: key var(--key);
|
||||
content: counter(key);
|
||||
}
|
||||
</style>
|
||||
@@ -576,10 +576,10 @@ hasOutline: false
|
||||
<wa-radio value="yellow"><span style="background-color:var(--wa-color-yellow-60);"></span></wa-radio>
|
||||
<wa-radio value="lime"><span style="background-color:var(--wa-color-lime-60);"></span></wa-radio>
|
||||
<wa-radio value="green"><span style="background-color:var(--wa-color-green-60);"></span></wa-radio>
|
||||
<wa-radio value="teal"><span style="background-color:var(--wa-color-teal-60);"></span></wa-radio>
|
||||
<wa-radio value="cyan"><span style="background-color:var(--wa-color-cyan-60);"></span></wa-radio>
|
||||
<wa-radio value="blue"><span style="background-color:var(--wa-color-blue-60);"></span></wa-radio>
|
||||
<wa-radio value="indigo"><span style="background-color:var(--wa-color-indigo-60);"></span></wa-radio>
|
||||
<wa-radio value="violet"><span style="background-color:var(--wa-color-violet-60);"></span></wa-radio>
|
||||
<wa-radio value="purple"><span style="background-color:var(--wa-color-purple-60);"></span></wa-radio>
|
||||
<wa-radio value="gray"><span style="background-color:var(--wa-color-gray-60);"></span></wa-radio>
|
||||
</wa-radio-group>
|
||||
</wa-details>
|
||||
@@ -803,7 +803,7 @@ hasOutline: false
|
||||
const queue = [];
|
||||
let inputTimeout;
|
||||
|
||||
variantInput.addEventlistener('change', () => {
|
||||
variantInput.addEventListener('change', () => {
|
||||
iconList.dataset.variant = variantInput.value;
|
||||
});
|
||||
|
||||
@@ -1084,10 +1084,10 @@ hasOutline: false
|
||||
el.classList.add(`wa-theme-${theme}-${colorMode}`);
|
||||
}
|
||||
|
||||
colorModeSelect.addEventlistener('change', setColorMode);
|
||||
colorModeSelect.addEventListener('change', setColorMode);
|
||||
|
||||
// Theme Switcher
|
||||
themeSelect.addEventlistener('change', event => {
|
||||
themeSelect.addEventListener('change', event => {
|
||||
const theme = event.target.value
|
||||
const newStylesheet = Object.assign(document.createElement("link"), {
|
||||
// This media: "print" allows us to lazy load the stylesheet then hot swap it on load.
|
||||
@@ -1132,14 +1132,14 @@ hasOutline: false
|
||||
});
|
||||
|
||||
// Color Palette
|
||||
colorSelect.addEventlistener('change', event => {
|
||||
colorSelect.addEventListener('change', event => {
|
||||
const colorPalette = event.target.value;
|
||||
|
||||
colorStylesheet.href = `/dist/styles/themes/color/${colorPalette}.css`;
|
||||
});
|
||||
|
||||
// Brand Color
|
||||
brandColor.addEventlistener('change', event => {
|
||||
brandColor.addEventListener('change', event => {
|
||||
const documentStyles = document.documentElement.style
|
||||
documentStyles.setProperty('--wa-color-primary-95', `var(--wa-color-${event.target.value}-95)`);
|
||||
documentStyles.setProperty('--wa-color-primary-90', `var(--wa-color-${event.target.value}-90)`);
|
||||
@@ -1223,7 +1223,7 @@ hasOutline: false
|
||||
})
|
||||
|
||||
// Pre-generated logos
|
||||
logoSelector.addEventlistener('change', event => {
|
||||
logoSelector.addEventListener('change', event => {
|
||||
const value = event.currentTarget.value
|
||||
|
||||
const projectLogo = previewContainer.querySelector("#project-logo");
|
||||
@@ -1279,7 +1279,7 @@ hasOutline: false
|
||||
})
|
||||
}
|
||||
|
||||
themeSelect.addEventlistener('change', setLogoIcons);
|
||||
themeSelect.addEventListener('change', setLogoIcons);
|
||||
|
||||
// Project Name
|
||||
container.querySelector('[name="project-name"]').addEventListener('input', event => {
|
||||
@@ -1293,7 +1293,7 @@ hasOutline: false
|
||||
});
|
||||
|
||||
// Heading text
|
||||
fontFamilyHeading.addEventlistener('change', event => {
|
||||
fontFamilyHeading.addEventListener('change', event => {
|
||||
let fontFamily;
|
||||
switch (event.target.value) {
|
||||
case 'assistant':
|
||||
@@ -1351,7 +1351,7 @@ hasOutline: false
|
||||
})
|
||||
|
||||
// Body text
|
||||
fontFamilyBody.addEventlistener('change', event => {
|
||||
fontFamilyBody.addEventListener('change', event => {
|
||||
let fontFamily;
|
||||
switch (event.target.value) {
|
||||
case 'assistant':
|
||||
@@ -1580,7 +1580,7 @@ hasOutline: false
|
||||
}
|
||||
|
||||
// Swaps icons to the preferred set for the selected theme
|
||||
themeSelect.addEventlistener('change', event => {
|
||||
themeSelect.addEventListener('change', event => {
|
||||
setPreferredIcons();
|
||||
showIconStyleOptions();
|
||||
syncLogoIcon();
|
||||
@@ -1599,13 +1599,13 @@ hasOutline: false
|
||||
});
|
||||
|
||||
// Changes available Icon Styles and swaps icons based on the selected Icon Family
|
||||
iconFamily.addEventlistener('change', event => {
|
||||
iconFamily.addEventListener('change', event => {
|
||||
useFaIcons();
|
||||
showIconStyleOptions();
|
||||
});
|
||||
|
||||
// Swaps icons based on the selected Icon Style
|
||||
iconStyle.addEventlistener('change', useFaIcons);
|
||||
iconStyle.addEventListener('change', useFaIcons);
|
||||
|
||||
|
||||
// Corners
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
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.
|
||||
layout: overview
|
||||
parentOf: layout
|
||||
categories: ["components", "utilities"]
|
||||
override:tags: []
|
||||
---
|
||||
@@ -22,4 +23,4 @@ Or, you can choose to import _only_ the utilities:
|
||||
```html
|
||||
<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>
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
Basic:
|
||||
|
||||
@@ -9,19 +9,11 @@ elements:
|
||||
file: styles/native/radio.css
|
||||
---
|
||||
|
||||
<style>
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
label + label {
|
||||
margin-inline-start: var(--wa-space);
|
||||
}
|
||||
</style>
|
||||
|
||||
```html {.example}
|
||||
<label><input type="radio" name="radio" value="1"> Option 1</label>
|
||||
<label><input type="radio" name="radio" value="2"> Option 2</label>
|
||||
<div class="wa-cluster">
|
||||
<label><input type="radio" name="radio" value="1"> Option 1</label>
|
||||
<label><input type="radio" name="radio" value="2"> Option 2</label>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Examples
|
||||
@@ -31,9 +23,11 @@ file: styles/native/radio.css
|
||||
To set the initial value and checked state, use the `checked` attribute on the corresponding radio.
|
||||
|
||||
```html {.example}
|
||||
<label><input type="radio" name="radio" value="1"> Option 1</label>
|
||||
<label><input type="radio" name="radio" value="2"> Option 2</label>
|
||||
<label><input type="radio" name="radio" value="3" checked> Option 3</label>
|
||||
<div class="wa-cluster">
|
||||
<label><input type="radio" name="radio" value="1"> Option 1</label>
|
||||
<label><input type="radio" name="radio" value="2"> Option 2</label>
|
||||
<label><input type="radio" name="radio" value="3" checked> Option 3</label>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Disabled
|
||||
@@ -41,9 +35,11 @@ To set the initial value and checked state, use the `checked` attribute on the c
|
||||
Use the `disabled` attribute to disable a radio.
|
||||
|
||||
```html {.example}
|
||||
<label><input type="radio" name="radio" value="1"> Option 1</label>
|
||||
<label><input type="radio" name="radio" value="2" disabled> Option 2</label>
|
||||
<label><input type="radio" name="radio" value="3"> Option 3</label>
|
||||
<div class="wa-cluster">
|
||||
<label><input type="radio" name="radio" value="1"> Option 1</label>
|
||||
<label><input type="radio" name="radio" value="2" disabled> Option 2</label>
|
||||
<label><input type="radio" name="radio" value="3"> Option 3</label>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Sizes
|
||||
@@ -51,27 +47,47 @@ Use the `disabled` attribute to disable a radio.
|
||||
Use the [size utilities](/docs/utilities/size) to change the radios' size.
|
||||
|
||||
```html {.example}
|
||||
<fieldset class="wa-size-s">
|
||||
<legend>Small</legend>
|
||||
<label><input type="radio" name="radio" value="1"> Option 1</label>
|
||||
<label><input type="radio" name="radio" value="2"> Option 2</label>
|
||||
<label><input type="radio" name="radio" value="3"> Option 3</label>
|
||||
<fieldset class="wa-size-s wa-cluster">
|
||||
<legend>Small</legend>
|
||||
<label><input type="radio" name="radio" value="1"> Option 1</label>
|
||||
<label><input type="radio" name="radio" value="2"> Option 2</label>
|
||||
<label><input type="radio" name="radio" value="3"> Option 3</label>
|
||||
</fieldset>
|
||||
|
||||
<br />
|
||||
<fieldset class="wa-size-m">
|
||||
<legend>Medium</legend>
|
||||
<label><input type="radio" name="radio" value="1"> Option 1</label>
|
||||
<label><input type="radio" name="radio" value="2"> Option 2</label>
|
||||
<label><input type="radio" name="radio" value="3"> Option 3</label>
|
||||
<fieldset class="wa-size-m wa-cluster">
|
||||
<legend>Medium</legend>
|
||||
<label><input type="radio" name="radio" value="1"> Option 1</label>
|
||||
<label><input type="radio" name="radio" value="2"> Option 2</label>
|
||||
<label><input type="radio" name="radio" value="3"> Option 3</label>
|
||||
</fieldset>
|
||||
|
||||
<br />
|
||||
|
||||
<fieldset class="wa-size-l">
|
||||
<legend>Large</legend>
|
||||
<label><input type="radio" name="radio" value="1"> Option 1</label>
|
||||
<label><input type="radio" name="radio" value="2"> Option 2</label>
|
||||
<label><input type="radio" name="radio" value="3"> Option 3</label>
|
||||
<fieldset class="wa-size-l wa-cluster">
|
||||
<legend>Large</legend>
|
||||
<label><input type="radio" name="radio" value="1"> Option 1</label>
|
||||
<label><input type="radio" name="radio" value="2"> Option 2</label>
|
||||
<label><input type="radio" name="radio" value="3"> Option 3</label>
|
||||
</fieldset>
|
||||
```
|
||||
|
||||
### Orientation
|
||||
|
||||
You can wrap native radios in a flex container to give them a horizontal or vertical orientation with even spacing. The convenience [`wa-cluster`](/docs/utilities/cluster) and [`wa-stack`](/docs/utilities/stack) utilities make this easy.
|
||||
|
||||
```html {.example}
|
||||
<div class="wa-cluster">
|
||||
<label><input type="radio" name="radio" value="1"> Option 1</label>
|
||||
<label><input type="radio" name="radio" value="2"> Option 2</label>
|
||||
<label><input type="radio" name="radio" value="3" checked> Option 3</label>
|
||||
</div>
|
||||
```
|
||||
|
||||
```html {.example}
|
||||
<div class="wa-stack">
|
||||
<label><input type="radio" name="radio" value="1"> Option 1</label>
|
||||
<label><input type="radio" name="radio" value="2"> Option 2</label>
|
||||
<label><input type="radio" name="radio" value="3" checked> Option 3</label>
|
||||
</div>
|
||||
```
|
||||
|
||||
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
|
||||
description: This is the palette used in the default theme.
|
||||
order: 0
|
||||
---
|
||||
|
||||
@@ -5,6 +5,6 @@ layout: overview
|
||||
override:tags: []
|
||||
forTag: palette
|
||||
categories:
|
||||
tags: [other, pro]
|
||||
other: Free
|
||||
pro: Pro
|
||||
---
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"layout": "palette.njk",
|
||||
"tags": ["palettes", "palette"],
|
||||
"wide": true,
|
||||
"eleventyComputed": {
|
||||
"snippet": ".wa-palette-{{ page.fileSlug }}",
|
||||
"icon": "palette"
|
||||
"icon": "palette",
|
||||
"file": "styles/color/{{ page.fileSlug }}.css"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,5 @@
|
||||
title: Patterns
|
||||
description: Patterns are reusable solutions to common design problems.
|
||||
layout: overview
|
||||
categories: ["e-commerce"]
|
||||
listChildren: true
|
||||
override:tags: []
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Changelog
|
||||
description: Changes to each version of the project are documented here.
|
||||
layout: page
|
||||
layout: page-outline
|
||||
---
|
||||
|
||||
Web Awesome follows [Semantic Versioning](https://semver.org/). Breaking changes in components with the <wa-badge variant="brand" pill>Stable</wa-badge> badge will not be accepted until the next major version. As such, all contributions must consider the project's roadmap and take this into consideration. Features that are deemed no longer necessary will be deprecated but not removed.
|
||||
@@ -12,6 +12,78 @@ Components with the <wa-badge variant="warning" pill>Experimental</wa-badge> bad
|
||||
During the alpha period, things might break! We take breaking changes very seriously, but sometimes they're necessary to make the final product that much better. We appreciate your patience!
|
||||
:::
|
||||
|
||||
## 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 palette tweaking UI. Tweak hue, grays, overall colorfulness, save or share the results.
|
||||
- Added a `pink` scale to all color palettes
|
||||
- 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
|
||||
|
||||
- Added `--wa-color-[hue]` tokens with the "core" color of each scale, regardless of which tint it lives on.
|
||||
You can find them in the first column of each color palette.
|
||||
|
||||
### Themes
|
||||
|
||||
- Improved UI for theme remixing:
|
||||
- You can now override the brand color of any theme with any of the 9 hues supported.
|
||||
- Rich previews
|
||||
- Generated copyable code snippets.
|
||||
- Permalinks
|
||||
- Updated Active, Glossy, Playful, and Premium themes so that `--wa-color-brand-fill-loud` uses the core color of the chosen brand color, regardless of tint.
|
||||
|
||||
### Components
|
||||
|
||||
#### `<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.
|
||||
|
||||
#### `<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
|
||||
|
||||
- Added an orientation example to the native radio docs
|
||||
- Fixed a number of broken event listeners throughout the docs
|
||||
|
||||
## 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.
|
||||
|
||||
1
docs/docs/themes/active.md
vendored
1
docs/docs/themes/active.md
vendored
@@ -4,4 +4,5 @@ description: Energetic and tactile, always in motion.
|
||||
isPro: true
|
||||
tags: pro
|
||||
palette: rudimentary
|
||||
brand: green
|
||||
---
|
||||
|
||||
1
docs/docs/themes/awesome.md
vendored
1
docs/docs/themes/awesome.md
vendored
@@ -3,4 +3,5 @@ title: Awesome
|
||||
description: Punchy and vibrant, the rockstar of themes.
|
||||
order: 0.2
|
||||
palette: bright
|
||||
brand: blue
|
||||
---
|
||||
|
||||
1
docs/docs/themes/brutalist.md
vendored
1
docs/docs/themes/brutalist.md
vendored
@@ -4,4 +4,5 @@ description: Sharp, square, and unapologetically bold.
|
||||
isPro: true
|
||||
tags: pro
|
||||
palette: default
|
||||
brand: blue
|
||||
---
|
||||
|
||||
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
|
||||
.wa-dark,
|
||||
.wa-invert,
|
||||
:is(:host-context(.wa-dark)) {
|
||||
.wa-invert {
|
||||
/* your custom styles here */
|
||||
}
|
||||
```
|
||||
|
||||
1
docs/docs/themes/default.md
vendored
1
docs/docs/themes/default.md
vendored
@@ -3,4 +3,5 @@ title: Default
|
||||
description: Your trusty companion, like a perfectly broken-in pair of jeans.
|
||||
order: 0
|
||||
palette: default
|
||||
brand: blue
|
||||
---
|
||||
|
||||
69
docs/docs/themes/demo.njk
vendored
69
docs/docs/themes/demo.njk
vendored
@@ -10,16 +10,17 @@ override:tags: []
|
||||
eleventyComputed:
|
||||
forceTheme: "{{ theme.fileSlug }}"
|
||||
---
|
||||
|
||||
<link rel="stylesheet" href="/dist/styles/themes/{{ theme.fileSlug }}.css" />
|
||||
{% set isPro = theme.data.isPro %}
|
||||
{% set status = theme.data.status %}
|
||||
{% set since = theme.data.since %}
|
||||
<link rel="stylesheet" href="/docs/themes/showcase.css" />
|
||||
|
||||
{% set content %}
|
||||
<header>
|
||||
{% include 'breadcrumbs.njk' %}
|
||||
<h1 class="title">{{ theme.data.title }}</h1>
|
||||
<p id="mix_and_match" hidden class="wa-size-s"></p>
|
||||
<p>{% include 'status.njk' %}</p>
|
||||
<p id="mix_and_match" class="wa-size-s"></p>
|
||||
<p id="theme-status">{% include 'status.njk' %}</p>
|
||||
<p id="theme-showcase-description">{{ theme.data.description | inlineMarkdown | safe }}</p>
|
||||
</header>
|
||||
{% include 'theme-showcase.njk' %}
|
||||
@@ -34,46 +35,56 @@ eleventyComputed:
|
||||
</div>
|
||||
</wa-image-comparer>
|
||||
|
||||
<script>
|
||||
<script type="module">
|
||||
import { urls as stylesheetURLs, docsURLs, icons } from "/assets/scripts/tweak/data.js";
|
||||
import { getThemeCode } from "/assets/scripts/tweak/code.js";
|
||||
|
||||
function updateTheme() {
|
||||
let params = new URLSearchParams(window.location.search);
|
||||
let script = document.currentScript;
|
||||
const stylesheetURLs = {
|
||||
colors: id => `/dist/styles/themes/${ id }/color.css`,
|
||||
palette: id => `/dist/styles/color/${ id }.css`,
|
||||
typography: id => `/dist/styles/themes/${ id }/typography.css`
|
||||
};
|
||||
const icons = {
|
||||
colors: 'palette',
|
||||
palette: 'swatchbook',
|
||||
typography: 'font-case'
|
||||
}
|
||||
params = Object.fromEntries(params.entries());
|
||||
|
||||
for (let link of document.querySelectorAll('link.mix-and-match')) {
|
||||
link.remove();
|
||||
}
|
||||
|
||||
let msgs = [];
|
||||
let tweaks = [];
|
||||
let code = getThemeCode("{{ theme.fileSlug }}", params, {attributes: 'class="mix-and-match"'});
|
||||
document.head.insertAdjacentHTML('beforeend', code);
|
||||
|
||||
for (let name in stylesheetURLs) {
|
||||
if (params.get(name)) {
|
||||
let url = stylesheetURLs[name](params.get(name));
|
||||
script.insertAdjacentHTML('afterend', `<link rel="stylesheet" href="${ url }" class="mix-and-match" />`);
|
||||
let docsURL = (name === 'palette' ? '/docs/palettes/' : '/docs/themes/') + params.get(name) + '/';
|
||||
let title = params.get(name).replace(/^[a-z]/g, c => c.toUpperCase());
|
||||
let override = params[name];
|
||||
if (override) {
|
||||
let docsURL = docsURLs[name] ? docsURLs[name] + override + '/' : '';
|
||||
let title = override.replace(/^[a-z]/g, c => c.toUpperCase());
|
||||
|
||||
if (docsURL) {
|
||||
title = `<a href="${ docsURL }">${ title }</a>`;
|
||||
}
|
||||
|
||||
let icon = icons[name];
|
||||
msgs.push(`<wa-icon name="${icon}" variant="regular"></wa-icon> <a href="${ docsURL }">${ title }</a>`);
|
||||
tweaks.push(`<wa-icon name="${icon}" variant="regular"></wa-icon> ${ title }`);
|
||||
}
|
||||
}
|
||||
|
||||
for (let p of mix_and_match) {
|
||||
p.hidden = msgs.length === 0;
|
||||
if (msgs.length) {
|
||||
let icon =
|
||||
p.innerHTML = `<strong><wa-icon name="merge"></wa-icon> Remixed</strong> ` + msgs.map(msg => `<wa-badge appearance=outlined>
|
||||
${ msg }</wa-badge>`).join(' ');
|
||||
let isRemixed = tweaks.length > 0;
|
||||
document.documentElement.classList.toggle('is-remixed', isRemixed);
|
||||
|
||||
if (isRemixed) {
|
||||
for (let p of document.querySelectorAll("#theme-status")) {
|
||||
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();
|
||||
</script>
|
||||
|
||||
1
docs/docs/themes/glossy.md
vendored
1
docs/docs/themes/glossy.md
vendored
@@ -4,4 +4,5 @@ description: Bustling with plenty of luster and shine.
|
||||
isPro: true
|
||||
tags: pro
|
||||
palette: elegant
|
||||
brand: indigo
|
||||
---
|
||||
|
||||
9
docs/docs/themes/index.njk
vendored
9
docs/docs/themes/index.njk
vendored
@@ -1,13 +1,13 @@
|
||||
---
|
||||
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).
|
||||
layout: overview
|
||||
override:tags: []
|
||||
forTag: theme
|
||||
categories:
|
||||
tags: [other, pro]
|
||||
other: Free
|
||||
pro: Pro
|
||||
---
|
||||
|
||||
<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.
|
||||
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:
|
||||
|
||||
@@ -44,8 +44,7 @@ For example, the default theme is set up like this:
|
||||
}
|
||||
|
||||
.wa-dark,
|
||||
.wa-invert,
|
||||
:host-context(.wa-dark) {
|
||||
.wa-invert {
|
||||
/* subset of CSS custom properties for a dark color scheme */
|
||||
}
|
||||
```
|
||||
|
||||
1
docs/docs/themes/matter.md
vendored
1
docs/docs/themes/matter.md
vendored
@@ -4,6 +4,7 @@ description: Digital design inspired by the real world.
|
||||
isPro: true
|
||||
tags: pro
|
||||
palette: mild
|
||||
brand: indigo
|
||||
---
|
||||
|
||||
Set the page theme to "{{ title }}" from the top right to preview the following examples.
|
||||
|
||||
1
docs/docs/themes/playful.md
vendored
1
docs/docs/themes/playful.md
vendored
@@ -4,4 +4,5 @@ description: Cheerful and engaging, like a playground on screen.
|
||||
isPro: true
|
||||
tags: pro
|
||||
palette: rudimentary
|
||||
brand: purple
|
||||
---
|
||||
|
||||
1
docs/docs/themes/premium.md
vendored
1
docs/docs/themes/premium.md
vendored
@@ -4,4 +4,5 @@ description: The ultimate in sophistication and style.
|
||||
isPro: true
|
||||
tags: pro
|
||||
palette: anodized
|
||||
brand: cyan
|
||||
---
|
||||
|
||||
102
docs/docs/themes/remix.css
vendored
Normal file
102
docs/docs/themes/remix.css
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
#mix_and_match {
|
||||
margin-block-end: var(--wa-space-2xl);
|
||||
|
||||
&::part(content) {
|
||||
display: flex;
|
||||
gap: var(--wa-space-xl);
|
||||
padding-block-start: var(--wa-space-m);
|
||||
}
|
||||
|
||||
> [slot='summary'] {
|
||||
margin: 0;
|
||||
|
||||
wa-icon:first-of-type {
|
||||
vertical-align: -0.15em;
|
||||
color: var(--wa-color-text-quiet);
|
||||
}
|
||||
|
||||
wa-icon#what-is-remixing {
|
||||
vertical-align: -0.1em;
|
||||
font-size: var(--wa-font-size-s);
|
||||
color: var(--wa-color-text-quiet);
|
||||
}
|
||||
}
|
||||
|
||||
wa-select {
|
||||
&::part(label) {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
}
|
||||
|
||||
wa-select:has(> wa-option > wa-card) {
|
||||
&::part(listbox) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
width: min(90vw, 800px);
|
||||
}
|
||||
|
||||
&:state(blank)::part(display-input) {
|
||||
font-style: italic;
|
||||
color: var(--wa-color-text-quiet);
|
||||
}
|
||||
}
|
||||
|
||||
wa-option:has(> wa-card) {
|
||||
position: relative;
|
||||
padding: var(--wa-space-smaller);
|
||||
|
||||
&::part(checked-icon) {
|
||||
position: absolute;
|
||||
inset-block-start: calc(var(--wa-space-smaller) - 0.5em);
|
||||
inset-inline-end: calc(var(--wa-space-smaller) - 0.5em);
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
line-height: 1em;
|
||||
padding: 0.4em;
|
||||
border-radius: var(--wa-border-radius-circle);
|
||||
text-align: center;
|
||||
background: var(--wa-color-brand-fill-loud);
|
||||
color: var(--wa-color-brand-on-loud);
|
||||
font-size: var(--wa-font-size-xs);
|
||||
}
|
||||
|
||||
wa-card {
|
||||
--spacing: var(--wa-space-smaller);
|
||||
|
||||
&::part(body) {
|
||||
background: var(--wa-color-neutral-fill-quiet);
|
||||
padding-block: var(--wa-space-s);
|
||||
}
|
||||
}
|
||||
|
||||
&:state(current),
|
||||
&:state(hover),
|
||||
&:state(selected),
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
wa-card {
|
||||
border-color: var(--wa-color-brand-border-loud);
|
||||
box-shadow: 0 0 0 var(--wa-border-width-m) var(--wa-color-brand-border-normal);
|
||||
}
|
||||
}
|
||||
|
||||
&[aria-selected='true'] {
|
||||
wa-card {
|
||||
--border-color: var(--wa-color-brand-border-loud);
|
||||
box-shadow: 0 0 0 var(--wa-border-width-m) var(--wa-color-brand-border-loud);
|
||||
|
||||
&::part(body) {
|
||||
background: var(--wa-color-brand-fill-quiet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#test_select wa-option:state(selected) {
|
||||
outline: 2px solid red;
|
||||
background: yellow;
|
||||
}
|
||||
166
docs/docs/themes/remix.js
vendored
Normal file
166
docs/docs/themes/remix.js
vendored
Normal file
@@ -0,0 +1,166 @@
|
||||
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)));
|
||||
|
||||
const domChange = document.startViewTransition ? document.startViewTransition.bind(document) : fn => fn();
|
||||
|
||||
let selects, data, codeSnippets;
|
||||
|
||||
let computed = {
|
||||
get isRemixed() {
|
||||
return Object.values(data.params).filter(Boolean).length > 0;
|
||||
},
|
||||
get palette() {
|
||||
return data.params.palette || data.defaultParams.palette;
|
||||
},
|
||||
get brand() {
|
||||
return data.params.brand || data.defaultParams.brand;
|
||||
},
|
||||
};
|
||||
|
||||
function selectsChanged(event) {
|
||||
data.params[event.target.name] = event.target.value;
|
||||
render(event.target.name);
|
||||
}
|
||||
|
||||
function init() {
|
||||
selects = Object.fromEntries(
|
||||
[...document.querySelectorAll('#mix_and_match wa-select')].map(select => [select.getAttribute('name'), select]),
|
||||
);
|
||||
|
||||
codeSnippets = document.querySelector('#usage ~ wa-tab-group.import-stylesheet-code:first-of-type');
|
||||
codeSnippets = {
|
||||
html: codeSnippets.querySelector('code.language-html'),
|
||||
css: codeSnippets.querySelector('code.language-css'),
|
||||
};
|
||||
|
||||
data = {
|
||||
baseTheme: wa_data.baseTheme,
|
||||
themes: wa_data.themes,
|
||||
palettes: wa_data.palettes,
|
||||
defaultParams: {
|
||||
colors: '',
|
||||
get palette() {
|
||||
let colors = data.params.colors || data.baseTheme;
|
||||
return data.themes[colors].palette;
|
||||
},
|
||||
get brand() {
|
||||
let colors = data.params.colors || data.baseTheme;
|
||||
return data.themes[colors].brand;
|
||||
},
|
||||
typography: '',
|
||||
},
|
||||
params: { colors: '', palette: '', brand: '', typography: '' },
|
||||
urlParams: new Permalink(),
|
||||
};
|
||||
|
||||
// Apply params from permalink
|
||||
for (let key in data.params) {
|
||||
if (data.urlParams.has(key)) {
|
||||
data.params[key] = data.urlParams.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (computed.isRemixed) {
|
||||
// Start with the remixing UI open if the theme has been remixed
|
||||
mix_and_match.setAttribute('open', '');
|
||||
mix_and_match.open = true;
|
||||
}
|
||||
|
||||
for (let name in selects) {
|
||||
selects[name].addEventListener('change', selectsChanged);
|
||||
}
|
||||
|
||||
Promise.all(Object.values(selects).map(select => select.updateComplete)).then(() => render());
|
||||
|
||||
return { selects, codeSnippets, data, computed, render };
|
||||
}
|
||||
|
||||
globalThis.remixApp = init();
|
||||
|
||||
// Async load CSS for other themes *before* current theme stylesheet
|
||||
let themeStylesheet = document.querySelector('#theme-stylesheet');
|
||||
|
||||
for (const theme in data.themes) {
|
||||
themeStylesheet.insertAdjacentHTML(
|
||||
'beforebegin',
|
||||
`<link rel="preload" as="style" href="/dist/styles/themes/${theme}/color.css" onload="this.rel = 'stylesheet'" />
|
||||
<link rel="preload" as="style" href="/dist/styles/themes/${theme}/typography.css" onload="this.rel = 'stylesheet'" />`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const palette in data.palettes) {
|
||||
themeStylesheet.insertAdjacentHTML(
|
||||
'beforebegin',
|
||||
`<link rel="preload" as="style" href="/dist/styles/color/${palette}.css" onload="this.rel = 'stylesheet'" />`,
|
||||
);
|
||||
}
|
||||
|
||||
function setDefault(select, value) {
|
||||
let oldDefaultOption = select.querySelector(`wa-option[value=""]:not([data-id="${value}"])`);
|
||||
let newDefaultOption = select.querySelector(`wa-option[value="${value}"]`);
|
||||
|
||||
if (oldDefaultOption) {
|
||||
oldDefaultOption.value = oldDefaultOption.dataset.id;
|
||||
}
|
||||
|
||||
if (newDefaultOption) {
|
||||
newDefaultOption.dataset.id ??= newDefaultOption.value;
|
||||
newDefaultOption.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function render(changedAspect) {
|
||||
if (!globalThis.demo) {
|
||||
return;
|
||||
}
|
||||
|
||||
let url = new URL(demo.src);
|
||||
|
||||
if (!changedAspect || changedAspect === 'colors') {
|
||||
// Update the default palette when the theme colors change to the default palette of that theme
|
||||
setDefault(selects.palette, data.defaultParams.palette);
|
||||
setDefault(selects.brand, data.defaultParams.brand);
|
||||
}
|
||||
|
||||
let brand = data.params.brand || data.defaultParams.brand;
|
||||
selects.brand.style.setProperty('--color', `var(--wa-color-${brand})`);
|
||||
|
||||
// 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) {
|
||||
let value = data.params[aspect];
|
||||
selects[aspect].value = value;
|
||||
}
|
||||
|
||||
for (let key in data.params) {
|
||||
if (data.params[key]) {
|
||||
data.urlParams.set(key, data.params[key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Update demo URL
|
||||
domChange(() => {
|
||||
url.search = data.urlParams;
|
||||
demo.src = url;
|
||||
return new Promise(resolve => (demo.onload = resolve));
|
||||
});
|
||||
|
||||
// Update page URL
|
||||
data.urlParams.updateLocation();
|
||||
|
||||
// Update code snippets
|
||||
for (let language in codeSnippets) {
|
||||
let codeSnippet = codeSnippets[language];
|
||||
let code = getThemeCode(data.baseTheme, data.params, { language, cdnUrl });
|
||||
codeSnippet.textContent = code;
|
||||
Prism.highlightElement(codeSnippet);
|
||||
}
|
||||
}
|
||||
|
||||
addEventListener('turbo:render', event => {
|
||||
remixApp = init();
|
||||
});
|
||||
18
docs/docs/themes/showcase.css
vendored
18
docs/docs/themes/showcase.css
vendored
@@ -12,9 +12,25 @@ body,
|
||||
#mix_and_match {
|
||||
font-weight: var(--wa-font-weight-semibold);
|
||||
color: var(--wa-color-text-quiet);
|
||||
margin-block-end: var(--wa-space-xs);
|
||||
|
||||
html:not(.is-remixed) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
wa-icon {
|
||||
vertical-align: middle;
|
||||
vertical-align: -0.15em;
|
||||
}
|
||||
|
||||
> strong {
|
||||
margin-inline-end: var(--wa-space-2xs);
|
||||
}
|
||||
|
||||
wa-badge {
|
||||
> a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1
docs/docs/themes/tailspin.md
vendored
1
docs/docs/themes/tailspin.md
vendored
@@ -4,4 +4,5 @@ description: Like a bird in flight, guiding you from there to here.
|
||||
isPro: true
|
||||
tags: pro
|
||||
palette: vogue
|
||||
brand: indigo
|
||||
---
|
||||
|
||||
6
docs/docs/themes/themes.json
vendored
6
docs/docs/themes/themes.json
vendored
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"layout": "theme.njk",
|
||||
"wide": true,
|
||||
"tags": ["themes", "theme"]
|
||||
"tags": ["themes", "theme"],
|
||||
"brand": "blue",
|
||||
"eleventyComputed": {
|
||||
"file": "styles/themes/{{ page.fileSlug }}.css"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user