mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 20:19:13 +00:00
Compare commits
87 Commits
isolate
...
dialog-fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f621d3890a | ||
|
|
09f668fc99 | ||
|
|
d451ba98e5 | ||
|
|
fd287edd56 | ||
|
|
8424b49646 | ||
|
|
fa24c0f70e | ||
|
|
1bba87c66d | ||
|
|
0db9ca12e3 | ||
|
|
041555fe99 | ||
|
|
b41dbd2de7 | ||
|
|
7c6f31e0c7 | ||
|
|
9e84274a93 | ||
|
|
2b3803f91e | ||
|
|
faed8da3cd | ||
|
|
17cf902f53 | ||
|
|
8214ff6b2d | ||
|
|
c9979e15f8 | ||
|
|
fcfe2bde7d | ||
|
|
59dcaaff83 | ||
|
|
5bad30ec30 | ||
|
|
87c1762146 | ||
|
|
899edd1d5e | ||
|
|
872a110b1e | ||
|
|
07fe6d598e | ||
|
|
79bafc513a | ||
|
|
1d03f7bee0 | ||
|
|
a9bf1bd838 | ||
|
|
c0ca739366 | ||
|
|
a6745602d6 | ||
|
|
da4f619d95 | ||
|
|
1283a696a5 | ||
|
|
d12b97b0b0 | ||
|
|
fb94eaea42 | ||
|
|
5b40020acb | ||
|
|
95819703f5 | ||
|
|
23dfdd907d | ||
|
|
f4e46ac422 | ||
|
|
e5c2884880 | ||
|
|
1d600a77c4 | ||
|
|
db3c568ba2 | ||
|
|
4bb9805ba6 | ||
|
|
bd935fa8d5 | ||
|
|
c3e582b47b | ||
|
|
4d094a4e19 | ||
|
|
782c404bdf | ||
|
|
f1438981b2 | ||
|
|
18b88c2f5c | ||
|
|
a2d85f49a3 | ||
|
|
be00026cd3 | ||
|
|
58ed88bc5a | ||
|
|
1d14e186f3 | ||
|
|
5f672aabc2 | ||
|
|
db08e12a32 | ||
|
|
e0fc639226 | ||
|
|
e6c662b543 | ||
|
|
d1de9a9a73 | ||
|
|
4931de8eb4 | ||
|
|
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 |
9
.github/workflows/ssr_tests.js.yml
vendored
9
.github/workflows/ssr_tests.js.yml
vendored
@@ -1,11 +1,12 @@
|
||||
# # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: SSR Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [next]
|
||||
# push:
|
||||
# branches: [next]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
ssr_test:
|
||||
|
||||
@@ -13,4 +13,4 @@ package-lock.json
|
||||
tsconfig.json
|
||||
cdn
|
||||
_site
|
||||
docs/assets/scripts/prism.js
|
||||
docs/assets/scripts/prism-downloaded.js
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as path from 'node:path';
|
||||
import { anchorHeadingsPlugin } from './_utils/anchor-headings.js';
|
||||
import { codeExamplesPlugin } from './_utils/code-examples.js';
|
||||
import { copyCodePlugin } from './_utils/copy-code.js';
|
||||
@@ -6,9 +7,10 @@ import { highlightCodePlugin } from './_utils/highlight-code.js';
|
||||
import { markdown } from './_utils/markdown.js';
|
||||
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 litPlugin from '@lit-labs/eleventy-plugin-lit';
|
||||
import { readFile } from 'fs/promises';
|
||||
import componentList from './_data/componentList.js';
|
||||
import nunjucks from 'nunjucks';
|
||||
// import componentList from './_data/componentList.js';
|
||||
import * as filters from './_utils/filters.js';
|
||||
import { outlinePlugin } from './_utils/outline.js';
|
||||
import { replaceTextPlugin } from './_utils/replace-text.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,22 @@ 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 +70,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 +140,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 +163,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 +189,31 @@ export default function (eleventyConfig) {
|
||||
eleventyConfig.addPassthroughCopy(glob);
|
||||
}
|
||||
|
||||
// // SSR plugin
|
||||
// // Make sure this is the last thing, we don't 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 { hueRanges as default } from '../assets/scripts/tweak/data.js';
|
||||
@@ -1 +1 @@
|
||||
["red", "yellow", "green", "cyan", "blue", "indigo", "purple", "gray"]
|
||||
["red", "orange", "yellow", "green", "cyan", "blue", "indigo", "purple", "pink", "gray"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default as default } from '../../src/styles/color/palettes-analyzed.js';
|
||||
export { default as default } from '../../src/styles/color/scripts/palettes-analyzed.js';
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-fa-kit-code="b10bfbde90" data-cdn-url="{% cdnUrl %}">
|
||||
<html lang="en" data-fa-kit-code="b10bfbde90" data-cdn-url="{% cdnUrl %}" class="wa-cloak">
|
||||
<head>
|
||||
{% include 'head.njk' %}
|
||||
<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 #}
|
||||
@@ -50,6 +50,9 @@
|
||||
Search
|
||||
<kbd slot="suffix" class="only-desktop">/</kbd>
|
||||
</wa-button>
|
||||
|
||||
{# Login #}
|
||||
{% server "loginOrAvatar" %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -76,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 }}">
|
||||
<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 and pages.length > 0 %}
|
||||
<h2 class="index-category" id="{{ category | slugify }}">
|
||||
{% 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>
|
||||
|
||||
@@ -23,10 +23,12 @@
|
||||
<script src="/assets/scripts/hydration-errors.js"></script>
|
||||
<link rel="stylesheet" href="/assets/styles/hydration-errors.css">
|
||||
<link rel="preconnect" href="https://cdn.jsdelivr.net">
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.10/+esm"></script>
|
||||
|
||||
{# Internal components #}
|
||||
<script type="module" src="/assets/components/scoped.js"></script>
|
||||
|
||||
{# Web Awesome #}
|
||||
<script type="module" src="/dist/webawesome.ssr-loader.js"></script>
|
||||
<script type="module" src="/dist/webawesome.loader.js"></script>
|
||||
|
||||
<script type="module" src="/assets/scripts/theme-picker.js"></script>
|
||||
{# Preset Theme #}
|
||||
@@ -47,3 +49,6 @@
|
||||
<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" %}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<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 -%}
|
||||
|
||||
@@ -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 page | show -%}
|
||||
<li>
|
||||
<a href="{{ page.url }}">{{ page.data.title }}</a>
|
||||
{% if page.data.status == 'experimental' %}<wa-icon name="flask"></wa-icon>{% endif %}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
{% set paletteId = palette.fileSlug or page.fileSlug %}
|
||||
{% set tints = [80, 60, 40, 20] %}
|
||||
{% 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>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{% set themeId = theme.fileSlug %}
|
||||
|
||||
<div>
|
||||
<template shadowrootmode="open">
|
||||
<wa-scoped class="theme-icon-host theme-color-icon-host">
|
||||
<template>
|
||||
<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="theme-icon 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>
|
||||
@@ -21,4 +21,4 @@
|
||||
{# <div class="wa-warning wa-outlined wa-filled"><wa-icon slot="icon" name="triangle-exclamation" variant="regular"></wa-icon></div> #}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</wa-scoped>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{% set themeId = theme.fileSlug %}
|
||||
{% set themeId = theme.fileSlug or page.fileSlug %}
|
||||
|
||||
<div>
|
||||
<template shadowrootmode="open">
|
||||
<wa-scoped class="theme-icon-host theme-typography-icon-host">
|
||||
<template>
|
||||
<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">
|
||||
<div class="theme-icon theme-typography-icon wa-theme-{{ themeId }}" data-no-outline data-no-anchor role="presentation">
|
||||
<h3>Title</h3>
|
||||
<p>Body text</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</wa-scoped>
|
||||
|
||||
29
docs/_includes/svgs/theme.njk
Normal file
29
docs/_includes/svgs/theme.njk
Normal file
@@ -0,0 +1,29 @@
|
||||
{% set themeId = theme.fileSlug or page.fileSlug %}
|
||||
|
||||
|
||||
<wa-scoped class="theme-icon-host theme-overall-icon-host">
|
||||
<template>
|
||||
<link rel="stylesheet" href="/dist/styles/utilities.css">
|
||||
<link rel="stylesheet" href="/dist/styles/native/content.css">
|
||||
<link rel="stylesheet" href="/dist/styles/themes/{{ themeId }}.css">
|
||||
<link rel="stylesheet" href="/assets/styles/theme-icons.css">
|
||||
|
||||
<div class="theme-icon theme-overall-icon" role="presentation" data-no-anchor data-no-outline>
|
||||
<div class="row row-1">
|
||||
<h2>Aa</h2>
|
||||
<div class="swatches">
|
||||
<div class="wa-brand"></div>
|
||||
|
||||
<div class="wa-success"></div>
|
||||
<div class="wa-warning"></div>
|
||||
<div class="wa-danger"></div>
|
||||
<div class="wa-neutral"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-2">
|
||||
<wa-input value="Input" size="small"></wa-input>
|
||||
<wa-button size="small" variant="brand">Go</wa-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</wa-scoped>
|
||||
@@ -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,17 +1,83 @@
|
||||
{% 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 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 }}../tweak.css" rel="stylesheet">
|
||||
<script type="module" src="{{ page.url }}../tweak.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<div id="palette-app" data-palette-id="{{ paletteId }}">
|
||||
<div
|
||||
:class="{
|
||||
tweaking: tweaking.chroma,
|
||||
'tweaking-chroma': tweaking.chroma,
|
||||
'tweaking-hue': tweaking.chroma,
|
||||
'tweaking-gray-chroma': tweaking.grayChroma,
|
||||
'tweaked-chroma': tweaked?.chroma,
|
||||
'tweaked-hue': tweaked?.hue,
|
||||
'tweaked-any': tweaked
|
||||
}"
|
||||
:style="{
|
||||
'--chroma-scale': chromaScale,
|
||||
'--gray-chroma': tweaked?.grayChroma ? grayChroma : '',
|
||||
}">
|
||||
|
||||
{% include 'breadcrumbs.njk' %}
|
||||
|
||||
<h1 v-if="saved" class="title">
|
||||
{% raw %}{{ saved.title }}{% endraw %}
|
||||
<wa-icon-button name="pencil" label="Rename palette" @click="rename"></wa-icon-button>
|
||||
<wa-icon-button class="delete" name="trash" label="Delete palette" @click="deleteSaved"></wa-icon-button>
|
||||
</h1>
|
||||
<h1 v-if="!saved" class="title">{{ title }}</h1>
|
||||
|
||||
<div class="block-info">
|
||||
<code class="class">.wa-palette-{{ paletteId }}</code>
|
||||
{% include '../_includes/status.njk' %}
|
||||
{% if not isPro %}
|
||||
<wa-badge class="pro" v-if="tweaked">PRO</wa-badge>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if description %}
|
||||
<p class="summary">
|
||||
{{ description | inlineMarkdown | safe }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block afterContent %}
|
||||
|
||||
{% set maxChroma = 0 %}
|
||||
|
||||
<wa-callout size="small" class="tweaked-callout" variant="warning">
|
||||
<wa-icon name="sliders-simple" slot="icon" variant="regular"></wa-icon>
|
||||
This palette has been tweaked.
|
||||
<div class="wa-cluster wa-gap-xs">
|
||||
<wa-tag v-for="tweakHumanReadable, param in tweaksHumanReadable" removable @wa-remove="reset(param)">{% raw %}{{ tweakHumanReadable }}{% endraw %}</wa-tag>
|
||||
</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-button v-if="!saved" @click="save" variant="success">
|
||||
<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>
|
||||
Save
|
||||
</wa-button>
|
||||
</wa-callout>
|
||||
|
||||
<table class="colors main wa-palette-{{ paletteId }}">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
@@ -21,18 +87,100 @@
|
||||
{%- endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
{# Initialize to last hue before gray #}
|
||||
{%- set hueBefore = hues[hues|length - 2] -%}
|
||||
{% for hue in hues -%}
|
||||
<tr>
|
||||
<th>{{ hue | capitalize }}</th>
|
||||
<td class="core-column">
|
||||
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}); color: var(--wa-color-{{ hue }}-{{ '05' if palettes[paletteId][hue].maxChromaTint > 60 else '95' }});">
|
||||
{{ palettes[paletteId][hue].maxChromaTint }}
|
||||
<wa-copy-button value="--wa-color-{{ hue }}" copy-label="--wa-color-{{ hue }}"></wa-copy-button>
|
||||
</div>
|
||||
{% set coreTint = palettes[paletteId][hue].maxChromaTint %}
|
||||
{%- set coreColor = palettes[paletteId][hue][coreTint] -%}
|
||||
{%- set maxChroma = coreColor.c if coreColor.c > maxChroma else maxChroma -%}
|
||||
{% if hue === 'gray' %}
|
||||
<tr data-hue="{{ hue }}" class="color-scale"
|
||||
:class="{tweaking: tweaking.grayChroma, tweaked: tweaked.grayChroma || tweaked.grayColor }">
|
||||
{% else %}
|
||||
<tr data-hue="{{ hue }}" class="color-scale"
|
||||
:class="{tweaking: tweaking.{{ hue }}, tweaked: hueShifts.{{ hue }} }"
|
||||
:style="{ '--hue-shift': hueShifts.{{ hue }} || '' }">
|
||||
{% endif %}
|
||||
<th>
|
||||
{{ hue | capitalize }}
|
||||
</th>
|
||||
<td class="core-column"
|
||||
style="--color: var(--wa-color-{{ hue }})"
|
||||
:style="{
|
||||
'--color-tweaked': colors.{{ hue }}[{{ coreTint }}],
|
||||
'--color-gray-undertone': colors[grayColor][{{coreTint}}],
|
||||
'--color-tweaked-no-gray-chroma': colorsMinusGrayChroma.{{ hue }}[{{ coreTint }}],
|
||||
}">
|
||||
<wa-dropdown>
|
||||
<div slot="trigger" id="core-{{ hue }}-swatch" data-tint="core" class="color swatch"
|
||||
style="background-color: var(--wa-color-{{ hue }}); color: var(--wa-color-{{ hue }}-{{ '05' if palettes[paletteId][hue].maxChromaTint > 60 else '95' }});"
|
||||
>
|
||||
{{ palettes[paletteId][hue].maxChromaTint }}
|
||||
<wa-icon name="sliders-simple" class="tweak-icon"></wa-icon>
|
||||
</div>
|
||||
<div class="popup">
|
||||
{% if hue === 'gray' %}
|
||||
<wa-radio-group class="core-color" orientation="horizontal" v-model="grayColor">
|
||||
{% for h in hues -%}
|
||||
{%- if h !== 'gray' -%}
|
||||
<wa-radio-button id="gray-undertone-{{ h }}" value="{{ h }}" label="{{ h | capitalize }}" style="--color: var(--wa-color-{{ h }})"></wa-radio-button>
|
||||
<wa-tooltip for="gray-undertone-{{ h }}" hoist>
|
||||
{{ h | capitalize }}
|
||||
</wa-tooltip>
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
<div slot="label">
|
||||
Gray undertone
|
||||
</div>
|
||||
</wa-radio-group>
|
||||
<div class="decorated-slider gray-chroma-slider" :style="{'--max': maxGrayChroma}">
|
||||
<wa-slider name="gray-chroma" v-model="grayChroma" ref="grayChromaSlider"
|
||||
value="0" min="0" :max="maxGrayChroma" step="0.01"
|
||||
@input="tweaking.grayChroma = true" @change="tweaking.grayChroma = false">
|
||||
<div slot="label">
|
||||
Gray colorfulness
|
||||
<wa-icon-button @click="grayChroma = originalGrayChroma" class="clear-button" name="circle-xmark" library="system" variant="regular" label="Reset"></wa-icon-button>
|
||||
</div>
|
||||
</wa-slider>
|
||||
<div class="label-min">Neutral</div>
|
||||
<div class="label-max" v-content="moreHue[grayColor]">Warmer/Cooler</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{%- set hueAfter = hues[loop.index0 + 1] -%}
|
||||
{%- set hueAfter = hues[0] if hueAfter == 'gray' else hueAfter -%}
|
||||
{%- set minShift = hueRanges[hue].min - coreColor.h | round -%}
|
||||
{%- set maxShift = hueRanges[hue].max - coreColor.h | round -%}
|
||||
|
||||
<div class="decorated-slider hue-shift-slider" style="--min: {{ minShift }}; --max: {{ maxShift }};">
|
||||
<wa-slider name="{{ hue }}-shift" v-model="hueShifts.{{ hue }}" value="0"
|
||||
min="{{ minShift }}" max="{{ maxShift }}" step="1"
|
||||
@input="tweaking.hue = tweaking.{{hue}} = true"
|
||||
@change="tweaking.hue = tweaking.{{ hue }} = false">
|
||||
<div slot="label">
|
||||
Tweak {{ hue }} hue
|
||||
<wa-icon-button @click="hueShifts.{{ hue }} = 0" class="clear-button" name="circle-xmark" library="system" variant="regular" label="Reset"></wa-icon-button>
|
||||
</div>
|
||||
</wa-slider>
|
||||
<div class="label-min">More {{hueBefore}}</div>
|
||||
<div class="label-max">More {{hueAfter}}</div>
|
||||
</div>
|
||||
{%- set hueBefore = hue -%}
|
||||
{% endif %}
|
||||
<div class="wa-gap-s">
|
||||
<code>--wa-color-{{ hue }}</code>
|
||||
<wa-copy-button value="--wa-color-{{ hue }}" copy-label="--wa-color-{{ hue }}"></wa-copy-button>
|
||||
</div>
|
||||
</div>`
|
||||
</wa-dropdown>
|
||||
</td>
|
||||
{% for tint in tints -%}
|
||||
<td>
|
||||
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}-{{ tint }})">
|
||||
{%- set color = palettes[paletteId][hue][tint] -%}
|
||||
<td data-tint="{{ tint }}" style="--color: var(--wa-color-{{ hue }}-{{ tint }})"
|
||||
:style="{
|
||||
'--color-tweaked': colors.{{ hue }}[{{ tint }}],
|
||||
'--color-tweaked-no-gray-chroma': colorsMinusGrayChroma.{{ hue }}[{{ tint }}],
|
||||
}">
|
||||
<div class="color swatch" style="--color: var(--wa-color-{{ hue }}-{{ tint }})">
|
||||
<wa-copy-button value="--wa-color-{{ hue }}-{{ tint }}" copy-label="--wa-color-{{ hue }}-{{ tint }}"></wa-copy-button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -41,6 +189,26 @@
|
||||
{%- endfor %}
|
||||
</table>
|
||||
|
||||
{% set chromaScaleBounds = [
|
||||
(0.08 / maxChroma) | number({maximumFractionDigits: 2}),
|
||||
(0.3 / maxChroma]) | number({maximumFractionDigits: 2}) -%}
|
||||
<div class="decorated-slider chroma-scale-slider wa-palette-{{ paletteId }}"
|
||||
:class="{ tweaked: chromaScale !== 1 }"
|
||||
style="--min: {{ chromaScaleBounds[0] }}; --max: {{ chromaScaleBounds[1] }};">
|
||||
<wa-slider name="chroma-scale" ref="chromaScaleSlider"
|
||||
v-model="chromaScale" value="1" step="0.01"
|
||||
min="{{ chromaScaleBounds[0] }}" max="{{ chromaScaleBounds[1] }}"
|
||||
@input="tweaking.chroma = true"
|
||||
@change="tweaking.chroma = false">
|
||||
<div slot="label">
|
||||
Overall colorfulness
|
||||
<wa-icon-button @click="chromaScale = 1" class="clear-button" name="circle-xmark" library="system" variant="regular" label="Reset"></wa-icon-button>
|
||||
</div>
|
||||
</wa-slider>
|
||||
<div class="label-min">More muted</div>
|
||||
<div class="label-max">More vibrant</div>
|
||||
</div>
|
||||
|
||||
<h2>Used By</h2>
|
||||
|
||||
<section class="index-grid">
|
||||
@@ -65,6 +233,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 %}
|
||||
@@ -84,6 +253,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 %}
|
||||
@@ -102,6 +272,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 %}
|
||||
@@ -114,13 +285,34 @@ 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 %}
|
||||
</div></div> {# end palette app #}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ wa_data.palettes = {
|
||||
<wa-option label="{{ palette.data.title }}" value="{{ palette.fileSlug if not currentPalette }}" {{ (palette.fileSlug if currentPalette) | attr('data-id') }}>
|
||||
<wa-card with-header>
|
||||
<div slot="header">
|
||||
{% include "svgs/" + (palette.data.icon or "thumbnail-placeholder") + ".njk" %}
|
||||
{% include "svgs/" + (palette.data.icon or "thumbnail-placeholder") + ".njk" ignore missing %}
|
||||
</div>
|
||||
<span class="page-name">
|
||||
{{ palette.data.title }}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -177,72 +178,196 @@ export function sort(arr, by = { 'data.order': 1, 'data.title': '' }) {
|
||||
});
|
||||
}
|
||||
|
||||
export function show(page) {
|
||||
return !(page.data.noAlpha && page.data.isAlpha) && !page.data.unlisted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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', filter = show } = 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 = {};
|
||||
|
||||
if (filter) {
|
||||
collection = collection.filter(filter);
|
||||
}
|
||||
|
||||
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);
|
||||
} else {
|
||||
// At least make sure other is last
|
||||
if (ret.other) {
|
||||
let otherGroup = ret.other;
|
||||
delete ret.other;
|
||||
ret.other = otherGroup;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -39,7 +39,7 @@ export function outlinePlugin(options = {}) {
|
||||
}
|
||||
|
||||
// Create a clone of the heading so we can remove links and [data-no-outline] elements from the text content
|
||||
clone.querySelectorAll('a').forEach(a => a.remove());
|
||||
clone.querySelectorAll('.wa-visually-hidden, [hidden], [aria-hidden="true"]').forEach(el => el.remove());
|
||||
clone.querySelectorAll('[data-no-outline]').forEach(el => el.remove());
|
||||
|
||||
// Generate the link
|
||||
|
||||
169
docs/assets/components/scoped.js
Normal file
169
docs/assets/components/scoped.js
Normal file
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* Low-level utility to encapsulate a bit of HTML (mainly to apply certain stylesheets to it without them leaking to the rest of the page)
|
||||
* Usage: <wa-scoped><template><!-- your HTML here --></template></wa-scoped>
|
||||
*/
|
||||
|
||||
// Map of <wa-scoped> elements to any global <style> elements they’ve created
|
||||
const imports = new Set();
|
||||
const fontFaceRules = new Set();
|
||||
|
||||
export default class WaScoped extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
|
||||
this.observer = new MutationObserver(() => this.render());
|
||||
this.observer.observe(this, { childList: true, subtree: true, characterData: true });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
this.ownerDocument.documentElement.addEventListener('wa-color-scheme-change', e =>
|
||||
this.#applyDarkMode(e.detail.dark),
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
this.observer.takeRecords();
|
||||
this.observer.disconnect();
|
||||
|
||||
this.shadowRoot.innerHTML = '';
|
||||
|
||||
// To avoid mutating this.childNodes while iterating over it
|
||||
let nodes = [];
|
||||
|
||||
for (let template of this.childNodes) {
|
||||
// Other solutions we can try if needed: <script type="text/html">, or comment nodes
|
||||
if (template instanceof HTMLTemplateElement) {
|
||||
if (template.content.childNodes.length > 0) {
|
||||
nodes.push(template.content.cloneNode(true));
|
||||
} else if (template.childNodes.length > 0) {
|
||||
// Fake template, suck its children out of the light DOM
|
||||
nodes.push(...template.childNodes);
|
||||
}
|
||||
} else {
|
||||
// Regular child, suck it out of the light DOM
|
||||
nodes.push(template);
|
||||
}
|
||||
}
|
||||
|
||||
this.shadowRoot.append(...nodes);
|
||||
|
||||
this.#fixStyles();
|
||||
this.#applyDarkMode();
|
||||
|
||||
this.observer.observe(this, { childList: true, subtree: true, characterData: true });
|
||||
}
|
||||
|
||||
#applyDarkMode(isDark = getComputedStyle(this).colorScheme === 'dark') {
|
||||
// Hack to make dark mode work
|
||||
// NOTE If any child nodes actually have .wa-dark, this will override it
|
||||
for (let node of this.shadowRoot.children) {
|
||||
node.classList.toggle('wa-dark', isDark);
|
||||
}
|
||||
this.classList.toggle('wa-dark', isDark);
|
||||
}
|
||||
|
||||
/**
|
||||
* @font-face does not work in shadow DOM in Chrome & FF, as of March 2025 https://issues.chromium.org/issues/41085401
|
||||
* This works around this issue by traversing the shadow DOM CSS looking
|
||||
* for @font-face rules or CSS imports to known font providers and copies them to the main document
|
||||
*/
|
||||
async #fixStyles() {
|
||||
let styleElements = [...this.shadowRoot.querySelectorAll('link[rel="stylesheet"], style')];
|
||||
|
||||
let loadStates = styleElements.map(element => {
|
||||
try {
|
||||
if (element.sheet?.cssRules) {
|
||||
// Already loaded
|
||||
return Promise.resolve(element.sheet);
|
||||
}
|
||||
} catch (e) {
|
||||
// CORS
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
element.addEventListener('load', e => resolve(element.sheet));
|
||||
element.addEventListener('error', e => reject(null));
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.allSettled(loadStates);
|
||||
|
||||
let fontRules = findFontFaceRules(...this.shadowRoot.styleSheets);
|
||||
|
||||
if (!fontRules.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let doc = this.ownerDocument;
|
||||
// Why not adoptedStyleSheets? Can't have @import in those yet
|
||||
let id = `wa-scoped-hoisted-fonts`;
|
||||
let style = doc.head.querySelector('style#' + id);
|
||||
if (!style) {
|
||||
style = Object.assign(doc.createElement('style'), { id, textContent: ' ' });
|
||||
doc.head.append(style);
|
||||
}
|
||||
let sheet = style.sheet;
|
||||
|
||||
for (let rule of fontRules) {
|
||||
let cssText = rule.cssText;
|
||||
if (rule.type === CSSRule.FONT_FACE_RULE) {
|
||||
if (fontFaceRules.has(cssText)) {
|
||||
continue;
|
||||
}
|
||||
fontFaceRules.add(cssText);
|
||||
sheet.insertRule(cssText);
|
||||
} else if (rule.type === CSSRule.IMPORT_RULE) {
|
||||
if (imports.has(rule.href)) {
|
||||
continue;
|
||||
}
|
||||
imports.add(rule.href);
|
||||
sheet.insertRule(cssText, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static observedAttributes = [];
|
||||
}
|
||||
|
||||
customElements.define('wa-scoped', WaScoped);
|
||||
|
||||
export const WEB_FONT_HOSTS = [
|
||||
'fonts.googleapis.com',
|
||||
'fonts.gstatic.com',
|
||||
'use.typekit.net',
|
||||
'fonts.adobe.com',
|
||||
'kit.fontawesome.com',
|
||||
'pro.fontawesome.com',
|
||||
'cdn.materialdesignicons.com',
|
||||
];
|
||||
|
||||
function findFontFaceRules(...stylesheets) {
|
||||
let ret = [];
|
||||
|
||||
for (let sheet of stylesheets) {
|
||||
let rules;
|
||||
try {
|
||||
rules = sheet.cssRules;
|
||||
} catch (e) {
|
||||
// CORS
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let rule of rules) {
|
||||
if (rule.type === CSSRule.FONT_FACE_RULE) {
|
||||
ret.push(rule);
|
||||
} else if (rule.type === CSSRule.IMPORT_RULE) {
|
||||
if (WEB_FONT_HOSTS.some(host => rule.href.includes(host))) {
|
||||
ret.push(rule);
|
||||
} else if (rule.styleSheet) {
|
||||
ret.push(...findFontFaceRules(rule.styleSheet));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -1,3 +1,11 @@
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function (...args) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
function updateResults(input) {
|
||||
const filter = input.value.toLowerCase().trim();
|
||||
let filtered = Boolean(filter);
|
||||
@@ -18,8 +26,10 @@ function updateResults(input) {
|
||||
}
|
||||
}
|
||||
|
||||
const debouncedUpdateResults = debounce(updateResults, 300);
|
||||
|
||||
document.documentElement.addEventListener('input', e => {
|
||||
if (e.target?.matches('#block-filter wa-input')) {
|
||||
updateResults(e.target);
|
||||
debouncedUpdateResults(e.target);
|
||||
}
|
||||
});
|
||||
|
||||
8
docs/assets/scripts/prism-downloaded.js
Normal file
8
docs/assets/scripts/prism-downloaded.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* Get import code for remixed themes.
|
||||
*/
|
||||
export const urls = {
|
||||
colors: id => `styles/themes/${id}/color.css`,
|
||||
palette: id => `styles/color/${id}.css`,
|
||||
brand: id => `styles/brand/${id}.css`,
|
||||
typography: id => `styles/themes/${id}/typography.css`,
|
||||
};
|
||||
|
||||
function getImport(url, options = {}) {
|
||||
let { language = 'html', cdnUrl = '/dist/', attributes } = options;
|
||||
url = cdnUrl + url;
|
||||
|
||||
if (language === 'css') {
|
||||
return `@import url('${url}');`;
|
||||
} else {
|
||||
attributes = attributes ? ` ${attributes}` : '';
|
||||
return `<link rel="stylesheet" href="${url}"${attributes} />`;
|
||||
}
|
||||
}
|
||||
|
||||
export function getCode(base, params, options) {
|
||||
let ret = [];
|
||||
|
||||
if (base) {
|
||||
ret.push(`styles/themes/${base}.css`);
|
||||
}
|
||||
|
||||
ret.push(
|
||||
...Object.entries(params)
|
||||
.filter(([aspect, id]) => Boolean(id))
|
||||
.map(([aspect, id]) => urls[aspect](id)),
|
||||
);
|
||||
|
||||
return ret.map(url => getImport(url, options)).join('\n');
|
||||
}
|
||||
254
docs/assets/scripts/sidebar-tweaks.js
Normal file
254
docs/assets/scripts/sidebar-tweaks.js
Normal file
@@ -0,0 +1,254 @@
|
||||
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();
|
||||
},
|
||||
|
||||
updateSaved() {
|
||||
this.saved = localStorage.savedPalettes ? JSON.parse(localStorage.savedPalettes) : [];
|
||||
},
|
||||
|
||||
save(saved = this.saved) {
|
||||
this.saved = saved ?? [];
|
||||
|
||||
if (saved.length > 0) {
|
||||
localStorage.savedPalettes = JSON.stringify(saved);
|
||||
} else {
|
||||
delete localStorage.savedPalettes;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
sidebar.palettes.updateSaved();
|
||||
addEventListener('storage', event => sidebar.palettes.updateSaved());
|
||||
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO improve UX of this
|
||||
if (!confirm(`Are you sure you want to delete palette “${palette.title}”?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
savedPalettes = savedPalettes.filter(p => !sidebar.palette.equals(palette, p));
|
||||
|
||||
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.save(savedPalettes);
|
||||
|
||||
if (sidebar.palette.equals(globalThis.paletteApp?.saved, palette)) {
|
||||
paletteApp.postDelete();
|
||||
}
|
||||
},
|
||||
|
||||
getSaved(palette, savedPalettes = sidebar.palettes.saved) {
|
||||
return savedPalettes.find(p => sidebar.palette.equals(p, palette));
|
||||
},
|
||||
|
||||
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(palette, saved) {
|
||||
let savedPalettes = sidebar.palettes.saved;
|
||||
let existing = this.getSaved(saved ?? palette, savedPalettes);
|
||||
let oldValues;
|
||||
|
||||
if (existing) {
|
||||
// Rename
|
||||
oldValues = { ...existing };
|
||||
Object.assign(existing, palette);
|
||||
} else {
|
||||
savedPalettes.push(palette);
|
||||
}
|
||||
|
||||
this.render(palette, oldValues);
|
||||
sidebar.updateCurrent();
|
||||
|
||||
sidebar.palettes.save(savedPalettes);
|
||||
},
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,12 +1,30 @@
|
||||
let initialPageLoadComplete = false;
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
initialPageLoadComplete = true;
|
||||
});
|
||||
|
||||
// Helper for view transitions
|
||||
export function domChange(fn, { behavior = 'smooth' } = {}) {
|
||||
export function domChange(fn, { behavior = 'smooth', ignoreInitialLoad = true } = {}) {
|
||||
const canUseViewTransitions =
|
||||
document.startViewTransition && !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
// Skip transitions on initial page load
|
||||
if (!initialPageLoadComplete && ignoreInitialLoad) {
|
||||
fn(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (canUseViewTransitions && behavior === 'smooth') {
|
||||
document.startViewTransition(fn);
|
||||
const transition = document.startViewTransition(() => {
|
||||
fn(true);
|
||||
// Wait a brief delay before finishing the transition to prevent jumpiness
|
||||
return new Promise(resolve => setTimeout(resolve, 200));
|
||||
});
|
||||
return transition;
|
||||
} else {
|
||||
fn(true);
|
||||
fn(false);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +118,7 @@ const colorScheme = new ThemeAspect({
|
||||
domChange(() => {
|
||||
let dark = this.computedValue === 'dark';
|
||||
document.documentElement.classList.toggle(`wa-dark`, dark);
|
||||
document.documentElement.dispatchEvent(new CustomEvent('wa-color-scheme-change', { detail: { dark } }));
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import 'https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.10/+esm';
|
||||
import { preventTurboFouce } from '/dist/webawesome.js';
|
||||
|
||||
if (!window.___turboScrollPositions___) {
|
||||
window.___turboScrollPositions___ = {};
|
||||
}
|
||||
@@ -70,3 +73,4 @@ function fixDSD(e) {
|
||||
window.addEventListener('turbo:before-cache', saveScrollPosition);
|
||||
window.addEventListener('turbo:before-render', restoreScrollPosition);
|
||||
window.addEventListener('turbo:render', restoreScrollPosition);
|
||||
preventTurboFouce();
|
||||
|
||||
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 { cdnUrl, hueRanges, 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}`;
|
||||
}
|
||||
73
docs/assets/scripts/tweak/data.js
Normal file
73
docs/assets/scripts/tweak/data.js
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* 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 selectors = {
|
||||
palette: id =>
|
||||
[':where(:root)', ':host', ":where([class^='wa-theme-'], [class*=' wa-theme-'])", `.wa-palette-${id}`].join(',\n'),
|
||||
};
|
||||
|
||||
export const hueRanges = {
|
||||
red: { min: 5, max: 35 }, // 30
|
||||
orange: { min: 35, max: 60 }, // 25
|
||||
yellow: { min: 60, max: 112 }, // 45
|
||||
green: { min: 112, 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: 365 }, // 45
|
||||
};
|
||||
|
||||
export const moreHue = {
|
||||
red: 'Redder',
|
||||
orange: 'More orange', // https://www.reddit.com/r/grammar/comments/u9n0uo/is_it_oranger_or_more_orange/
|
||||
yellow: 'Yellower',
|
||||
green: 'Greener',
|
||||
cyan: 'More cyan',
|
||||
blue: 'Bluer',
|
||||
indigo: 'More indigo',
|
||||
pink: 'Pinker',
|
||||
};
|
||||
|
||||
/**
|
||||
* Max gray chroma (% of chroma of undertone) per hue
|
||||
*/
|
||||
export const maxGrayChroma = {
|
||||
red: 0.2,
|
||||
orange: 0.2,
|
||||
yellow: 0.25,
|
||||
green: 0.25,
|
||||
cyan: 0.3,
|
||||
blue: 0.35,
|
||||
indigo: 0.35,
|
||||
purple: 0.3,
|
||||
pink: 0.25,
|
||||
};
|
||||
|
||||
export const docsURLs = {
|
||||
colors: '/docs/themes/',
|
||||
palette: '/docs/palettes/',
|
||||
typography: '/docs/themes/',
|
||||
};
|
||||
|
||||
export const icons = {
|
||||
colors: 'palette',
|
||||
palette: 'swatchbook',
|
||||
brand: 'droplet',
|
||||
typography: 'font-case',
|
||||
};
|
||||
|
||||
export const hues = Object.keys(hueRanges);
|
||||
|
||||
export const tints = ['05', '10', '20', '30', '40', '50', '60', '70', '80', '90', '95'];
|
||||
81
docs/assets/scripts/tweak/permalink.js
Normal file
81
docs/assets/scripts/tweak/permalink.js
Normal file
@@ -0,0 +1,81 @@
|
||||
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());
|
||||
}
|
||||
|
||||
#mappings = new WeakMap();
|
||||
|
||||
mapObject(obj, mapping = {}) {
|
||||
this.#mappings.set(obj, mapping);
|
||||
}
|
||||
|
||||
readFrom(obj) {
|
||||
let mapping = this.#mappings.get(obj) ?? {};
|
||||
let { keyFrom = IDENTITY, valueFrom = IDENTITY } = mapping;
|
||||
|
||||
for (let key in obj) {
|
||||
let value = obj[key];
|
||||
let mappedValue = valueFrom(value);
|
||||
let mappedKey = keyFrom(key);
|
||||
this.set(mappedKey, mappedValue);
|
||||
}
|
||||
}
|
||||
|
||||
writeTo(obj) {
|
||||
let mapping = this.#mappings.get(obj) ?? {};
|
||||
let { keyTo = IDENTITY, valueTo = IDENTITY, canExtend = false } = mapping;
|
||||
|
||||
for (let [key, value] of this) {
|
||||
let mappedKey = keyTo(key);
|
||||
let mappedValue = valueTo(value);
|
||||
|
||||
if (canExtend || mappedKey in obj) {
|
||||
obj[mappedKey] = mappedValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set(key, value, defaultValue) {
|
||||
let oldValue = this.get(key);
|
||||
|
||||
if (!value || value == defaultValue) {
|
||||
super.delete(key);
|
||||
|
||||
if (oldValue) {
|
||||
this.changed = true;
|
||||
}
|
||||
} else {
|
||||
super.set(key, value);
|
||||
|
||||
if (String(value) !== String(oldValue)) {
|
||||
this.changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
docs/assets/scripts/tweak/util.js
Normal file
36
docs/assets/scripts/tweak/util.js
Normal file
@@ -0,0 +1,36 @@
|
||||
export function normalizeAngles(angles) {
|
||||
// First, normalize
|
||||
angles = angles.map(h => ((h % 360) + 360) % 360);
|
||||
|
||||
// Remove top and bottom 25% and find average
|
||||
let averageHue =
|
||||
angles
|
||||
.toSorted((a, b) => a - b)
|
||||
.slice(angles.length / 4, -angles.length / 4)
|
||||
.reduce((a, b) => a + b, 0) / angles.length;
|
||||
|
||||
for (let i = 0; i < angles.length; i++) {
|
||||
let h = angles[i];
|
||||
let prevHue = angles[i - 1];
|
||||
let delta = h - prevHue;
|
||||
|
||||
if (Math.abs(delta) > 180) {
|
||||
let equivalent = [h + 360, h - 360];
|
||||
// Offset hue to minimize difference in the direction that brings it closer to the average
|
||||
let delta = h - averageHue;
|
||||
|
||||
if (Math.abs(equivalent[0] - prevHue) <= Math.abs(equivalent[1] - prevHue)) {
|
||||
angles[i] = equivalent[0];
|
||||
} else {
|
||||
angles[i] = equivalent[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return angles;
|
||||
}
|
||||
|
||||
export function subtractAngles(θ1, θ2) {
|
||||
let [a, b] = normalizeAngles([θ1, θ2]);
|
||||
return a - b;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
@import 'outline.css';
|
||||
@import 'search.css';
|
||||
@import 'cera_typeface.css';
|
||||
@import 'theme-icons.css';
|
||||
|
||||
:root {
|
||||
--wa-brand-orange: #f36944;
|
||||
@@ -156,7 +157,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 +189,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 +256,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 */
|
||||
@@ -331,10 +371,22 @@ wa-page > main:has(> .index-grid) {
|
||||
|
||||
.index-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(22ch, 100%), 1fr));
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--wa-space-2xl);
|
||||
margin-block-end: var(--wa-space-3xl);
|
||||
|
||||
@media screen and (max-width: 1470px) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 960px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
}
|
||||
|
||||
a {
|
||||
border-radius: var(--wa-border-radius-l);
|
||||
text-decoration: none;
|
||||
@@ -361,7 +413,6 @@ wa-page > main:has(> .index-grid) {
|
||||
|
||||
&::part(header) {
|
||||
background-color: var(--header-background, var(--wa-color-neutral-fill-quiet));
|
||||
border-bottom: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -400,9 +451,18 @@ wa-page > main:has(> .index-grid) {
|
||||
|
||||
&.color {
|
||||
border-color: transparent;
|
||||
transition: background var(--wa-transition-slow);
|
||||
background: linear-gradient(var(--color-2, transparent) 0% 100%) no-repeat border-box var(--color,);
|
||||
background-position: var(--color-2-position, bottom);
|
||||
background-size: var(--color-2-width, 100%) var(--color-2-height, 50%);
|
||||
|
||||
&.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;
|
||||
@@ -463,6 +523,55 @@ table.colors {
|
||||
}
|
||||
}
|
||||
|
||||
.value-up,
|
||||
.value-down {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: ' ' var(--icon);
|
||||
position: absolute;
|
||||
margin-inline-start: 3em;
|
||||
scale: 1 0.6;
|
||||
color: color-mix(in oklch, oklch(from var(--icon-color) none c h) 0%, oklch(from currentColor l none none));
|
||||
font-size: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
.value-down {
|
||||
--icon: '▼';
|
||||
--icon-color: var(--wa-color-danger-fill-quiet);
|
||||
|
||||
&::after {
|
||||
margin-block-end: -0.2em;
|
||||
}
|
||||
}
|
||||
|
||||
.value-up {
|
||||
--icon: '▲';
|
||||
--icon-color: var(--wa-color-success-fill-quiet);
|
||||
}
|
||||
|
||||
.icon-modifier {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
|
||||
.modifier {
|
||||
position: absolute;
|
||||
bottom: -0.1em;
|
||||
right: -0.3em;
|
||||
font-size: 60%;
|
||||
|
||||
&::part(svg) {
|
||||
stroke: var(--background-color, var(--wa-color-surface-default));
|
||||
stroke-width: 100px;
|
||||
paint-order: stroke;
|
||||
overflow: visible;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Layout Examples */
|
||||
.layout-example-boundary {
|
||||
border: var(--wa-border-width-s) dashed var(--wa-color-neutral-border-normal);
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
|
||||
&::part(base) {
|
||||
margin-block: 10rem;
|
||||
&::part(dialog) {
|
||||
margin-block-start: 10vh;
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
&::part(body) {
|
||||
@@ -23,20 +24,20 @@
|
||||
@media screen and (max-width: 900px) {
|
||||
max-width: calc(100% - 2rem);
|
||||
|
||||
&::part(base) {
|
||||
&::part(dialog) {
|
||||
margin-block: 1rem;
|
||||
}
|
||||
|
||||
#site-search-container {
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#site-search-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - 20rem);
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
max-height: calc(100dvh - 2rem);
|
||||
}
|
||||
max-height: calc(100vh - 18rem);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
|
||||
@@ -1,3 +1,37 @@
|
||||
wa-card:has(> .theme-icon-host, > [slot='header'] > .theme-icon-host) {
|
||||
min-width: 22ch;
|
||||
|
||||
&::part(header) {
|
||||
/* We want to add a background color, so any spacing needs to go on .theme-icon */
|
||||
padding: 0;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
[slot='header'] {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border-radius: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-icon-host {
|
||||
display: block;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.theme-icon {
|
||||
padding: var(--wa-space-xs) var(--wa-space-m);
|
||||
border-radius: inherit;
|
||||
box-sizing: border-box;
|
||||
|
||||
h2,
|
||||
h3,
|
||||
p {
|
||||
margin-block: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-color-icon {
|
||||
display: grid;
|
||||
gap: var(--wa-space-xs);
|
||||
@@ -25,10 +59,50 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--wa-space-xs);
|
||||
}
|
||||
|
||||
h3,
|
||||
p {
|
||||
margin-block: 0;
|
||||
padding: 0;
|
||||
.theme-overall-icon {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: var(--wa-space-xs);
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-height: 7.5rem;
|
||||
box-sizing: border-box;
|
||||
background: var(--wa-color-surface-lowered);
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: var(--wa-space-xs);
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.row-2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
contain: inline-size;
|
||||
width: 100%;
|
||||
|
||||
wa-input {
|
||||
min-width: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.swatches {
|
||||
display: flex;
|
||||
gap: var(--wa-space-3xs);
|
||||
|
||||
> div {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border-radius: var(--wa-border-radius-s);
|
||||
background: var(--wa-color-fill-loud);
|
||||
color: var(--wa-color-on-loud);
|
||||
|
||||
&.wa-brand {
|
||||
width: 2.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,27 +15,17 @@ icon: card
|
||||
|
||||
<strong>Mittens</strong><br />
|
||||
This kitten is as cute as he is playful. Bring him home today!<br />
|
||||
<small>6 weeks old</small>
|
||||
<small class="wa-caption-m">6 weeks old</small>
|
||||
|
||||
<div slot="footer">
|
||||
<div slot="footer" class="wa-split">
|
||||
<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;
|
||||
}
|
||||
|
||||
.card-overview small {
|
||||
color: var(--wa-color-text-quiet);
|
||||
}
|
||||
|
||||
.card-overview [slot='footer'] {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 300px;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
@@ -65,9 +55,9 @@ If using SSR, you need to also use the `with-header` attribute to add a header t
|
||||
|
||||
```html {.example}
|
||||
<wa-card with-header class="card-header">
|
||||
<div slot="header">
|
||||
<div slot="header" class="wa-split">
|
||||
Header Title
|
||||
<wa-icon-button name="gear" variant="solid" label="Settings"></wa-icon-button>
|
||||
<wa-icon-button name="gear" variant="solid" label="Settings" class="wa-size-m"></wa-icon-button>
|
||||
</div>
|
||||
|
||||
This card has a header. You can put all sorts of things in it!
|
||||
@@ -78,19 +68,9 @@ If using SSR, you need to also use the `with-header` attribute to add a header t
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.card-header [slot='header'] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.card-header h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card-header wa-icon-button {
|
||||
font-size: var(--wa-font-size-m);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
@@ -103,7 +83,7 @@ If using SSR, you need to also use the `with-footer` attribute to add a footer t
|
||||
<wa-card with-footer class="card-footer">
|
||||
This card has a footer. You can put all sorts of things in it!
|
||||
|
||||
<div slot="footer">
|
||||
<div slot="footer" class="wa-split">
|
||||
<wa-rating></wa-rating>
|
||||
<wa-button variant="brand">Preview</wa-button>
|
||||
</div>
|
||||
@@ -113,12 +93,6 @@ If using SSR, you need to also use the `with-footer` attribute to add a footer t
|
||||
.card-footer {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.card-footer [slot='footer'] {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
@@ -153,7 +127,7 @@ Use the `size` attribute to change a card's size.
|
||||
<wa-card with-footer size="small">
|
||||
This is a small card.
|
||||
|
||||
<footer slot="footer" class="wa-flank">
|
||||
<footer slot="footer" class="wa-split">
|
||||
<wa-button variant="brand" pill>More Info</wa-button>
|
||||
<wa-rating></wa-rating>
|
||||
</footer>
|
||||
@@ -162,7 +136,7 @@ Use the `size` attribute to change a card's size.
|
||||
<wa-card with-footer size="medium">
|
||||
This is a medium card (default).
|
||||
|
||||
<footer slot="footer" class="wa-flank">
|
||||
<footer slot="footer" class="wa-split">
|
||||
<wa-button variant="brand" pill>More Info</wa-button>
|
||||
<wa-rating></wa-rating>
|
||||
</footer>
|
||||
@@ -171,14 +145,39 @@ Use the `size` attribute to change a card's size.
|
||||
<wa-card with-footer size="large">
|
||||
This is a large card.
|
||||
|
||||
<footer slot="footer" class="wa-flank">
|
||||
<footer slot="footer" class="wa-split">
|
||||
<wa-button variant="brand" pill>More Info</wa-button>
|
||||
<wa-rating></wa-rating>
|
||||
</footer>
|
||||
</wa-card>
|
||||
</div>
|
||||
|
||||
```
|
||||
|
||||
<style>
|
||||
</style>
|
||||
### Appearance
|
||||
|
||||
Use the `appearance` attribute to change the card's visual appearance.
|
||||
|
||||
```html {.example}
|
||||
<div class="wa-grid">
|
||||
<wa-card>
|
||||
<img
|
||||
slot="image"
|
||||
src="https://images.unsplash.com/photo-1559209172-0ff8f6d49ff7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=80"
|
||||
alt="A kitten sits patiently between a terracotta pot and decorative grasses."
|
||||
/>
|
||||
<div slot="header">Outlined (default)</div>
|
||||
Card content.
|
||||
</wa-card>
|
||||
{% for appearance in ['outlined filled', 'outlined accent', 'plain', 'filled', 'accent'] -%}
|
||||
<wa-card appearance="{{ appearance }}">
|
||||
<img
|
||||
slot="image"
|
||||
src="https://images.unsplash.com/photo-1559209172-0ff8f6d49ff7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=80"
|
||||
alt="A kitten sits patiently between a terracotta pot and decorative grasses."
|
||||
/>
|
||||
<div slot="header">{{ appearance | capitalize }}</div>
|
||||
Card content.
|
||||
</wa-card>
|
||||
{%- endfor %}
|
||||
</div>
|
||||
```
|
||||
|
||||
@@ -77,6 +77,31 @@ The details component automatically adapts to right-to-left languages:
|
||||
</wa-details>
|
||||
```
|
||||
|
||||
### Appearance
|
||||
|
||||
Use the `appearance` attribute to change the element’s visual appearance.
|
||||
|
||||
```html {.example}
|
||||
<div class="wa-stack">
|
||||
<wa-details summary="Outlined (default)">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</wa-details>
|
||||
<wa-details summary="Filled" appearance="filled">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</wa-details>
|
||||
<wa-details summary="Filled + Outlined" appearance="filled outlined">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</wa-details>
|
||||
<wa-details summary="Plain" appearance="plain">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</wa-details>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Grouping Details
|
||||
|
||||
Details are designed to function independently, but you can simulate a group or "accordion" where only one is shown at a time by listening for the `wa-show` event.
|
||||
|
||||
@@ -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'
|
||||
---
|
||||
|
||||
@@ -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>
|
||||
```
|
||||
|
||||
@@ -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>
|
||||
@@ -37,10 +37,6 @@ This snippet includes three parts:
|
||||
|
||||
Now you can [start using Web Awesome!](/docs/usage)
|
||||
|
||||
:::info
|
||||
While convenient, autoloading may lead to a [Flash of Undefined Custom Elements](https://www.abeautifulsite.net/posts/flash-of-undefined-custom-elements/). The linked article describes some ways to alleviate it.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Using Font Awesome Kit Codes
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -33,7 +33,7 @@ Use the [variant utility classes](../utilities/color.md) to set the button's sem
|
||||
|
||||
### Appearance
|
||||
|
||||
Use the [appearance utility classes](../utilities/appearance.md) to change the button's visual appearance:
|
||||
Use the [appearance utility classes](/docs/utilities/appearance) to change the button's visual appearance:
|
||||
|
||||
```html {.example}
|
||||
<div style="margin-block-end: 1rem;">
|
||||
|
||||
@@ -57,7 +57,7 @@ Use the [variant utility classes](../utilities/color.md) to set the callout's co
|
||||
|
||||
### Appearance
|
||||
|
||||
Use the [appearance utility classes](../utilities/appearance.md) to change the callout's visual appearance (the default is `outlined filled`).
|
||||
Use the [appearance utility classes](/docs/utilities/appearance) to change the callout's visual appearance (the default is `outlined filled`).
|
||||
|
||||
```html {.example}
|
||||
<article class="wa-callout wa-brand wa-outlined wa-accent">
|
||||
|
||||
@@ -19,6 +19,35 @@ file: styles/native/details.css
|
||||
|
||||
## Examples
|
||||
|
||||
### Appearance
|
||||
|
||||
Use the [appearance utility classes](/docs/utilities/appearance) to change the element's visual appearance:
|
||||
|
||||
```html {.example}
|
||||
<div class="wa-stack">
|
||||
<details>
|
||||
<summary>Outlined (default)</summary>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</details>
|
||||
<details class="wa-filled">
|
||||
<summary>Filled</summary>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</details>
|
||||
<details class="wa-filled wa-outlined">
|
||||
<summary>Filled + Outlined</summary>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</details>
|
||||
<details class="wa-plain">
|
||||
<summary>Plain</summary>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</details>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Right-to-Left Languages
|
||||
|
||||
The details styling automatically adapts to right-to-left languages:
|
||||
|
||||
@@ -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:
|
||||
|
||||
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,6 +1,7 @@
|
||||
{
|
||||
"layout": "palette.njk",
|
||||
"tags": ["palettes", "palette"],
|
||||
"wide": true,
|
||||
"eleventyComputed": {
|
||||
"snippet": ".wa-palette-{{ page.fileSlug }}",
|
||||
"icon": "palette",
|
||||
|
||||
205
docs/docs/palettes/tweak.css
Normal file
205
docs/docs/palettes/tweak.css
Normal file
@@ -0,0 +1,205 @@
|
||||
:root {
|
||||
--fa-sliders-simple: '\f1de';
|
||||
}
|
||||
|
||||
.core-column {
|
||||
position: relative;
|
||||
|
||||
> wa-dropdown {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
wa-dropdown > .color.swatch {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.decorated-slider {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
margin-block-end: var(--wa-space-xl);
|
||||
|
||||
wa-slider {
|
||||
grid-column: 1 / -1;
|
||||
--track-height: 1em;
|
||||
--track-color-inactive: transparent;
|
||||
--track-color-active: transparent;
|
||||
--thumb-color: var(--color-tweaked, var(--color));
|
||||
--thumb-shadow: 0 0 0 var(--thumb-gap) var(--wa-color-surface-default),
|
||||
var(--wa-shadow-offset-x-m) var(--wa-shadow-offset-y-m) var(--wa-shadow-blur-m)
|
||||
calc(var(--wa-shadow-offset-x-m) * -1 + var(--thumb-gap)) var(--wa-color-shadow);
|
||||
|
||||
&:active {
|
||||
--thumb-size: 2em;
|
||||
}
|
||||
|
||||
&::part(base) {
|
||||
background: linear-gradient(to right in var(--color-interpolation-space, oklab), var(--color-1), var(--color-2));
|
||||
}
|
||||
}
|
||||
|
||||
[slot='label'] {
|
||||
min-height: 1.1lh;
|
||||
}
|
||||
|
||||
.clear-button {
|
||||
vertical-align: middle;
|
||||
font-size: var(--wa-font-size-xs);
|
||||
|
||||
&:not(.tweaked *) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.hue-shift-slider {
|
||||
--color-1: oklch(from var(--color) l c calc(h + var(--min, 0)));
|
||||
--color-2: oklch(from var(--color) l c calc(h + var(--max, 0)));
|
||||
--color-interpolation-space: oklch;
|
||||
}
|
||||
|
||||
.chroma-scale-slider {
|
||||
--color: var(--wa-color-brand);
|
||||
--color-1: oklch(from var(--color) l calc(c * var(--min)) h);
|
||||
--color-2: oklch(from var(--color) l calc(c * var(--max)) h);
|
||||
}
|
||||
|
||||
.gray-chroma-slider {
|
||||
--color: var(--wa-color-gray);
|
||||
--color-1: oklch(from var(--wa-color-gray) l 0 none);
|
||||
--color-2: oklch(from var(--color-gray-undertone) l calc(c * var(--max)) h);
|
||||
margin-top: var(--wa-space-m);
|
||||
}
|
||||
|
||||
.popup {
|
||||
background: var(--wa-color-surface-default);
|
||||
border: 1px solid var(--wa-color-surface-border);
|
||||
padding: var(--wa-space-xl);
|
||||
border-radius: var(--wa-border-radius-m);
|
||||
|
||||
code {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.color-scale {
|
||||
th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
td:not([data-hue='gray'] *) {
|
||||
--tweak-c: calc(c * var(--chroma-scale, 1));
|
||||
--tweak-h: calc(h + var(--hue-shift, 0));
|
||||
|
||||
--color-tweaked-no-chroma-scale: oklch(from var(--color) l c var(--tweak-h));
|
||||
--color-tweaked-no-hue-shift: oklch(from var(--color) l var(--tweak-c) h);
|
||||
|
||||
&:is([data-tint='90'], [data-tint='95']) {
|
||||
/* Work around https://bugs.webkit.org/show_bug.cgi?id=287637 */
|
||||
|
||||
--color-tweaked-no-chroma-scale: lch(from var(--color) l c var(--tweak-h));
|
||||
--color-tweaked-no-hue-shift: lch(from var(--color) l var(--tweak-c) h);
|
||||
|
||||
/* outline: 1px dashed red; */
|
||||
}
|
||||
}
|
||||
|
||||
.color.swatch {
|
||||
--color-2: var(--color-tweaked);
|
||||
--color-2-height: 100%;
|
||||
|
||||
&:is(.tweaking *) {
|
||||
--color-2-height: 70%;
|
||||
}
|
||||
|
||||
&:is(.tweaking-chroma *) {
|
||||
--color: var(--color-tweaked-no-chroma-scale);
|
||||
}
|
||||
|
||||
&:is(.tweaking-hue *) {
|
||||
--color: var(--color-tweaked-no-hue-shift);
|
||||
}
|
||||
|
||||
&:is(.tweaking-gray-chroma *) {
|
||||
--color: var(--color-tweaked-no-gray-chroma);
|
||||
}
|
||||
}
|
||||
|
||||
.tweak-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: var(--wa-space-s);
|
||||
opacity: var(--tweak-icon-opacity, 0%);
|
||||
}
|
||||
|
||||
.core-column: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);
|
||||
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'] {
|
||||
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);
|
||||
}
|
||||
}
|
||||
580
docs/docs/palettes/tweak.js
Normal file
580
docs/docs/palettes/tweak.js
Normal file
@@ -0,0 +1,580 @@
|
||||
// TODO move these to local imports
|
||||
import Color from 'https://colorjs.io/dist/color.js';
|
||||
import { createApp, nextTick } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';
|
||||
import { cdnUrl, hueRanges, hues, Permalink, tints } from '../../assets/scripts/tweak.js';
|
||||
import { cssImport, cssLiteral, cssRule } from '../../assets/scripts/tweak/code.js';
|
||||
import { maxGrayChroma, moreHue, selectors, urls } from '../../assets/scripts/tweak/data.js';
|
||||
import { subtractAngles } from '../../assets/scripts/tweak/util.js';
|
||||
import Prism from '/assets/scripts/prism.js';
|
||||
|
||||
await Promise.all(['wa-slider'].map(tag => customElements.whenDefined(tag)));
|
||||
|
||||
// // Detect https://bugs.webkit.org/show_bug.cgi?id=287637
|
||||
// const SAFARI_OKLCH_BUG = (() => {
|
||||
// let dummy = document.createElement('div');
|
||||
// document.body.appendChild(dummy);
|
||||
// dummy.style.color = 'oklch(from #d5e0e6 l c h)';
|
||||
// let computedColor = getComputedStyle(dummy).color;
|
||||
// dummy.remove();
|
||||
// return computedColor.endsWith(' 0)');
|
||||
// })();
|
||||
|
||||
let allPalettes = await fetch('/docs/palettes/data.json').then(r => r.json());
|
||||
globalThis.allPalettes = allPalettes;
|
||||
|
||||
for (let palette in allPalettes) {
|
||||
for (let hue in allPalettes[palette].colors) {
|
||||
let scale = allPalettes[palette].colors[hue];
|
||||
for (let tint of tints) {
|
||||
let color = scale[tint];
|
||||
|
||||
if (Array.isArray(color)) {
|
||||
scale[tint] = new Color('oklch', color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const percentFormatter = value => value.toLocaleString(undefined, { style: 'percent' });
|
||||
|
||||
let paletteAppSpec = {
|
||||
data() {
|
||||
let appRoot = document.querySelector('#palette-app');
|
||||
let paletteId = appRoot.dataset.paletteId;
|
||||
let palette = allPalettes[paletteId];
|
||||
|
||||
return {
|
||||
uid: undefined,
|
||||
paletteId,
|
||||
paletteTitle: palette.title,
|
||||
originalColors: palette.colors,
|
||||
permalink: new Permalink(),
|
||||
hueRanges,
|
||||
hueShifts: Object.fromEntries(hues.map(hue => [hue, 0])),
|
||||
chromaScale: 1,
|
||||
grayChroma: undefined,
|
||||
grayColor: undefined,
|
||||
tweaking: {},
|
||||
saved: null,
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
// Non-reactive variables to expose
|
||||
Object.assign(this, { moreHue });
|
||||
|
||||
// Read URL params and apply them. This facilitates permalinks.
|
||||
this.permalink.mapObject(this.hueShifts, {
|
||||
keyTo: key => key.replace(/-shift$/, ''),
|
||||
keyFrom: key => key + '-shift',
|
||||
valueFrom: value => (!value ? '' : Number(value)),
|
||||
valueTo: value => (!value ? 0 : Number(value)),
|
||||
});
|
||||
|
||||
this.grayChroma = this.originalGrayChroma;
|
||||
this.grayColor = this.originalGrayColor;
|
||||
|
||||
if (location.search) {
|
||||
// Update from URL
|
||||
this.permalink.writeTo(this.hueShifts);
|
||||
|
||||
for (let param of ['chroma-scale', 'gray-color', 'gray-chroma']) {
|
||||
if (this.permalink.has(param)) {
|
||||
let value = this.permalink.get(param);
|
||||
|
||||
if (!isNaN(value)) {
|
||||
// Convert numeric values to numbers
|
||||
value = Number(value);
|
||||
}
|
||||
|
||||
let prop = camelCase(param);
|
||||
this[prop] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.permalink.has('uid')) {
|
||||
this.uid = Number(this.permalink.get('uid'));
|
||||
}
|
||||
|
||||
this.saved = sidebar.palette.getSaved(this.getPalette());
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
for (let ref in this.$refs) {
|
||||
this.$refs[ref].tooltipFormatter = percentFormatter;
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
tweaks() {
|
||||
return {
|
||||
hueShifts: this.hueShifts,
|
||||
chromaScale: this.chromaScale,
|
||||
grayColor: this.grayColor,
|
||||
grayChroma: this.grayChroma,
|
||||
};
|
||||
},
|
||||
|
||||
isTweaked() {
|
||||
return Object.values(this.hueShifts).some(Boolean);
|
||||
},
|
||||
|
||||
code() {
|
||||
let ret = {};
|
||||
for (let language of ['html', 'css']) {
|
||||
let code = getPaletteCode(this.paletteId, this.colors, this.tweaked, { language, cdnUrl });
|
||||
ret[language] = {
|
||||
raw: code,
|
||||
highlighted: Prism.highlight(code, Prism.languages[language], language),
|
||||
};
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
colors() {
|
||||
return applyTweaks.call(this, this.originalColors, this.tweaks, this.tweaked);
|
||||
},
|
||||
|
||||
colorsMinusChromaScale() {
|
||||
let tweaked = { ...this.tweaked, chromaScale: false };
|
||||
return applyTweaks.call(this, this.originalColors, this.tweaks, tweaked);
|
||||
},
|
||||
|
||||
colorsMinusHueShifts() {
|
||||
let tweaked = { ...this.tweaked, hue: false };
|
||||
return applyTweaks.call(this, this.originalColors, this.tweaks, tweaked);
|
||||
},
|
||||
|
||||
colorsMinusGrayChroma() {
|
||||
let tweaked = { ...this.tweaked, grayChroma: false };
|
||||
return applyTweaks.call(this, this.originalColors, this.tweaks, tweaked);
|
||||
},
|
||||
|
||||
tweaked() {
|
||||
let anyHueTweaked = Object.values(this.hueShifts).some(Boolean);
|
||||
let hue = anyHueTweaked
|
||||
? Object.fromEntries(Object.entries(this.hueShifts).map(([hue, shift]) => [hue, shift !== 0]))
|
||||
: false;
|
||||
|
||||
let ret = {
|
||||
chromaScale: this.chromaScale !== 1,
|
||||
hue,
|
||||
grayChroma: this.grayChroma !== this.originalGrayChroma,
|
||||
grayColor: this.grayColor !== this.originalGrayColor,
|
||||
};
|
||||
|
||||
let anyTweaked = Object.values(ret).some(Boolean);
|
||||
return anyTweaked ? ret : false;
|
||||
},
|
||||
|
||||
tweaksHumanReadable() {
|
||||
let ret = {};
|
||||
|
||||
if (this.chromaScale !== 1) {
|
||||
ret.chromaScale = 'More ' + (this.chromaScale > 1 ? 'vibrant' : 'muted');
|
||||
}
|
||||
|
||||
for (let hue in this.hueShifts) {
|
||||
let shift = this.hueShifts[hue];
|
||||
|
||||
if (!shift) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let relHue = shift < 0 ? arrayPrevious(hues, hue) : arrayNext(hues, hue);
|
||||
let hueTweak = moreHue[relHue] ?? relHue + 'er';
|
||||
|
||||
ret[hue] = capitalize(hueTweak + ' ' + hue + 's');
|
||||
}
|
||||
|
||||
if (this.tweaked.grayChroma || this.tweaked.grayColor) {
|
||||
if (this.tweaked.grayChroma === 0) {
|
||||
ret.grayChroma = 'Achromatic grays';
|
||||
} else {
|
||||
if (this.tweaked.grayColor) {
|
||||
ret.grayColor = capitalize(this.grayColor) + ' gray undertone';
|
||||
}
|
||||
|
||||
if (this.tweaked.grayChroma) {
|
||||
let more = this.tweaked.grayChroma > this.originalGrayChroma;
|
||||
ret.grayChroma = `More ${more ? 'colorful' : 'neutral'} grays`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
originalContrasts() {
|
||||
return getContrasts(this.originalColors);
|
||||
},
|
||||
|
||||
contrasts() {
|
||||
return getContrasts(this.colors, this.originalContrasts);
|
||||
},
|
||||
|
||||
originalCoreColors() {
|
||||
let ret = {};
|
||||
for (let hue in this.originalColors) {
|
||||
let maxChromaTintRaw = this.originalColors[hue].maxChromaTintRaw;
|
||||
ret[hue] = this.originalColors[hue][maxChromaTintRaw];
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
|
||||
coreColors() {
|
||||
let ret = {};
|
||||
for (let hue in this.colors) {
|
||||
let maxChromaTintRaw = this.colors[hue].maxChromaTintRaw;
|
||||
ret[hue] = this.colors[hue][maxChromaTintRaw];
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
originalGrayColor() {
|
||||
let grayHue = this.originalCoreColors.gray.get('h');
|
||||
let minDistance = Infinity;
|
||||
let closestHue = null;
|
||||
|
||||
for (let name in this.originalCoreColors) {
|
||||
if (name === 'gray') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let hue = this.originalCoreColors[name].get('h');
|
||||
let distance = Math.abs(subtractAngles(hue, grayHue));
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closestHue = name;
|
||||
}
|
||||
}
|
||||
|
||||
return closestHue ?? 'indigo';
|
||||
},
|
||||
|
||||
originalGrayChroma() {
|
||||
let coreTint = this.originalColors.gray.maxChromaTint;
|
||||
let grayChroma = this.originalColors.gray[coreTint].get('c');
|
||||
if (grayChroma === 0 || grayChroma === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let grayColorChroma = this.originalColors[this.originalGrayColor][coreTint].get('c');
|
||||
return grayChroma / grayColorChroma;
|
||||
},
|
||||
|
||||
/**
|
||||
* We want to preserve the original grayChroma selection so that when the user switches to another undertone
|
||||
* that supports higher chromas, their selection will be there.
|
||||
* This property is the gray chroma % that is actually applied.
|
||||
*/
|
||||
computedGrayChroma() {
|
||||
return Math.min(this.grayChroma, this.maxGrayChroma);
|
||||
},
|
||||
|
||||
maxGrayChroma() {
|
||||
return maxGrayChroma[this.grayColor] ?? 0.3;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
hueShifts: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.permalink.readFrom(this.hueShifts);
|
||||
},
|
||||
},
|
||||
|
||||
chromaScale() {
|
||||
this.permalink.set('chroma-scale', this.chromaScale, 1);
|
||||
},
|
||||
|
||||
grayColor() {
|
||||
this.permalink.set('gray-color', this.grayColor, this.originalGrayColor);
|
||||
},
|
||||
|
||||
grayChroma() {
|
||||
this.permalink.set('gray-chroma', this.grayChroma, this.originalGrayChroma);
|
||||
},
|
||||
|
||||
tweaks: {
|
||||
deep: true,
|
||||
async handler(value, oldValue) {
|
||||
await nextTick(); // must run after individual watchers
|
||||
|
||||
// Update page URL
|
||||
this.permalink.updateLocation();
|
||||
|
||||
if (this.saved) {
|
||||
this.save({ silent: true });
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
getPalette() {
|
||||
return { id: this.paletteId, uid: this.uid, search: location.search };
|
||||
},
|
||||
|
||||
save({ silent } = {}) {
|
||||
let title = silent
|
||||
? (this.saved?.title ?? this.paletteTitle)
|
||||
: prompt('Palette title:', `${this.paletteTitle} (tweaked)`);
|
||||
|
||||
if (!title) {
|
||||
return;
|
||||
}
|
||||
|
||||
let uid = this.uid;
|
||||
|
||||
if (!uid) {
|
||||
// First time saving
|
||||
this.uid = uid = sidebar.palette.getUid();
|
||||
|
||||
this.permalink.set('uid', uid);
|
||||
this.permalink.updateLocation();
|
||||
}
|
||||
|
||||
let palette = { ...this.getPalette(), uid, title };
|
||||
|
||||
sidebar.palette.save(palette, this.saved);
|
||||
this.saved = palette;
|
||||
},
|
||||
|
||||
rename() {
|
||||
if (!this.saved) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newTitle = prompt('New title:', this.saved.title);
|
||||
|
||||
if (!newTitle) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.saved.title = newTitle;
|
||||
sidebar.palette.save(this.saved);
|
||||
},
|
||||
|
||||
deleteSaved() {
|
||||
sidebar.palette.delete(this.saved);
|
||||
},
|
||||
|
||||
postDelete() {
|
||||
this.saved = null;
|
||||
this.permalink.delete('uid');
|
||||
this.uid = undefined;
|
||||
this.permalink.updateLocation();
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a specific tweak or all tweaks
|
||||
* @param {string} [param] - The tweak to remove. If not provided, all tweaks are removed.
|
||||
*/
|
||||
reset(param) {
|
||||
if (!param || param === 'chromaScale') {
|
||||
this.chromaScale = 1;
|
||||
}
|
||||
|
||||
if (param in this.hueShifts) {
|
||||
this.hueShifts[param] = 0;
|
||||
} else if (!param) {
|
||||
for (let hue in this.hueShifts) {
|
||||
this.hueShifts[hue] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!param || param === 'grayColor') {
|
||||
this.grayColor = this.originalGrayColor;
|
||||
}
|
||||
|
||||
if (!param || param === 'grayChroma') {
|
||||
this.grayChroma = this.originalGrayChroma;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
directives: {
|
||||
// Like v-text, but doesn't complain if the element has content,
|
||||
// making it possible to use in a PE fashion, with the contents being the fallback
|
||||
content(el, { value, arg }) {
|
||||
if (!el.dataset.fallback) {
|
||||
// Store the original content as a fallback the first time
|
||||
el.dataset.fallback = el.textContent;
|
||||
}
|
||||
|
||||
if (value === '') {
|
||||
value = el.dataset.fallback;
|
||||
} else {
|
||||
if (arg === 'number') {
|
||||
value = Number(value).toLocaleString(undefined, { maximumSignificantDigits: 2 });
|
||||
}
|
||||
}
|
||||
|
||||
if (arg === 'html') {
|
||||
el.innerHTML = value;
|
||||
} else {
|
||||
el.textContent = value;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
compilerOptions: {
|
||||
isCustomElement: tag => tag.startsWith('wa-'),
|
||||
},
|
||||
};
|
||||
|
||||
function init() {
|
||||
let paletteAppContainer = document.querySelector('#palette-app');
|
||||
globalThis.paletteApp?.unmount?.();
|
||||
|
||||
if (!paletteAppContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
globalThis.paletteApp = createApp(paletteAppSpec).mount(paletteAppContainer);
|
||||
}
|
||||
|
||||
init();
|
||||
addEventListener('turbo:render', init);
|
||||
|
||||
export function getPaletteCode(paletteId, colors, tweaked, options) {
|
||||
let imports = [];
|
||||
|
||||
if (paletteId) {
|
||||
imports.push(urls.palette(paletteId));
|
||||
}
|
||||
|
||||
let css = '';
|
||||
let declarations = [];
|
||||
|
||||
if (tweaked) {
|
||||
for (let hue in colors) {
|
||||
if (hue === 'orange') {
|
||||
continue;
|
||||
} else if (hue === 'gray') {
|
||||
if (!tweaked.grayChroma && !tweaked.grayColor) {
|
||||
continue;
|
||||
}
|
||||
} else if (!tweaked.chromaScale && !tweaked.hue?.[hue]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let tint of tints) {
|
||||
let color = colors[hue][tint];
|
||||
let stringified = color.toString({ format: color.inGamut('srgb') ? 'hex' : undefined });
|
||||
declarations.push(`--wa-color-${hue}-${tint}: ${stringified};`);
|
||||
}
|
||||
|
||||
declarations.push('');
|
||||
}
|
||||
|
||||
if (declarations.length > 0) {
|
||||
css += cssRule(selectors.palette(paletteId), declarations);
|
||||
}
|
||||
}
|
||||
|
||||
let ret = imports.map(url => cssImport(url, options)).join('\n');
|
||||
|
||||
if (css) {
|
||||
ret += `\n\n${cssLiteral(css, options)}`;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function arrayNext(array, element) {
|
||||
let index = array.indexOf(element);
|
||||
return array[(index + 1) % array.length];
|
||||
}
|
||||
|
||||
function arrayPrevious(array, element) {
|
||||
let index = array.indexOf(element);
|
||||
return array[(index - 1 + array.length) % array.length];
|
||||
}
|
||||
|
||||
function applyTweaks(originalColors, tweaks, tweaked) {
|
||||
let ret = {};
|
||||
let { hueShifts, chromaScale = 1, grayColor, grayChroma } = tweaks;
|
||||
|
||||
if (!tweaked) {
|
||||
return originalColors;
|
||||
}
|
||||
|
||||
if (tweaked.grayChroma) {
|
||||
grayChroma = this.computedGrayChroma;
|
||||
}
|
||||
|
||||
for (let hue in originalColors) {
|
||||
let originalScale = originalColors[hue];
|
||||
let scale = (ret[hue] = {});
|
||||
let descriptors = Object.getOwnPropertyDescriptors(originalScale);
|
||||
Object.defineProperties(scale, {
|
||||
maxChromaTint: { ...descriptors.maxChromaTint, enumerable: false },
|
||||
maxChromaTintRaw: { ...descriptors.maxChromaTintRaw, enumerable: false },
|
||||
});
|
||||
|
||||
for (let tint of tints) {
|
||||
let color = originalScale[tint].clone();
|
||||
|
||||
if (tweaked.hue && hueShifts[hue]) {
|
||||
color.set({ h: h => h + hueShifts[hue] });
|
||||
}
|
||||
|
||||
if (tweaked.chromaScale && chromaScale !== 1) {
|
||||
color.set({ c: c => c * chromaScale });
|
||||
}
|
||||
|
||||
if (hue === 'gray' && (tweaked.grayChroma || tweaked.grayColor)) {
|
||||
let colorUndertone = originalColors[grayColor][tint].clone();
|
||||
color = colorUndertone.set({ c: c => c * grayChroma });
|
||||
}
|
||||
|
||||
scale[tint] = color;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function camelCase(str) {
|
||||
return (str + '').replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
||||
}
|
||||
|
||||
function capitalize(str) {
|
||||
return str[0].toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
function getContrasts(colors, originalContrasts) {
|
||||
let ret = {};
|
||||
|
||||
for (let hue in colors) {
|
||||
ret[hue] = {};
|
||||
|
||||
for (let tintBg of tints) {
|
||||
ret[hue][tintBg] = {};
|
||||
let bgColor = colors[hue][tintBg];
|
||||
|
||||
if (!bgColor || !bgColor.contrast) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let tintFg of tints) {
|
||||
let fgColor = colors[hue][tintFg];
|
||||
let value = bgColor.contrast(fgColor, 'WCAG21');
|
||||
if (originalContrasts) {
|
||||
let original = originalContrasts[hue][tintBg][tintFg];
|
||||
ret[hue][tintBg][tintFg] = { value, original, bgColor, fgColor };
|
||||
} else {
|
||||
ret[hue][tintBg][tintFg] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -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.
|
||||
@@ -14,11 +14,90 @@ During the alpha period, things might break! We take breaking changes very serio
|
||||
|
||||
## Next
|
||||
|
||||
- Added an orientation example to the native radio docs
|
||||
- Fixed the search dialog's styles so it doesn't jump around as you search
|
||||
- Removed close watcher logic to backdrop hide animation bugs in `<wa-dialog>` and `<wa-drawer>`; this logic is already handled and we'll revisit `CloseWatcher` when browser support is better and behaviors are consistent
|
||||
- Revert `<wa-dialog>` structure and CSS to fix clipped content in dialogs (WA-A #123) and light dismiss in iOS Safari (WA-A #201)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Added `appearance` to [`<wa-details>`](/docs/components/details) and [`<wa-card>`](/docs/components/card) and support for the [appearance utilities](/docs/utilities/appearance/) in the [`<details>` native styles](/docs/native/details).
|
||||
- Added an `orange` scale to all color palettes
|
||||
- Added the `.wa-cloak` utility to prevent FOUCE
|
||||
- Added the `allDefined()` utility for awaiting component registration
|
||||
|
||||
### Bugfixes
|
||||
|
||||
- Specifying inherited CSS properties on `<wa-tooltip>` now works as expected ([thanks Dennis!](https://github.com/shoelace-style/webawesome-alpha/discussions/203))
|
||||
- Fixed a bug in `<wa-select>` that made it hard to use with VueJS, Svelte, and many other frameworks
|
||||
- Fixed the `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
|
||||
- Fixed a bug in `<wa-card>` that prevented slots from showing automatically without `with-` attributes
|
||||
- Fixed a bug in `<wa-select>` that prevented the placeholder color from being customized with the `--wa-form-control-placeholder-color` token
|
||||
|
||||
## 3.0.0-alpha.10
|
||||
|
||||
|
||||
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 */
|
||||
}
|
||||
```
|
||||
|
||||
52
docs/docs/themes/demo.njk
vendored
52
docs/docs/themes/demo.njk
vendored
@@ -10,15 +10,17 @@ override:tags: []
|
||||
eleventyComputed:
|
||||
forceTheme: "{{ theme.fileSlug }}"
|
||||
---
|
||||
|
||||
{% set isPro = theme.data.isPro %}
|
||||
{% set status = theme.data.status %}
|
||||
{% set since = theme.data.since %}
|
||||
<link rel="stylesheet" href="/docs/themes/showcase.css" />
|
||||
|
||||
{% 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,30 +36,19 @@ eleventyComputed:
|
||||
</wa-image-comparer>
|
||||
|
||||
<script type="module">
|
||||
import { getCode, urls as stylesheetURLs } from "/assets/scripts/remix.js";
|
||||
import { urls as stylesheetURLs, docsURLs, icons } from "/assets/scripts/tweak/data.js";
|
||||
import { getThemeCode } from "/assets/scripts/tweak/code.js";
|
||||
|
||||
function updateTheme() {
|
||||
let params = new URLSearchParams(window.location.search);
|
||||
params = Object.fromEntries(params.entries());
|
||||
|
||||
const docsURLs = {
|
||||
colors: '/docs/themes/',
|
||||
palette: '/docs/palettes/',
|
||||
typography: '/docs/themes/'
|
||||
};
|
||||
const icons = {
|
||||
colors: 'palette',
|
||||
palette: 'swatchbook',
|
||||
brand: 'droplet',
|
||||
typography: 'font-case'
|
||||
};
|
||||
|
||||
for (let link of document.querySelectorAll('link.mix-and-match')) {
|
||||
link.remove();
|
||||
}
|
||||
|
||||
let msgs = [];
|
||||
let code = getCode("{{ theme.fileSlug }}", params, {attributes: 'class="mix-and-match"'});
|
||||
let tweaks = [];
|
||||
let code = getThemeCode("{{ theme.fileSlug }}", params, {attributes: 'class="mix-and-match"'});
|
||||
document.head.insertAdjacentHTML('beforeend', code);
|
||||
|
||||
for (let name in stylesheetURLs) {
|
||||
@@ -71,18 +62,29 @@ function updateTheme() {
|
||||
}
|
||||
|
||||
let icon = icons[name];
|
||||
msgs.push(`<wa-icon name="${icon}" variant="regular"></wa-icon> ${ title }`);
|
||||
tweaks.push(`<wa-icon name="${icon}" variant="regular"></wa-icon> ${ title }`);
|
||||
}
|
||||
}
|
||||
|
||||
for (let p of mix_and_match) {
|
||||
p.hidden = msgs.length === 0;
|
||||
if (msgs.length) {
|
||||
let icon =
|
||||
p.innerHTML = `<strong><wa-icon name="arrows-rotate"></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>
|
||||
|
||||
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 */
|
||||
}
|
||||
```
|
||||
|
||||
45
docs/docs/themes/remix.js
vendored
45
docs/docs/themes/remix.js
vendored
@@ -1,12 +1,6 @@
|
||||
import { getCode } from '/assets/scripts/remix.js';
|
||||
|
||||
import Prism from '/assets/scripts/prism.js';
|
||||
import { cdnUrl, getThemeCode, Permalink } from '/assets/scripts/tweak.js';
|
||||
await Promise.all(['wa-select', 'wa-option', 'wa-details'].map(tag => customElements.whenDefined(tag)));
|
||||
globalThis.Prism = globalThis.Prism || {};
|
||||
globalThis.Prism.manual = true;
|
||||
await import('/assets/scripts/prism.js');
|
||||
Prism.plugins.customClass.prefix('code-');
|
||||
|
||||
const cdnUrl = document.documentElement.dataset.cdnUrl;
|
||||
|
||||
const domChange = document.startViewTransition ? document.startViewTransition.bind(document) : fn => fn();
|
||||
|
||||
@@ -57,17 +51,11 @@ function init() {
|
||||
typography: '',
|
||||
},
|
||||
params: { colors: '', palette: '', brand: '', typography: '' },
|
||||
urlParams: new URLSearchParams(location.search),
|
||||
urlParams: new Permalink(),
|
||||
};
|
||||
|
||||
// Read URL params and apply them. This facilitates permalinks.
|
||||
if (location.search) {
|
||||
for (let aspect in data.params) {
|
||||
if (data.urlParams.has(aspect)) {
|
||||
data.params[aspect] = data.urlParams.get(aspect);
|
||||
}
|
||||
}
|
||||
}
|
||||
data.urlParams.mapObject(data.params);
|
||||
data.urlParams.writeTo(data.params);
|
||||
|
||||
if (computed.isRemixed) {
|
||||
// Start with the remixing UI open if the theme has been remixed
|
||||
@@ -119,6 +107,10 @@ function setDefault(select, value) {
|
||||
}
|
||||
|
||||
function render(changedAspect) {
|
||||
if (!globalThis.demo) {
|
||||
return;
|
||||
}
|
||||
|
||||
let url = new URL(demo.src);
|
||||
|
||||
if (!changedAspect || changedAspect === 'colors') {
|
||||
@@ -133,16 +125,11 @@ function render(changedAspect) {
|
||||
|
||||
for (let aspect in data.params) {
|
||||
let value = data.params[aspect];
|
||||
|
||||
if (value) {
|
||||
data.urlParams.set(aspect, value);
|
||||
} else {
|
||||
data.urlParams.delete(aspect);
|
||||
}
|
||||
|
||||
selects[aspect].value = value;
|
||||
}
|
||||
|
||||
data.urlParams.readFrom(data.params);
|
||||
|
||||
// Update demo URL
|
||||
domChange(() => {
|
||||
url.search = data.urlParams;
|
||||
@@ -150,18 +137,14 @@ function render(changedAspect) {
|
||||
return new Promise(resolve => (demo.onload = resolve));
|
||||
});
|
||||
|
||||
// Update page URL. If there’s already a search, replace it.
|
||||
// We don’t want to clog the user’s history while they iterate
|
||||
let historyAction = location.search ? 'replaceState' : 'pushState';
|
||||
history[historyAction](null, '', `?${data.urlParams}`);
|
||||
// Update page URL
|
||||
data.urlParams.updateLocation();
|
||||
|
||||
// Update code snippets
|
||||
for (let language in codeSnippets) {
|
||||
let codeSnippet = codeSnippets[language];
|
||||
let copyButton = codeSnippet.previousElementSibling;
|
||||
let code = getCode(data.baseTheme, data.params, { language, cdnUrl });
|
||||
let code = getThemeCode(data.baseTheme, data.params, { language, cdnUrl });
|
||||
codeSnippet.textContent = code;
|
||||
copyButton.value = code;
|
||||
Prism.highlightElement(codeSnippet);
|
||||
}
|
||||
}
|
||||
|
||||
5
docs/docs/themes/showcase.css
vendored
5
docs/docs/themes/showcase.css
vendored
@@ -12,6 +12,11 @@ body,
|
||||
#mix_and_match {
|
||||
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: -0.15em;
|
||||
|
||||
1
docs/docs/themes/themes.json
vendored
1
docs/docs/themes/themes.json
vendored
@@ -3,6 +3,7 @@
|
||||
"wide": true,
|
||||
"tags": ["themes", "theme"],
|
||||
"brand": "blue",
|
||||
"icon": "theme",
|
||||
"eleventyComputed": {
|
||||
"file": "styles/themes/{{ page.fileSlug }}.css"
|
||||
}
|
||||
|
||||
@@ -3,4 +3,5 @@ title: Design Tokens
|
||||
description: A theme is a collection of predefined CSS custom properties that control global styles from color to shadows. These custom properties thread through all Web Awesome components for a consistent look and feel.
|
||||
layout: overview
|
||||
override:tags: []
|
||||
categories: {tags: true}
|
||||
---
|
||||
|
||||
@@ -8,6 +8,30 @@ Web Awesome components are just regular HTML elements, or [custom elements](http
|
||||
|
||||
If you're new to custom elements, often referred to as "web components," this section will familiarize you with how to use them.
|
||||
|
||||
## Awaiting Registration
|
||||
|
||||
Unlike traditional frameworks, custom elements don't have a centralized initialization phase. This means you need to verify that a custom element has been properly registered before attempting to interact with its properties or methods.
|
||||
|
||||
You can use the [`customElements.whenDefined()`](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/whenDefined) method to ensure a specific component is ready:
|
||||
|
||||
```ts
|
||||
await customElements.whenDefined('wa-button');
|
||||
|
||||
// <wa-button> is ready to use!
|
||||
const button = document.querySelector('wa-button');
|
||||
```
|
||||
|
||||
When working with multiple components, checking each one individually can become tedious. For convenience, Web Awesome provides the `allDefined()` function which automatically detects and waits for all Web Awesome components in the DOM to be initialized before resolving.
|
||||
|
||||
```ts
|
||||
import { allDefined } from '/dist/webawesome.js';
|
||||
|
||||
// Waits for all Web Awesome components in the DOM to be registered
|
||||
await allDefined();
|
||||
|
||||
// All Web Awesome components on the page are ready!
|
||||
```
|
||||
|
||||
## Attributes & Properties
|
||||
|
||||
Many components have properties that can be set using attributes. For example, buttons accept a `size` attribute that maps to the `size` property which dictates the button's size.
|
||||
|
||||
32
docs/docs/utilities/fouce.md
Normal file
32
docs/docs/utilities/fouce.md
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: Reduce FOUCE
|
||||
description: Utility to improve the loading experience by hiding non-prerendered custom elements until they are registered.
|
||||
file: styles/utilities/fouce.css
|
||||
icon: spinner
|
||||
---
|
||||
|
||||
While convenient, autoloading can lead to a [Flash of Undefined Custom Elements](https://www.abeautifulsite.net/posts/flash-of-undefined-custom-elements/).
|
||||
The [FOUCE style utility](/docs/utilities/fouce/#opting-in) (which is automatically applied if you use the [Web Awesome utilities](/docs/utilities/)) takes care of hiding custom elements until they and their contents have been registered, up to a maximum of two seconds.
|
||||
|
||||
In many cases, this is not enough, and you may wish to hide a broader wrapper element or even the entire page until all WA elements within it have loaded. To do that, you can add the `wa-reduce-fouce` class to any element on the page or even apply it to the whole page by placing the class on the `<html>` element.
|
||||
|
||||
```html
|
||||
<html class="wa-cloak">
|
||||
...
|
||||
</html>
|
||||
```
|
||||
|
||||
As soon as all elements are registered _or_ after two seconds have elapsed, the autoloader will show the page. The two-second timeout prevents blank screens from persisting on slow networks and pages that have errors.
|
||||
|
||||
:::details Are you using Turbo in your app?
|
||||
|
||||
If you're using [Turbo](https://turbo.hotwired.dev/) to serve a multi-page application (MPA) as a single page application (SPA), you might notice FOUCE when navigating from page to page. This is because Turbo renders the new page's content before the autoloader has a change to register new components.
|
||||
|
||||
The following function acts as a middleware to ensure components are registered _before_ the page shows, eliminating FOUCE for page-to-page navigation with Turbo.
|
||||
|
||||
```js
|
||||
import { preventTurboFouce } from '/dist/webawesome.js';
|
||||
|
||||
preventTurboFouce();
|
||||
```
|
||||
:::
|
||||
@@ -1,131 +0,0 @@
|
||||
---
|
||||
title: Reduce FOUCE
|
||||
description: Utility to improve the loading experience by hiding non-prerendered custom elements until they are registered.
|
||||
file: styles/utilities/fouce.css
|
||||
icon: spinner
|
||||
---
|
||||
{% markdown %}
|
||||
No class is needed to use this utility, it will be applied automatically as long as it its CSS is included.
|
||||
|
||||
Here is a comparison of the loading experience with and without this utility,
|
||||
with a simulated slow loading time:
|
||||
|
||||
{% endmarkdown %}
|
||||
|
||||
|
||||
<div class="wa-split wa-align-items-end">
|
||||
<strong>Normal loading</strong>
|
||||
<wa-button onclick="document.querySelectorAll('iframe').forEach(iframe => iframe.srcdoc = iframe.srcdoc)">
|
||||
<wa-icon name="refresh"></wa-icon>
|
||||
Refresh
|
||||
</wa-button>
|
||||
<strong>With FOUCE reduction</strong>
|
||||
</div>
|
||||
|
||||
|
||||
{% set sample_card %}
|
||||
<link id="theme-stylesheet" rel="stylesheet" href="/dist/styles/themes/default.css" render="blocking" fetchpriority="high">
|
||||
<link rel="stylesheet" href="/dist/styles/webawesome.css">
|
||||
<link rel="stylesheet" href="/dist/styles/forms.css">
|
||||
<script type=module>
|
||||
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
const loadScript = src => new Promise((resolve, reject) => {
|
||||
const script = document.createElement("script");
|
||||
script.src = src;
|
||||
script.type = "module";
|
||||
script.onload = resolve;
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
|
||||
await delay(500);
|
||||
await loadScript("/dist/components/button/button.js");
|
||||
await delay(500);
|
||||
await loadScript("/dist/components/card/card.js");
|
||||
await delay(500);
|
||||
await loadScript("/dist/components/rating/rating.js");
|
||||
</script>
|
||||
|
||||
<wa-card with-footer with-image class="card-overview">
|
||||
<img
|
||||
slot="image"
|
||||
src="https://images.unsplash.com/photo-1559209172-0ff8f6d49ff7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=80"
|
||||
alt="A kitten sits patiently between a terracotta pot and decorative grasses."
|
||||
/>
|
||||
|
||||
<strong>Mittens</strong><br />
|
||||
This kitten is as cute as he is playful. Bring him home today!<br />
|
||||
<small>6 weeks old</small>
|
||||
|
||||
<div slot="footer">
|
||||
<wa-button variant="brand" pill>More Info</wa-button>
|
||||
<wa-rating></wa-rating>
|
||||
</div>
|
||||
</wa-card>
|
||||
|
||||
<style>
|
||||
.card-overview small {
|
||||
color: var(--wa-color-text-quiet);
|
||||
}
|
||||
|
||||
.card-overview [slot=footer] {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
{% endset %}
|
||||
|
||||
|
||||
<div class="iframes">
|
||||
<iframe srcdoc='<body class="wa-fouce-off">{{ sample_card }}</body>'></iframe>
|
||||
<iframe srcdoc='{{ sample_card }}'></iframe>
|
||||
</div>
|
||||
<style>
|
||||
.iframes {
|
||||
display: flex;
|
||||
gap: var(--wa-space-m);
|
||||
margin-top: var(--wa-space-l);
|
||||
|
||||
iframe {
|
||||
flex: 1;
|
||||
height: 60ch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
{% markdown %}
|
||||
## How does it work?
|
||||
|
||||
The utility consists of a timeout (`2s` by default) and a fade duration (`200ms` by default).
|
||||
- If the element is _ready_ before the timeout, it will appear immediately.
|
||||
- If it takes longer than _timeout_ + _fade_, it will fade in over the fade duration.
|
||||
- If it takes somewhere between _timeout_ and _timeout_ + _fade_, you will get an interrupted fade.
|
||||
|
||||
An element is considered ready when both of these are true:
|
||||
1. Either It has been registered or has a `did-ssr` attribute (indicating it was pre-rendered)
|
||||
2. If it’s a Web Awesome component, its contents are also ready
|
||||
|
||||
## Customization
|
||||
|
||||
You can use the following CSS variables to customize the behavior:
|
||||
|
||||
| Variable | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `--wa-fouce-fade` | The transition duration for the fade effect. | `200ms` |
|
||||
| `--wa-fouce-timeout` | The timeout after which elements will appear even if not registered | `2s` |
|
||||
|
||||
The fade duration cannot be longer than the timeout.
|
||||
This means that you can disable FOUCE reduction on an element by setting `--wa-fouce-timeout: 0s`.
|
||||
|
||||
For example, if instead of `did-ssr` you used an `ssr` attribute to mark elements that were pre-rendered, you can do this to get them to appear immediately:
|
||||
|
||||
```css
|
||||
[ssr] {
|
||||
--wa-fouce-timeout: 0s;
|
||||
}
|
||||
```
|
||||
|
||||
You can also opt-out from FOUCE reduction for an element and its contents by adding the `.wa-fouce-off` class to it.
|
||||
Applying this class to the root element will disable the utility for the entire page.
|
||||
{% endmarkdown %}
|
||||
@@ -4,8 +4,6 @@ description: Build better with Web Awesome, the open source library of web compo
|
||||
layout: page
|
||||
---
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
.title,
|
||||
.anchor-heading a,
|
||||
@@ -387,4 +385,4 @@ layout: page
|
||||
© Fonticons, Inc.
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@shoelace-style/webawesome",
|
||||
"version": "3.0.0-alpha.10",
|
||||
"version": "3.0.0-alpha.11",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@shoelace-style/webawesome",
|
||||
"version": "3.0.0-alpha.10",
|
||||
"version": "3.0.0-alpha.11",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "^4.1.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@shoelace-style/webawesome",
|
||||
"description": "A forward-thinking library of web components.",
|
||||
"version": "3.0.0-alpha.10",
|
||||
"version": "3.0.0-alpha.11",
|
||||
"homepage": "https://webawesome.com/",
|
||||
"author": "Web Awesome",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { execSync } from 'child_process';
|
||||
import { deleteAsync } from 'del';
|
||||
import esbuild from 'esbuild';
|
||||
import { replace } from 'esbuild-plugin-replace';
|
||||
|
||||
import { mkdir, readFile } from 'fs/promises';
|
||||
import getPort, { portNumbers } from 'get-port';
|
||||
import { globby } from 'globby';
|
||||
@@ -266,6 +267,13 @@ async function regenerateBundle() {
|
||||
* Generates the documentation site.
|
||||
*/
|
||||
async function generateDocs() {
|
||||
/**
|
||||
* Used by the webawesome-app to skip doc generation since it will do its own.
|
||||
*/
|
||||
if (process.env.SKIP_ELEVENTY === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
spinner.start('Writing the docs');
|
||||
|
||||
const args = [];
|
||||
|
||||
@@ -52,6 +52,7 @@ import styles from './button.css';
|
||||
@customElement('wa-button')
|
||||
export default class WaButton extends WebAwesomeFormAssociatedElement {
|
||||
static shadowStyle = [variantStyles, appearanceStyles, sizeStyles, nativeStyles, styles];
|
||||
static rectProxy = 'button';
|
||||
|
||||
static get validators() {
|
||||
return [...super.validators, MirrorValidator()];
|
||||
@@ -108,7 +109,7 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
|
||||
@property({ reflect: true }) value: string | null = null;
|
||||
|
||||
/** When set, the underlying button will be rendered as an `<a>` with this `href` instead of a `<button>`. */
|
||||
@property() href = '';
|
||||
@property({ reflect: true }) href = null;
|
||||
|
||||
/** Tells the browser where to open the link. Only used when `href` is present. */
|
||||
@property() target: '_blank' | '_parent' | '_self' | '_top';
|
||||
@@ -224,17 +225,6 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
|
||||
this.button.blur();
|
||||
}
|
||||
|
||||
getBoundingClientRect(): DOMRect {
|
||||
let rect = super.getBoundingClientRect();
|
||||
let buttonRect = this.button.getBoundingClientRect();
|
||||
|
||||
if (rect.width === 0 && buttonRect.width > 0) {
|
||||
return buttonRect;
|
||||
}
|
||||
|
||||
return rect;
|
||||
}
|
||||
|
||||
render() {
|
||||
const isLink = this.isLink();
|
||||
const tag = isLink ? literal`a` : literal`button`;
|
||||
|
||||
@@ -5,15 +5,16 @@
|
||||
|
||||
--spacing: var(--wa-space);
|
||||
--border-width: var(--wa-panel-border-width);
|
||||
--border-color: var(--wa-color-surface-border);
|
||||
--outlined-background-color: var(--wa-color-surface-default);
|
||||
--outlined-border-color: var(--wa-color-surface-border);
|
||||
--border-radius: var(--wa-panel-border-radius);
|
||||
|
||||
--inner-border-radius: calc(var(--border-radius) - var(--border-width));
|
||||
--inner-border-color: var(--outlined-border-color);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--wa-color-surface-default);
|
||||
border-color: var(--border-color);
|
||||
background-color: var(--background-color, var(--wa-color-surface-default));
|
||||
border-color: var(--border-color, var(--wa-color-surface-border));
|
||||
border-radius: var(--border-radius);
|
||||
border-style: var(--wa-panel-border-style);
|
||||
box-shadow: var(--wa-shadow-s);
|
||||
@@ -21,6 +22,20 @@
|
||||
color: var(--wa-color-text-normal);
|
||||
}
|
||||
|
||||
:host(:is([appearance~='accent'], .wa-accent)) {
|
||||
color: var(--text-color, var(--wa-color-text-normal));
|
||||
}
|
||||
|
||||
:host([appearance~='filled']),
|
||||
:host(.wa-filled) {
|
||||
--inner-border-color: oklab(from var(--outlined-border-color) l a b / 65%);
|
||||
}
|
||||
|
||||
:host([appearance='plain']) {
|
||||
--inner-border-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Take care of top and bottom radii */
|
||||
.image,
|
||||
:host(:not([with-image])) .header,
|
||||
@@ -46,10 +61,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Round all corners for plain appearance */
|
||||
:host([appearance='plain']) .image {
|
||||
border-radius: var(--inner-border-radius);
|
||||
|
||||
&::slotted(img) {
|
||||
border-radius: inherit !important;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: block;
|
||||
border-block-end-style: inherit;
|
||||
border-block-end-color: var(--border-color);
|
||||
border-block-end-color: var(--inner-border-color);
|
||||
border-block-end-width: var(--border-width);
|
||||
padding: calc(var(--spacing) / 2) var(--spacing);
|
||||
}
|
||||
@@ -62,7 +86,7 @@
|
||||
.footer {
|
||||
display: block;
|
||||
border-block-start-style: inherit;
|
||||
border-block-start-color: var(--border-color);
|
||||
border-block-start-color: var(--inner-border-color);
|
||||
border-block-start-width: var(--border-width);
|
||||
padding: var(--spacing);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { HasSlotController } from '../../internal/slot.js';
|
||||
import WebAwesomeElement from '../../internal/webawesome-element.js';
|
||||
import appearanceStyles from '../../styles/utilities/appearance.css';
|
||||
import sizeStyles from '../../styles/utilities/size.css';
|
||||
import styles from './card.css';
|
||||
|
||||
@@ -22,19 +23,24 @@ import styles from './card.css';
|
||||
* @csspart footer - The container that wraps the card's footer.
|
||||
*
|
||||
* @cssproperty [--border-radius=var(--wa-panel-border-radius)] - The radius for the card's corners. Expects a single value.
|
||||
* @cssproperty [--border-color=var(--wa-color-surface-border)] - The color of the card's borders, including inner borders. Expects a single value.
|
||||
* @cssproperty [--border-color=var(--wa-color-surface-border)] - The color of the card's borders. Expects a single value.
|
||||
* @cssproperty [--inner-border-color=var(--wa-color-surface-border)] - The color of the card's inner borders, e.g. those separating headers and footers from the main content. Expects a single value.
|
||||
* @cssproperty [--border-width=var(--wa-panel-border-width)] - The width of the card's borders. Expects a single value.
|
||||
* @cssproperty [--spacing=var(--wa-space)] - The amount of space around and between sections of the card. Expects a single value.
|
||||
*/
|
||||
@customElement('wa-card')
|
||||
export default class WaCard extends WebAwesomeElement {
|
||||
static shadowStyle = [sizeStyles, styles];
|
||||
static shadowStyle = [sizeStyles, appearanceStyles, styles];
|
||||
|
||||
private readonly hasSlotController = new HasSlotController(this, 'footer', 'header', 'image');
|
||||
|
||||
/** The component's size. Will be inherited by any descendants with a `size` attribute. */
|
||||
@property({ reflect: true, initial: 'medium' }) size: 'small' | 'medium' | 'large' | 'inherit' = 'inherit';
|
||||
|
||||
/** The card's visual appearance. */
|
||||
@property({ reflect: true })
|
||||
appearance: 'accent' | 'filled' | 'outlined' | 'plain' = 'outlined';
|
||||
|
||||
/** Renders the card with a header. Only needed for SSR, otherwise is automatically added. */
|
||||
@property({ attribute: 'with-header', type: Boolean, reflect: true }) withHeader = false;
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ export default class WaCheckbox extends WebAwesomeFormAssociatedElement {
|
||||
@property({ type: Boolean, reflect: true }) required = false;
|
||||
|
||||
/** The checkbox's hint. If you need to display HTML, use the `hint` slot instead. */
|
||||
@property({ attribute: 'hint' }) hint = '';
|
||||
@property() hint = '';
|
||||
|
||||
private handleClick() {
|
||||
this.hasInteracted = true;
|
||||
|
||||
@@ -278,11 +278,11 @@
|
||||
}
|
||||
|
||||
/*
|
||||
* Color dropdown
|
||||
*/
|
||||
* Color dropdown
|
||||
*/
|
||||
|
||||
.color-dropdown {
|
||||
display: flex;
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.color-dropdown::part(panel) {
|
||||
|
||||
@@ -55,12 +55,6 @@ details {
|
||||
padding-block-start: var(--spacing);
|
||||
}
|
||||
|
||||
.show {
|
||||
}
|
||||
|
||||
.hide {
|
||||
}
|
||||
|
||||
@keyframes show {
|
||||
from {
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getTargetElement, waitForEvent } from '../../internal/event.js';
|
||||
import { watch } from '../../internal/watch.js';
|
||||
import WebAwesomeElement from '../../internal/webawesome-element.js';
|
||||
import nativeStyles from '../../styles/native/details.css';
|
||||
import appearanceStyles from '../../styles/utilities/appearance.css';
|
||||
import { LocalizeController } from '../../utilities/localize.js';
|
||||
import '../icon/icon.js';
|
||||
import styles from './details.css';
|
||||
@@ -45,7 +46,7 @@ import styles from './details.css';
|
||||
*/
|
||||
@customElement('wa-details')
|
||||
export default class WaDetails extends WebAwesomeElement {
|
||||
static shadowStyle = [nativeStyles, styles];
|
||||
static shadowStyle = [appearanceStyles, nativeStyles, styles];
|
||||
|
||||
private detailsObserver: MutationObserver;
|
||||
private readonly localize = new LocalizeController(this);
|
||||
@@ -67,6 +68,9 @@ export default class WaDetails extends WebAwesomeElement {
|
||||
/** Disables the details so it can't be toggled. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
/** The element's visual appearance. */
|
||||
@property({ reflect: true }) appearance: 'filled' | 'outlined' | 'plain' = 'outlined';
|
||||
|
||||
firstUpdated() {
|
||||
this.body.style.height = this.open ? 'auto' : '0';
|
||||
if (this.open) {
|
||||
|
||||
@@ -1,24 +1,89 @@
|
||||
:host {
|
||||
--background-color: var(--wa-color-surface-raised);
|
||||
--border-radius: var(--wa-panel-border-radius);
|
||||
--box-shadow: var(--wa-shadow-l);
|
||||
--width: 31rem;
|
||||
--spacing: var(--wa-space-xl);
|
||||
--show-duration: 200ms;
|
||||
--hide-duration: 200ms;
|
||||
|
||||
display: contents;
|
||||
}
|
||||
|
||||
:host(:not([open])) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
dialog {
|
||||
width: inherit;
|
||||
max-width: inherit;
|
||||
max-height: inherit;
|
||||
background-color: inherit;
|
||||
border-radius: inherit;
|
||||
border: inherit;
|
||||
box-shadow: inherit;
|
||||
padding: inherit;
|
||||
:host([open]) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: var(--width);
|
||||
max-width: calc(100% - var(--wa-space-2xl));
|
||||
max-height: calc(100% - var(--wa-space-2xl));
|
||||
background-color: var(--background-color);
|
||||
border-radius: var(--border-radius);
|
||||
border: none;
|
||||
box-shadow: var(--box-shadow);
|
||||
padding: 0;
|
||||
margin: auto;
|
||||
transition: inherit;
|
||||
|
||||
&.show {
|
||||
animation: show-dialog var(--show-duration) ease;
|
||||
|
||||
&::backdrop {
|
||||
animation: show-backdrop var(--show-duration, 200ms) ease;
|
||||
}
|
||||
}
|
||||
|
||||
&.hide {
|
||||
animation: show-dialog var(--hide-duration) ease reverse;
|
||||
|
||||
&::backdrop {
|
||||
animation: show-backdrop var(--hide-duration, 200ms) ease reverse;
|
||||
}
|
||||
}
|
||||
|
||||
&.pulse {
|
||||
animation: pulse 250ms ease;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Ensure there's enough vertical padding for phones that don't update vh when chrome appears (e.g. iPhone) */
|
||||
@media screen and (max-width: 420px) {
|
||||
.dialog {
|
||||
max-height: 80vh;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog--open {
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.header {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
padding: var(--spacing);
|
||||
padding-block-end: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
align-self: center;
|
||||
flex: 1 1 auto;
|
||||
font-family: inherit;
|
||||
font-size: var(--wa-font-size-l);
|
||||
font-weight: var(--wa-font-weight-heading);
|
||||
line-height: var(--wa-line-height-condensed);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
@@ -28,13 +93,81 @@ dialog {
|
||||
flex-wrap: wrap;
|
||||
justify-content: end;
|
||||
gap: var(--wa-space-2xs);
|
||||
margin-inline-start: auto;
|
||||
padding-inline-start: var(--spacing);
|
||||
}
|
||||
|
||||
wa-icon-button,
|
||||
::slotted(wa-icon-button) {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--wa-font-size-m);
|
||||
.header-actions wa-icon-button,
|
||||
.header-actions ::slotted(wa-icon-button) {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--wa-font-size-m);
|
||||
}
|
||||
|
||||
.body {
|
||||
flex: 1 1 auto;
|
||||
display: block;
|
||||
padding: var(--spacing);
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.footer {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--wa-space-xs);
|
||||
justify-content: end;
|
||||
padding: var(--spacing);
|
||||
padding-block-start: 0;
|
||||
}
|
||||
|
||||
.footer ::slotted(wa-button:not(:first-of-type)) {
|
||||
margin-inline-start: var(--wa-spacing-xs);
|
||||
}
|
||||
|
||||
.dialog::backdrop {
|
||||
/*
|
||||
NOTE: the ::backdrop element doesn't inherit properly in Safari yet, but it will in 17.4! At that time, we can
|
||||
remove the fallback values here.
|
||||
*/
|
||||
background-color: var(--wa-color-overlay-modal, rgb(0 0 0 / 0.25));
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
scale: 1;
|
||||
}
|
||||
50% {
|
||||
scale: 1.02;
|
||||
}
|
||||
100% {
|
||||
scale: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes show-dialog {
|
||||
from {
|
||||
opacity: 0;
|
||||
scale: 0.8;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
scale: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes show-backdrop {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
.dialog {
|
||||
border: solid 1px white;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { html, isServer } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { WaAfterHideEvent } from '../../events/after-hide.js';
|
||||
import { WaAfterShowEvent } from '../../events/after-show.js';
|
||||
import { WaHideEvent } from '../../events/hide.js';
|
||||
@@ -8,7 +9,6 @@ import { animateWithClass } from '../../internal/animate.js';
|
||||
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll.js';
|
||||
import { watch } from '../../internal/watch.js';
|
||||
import WebAwesomeElement from '../../internal/webawesome-element.js';
|
||||
import dialogStyles from '../../styles/native/dialog.css';
|
||||
import { LocalizeController } from '../../utilities/localize.js';
|
||||
import '../icon-button/icon-button.js';
|
||||
import styles from './dialog.css';
|
||||
@@ -35,7 +35,6 @@ import styles from './dialog.css';
|
||||
* behavior such as data loss.
|
||||
* @event wa-after-hide - Emitted after the dialog closes and all animations are complete.
|
||||
*
|
||||
* @csspart base - The inner `<dialog>` used to render this component.
|
||||
* @csspart header - The dialog's header. This element wraps the title and header actions.
|
||||
* @csspart header-actions - Optional actions to add to the header. Works best with `<wa-icon-button>`.
|
||||
* @csspart title - The dialog's title.
|
||||
@@ -44,16 +43,22 @@ import styles from './dialog.css';
|
||||
* @csspart body - The dialog's body.
|
||||
* @csspart footer - The dialog's footer.
|
||||
*
|
||||
* @cssproperty --background-color - The dialog's background color.
|
||||
* @cssproperty --border-radius - The radius of the dialog's corners.
|
||||
* @cssproperty --box-shadow - The shadow effects around the edges of the dialog.
|
||||
* @cssproperty --spacing - The amount of space around and between the dialog's content.
|
||||
* @cssproperty --width - The preferred width of the dialog. Note that the dialog will shrink to accommodate smaller screens.
|
||||
* @cssproperty [--show-duration=200ms] - The animation duration when showing the dialog.
|
||||
* @cssproperty [--hide-duration=200ms] - The animation duration when hiding the dialog.
|
||||
*/
|
||||
@customElement('wa-dialog')
|
||||
export default class WaDialog extends WebAwesomeElement {
|
||||
static shadowStyle = [dialogStyles, styles];
|
||||
static shadowStyle = styles;
|
||||
|
||||
private readonly localize = new LocalizeController(this);
|
||||
private originalTrigger: HTMLElement | null;
|
||||
private closeWatcher: CloseWatcher | null;
|
||||
|
||||
@query('dialog') dialog: HTMLDialogElement;
|
||||
@query('.dialog') dialog: HTMLDialogElement;
|
||||
|
||||
/**
|
||||
* Indicates whether or not the dialog is open. You can toggle this attribute to show and hide the dialog, or you can
|
||||
@@ -76,9 +81,6 @@ export default class WaDialog extends WebAwesomeElement {
|
||||
/** When enabled, the dialog will be closed when the user clicks outside of it. */
|
||||
@property({ attribute: 'light-dismiss', type: Boolean }) lightDismiss = false;
|
||||
|
||||
@state()
|
||||
hasOpened = this.open;
|
||||
|
||||
firstUpdated() {
|
||||
if (this.open) {
|
||||
this.addOpenListeners();
|
||||
@@ -100,12 +102,14 @@ export default class WaDialog extends WebAwesomeElement {
|
||||
|
||||
if (waHideEvent.defaultPrevented) {
|
||||
this.open = true;
|
||||
animateWithClass(this.dialog, 'wa-dialog-pulse');
|
||||
animateWithClass(this.dialog, 'pulse');
|
||||
return;
|
||||
}
|
||||
|
||||
this.removeOpenListeners();
|
||||
|
||||
await animateWithClass(this.dialog, 'hide');
|
||||
|
||||
this.open = false;
|
||||
this.dialog.close();
|
||||
unlockBodyScrolling(this);
|
||||
@@ -120,16 +124,7 @@ export default class WaDialog extends WebAwesomeElement {
|
||||
}
|
||||
|
||||
private addOpenListeners() {
|
||||
if ('CloseWatcher' in window) {
|
||||
this.closeWatcher?.destroy();
|
||||
this.closeWatcher = new CloseWatcher();
|
||||
this.closeWatcher.onclose = () => {
|
||||
this.requestClose(this.dialog);
|
||||
};
|
||||
} else {
|
||||
this.closeWatcher?.destroy();
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
}
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
}
|
||||
|
||||
private removeOpenListeners() {
|
||||
@@ -161,7 +156,7 @@ export default class WaDialog extends WebAwesomeElement {
|
||||
if (this.lightDismiss) {
|
||||
this.requestClose(this.dialog);
|
||||
} else {
|
||||
await animateWithClass(this.dialog, 'wa-dialog-pulse');
|
||||
await animateWithClass(this.dialog, 'pulse');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,7 +193,6 @@ export default class WaDialog extends WebAwesomeElement {
|
||||
this.addOpenListeners();
|
||||
this.originalTrigger = document.activeElement as HTMLElement;
|
||||
this.open = true;
|
||||
this.hasOpened = true;
|
||||
this.dialog.showModal();
|
||||
|
||||
lockBodyScrolling(this);
|
||||
@@ -211,20 +205,28 @@ export default class WaDialog extends WebAwesomeElement {
|
||||
}
|
||||
});
|
||||
|
||||
await animateWithClass(this.dialog, 'show');
|
||||
|
||||
this.dispatchEvent(new WaAfterShowEvent());
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<dialog
|
||||
part="base"
|
||||
part="dialog"
|
||||
class=${classMap({
|
||||
dialog: true,
|
||||
'dialog--open': this.open,
|
||||
'dialog--with-header': this.withHeader,
|
||||
'dialog--with-footer': this.withFooter,
|
||||
})}
|
||||
@cancel=${this.handleDialogCancel}
|
||||
@click=${this.handleDialogClick}
|
||||
@pointerdown=${this.handleDialogPointerDown}
|
||||
>
|
||||
${this.withHeader
|
||||
? html`
|
||||
<header part="header">
|
||||
<header part="header" class="header">
|
||||
<h2 part="title" class="title" id="title">
|
||||
<!-- If there's no label, use an invisible character to prevent the header from collapsing -->
|
||||
<slot name="label"> ${this.label.length > 0 ? this.label : String.fromCharCode(65279)} </slot>
|
||||
@@ -246,11 +248,11 @@ export default class WaDialog extends WebAwesomeElement {
|
||||
`
|
||||
: ''}
|
||||
|
||||
<slot part="body" class="body"></slot>
|
||||
<div part="body" class="body"><slot></slot></div>
|
||||
|
||||
${this.withFooter
|
||||
? html`
|
||||
<footer part="footer">
|
||||
<footer part="footer" class="footer">
|
||||
<slot name="footer"></slot>
|
||||
</footer>
|
||||
`
|
||||
@@ -262,7 +264,7 @@ export default class WaDialog extends WebAwesomeElement {
|
||||
|
||||
// Ugly, but it fixes light dismiss in Safari: https://bugs.webkit.org/show_bug.cgi?id=267688
|
||||
if (!isServer) {
|
||||
document.body.addEventListener('pointerdown', () => {
|
||||
document.addEventListener('pointerdown', () => {
|
||||
/* empty */
|
||||
});
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ export default class WaDrawer extends WebAwesomeElement {
|
||||
|
||||
private readonly localize = new LocalizeController(this);
|
||||
private originalTrigger: HTMLElement | null;
|
||||
private closeWatcher: CloseWatcher | null;
|
||||
|
||||
@query('.drawer') drawer: HTMLDialogElement;
|
||||
|
||||
@@ -136,16 +135,7 @@ export default class WaDrawer extends WebAwesomeElement {
|
||||
}
|
||||
|
||||
private addOpenListeners() {
|
||||
if ('CloseWatcher' in window) {
|
||||
this.closeWatcher?.destroy();
|
||||
this.closeWatcher = new CloseWatcher();
|
||||
this.closeWatcher.onclose = () => {
|
||||
this.requestClose(this.drawer);
|
||||
};
|
||||
} else {
|
||||
this.closeWatcher?.destroy();
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
}
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
}
|
||||
|
||||
private removeOpenListeners() {
|
||||
|
||||
@@ -50,8 +50,6 @@ export default class WaDropdown extends WebAwesomeElement {
|
||||
@query('#trigger') trigger: HTMLSlotElement;
|
||||
@query('.panel') panel: HTMLSlotElement;
|
||||
|
||||
private closeWatcher: CloseWatcher | null;
|
||||
|
||||
/**
|
||||
* Indicates whether or not the dropdown is open. You can toggle this attribute to show and hide the dropdown, or you
|
||||
* can use the `show()` and `hide()` methods and this attribute will reflect the dropdown's open state.
|
||||
@@ -148,7 +146,7 @@ export default class WaDropdown extends WebAwesomeElement {
|
||||
private handleKeyDown = (event: KeyboardEvent) => {
|
||||
// Close when escape is pressed inside an open dropdown. We need to listen on the panel itself and stop propagation
|
||||
// in case any ancestors are also listening for this key.
|
||||
if (this.open && event.key === 'Escape' && !this.closeWatcher) {
|
||||
if (this.open && event.key === 'Escape') {
|
||||
event.stopPropagation();
|
||||
this.hide();
|
||||
this.focusOnTrigger();
|
||||
@@ -344,16 +342,7 @@ export default class WaDropdown extends WebAwesomeElement {
|
||||
|
||||
addOpenListeners() {
|
||||
this.panel.addEventListener('wa-select', this.handlePanelSelect);
|
||||
if ('CloseWatcher' in window) {
|
||||
this.closeWatcher?.destroy();
|
||||
this.closeWatcher = new CloseWatcher();
|
||||
this.closeWatcher.onclose = () => {
|
||||
this.hide();
|
||||
this.focusOnTrigger();
|
||||
};
|
||||
} else {
|
||||
this.panel.addEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
this.panel.addEventListener('keydown', this.handleKeyDown);
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.addEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
}
|
||||
@@ -365,7 +354,6 @@ export default class WaDropdown extends WebAwesomeElement {
|
||||
}
|
||||
document.removeEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
this.closeWatcher?.destroy();
|
||||
}
|
||||
|
||||
@watch('open', { waitUntilFirstUpdate: true })
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
:host {
|
||||
--background-color-hover: var(--wa-color-neutral-fill-quiet);
|
||||
--text-color-hover: color-mix(in oklab, currentColor, var(--wa-color-mix-hover));
|
||||
--background-color-active: transparent;
|
||||
--text-color-active: color-mix(in oklab, currentColor, var(--wa-color-mix-active));
|
||||
|
||||
display: inline-block;
|
||||
color: var(--wa-color-text-quiet);
|
||||
@@ -22,12 +25,13 @@
|
||||
|
||||
:host(:not([disabled])) .icon-button:hover,
|
||||
:host(:not([disabled])) .icon-button:focus-visible {
|
||||
background-color: var(--wa-color-neutral-fill-quiet);
|
||||
color: color-mix(in oklab, currentColor, var(--wa-color-mix-hover));
|
||||
background-color: var(--background-color-hover);
|
||||
color: var(--text-color-hover);
|
||||
}
|
||||
|
||||
:host(:not([disabled])) .icon-button:active {
|
||||
color: color-mix(in oklab, currentColor, var(--wa-color-mix-active));
|
||||
background-color: var(--background-color-active);
|
||||
color: var(--text-color-active);
|
||||
}
|
||||
|
||||
.icon-button:focus {
|
||||
|
||||
@@ -17,7 +17,10 @@ import styles from './icon-button.css';
|
||||
* @event blur - Emitted when the icon button loses focus.
|
||||
* @event focus - Emitted when the icon button gains focus.
|
||||
*
|
||||
* @cssproperty --background-color-hover - The color of the button's background on hover.
|
||||
* @cssproperty [--background-color-hover=var(--wa-color-neutral-fill-quiet)] - The color of the button's background on hover.
|
||||
* @cssproperty [--background-color-active=var(--wa-color-neutral-fill-quiet)] - The color of the button's background on `:active`.
|
||||
* @cssproperty --text-color-hover - The color of the button's background on hover.
|
||||
* @cssproperty --text-color-active - The color of the button's background on `:active`.
|
||||
*
|
||||
* @csspart base - The component's base wrapper.
|
||||
*/
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
--banner-height: 0px;
|
||||
--header-height: 0px;
|
||||
--subheader-height: 0px;
|
||||
--scroll-margin-top: calc(var(--header-height, 0px) + var(--subheader-height, 0px));
|
||||
--scroll-margin-top: calc(var(--header-height, 0px) + var(--subheader-height, 0px) + 0.5em);
|
||||
}
|
||||
|
||||
slot[name]:not([name='skip-to-content'], [name='navigation-toggle'])::slotted(*) {
|
||||
|
||||
@@ -50,6 +50,7 @@ import styles from './radio-button.css';
|
||||
@customElement('wa-radio-button')
|
||||
export default class WaRadioButton extends WebAwesomeFormAssociatedElement {
|
||||
static shadowStyle = [variantStyles, appearanceStyles, sizeStyles, nativeStyles, buttonStyles, styles];
|
||||
static rectProxy = 'input';
|
||||
|
||||
private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix');
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
:host(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.radio {
|
||||
display: flex;
|
||||
:host {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: top;
|
||||
font: inherit;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:host(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.checked-icon {
|
||||
display: flex;
|
||||
fill: currentColor;
|
||||
@@ -19,6 +19,11 @@
|
||||
}
|
||||
|
||||
/* When the control isn't checked, hide the circle for Windows High Contrast mode a11y */
|
||||
.radio:not(.radio--checked) svg circle {
|
||||
:host(:not(:state(checked))) svg circle {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[part~='hint'] {
|
||||
grid-column: 2;
|
||||
margin-block-start: var(--wa-space-3xs);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { PropertyValues } from 'lit';
|
||||
import { html, isServer } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { watch } from '../../internal/watch.js';
|
||||
import { HasSlotController } from '../../internal/slot.js';
|
||||
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-form-associated-element.js';
|
||||
import nativeStyles from '../../styles/native/radio.css';
|
||||
import formControlStyles from '../../styles/shadow/form-control.css';
|
||||
import sizeStyles from '../../styles/utilities/size.css';
|
||||
import '../icon/icon.js';
|
||||
import styles from './radio.css';
|
||||
@@ -17,14 +19,15 @@ import styles from './radio.css';
|
||||
* @dependency wa-icon
|
||||
*
|
||||
* @slot - The radio's label.
|
||||
* @slot hint - Text that describes how to use the checkbox. Alternatively, you can use the `hint` attribute.
|
||||
*
|
||||
* @event blur - Emitted when the control loses focus.
|
||||
* @event focus - Emitted when the control gains focus.
|
||||
*
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart control - The circular container that wraps the radio's checked state.
|
||||
* @csspart checked-icon - The checked icon.
|
||||
* @csspart label - The container that wraps the radio's label.
|
||||
* @csspart hint - The hint's wrapper.
|
||||
*
|
||||
* @cssproperty --background-color - The radio's background color.
|
||||
* @cssproperty --background-color-checked - The radio's background color when checked.
|
||||
@@ -42,7 +45,7 @@ import styles from './radio.css';
|
||||
*/
|
||||
@customElement('wa-radio')
|
||||
export default class WaRadio extends WebAwesomeFormAssociatedElement {
|
||||
static shadowStyle = [sizeStyles, nativeStyles, styles];
|
||||
static shadowStyle = [formControlStyles, sizeStyles, nativeStyles, styles];
|
||||
|
||||
@state() checked = false;
|
||||
|
||||
@@ -63,6 +66,11 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
|
||||
/** Disables the radio. */
|
||||
@property({ type: Boolean }) disabled = false;
|
||||
|
||||
/** The radio's hint. If you need to display HTML, use the `hint` slot instead. */
|
||||
@property() hint = '';
|
||||
|
||||
private readonly hasSlotController = new HasSlotController(this, 'hint');
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
if (!isServer) {
|
||||
@@ -81,11 +89,19 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
|
||||
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
|
||||
}
|
||||
|
||||
@watch('checked')
|
||||
handleCheckedChange() {
|
||||
this.toggleCustomState('checked', this.checked);
|
||||
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
|
||||
this.tabIndex = this.checked ? 0 : -1;
|
||||
updated(changedProperties: PropertyValues<this>) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has('checked')) {
|
||||
this.toggleCustomState('checked', this.checked);
|
||||
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
|
||||
this.tabIndex = this.checked ? 0 : -1;
|
||||
}
|
||||
|
||||
if (changedProperties.has('disabled')) {
|
||||
this.toggleCustomState('disabled', this.disabled);
|
||||
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,12 +111,6 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
|
||||
// We override `setValue` because we don't want to set form values from here. We want to do that in "RadioGroup" itself.
|
||||
}
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
this.toggleCustomState('disabled', this.disabled);
|
||||
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
|
||||
}
|
||||
|
||||
private handleClick = () => {
|
||||
if (!this.disabled) {
|
||||
this.checked = true;
|
||||
@@ -108,26 +118,30 @@ export default class WaRadio extends WebAwesomeFormAssociatedElement {
|
||||
};
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<span
|
||||
part="base"
|
||||
class=${classMap({
|
||||
radio: true,
|
||||
'radio--checked': this.checked,
|
||||
})}
|
||||
>
|
||||
<span part="control" class="control">
|
||||
${this.checked
|
||||
? html`
|
||||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" part="checked-icon" class="checked-icon">
|
||||
<circle cx="8" cy="8" r="8" />
|
||||
</svg>
|
||||
`
|
||||
: ''}
|
||||
</span>
|
||||
const hasHintSlot = isServer ? true : this.hasSlotController.test('hint');
|
||||
const hasHint = this.hint ? true : !!hasHintSlot;
|
||||
|
||||
<slot part="label" class="label"></slot>
|
||||
return html`
|
||||
<span part="control" class="control">
|
||||
${this.checked
|
||||
? html`
|
||||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" part="checked-icon" class="checked-icon">
|
||||
<circle cx="8" cy="8" r="8" />
|
||||
</svg>
|
||||
`
|
||||
: ''}
|
||||
</span>
|
||||
|
||||
<slot part="label" class="label"></slot>
|
||||
|
||||
<slot
|
||||
name="hint"
|
||||
aria-hidden=${hasHint ? 'false' : 'true'}
|
||||
class="${classMap({ 'has-slotted': hasHint })}"
|
||||
id="hint"
|
||||
part="hint"
|
||||
>${this.hint}</slot
|
||||
>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@
|
||||
align-items: center;
|
||||
color: var(--wa-color-neutral-on-quiet);
|
||||
transition: rotate var(--wa-transition-slow) ease;
|
||||
rotate: 0;
|
||||
rotate: 0deg;
|
||||
margin-inline-start: var(--wa-space-s);
|
||||
|
||||
.open & {
|
||||
@@ -194,7 +194,7 @@
|
||||
background: var(--wa-color-surface-raised);
|
||||
border-color: var(--wa-color-surface-border);
|
||||
border-radius: var(--wa-border-radius-m);
|
||||
border-style: var(--border-style);
|
||||
border-style: var(--wa-border-style);
|
||||
border-width: var(--border-width);
|
||||
padding-block: var(--wa-space-xs);
|
||||
padding-inline: 0;
|
||||
@@ -208,13 +208,13 @@
|
||||
&::slotted(wa-divider) {
|
||||
--spacing: var(--wa-space-xs);
|
||||
}
|
||||
|
||||
&::slotted(small) {
|
||||
display: block;
|
||||
font-size: var(--wa-font-size-s);
|
||||
font-weight: var(--wa-font-weight-semibold);
|
||||
color: var(--wa-color-text-quiet);
|
||||
padding-block: var(--wa-space-xs);
|
||||
padding-inline: var(--wa-space-xl);
|
||||
}
|
||||
}
|
||||
|
||||
slot:not([name])::slotted(small) {
|
||||
display: block;
|
||||
font-size: var(--wa-font-size-s);
|
||||
font-weight: var(--wa-font-weight-semibold);
|
||||
color: var(--wa-color-text-quiet);
|
||||
padding-block: var(--wa-space-xs);
|
||||
padding-inline: var(--wa-space-xl);
|
||||
}
|
||||
|
||||
@@ -647,6 +647,7 @@ describe('<wa-select>', () => {
|
||||
);
|
||||
const el = form.querySelector<WaSelect>('wa-select')!;
|
||||
|
||||
expect(el.defaultValue).to.equal('option-1');
|
||||
expect(el.value).to.equal('');
|
||||
expect(new FormData(form).get('select')).equal('');
|
||||
|
||||
@@ -657,6 +658,7 @@ describe('<wa-select>', () => {
|
||||
|
||||
await aTimeout(10);
|
||||
await el.updateComplete;
|
||||
expect(el.optionValues ? [...el.optionValues] : []).to.have.members(['option-1']);
|
||||
expect(el.value).to.equal('option-1');
|
||||
expect(new FormData(form).get('select')).equal('option-1');
|
||||
});
|
||||
@@ -745,6 +747,8 @@ describe('<wa-select>', () => {
|
||||
);
|
||||
|
||||
const el = form.querySelector<WaSelect>('wa-select')!;
|
||||
expect(el.optionValues ? [...el.optionValues] : []).to.have.members(['bar', 'baz']);
|
||||
expect(el.optionValues?.size).to.equal(2);
|
||||
expect(el.value).to.have.members(['bar', 'baz']);
|
||||
expect(el.value!.length).to.equal(2);
|
||||
expect(new FormData(form).getAll('select')).to.have.members(['bar', 'baz']);
|
||||
@@ -760,6 +764,36 @@ describe('<wa-select>', () => {
|
||||
expect(new FormData(form).getAll('select')).to.have.members(['foo', 'bar', 'baz']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('With setting the value via JS', () => {
|
||||
it('Should preserve value even if not returned', async () => {
|
||||
const form = await fixture<HTMLFormElement>(
|
||||
html` <form>
|
||||
<wa-select name="select">
|
||||
<wa-option value="bar">Bar</wa-option>
|
||||
<wa-option value="baz">Baz</wa-option>
|
||||
</wa-select>
|
||||
</form>`,
|
||||
);
|
||||
|
||||
const el = form.querySelector<WaSelect>('wa-select')!;
|
||||
expect(el.value).to.equal('');
|
||||
|
||||
el.value = 'foo';
|
||||
await aTimeout(10);
|
||||
await el.updateComplete;
|
||||
expect(el.value).to.equal('');
|
||||
|
||||
const option = document.createElement('wa-option');
|
||||
option.value = 'foo';
|
||||
option.innerText = 'Foo';
|
||||
el.append(option);
|
||||
|
||||
await aTimeout(10);
|
||||
await el.updateComplete;
|
||||
expect(el.value).to.equal('foo');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -102,7 +102,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
private readonly localize = new LocalizeController(this);
|
||||
private typeToSelectString = '';
|
||||
private typeToSelectTimeout: number;
|
||||
private closeWatcher: CloseWatcher | null;
|
||||
|
||||
@query('.select') popup: WaPopup;
|
||||
@query('.combobox') combobox: HTMLSlotElement;
|
||||
@@ -118,6 +117,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
@state() displayLabel = '';
|
||||
@state() currentOption: WaOption;
|
||||
@state() selectedOptions: WaOption[] = [];
|
||||
@state() optionValues: Set<string> | undefined;
|
||||
|
||||
/** The name of the select, submitted as a name/value pair with form data. */
|
||||
@property() name = '';
|
||||
@@ -158,7 +158,47 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
return val;
|
||||
}
|
||||
|
||||
@property({ attribute: false }) value: string | string[] | null = null;
|
||||
private _value: string[] | undefined;
|
||||
@property({ attribute: false })
|
||||
set value(val: string | string[]) {
|
||||
let oldValue = this.value;
|
||||
|
||||
if (!Array.isArray(val)) {
|
||||
val = val.split(' ');
|
||||
}
|
||||
|
||||
if (!this._value || this._value.join(' ') !== val.join(' ')) {
|
||||
this._value = val;
|
||||
let newValue = this.value;
|
||||
|
||||
if (newValue != oldValue) {
|
||||
this.requestUpdate('value', oldValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
get value() {
|
||||
let value = this._value ?? this.defaultValue;
|
||||
value = Array.isArray(value) ? value : [value];
|
||||
let optionsChanged = !this.optionValues;
|
||||
|
||||
if (optionsChanged) {
|
||||
this.optionValues = new Set(
|
||||
this.getAllOptions()
|
||||
.filter(option => !option.disabled)
|
||||
.map(option => option.value),
|
||||
);
|
||||
}
|
||||
|
||||
// Drop values not in the DOM
|
||||
let ret: string | string[] = value.filter(v => this.optionValues!.has(v));
|
||||
ret = this.multiple ? ret : (ret[0] ?? '');
|
||||
|
||||
if (optionsChanged) {
|
||||
this.requestUpdate('value');
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/** The select's size. */
|
||||
@property({ reflect: true, initial: 'medium' }) size: 'small' | 'medium' | 'large' | 'inherit' = 'inherit';
|
||||
@@ -250,7 +290,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
?pill=${this.pill}
|
||||
size=${this.size}
|
||||
removable
|
||||
@wa-remove=${(event: WaRemoveEvent) => this.handleTagRemove(event, option)}
|
||||
>
|
||||
${option.label}
|
||||
</wa-tag>
|
||||
@@ -280,17 +319,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
if (this.getRootNode() !== document) {
|
||||
this.getRootNode().addEventListener('focusin', this.handleDocumentFocusIn);
|
||||
}
|
||||
|
||||
if ('CloseWatcher' in window) {
|
||||
this.closeWatcher?.destroy();
|
||||
this.closeWatcher = new CloseWatcher();
|
||||
this.closeWatcher.onclose = () => {
|
||||
if (this.open) {
|
||||
this.hide();
|
||||
this.displayInput.focus({ preventScroll: true });
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private removeOpenListeners() {
|
||||
@@ -301,8 +329,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
if (this.getRootNode() !== document) {
|
||||
this.getRootNode().removeEventListener('focusin', this.handleDocumentFocusIn);
|
||||
}
|
||||
|
||||
this.closeWatcher?.destroy();
|
||||
}
|
||||
|
||||
private handleFocus() {
|
||||
@@ -538,21 +564,41 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
}
|
||||
|
||||
const allOptions = this.getAllOptions();
|
||||
const val = this.valueHasChanged ? this.value : this.defaultValue;
|
||||
const value = Array.isArray(val) ? val : [val];
|
||||
const values: string[] = [];
|
||||
this.optionValues = undefined; // dirty the value so it gets recalculated
|
||||
|
||||
// Check for duplicate values in menu items
|
||||
allOptions.forEach(option => values.push(option.value));
|
||||
const value = this.value;
|
||||
|
||||
// Select only the options that match the new value
|
||||
this.setSelectedOptions(allOptions.filter(el => value.includes(el.value)));
|
||||
}
|
||||
|
||||
private handleTagRemove(event: WaRemoveEvent, option: WaOption) {
|
||||
private handleTagRemove(event: WaRemoveEvent, directOption?: WaOption) {
|
||||
event.stopPropagation();
|
||||
|
||||
if (!this.disabled) {
|
||||
if (this.disabled) return;
|
||||
|
||||
// Use the directly provided option if available (from getTag method)
|
||||
let option = directOption;
|
||||
|
||||
// If no direct option was provided, find the option from the event path
|
||||
if (!option) {
|
||||
const tagElement = (event.target as Element).closest('wa-tag[part~=tag]');
|
||||
|
||||
if (tagElement) {
|
||||
// Find the index of this tag among all tags
|
||||
const tagsContainer = this.shadowRoot?.querySelector('[part="tags"]');
|
||||
if (tagsContainer) {
|
||||
const allTags = Array.from(tagsContainer.children);
|
||||
const index = allTags.indexOf(tagElement as HTMLElement);
|
||||
|
||||
if (index >= 0 && index < this.selectedOptions.length) {
|
||||
option = this.selectedOptions[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (option) {
|
||||
this.toggleOptionSelection(option, false);
|
||||
|
||||
// Emit after updating
|
||||
@@ -565,6 +611,9 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
|
||||
// Gets an array of all `<wa-option>` elements
|
||||
private getAllOptions() {
|
||||
if (!this?.querySelectorAll) {
|
||||
return [];
|
||||
}
|
||||
return [...this.querySelectorAll<WaOption>('wa-option')];
|
||||
}
|
||||
|
||||
@@ -628,11 +677,24 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
|
||||
// Update selected options cache
|
||||
this.selectedOptions = options.filter(el => el.selected);
|
||||
let selectedValues = new Set(this.selectedOptions.map(el => el.value));
|
||||
|
||||
// Toggle values present in the DOM from this.value, while preserving options NOT present in the DOM (for lazy loading)
|
||||
// Note that options NOT present in the DOM will be moved to the end after this
|
||||
if (selectedValues.size > 0 || this._value) {
|
||||
if (!this._value) {
|
||||
// First time it's set
|
||||
let value = this.defaultValue ?? [];
|
||||
this._value = Array.isArray(value) ? value : [value];
|
||||
}
|
||||
|
||||
// Filter out values that are in the DOM
|
||||
this._value = this._value.filter(value => !this.optionValues?.has(value));
|
||||
this._value.unshift(...selectedValues);
|
||||
}
|
||||
|
||||
// Update the value and display label
|
||||
if (this.multiple) {
|
||||
this.value = this.selectedOptions.map(el => el.value);
|
||||
|
||||
if (this.placeholder && this.value.length === 0) {
|
||||
// When no items are selected, keep the value empty so the placeholder shows
|
||||
this.displayLabel = '';
|
||||
@@ -641,7 +703,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
}
|
||||
} else {
|
||||
const selectedOption = this.selectedOptions[0];
|
||||
this.value = selectedOption?.value ?? '';
|
||||
this.displayLabel = selectedOption?.label ?? '';
|
||||
}
|
||||
|
||||
@@ -654,10 +715,8 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
return this.selectedOptions.map((option, index) => {
|
||||
if (index < this.maxOptionsVisible || this.maxOptionsVisible <= 0) {
|
||||
const tag = this.getTag(option, index);
|
||||
// Wrap so we can handle the remove
|
||||
return html`<div @wa-remove=${(e: WaRemoveEvent) => this.handleTagRemove(e, option)}>
|
||||
${typeof tag === 'string' ? unsafeHTML(tag) : tag}
|
||||
</div>`;
|
||||
if (!tag) return null;
|
||||
return typeof tag === 'string' ? unsafeHTML(tag) : tag;
|
||||
} else if (index === this.maxOptionsVisible) {
|
||||
// Hit tag limit
|
||||
return html`
|
||||
@@ -673,7 +732,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
>
|
||||
`;
|
||||
}
|
||||
return html``;
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -873,7 +932,9 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
/>
|
||||
|
||||
<!-- Tags need to wait for first hydration before populating otherwise it will create a hydration mismatch. -->
|
||||
${this.multiple && this.hasUpdated ? html`<div part="tags" class="tags">${this.tags}</div>` : ''}
|
||||
${this.multiple && this.hasUpdated
|
||||
? html`<div part="tags" class="tags" @wa-remove=${this.handleTagRemove}>${this.tags}</div>`
|
||||
: ''}
|
||||
|
||||
<input
|
||||
class="value-input"
|
||||
@@ -927,6 +988,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
|
||||
</div>
|
||||
|
||||
<slot
|
||||
id="hint"
|
||||
name="hint"
|
||||
part="hint"
|
||||
class=${classMap({
|
||||
|
||||
@@ -49,6 +49,7 @@ import styles from './switch.css';
|
||||
*/
|
||||
@customElement('wa-switch')
|
||||
export default class WaSwitch extends WebAwesomeFormAssociatedElement {
|
||||
static shadowRootOptions = { ...WebAwesomeFormAssociatedElement.shadowRootOptions, delegatesFocus: true };
|
||||
static shadowStyle = [formControlStyles, sizeStyles, styles];
|
||||
|
||||
static get validators() {
|
||||
|
||||
@@ -24,7 +24,6 @@ let id = 0;
|
||||
@customElement('wa-tab')
|
||||
export default class WaTab extends WebAwesomeElement {
|
||||
static shadowStyle = styles;
|
||||
public slot = 'nav'; // Auto-slot into nav slot
|
||||
|
||||
private readonly attrId = ++id;
|
||||
private readonly componentId = `wa-tab-${this.attrId}`;
|
||||
@@ -47,6 +46,9 @@ export default class WaTab extends WebAwesomeElement {
|
||||
@property({ type: Number, reflect: true }) tabIndex = 0;
|
||||
|
||||
connectedCallback() {
|
||||
// Auto-slot into nav slot
|
||||
this.slot ||= 'nav';
|
||||
|
||||
super.connectedCallback();
|
||||
this.setAttribute('role', 'tab');
|
||||
}
|
||||
|
||||
@@ -4,10 +4,16 @@
|
||||
--max-width: 30ch;
|
||||
--padding: var(--wa-space-2xs) var(--wa-space-xs);
|
||||
|
||||
/** These styles are added so we don't interfere in the DOM. */
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
|
||||
/** These styles are added so we dont interfere in the DOM. */
|
||||
/** Defaults for inherited CSS properties */
|
||||
color: var(--wa-tooltip-content-color);
|
||||
font-size: var(--wa-tooltip-font-size);
|
||||
line-height: var(--wa-tooltip-line-height);
|
||||
text-align: start;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
@@ -41,12 +47,6 @@
|
||||
max-width: var(--max-width);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--background-color);
|
||||
font: inherit;
|
||||
color: var(--wa-tooltip-content-color);
|
||||
font-size: var(--wa-tooltip-font-size);
|
||||
line-height: var(--wa-tooltip-line-height);
|
||||
text-align: start;
|
||||
white-space: normal;
|
||||
padding: var(--padding);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user