Compare commits

..

88 Commits

Author SHA1 Message Date
Cory LaViska
15c77082a4 remove outdated section 2025-03-28 10:49:56 -04:00
Cory LaViska
5161936cb0 add option as a dep of select 2025-03-28 10:48:11 -04:00
Cory LaViska
f6e754c1b9 Merge branch 'next' into bug-fixes 2025-03-28 10:43:05 -04:00
Cory LaViska
8ad1a58914 revert example 2025-03-28 10:38:29 -04:00
Cory LaViska
5b1ef2717c fix radio labels (ALPHA-211) 2025-03-28 09:59:18 -04:00
Cory LaViska
30302fe632 update display labels when changed; fixes #702 2025-03-28 09:47:31 -04:00
Cory LaViska
22c0fd2301 fix whitespace 2025-03-28 09:41:34 -04:00
Cory LaViska
d9b3df2bea remove wa-cloak after components load 2025-03-28 09:24:46 -04:00
Cory LaViska
2c047c2843 add FA kit code for codepen 🤞🏻 2025-03-28 09:24:29 -04:00
Konnor Rogers
21aa85acc0 fix search for webawesome app (#845)
* fix search for webawesome app

* prettier
2025-03-27 16:51:41 -04:00
Lea Verou
404c15b303 Fix race condition, closes #843 2025-03-27 16:14:24 -04:00
Cory LaViska
9d3c0142b8 update test 2025-03-27 15:12:21 -04:00
Cory LaViska
acf02a7a03 stop running SSR tests locally 2025-03-27 15:07:38 -04:00
Lea Verou
8a26afc334 Fix for theme icons + easier to generate palette icons (#841)
* Make sure components that only appear within page icons are still detected

* Palette icons

* Update theme-icons.css

* Reduce whitespace between swatches

---------

Co-authored-by: lindsaym-fa <dev@lindsaym.design>
2025-03-27 14:25:52 -04:00
Cory LaViska
50034527e7 add comment 2025-03-27 14:13:31 -04:00
Cory LaViska
96e8ce8bf6 fix native checkbox indeterminate icon; closes #386 2025-03-27 14:08:24 -04:00
Cory LaViska
b8dd847b4b fix progress animation in Safari; closes #356 2025-03-27 13:20:55 -04:00
Cory LaViska
95fd134104 fix dropdown alignment in button group; closes #374 2025-03-27 12:52:02 -04:00
Cory LaViska
c6ccb3280d Merge branch 'next' into bug-fixes 2025-03-27 12:16:22 -04:00
Cory LaViska
513a1e35a9 Dialog fixes (#790)
* revert structure and styles to fix WA-A #123

* fix WA-A #201

* update changelog

* fix search dialog position so it doesn't jump around

* remove close watcher; fix dialog/drawer backdrop animations
2025-03-27 12:14:35 -04:00
Cory LaViska
aeb1c57be0 revert 2025-03-27 11:51:36 -04:00
Lea Verou
09f668fc99 Workaround for dark mode 2025-03-26 18:31:08 -04:00
Lea Verou
d451ba98e5 Fix web fonts in theme icons
Instead of raw DSD, use a component that pulls in a child template and then goes over the CSS and extracts font-related rules into the document, just once per rule.
This also fixes theme icons in Vue.
2025-03-26 18:31:08 -04:00
lindsaym-fa
fd287edd56 Change balance of color swatches 2025-03-26 18:31:08 -04:00
Lea Verou
8424b49646 Theme icons, take 1 2025-03-26 18:31:08 -04:00
Cory LaViska
486cf3d408 revert 2025-03-26 16:14:25 -04:00
Cory LaViska
4f81a432df Revert "improve details styles; fixes #685"
This reverts commit 8151872d22.
2025-03-26 16:09:39 -04:00
Cory LaViska
bef6ee8503 update examples 2025-03-26 15:52:49 -04:00
Cory LaViska
8151872d22 improve details styles; fixes #685 2025-03-26 15:22:29 -04:00
Cory LaViska
d78b984bd0 fix focus ring in Safari; fixes #745 2025-03-26 15:01:03 -04:00
Cory LaViska
2615fc8ab9 fixes #840 2025-03-26 14:38:33 -04:00
Cory LaViska
0d8d8d4055 remove redundant styles 2025-03-26 14:38:24 -04:00
Cory LaViska
be3cfc3420 fix radio button pill styles; fixes #759 2025-03-26 13:46:30 -04:00
Lea Verou
fa24c0f70e Update changelog.md 2025-03-26 13:08:44 -04:00
Cory LaViska
f1149d22a0 add default icon spacing in tab; fixes #779 2025-03-26 13:05:57 -04:00
Cory LaViska
1bba87c66d Improve search lists (#837)
* add debounce to search so it feels more natural

* improve search grid styles
2025-03-26 16:07:09 +00:00
Cory LaViska
0db9ca12e3 Remove unused SSR module and remove first load fade (#835)
* disable SSR module in 11ty

* remove first load fade
2025-03-26 14:45:29 +00:00
Lea Verou
041555fe99 border-radius: 0 on plain details 2025-03-26 10:04:25 -04:00
Lea Verou
b41dbd2de7 Fix: Specify default card background 2025-03-25 16:53:16 -04:00
Lea Verou
7c6f31e0c7 [Card docs] Use style utilities instead of custom CSS 2025-03-25 16:31:40 -04:00
Lea Verou
9e84274a93 [Card] Round all corners of the image for appearance=plain 2025-03-25 16:31:40 -04:00
Lea Verou
2b3803f91e [Card] Support appearance, closes #609 2025-03-25 16:31:40 -04:00
Lea Verou
faed8da3cd Fix broken link 2025-03-25 14:14:53 -04:00
Lea Verou
17cf902f53 Add appearance to details, closes #569
Except `accent` as that's a) far less useful and b) trickier due to the icon color
2025-03-25 14:14:53 -04:00
Lea Verou
8214ff6b2d Several fixes around overviews, outlines etc (#825)
* Fix outline for headings that have links

Previously produced blank items because it assumed any link in a heading is an anchor

* Filter unlisted items from overviews

Previously they were filtered only when the card was rendered, so their heading was still shown

* [Overview] Add id to group headings

* Hide headings from empty groups

Should never happen but you never know

* [Overview] Ensure "Other" is always last even when no sorting
2025-03-25 11:39:04 -04:00
Cory LaViska
c9979e15f8 adds a hard coded delay to drastically reduce theme picker jank (#829) 2025-03-24 20:49:08 +00:00
Cory LaViska
fcfe2bde7d Add FOUCE utilities (#686)
* add fouce utilities

* add comment

* Update docs/docs/installation.md

Co-authored-by: Lea Verou <lea@verou.me>

* commit PR suggestion

* rename wa-reduce-fouce to wa-cloak

* remove class as requested

* add cloak class

* wait a cycle

* move turbo to same file

* reduce fade

* disable SSR and add Turbo FOUCE helper

* disable SSR

* fix test suite

* workflow dispatch

* update fouce util

* no need to remove cloak class

* simplify fouce util

* add allDefined util

* update changelog

---------

Co-authored-by: Lea Verou <lea@verou.me>
Co-authored-by: konnorrogers <konnor5456@gmail.com>
2025-03-24 20:33:24 +00:00
Lea Verou
59dcaaff83 Content hierarchy bugfixes & improvements (#821)
- Sidebar, overview listings, breadcrumbs now based on actual parent-child relationships, rather than increasingly outdated heuristics
- parent properties are now generated automatically from the URL structure, and need only be specified to override that default
- Ability to group by page hierarchy in overview pages, where pages that have >= 2 children become categories

Smaller improvements:
- More flexible syntax for specifying the params of overview pages
- [Overviews] Hide group heading if only one group is present
- parentItem and parentUrl properties that can be used on any page
- Alias a collection as the children of a page (useful for "virtual" parents like Layout)
- Do not error if a page card icon is missing
2025-03-21 16:30:06 -04:00
Cory LaViska
5bad30ec30 fix remove event and return null when empty (#819)
* fix remove event and return null when empty

* use closest
2025-03-21 13:01:49 -04:00
Lea Verou
87c1762146 Scrub :host-context() from everywhere 2025-03-21 12:55:25 -04:00
Konnor Rogers
899edd1d5e Konnorrogers/add a guard for non server deploys (#818)
* add a guard for non-server builds

* add a guard for non-server builds

* add a guard for non-server builds

* prettier
2025-03-20 16:37:22 -04:00
Konnor Rogers
872a110b1e reflect href on buttons (#817) 2025-03-20 14:58:21 -04:00
Lindsay M
07fe6d598e Add curated orange to all palettes, closes #657 (#798)
* Adjust `orange` in Default palette

* Adjust `orange`, `red`, and `yellow` in Classic palette

* Adjust `orange` in Anodized palette

* Adjust `orange` in Bright palette

* Adjust `orange` in Mild palette

* Adjust `orange` in Natural palette

* Adjust `orange` in Vogue palette

* Adjust `orange` in Rudimentary palette

* Adjust `orange` in Elegant palette
2025-03-18 16:08:31 -04:00
Konnor Rogers
79bafc513a 11ty for webawesome-app (#803)
* working on integration

* 11ty for webawesome + app

* add flashes

* additional changes

* prettier

* add note about nunjucks

* prettier
2025-03-18 13:04:24 -04:00
Lea Verou
1d03f7bee0 [Icon-button] Make --background-color-hover work + remaining 3 interaction properties (#801)
* [Icon-button] Make `--background-color-hover` work, fixes #800

* [Icon-button] Introduce `--text-color-hover`, `--background-color-active`, `--text-color-active`

* Oops
2025-03-14 09:29:04 -04:00
Lindsay M
a9bf1bd838 Add --wa-color-{role}-N variables, closes #785 (#797)
* Initial comment, based on #768

* Add `neutral` color variables

* Add `success`, `warning`, and `danger` variables

* Theme touch-ups

* Remove unused clamped tokens

* Re-add clamped tokens test page, refactor to be based on hue instead of `brand`
2025-03-13 17:07:03 -04:00
Lea Verou
c0ca739366 More robust dynamic value / options handling, fixes #789 2025-03-12 16:52:50 -04:00
Cory LaViska
a6745602d6 fix color picker light dismiss (#794)
* fix color picker light dismiss

* update changelog
2025-03-12 15:59:37 -04:00
Cory LaViska
da4f619d95 prevent card example from overflowing (#795) 2025-03-12 14:44:39 -04:00
Cory LaViska
1283a696a5 fix switch + tooltip behavior (#793) 2025-03-12 18:22:23 +00:00
Cory LaViska
d12b97b0b0 fix wa-pill and wa-input[pill] styles (#791) 2025-03-12 16:19:50 +00:00
Lea Verou
e5c2884880 [Tooltip] Specify inherited CSS properties on host, fixes #773 (#774)
* [Tooltip] Specify inherited CSS properties on host, fixes #773

* Remove unused `--show-delay` and `--hide-delay`
2025-03-10 15:08:27 -04:00
Lea Verou
1d600a77c4 Fix #566 2025-03-10 14:15:06 -04:00
lindsaym-fa
db3c568ba2 Add generated orange to Anodized palette 2025-03-05 22:27:11 -05:00
lindsaym-fa
4bb9805ba6 Add generated orange to Bright palette 2025-03-05 22:27:11 -05:00
lindsaym-fa
bd935fa8d5 Add generated orange to Classic palette 2025-03-05 22:27:11 -05:00
lindsaym-fa
c3e582b47b Add generated orange to Natural palette 2025-03-05 22:27:11 -05:00
lindsaym-fa
4d094a4e19 Add generated orange to Rudimentary palette 2025-03-05 22:27:11 -05:00
lindsaym-fa
782c404bdf Add generated orange to Default palette 2025-03-05 22:27:11 -05:00
lindsaym-fa
f1438981b2 Add generated orange to Elegant palette 2025-03-05 22:27:11 -05:00
lindsaym-fa
18b88c2f5c Add generated orange to Mild palette 2025-03-05 22:27:11 -05:00
lindsaym-fa
a2d85f49a3 Add generated orange to Vogue palette 2025-03-05 22:27:11 -05:00
Lea Verou
be00026cd3 Update postprocess.js 2025-03-05 22:27:11 -05:00
Lea Verou
58ed88bc5a Add orange to list of hues 2025-03-05 22:27:11 -05:00
Lea Verou
1d14e186f3 Generate missing hues from neighboring hues 2025-03-05 22:27:11 -05:00
Lea Verou
5f672aabc2 Refactor: variable rename for consistency 2025-03-05 22:27:11 -05:00
Lea Verou
db08e12a32 Pave the way for being able to have core colors that are not mapped to any tint 2025-03-05 22:27:11 -05:00
Lea Verou
e0fc639226 Only use hex when color is within sRGB 2025-03-05 22:27:11 -05:00
Lea Verou
e6c662b543 tintless.js -> postprocess.js 2025-03-05 22:27:11 -05:00
lindsaym-fa
d1de9a9a73 Fix incorrect sizing tokens in size utilities 2025-02-26 01:01:39 -05:00
lindsaym-fa
4931de8eb4 Fix text color for filled appearance 2025-02-26 01:01:39 -05:00
Lea Verou
71e7227763 Theme remixing fix: Order of params should not matter (#772)
Also renamed the `theme` export to `getThemeCode` since it was being renamed everywhere it was imported.
2025-02-21 14:03:55 -05:00
Lea Verou
dd671e15aa Changelog (#770) 2025-02-21 13:14:19 -05:00
Cory LaViska
2daeea0349 3.0.0-alpha.11 2025-02-21 12:53:05 -05:00
Cory LaViska
3cb6625c1d update changelog 2025-02-21 12:52:51 -05:00
Lea Verou
c4b5446d01 Fix boundingClientRect issue for elements whose host is display: contents 2025-02-21 12:02:20 -05:00
Lindsay M
41affca083 Allow color tweak tags to wrap (#769) 2025-02-21 11:50:13 -05:00
Lea Verou
132dbfabcc Gray tweaks prototype (#761)
Co-authored-by: Lindsay M <126139086+lindsaym-fa@users.noreply.github.com>
2025-02-20 12:10:43 -05:00
189 changed files with 5501 additions and 2123 deletions

View File

@@ -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 # 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 # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: SSR Tests name: SSR Tests
on: on:
push: # push:
branches: [next] # branches: [next]
workflow_dispatch:
jobs: jobs:
ssr_test: ssr_test:

View File

@@ -1,3 +1,4 @@
import * as path from 'node:path';
import { anchorHeadingsPlugin } from './_utils/anchor-headings.js'; import { anchorHeadingsPlugin } from './_utils/anchor-headings.js';
import { codeExamplesPlugin } from './_utils/code-examples.js'; import { codeExamplesPlugin } from './_utils/code-examples.js';
import { copyCodePlugin } from './_utils/copy-code.js'; import { copyCodePlugin } from './_utils/copy-code.js';
@@ -6,9 +7,10 @@ import { highlightCodePlugin } from './_utils/highlight-code.js';
import { markdown } from './_utils/markdown.js'; import { markdown } from './_utils/markdown.js';
import { removeDataAlphaElements } from './_utils/remove-data-alpha-elements.js'; import { removeDataAlphaElements } from './_utils/remove-data-alpha-elements.js';
// import { formatCodePlugin } from './_utils/format-code.js'; // import { formatCodePlugin } from './_utils/format-code.js';
import litPlugin from '@lit-labs/eleventy-plugin-lit'; // import litPlugin from '@lit-labs/eleventy-plugin-lit';
import { readFile } from 'fs/promises'; import { readFile } from 'fs/promises';
import componentList from './_data/componentList.js'; import nunjucks from 'nunjucks';
// import componentList from './_data/componentList.js';
import * as filters from './_utils/filters.js'; import * as filters from './_utils/filters.js';
import { outlinePlugin } from './_utils/outline.js'; import { outlinePlugin } from './_utils/outline.js';
import { replaceTextPlugin } from './_utils/replace-text.js'; import { replaceTextPlugin } from './_utils/replace-text.js';
@@ -16,7 +18,10 @@ import { searchPlugin } from './_utils/search.js';
import process from 'process'; import process from 'process';
const packageData = JSON.parse(await readFile('./package.json', 'utf-8')); import * as url from 'url';
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
const packageData = JSON.parse(await readFile(path.join(__dirname, '..', 'package.json'), 'utf-8'));
const isAlpha = process.argv.includes('--alpha'); const isAlpha = process.argv.includes('--alpha');
const isDev = process.argv.includes('--develop'); const isDev = process.argv.includes('--develop');
@@ -24,12 +29,22 @@ const globalData = {
package: packageData, package: packageData,
isAlpha, isAlpha,
layout: 'page.njk', layout: 'page.njk',
server: {
head: '',
loginOrAvatar: '',
flashes: '',
},
}; };
const passThroughExtensions = ['js', 'css', 'png', 'svg', 'jpg', 'mp4']; const passThroughExtensions = ['js', 'css', 'png', 'svg', 'jpg', 'mp4'];
const passThrough = [...passThroughExtensions.map(ext => 'docs/**/*.' + ext)]; const passThrough = [...passThroughExtensions.map(ext => 'docs/**/*.' + ext)];
export default function (eleventyConfig) { export default function (eleventyConfig) {
/**
* This is the guard we use for now to make sure our final built files dont need a 2nd pass by the server. This keeps us able to still deploy the bare HTML files on Vercel until the app is ready.
*/
const serverBuild = process.env.WEBAWESOME_SERVER === 'true';
// NOTE - alpha setting removes certain pages // NOTE - alpha setting removes certain pages
if (isAlpha) { if (isAlpha) {
eleventyConfig.ignores.add('**/experimental/**'); eleventyConfig.ignores.add('**/experimental/**');
@@ -55,7 +70,38 @@ export default function (eleventyConfig) {
// Shortcodes - {% shortCode arg1, arg2 %} // Shortcodes - {% shortCode arg1, arg2 %}
eleventyConfig.addShortcode('cdnUrl', location => { eleventyConfig.addShortcode('cdnUrl', location => {
return `https://early.webawesome.com/webawesome@${packageData.version}/dist/` + location.replace(/^\//, ''); return `https://early.webawesome.com/webawesome@${packageData.version}/dist/` + (location || '').replace(/^\//, '');
});
// Turns `{% server "foo" %} into `{{ server.foo | safe }}` when the WEBAWESOME_SERVER variable is set to "true"
eleventyConfig.addShortcode('server', function (property) {
if (serverBuild) {
return `{{ server.${property} | safe }}`;
}
return '';
});
eleventyConfig.addTransform('second-nunjucks-transform', function NunjucksTransform(content) {
// For a server build, we expect a server to run the second transform.
if (serverBuild) {
return content;
}
// Only run the transform on files nunjucks would transform.
if (!this.page.inputPath.match(/.(md|html|njk)$/)) {
return content;
}
/** This largely mimics what an app would do and just stubs out what we don't care about. */
return nunjucks.renderString(content, {
// Stub the server EJS shortcodes.
server: {
head: '',
loginOrAvatar: '',
flashes: '',
},
});
}); });
// Paired shortcodes - {% shortCode %}content{% endShortCode %} // Paired shortcodes - {% shortCode %}content{% endShortCode %}
@@ -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 // Build the search index
eleventyConfig.addPlugin( eleventyConfig.addPlugin(
searchPlugin({ searchPlugin({
@@ -166,6 +189,31 @@ export default function (eleventyConfig) {
eleventyConfig.addPassthroughCopy(glob); 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 { return {
markdownTemplateEngine: 'njk', markdownTemplateEngine: 'njk',
dir: { dir: {

View File

@@ -1 +1 @@
["red", "yellow", "green", "cyan", "blue", "indigo", "purple", "pink", "gray"] ["red", "orange", "yellow", "green", "cyan", "blue", "indigo", "purple", "pink", "gray"]

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html> <!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> <head>
{% include 'head.njk' %} {% include 'head.njk' %}
<meta name="theme-color" content="#f36944"> <meta name="theme-color" content="#f36944">
@@ -50,6 +50,9 @@
Search Search
<kbd slot="suffix" class="only-desktop">/</kbd> <kbd slot="suffix" class="only-desktop">/</kbd>
</wa-button> </wa-button>
{# Login #}
{% server "loginOrAvatar" %}
</div> </div>
</header> </header>
@@ -76,14 +79,19 @@
</aside> </aside>
{% endif %} {% endif %}
{# Main #} {# Main #}
<main id="content"> <main id="content">
{# Expandable outline #} {# Expandable outline #}
{% if hasOutline %}
<nav id="outline-expandable"> <nav id="outline-expandable">
<details class="outline-links"> <details class="outline-links">
<summary>On this page</summary> <summary>On this page</summary>
</details> </details>
</nav> </nav>
{% endif %}
<div id="flashes">{% server "flashes" %}</div>
{% block header %} {% block header %}
{% include 'breadcrumbs.njk' %} {% include 'breadcrumbs.njk' %}

View File

@@ -1,8 +1,11 @@
{% set breadcrumbs = page.url | breadcrumbs %} {% set ancestors = page.url | ancestors %}
{% if breadcrumbs.length > 0 %}
{% if ancestors.length > 0 %}
<wa-breadcrumb id="docs-breadcrumbs"> <wa-breadcrumb id="docs-breadcrumbs">
{% for crumb in breadcrumbs %} {% for ancestor in ancestors %}
<wa-breadcrumb-item href="{{ crumb.url }}">{{ crumb.title }}</wa-breadcrumb-item> {% if ancestor.page.url != "/" %}
<wa-breadcrumb-item href="{{ ancestor.page.url }}">{{ ancestor.data.title }}</wa-breadcrumb-item>
{% endif %}
{% endfor %} {% endfor %}
<wa-breadcrumb-item>{# Current page #}</wa-breadcrumb-item> <wa-breadcrumb-item>{# Current page #}</wa-breadcrumb-item>
</wa-breadcrumb> </wa-breadcrumb>

View File

@@ -1,12 +1,18 @@
{# Cards for pages listed by category #} {# Cards for pages listed by category #}
<section id="grid" class="index-grid"> <section id="grid" class="index-grid">
{% for category, pages in allPages | groupByTags(categories) -%} {% set groupedPages = allPages | groupPages(categories, page) %}
<h2 class="index-category">{{ category | getCategoryTitle(categories) }}</h2> {% for category, pages in groupedPages -%}
{%- for page in pages -%} {% if groupedPages.meta.groupCount > 1 and pages.length > 0 %}
{%- if not page.data.parent or listChildren -%} <h2 class="index-category" id="{{ category | slugify }}">
{% include "page-card.njk" %} {% if pages.meta.url %}<a href="{{ pages.meta.url }}">{{ pages.meta.title }}</a>
{%- endif -%} {% else %}
{%- endfor -%} {{ pages.meta.title }}
{% endif %}
</h2>
{% endif %}
{%- for page in pages -%}
{% include "page-card.njk" %}
{%- endfor -%}
{%- endfor -%} {%- endfor -%}
</section> </section>

View File

@@ -23,10 +23,12 @@
<script src="/assets/scripts/hydration-errors.js"></script> <script src="/assets/scripts/hydration-errors.js"></script>
<link rel="stylesheet" href="/assets/styles/hydration-errors.css"> <link rel="stylesheet" href="/assets/styles/hydration-errors.css">
<link rel="preconnect" href="https://cdn.jsdelivr.net"> <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 #} {# 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> <script type="module" src="/assets/scripts/theme-picker.js"></script>
{# Preset Theme #} {# Preset Theme #}
@@ -47,3 +49,6 @@
<link rel="stylesheet" href="/dist/styles/webawesome.css" /> <link rel="stylesheet" href="/dist/styles/webawesome.css" />
<link id="color-stylesheet" rel="stylesheet" href="/dist/styles/utilities.css" /> <link id="color-stylesheet" rel="stylesheet" href="/dist/styles/utilities.css" />
<link rel="stylesheet" href="/dist/styles/forms.css" /> <link rel="stylesheet" href="/dist/styles/forms.css" />
{# Used by Web Awesome App to inject other assets into the head. #}
{% server "head" %}

View File

@@ -2,7 +2,7 @@
<a href="{{ page.url }}"{{ page.data.keywords | attr('data-keywords') }}> <a href="{{ page.url }}"{{ page.data.keywords | attr('data-keywords') }}>
<wa-card with-header> <wa-card with-header>
<div slot="header"> <div slot="header">
{% include "svgs/" + (page.data.icon or "thumbnail-placeholder") + ".njk" %} {% include "svgs/" + (page.data.icon or "thumbnail-placeholder") + ".njk" ignore missing %}
</div> </div>
<span class="page-name">{{ page.data.title }}</span> <span class="page-name">{{ page.data.title }}</span>
{% if pageSubtitle -%} {% if pageSubtitle -%}

View File

@@ -1,9 +1,12 @@
{# Some collections (like "patterns") will not have any items in the alpha build for example. So this checks to make sure the collection exists. #} {# Some collections (like "patterns") will not have any items in the alpha build for example. So this checks to make sure the collection exists. #}
{% if collections[tag] -%} {% if collections[tag] -%}
{% set groupUrl %}/docs/{{ tag }}/{% endset %} {% set groupUrl %}/docs/{{ tag }}/{% endset %}
{% set groupItem = groupUrl | getCollectionItemFromUrl %}
{% set children = groupItem.data.children if groupItem.data.children.length > 0 else (collections[tag] | sort) %}
<wa-details {{ ((tag in (tags or [])) or (groupUrl in page.url)) | attr('open') }}> <wa-details {{ ((tag in (tags or [])) or (groupUrl in page.url)) | attr('open') }}>
<h2 slot="summary"> <h2 slot="summary">
{% if groupUrl | getCollectionItemFromUrl %} {% if groupItem %}
<a href="{{ groupUrl }}" title="Overview">{{ title or (tag | capitalize) }} <a href="{{ groupUrl }}" title="Overview">{{ title or (tag | capitalize) }}
<wa-icon name="grid-2"></wa-icon> <wa-icon name="grid-2"></wa-icon>
</a> </a>
@@ -12,10 +15,8 @@
{% endif %} {% endif %}
</h2> </h2>
<ul> <ul>
{% for page in collections[tag] | sort %} {% for page in children %}
{% if not page.data.parent -%}
{% include 'sidebar-link.njk' %} {% include 'sidebar-link.njk' %}
{%- endif %}
{% endfor %} {% endfor %}
</ul> </ul>
</wa-details> </wa-details>

View File

@@ -1,4 +1,4 @@
{% if not (isAlpha and page.data.noAlpha) and page.fileSlug != tag and not page.data.unlisted -%} {% if page | show -%}
<li> <li>
<a href="{{ page.url }}">{{ page.data.title }}</a> <a href="{{ page.url }}">{{ page.data.title }}</a>
{% if page.data.status == 'experimental' %}<wa-icon name="flask"></wa-icon>{% endif %} {% if page.data.status == 'experimental' %}<wa-icon name="flask"></wa-icon>{% endif %}

View File

@@ -1,31 +1,20 @@
{% set paletteId = palette.fileSlug or page.fileSlug %} {% set paletteId = palette.fileSlug or page.fileSlug %}
{% set suffixes = ['-80', '', '-20'] %} {% set suffixes = ['-80', '', '-20'] %}
{% set width = 20 %}
{% set height = 12 %}
{% set height_core = 20 %}
{% set gap_x = 4 %}
{% set gap_y = 4 %}
{% set total_width = (width + gap_x) * hues|length %} <wa-scoped class="palette-icon-host">
{% set total_height = (height + gap_y) * suffixes|length + (height_core - height) %} <template>
<svg viewBox="0 0 {{ total_width }} {{ total_height }}" fill="none" xmlns="http://www.w3.org/2000/svg" class="wa-palette-{{ paletteId }} palette-icon"> <link rel="stylesheet" href="/dist/styles/color/{{ paletteId }}.css">
<style> <link rel="stylesheet" href="/assets/styles/theme-icons.css">
@import url('/dist/styles/color/{{ paletteId }}.css') layer(palette.{{ paletteId }});
.palette-icon {
height: 8ch;
}
</style>
{% for hue in hues -%} <div class="palette-icon" style="--hues: {{ hues|length }}; --suffixes: {{ suffixes|length }}">
{% set hueIndex = loop.index0 %} {% for hue in hues -%}
{% set y = 0 %} {% set hueIndex = loop.index %}
{% for suffix in suffixes -%} {% for suffix in suffixes -%}
{% set swatch_height = height if suffix else height_core %} <div class="swatch"
data-hue="{{ hue }}" data-suffix="{{ suffix }}"
<rect x="{{ hueIndex * (width + gap_x) }}" y="{{ y }}" style="--color: var(--wa-color-{{ hue }}{{ suffix }}); grid-column: {{ hueIndex }}; grid-row: {{ loop.index }}">&nbsp;</div>
width="{{ width }}" height="{{ swatch_height }}" {%- endfor %}
fill="var(--wa-color-{{ hue }}{{ suffix }})" rx="2" /> {%- endfor %}
{% set y = y + swatch_height + gap_y %} </div>
{%- endfor %} </template>
{% endfor %} </wa-scoped>
</svg>

View File

@@ -1,13 +1,13 @@
{% set themeId = theme.fileSlug %} {% set themeId = theme.fileSlug %}
<div> <wa-scoped class="theme-icon-host theme-color-icon-host">
<template shadowrootmode="open"> <template>
<link rel="stylesheet" href="/dist/styles/utilities.css"> <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/{{ page.fileSlug or 'default' }}.css">
<link rel="stylesheet" href="/dist/styles/themes/{{ themeId }}/color.css"> <link rel="stylesheet" href="/dist/styles/themes/{{ themeId }}/color.css">
<link rel="stylesheet" href="/assets/styles/theme-icons.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-accent">A</div>
<div class="wa-brand wa-outlined">A</div> <div class="wa-brand wa-outlined">A</div>
<div class="wa-brand wa-filled">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 class="wa-warning wa-outlined wa-filled"><wa-icon slot="icon" name="triangle-exclamation" variant="regular"></wa-icon></div> #}
</div> </div>
</template> </template>
</div> </wa-scoped>

View File

@@ -1,16 +1,16 @@
{% set themeId = theme.fileSlug %} {% set themeId = theme.fileSlug or page.fileSlug %}
<div> <wa-scoped class="theme-icon-host theme-typography-icon-host">
<template shadowrootmode="open"> <template>
<link rel="stylesheet" href="/dist/styles/native/content.css"> <link rel="stylesheet" href="/dist/styles/native/content.css">
<link rel="stylesheet" href="/dist/styles/native/blockquote.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/{{ page.fileSlug or 'default' }}.css">
<link rel="stylesheet" href="/dist/styles/themes/{{ themeId }}/typography.css"> <link rel="stylesheet" href="/dist/styles/themes/{{ themeId }}/typography.css">
<link rel="stylesheet" href="/assets/styles/theme-icons.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> <h3>Title</h3>
<p>Body text</p> <p>Body text</p>
</div> </div>
</template> </template>
</div> </wa-scoped>

View 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>

View File

@@ -1,6 +1,5 @@
--- ---
layout: page-outline layout: page-outline
tags: ["overview"]
--- ---
{% set forTag = forTag or (page.url | split('/') | last) %} {% set forTag = forTag or (page.url | split('/') | last) %}
{% if description %} {% if description %}
@@ -13,8 +12,10 @@ tags: ["overview"]
</wa-input> </wa-input>
</div> </div>
{% set allPages = collections[forTag] %} {% set allPages = allPages or collections[forTag] %}
{% if allPages and allPages.length > 0 %}
{% include "grouped-pages.njk" %} {% include "grouped-pages.njk" %}
{% endif %}
<link href="/assets/styles/filter.css" rel="stylesheet"> <link href="/assets/styles/filter.css" rel="stylesheet">
<script type="module" src="/assets/scripts/filter.js"></script> <script type="module" src="/assets/scripts/filter.js"></script>

View File

@@ -1,4 +1,9 @@
{% set hasSidebar = true %} {% if hasSidebar == undefined %}
{% set hasOutline = false %} {% set hasSidebar = true %}
{% endif %}
{% if hasOutline == undefined %}
{% set hasOutline = false %}
{% endif %}
{% extends "../_includes/base.njk" %} {% extends "../_includes/base.njk" %}

View File

@@ -18,11 +18,15 @@
tweaking: tweaking.chroma, tweaking: tweaking.chroma,
'tweaking-chroma': tweaking.chroma, 'tweaking-chroma': tweaking.chroma,
'tweaking-hue': tweaking.chroma, 'tweaking-hue': tweaking.chroma,
'tweaked-chroma': tweaked.chroma, 'tweaking-gray-chroma': tweaking.grayChroma,
'tweaked-hue': tweaked.hue, 'tweaked-chroma': tweaked?.chroma,
'tweaked-any': tweaked.chroma || tweaked.hue 'tweaked-hue': tweaked?.hue,
'tweaked-any': tweaked
}" }"
:style="{ '--chroma-scale': chromaScale }"> :style="{
'--chroma-scale': chromaScale,
'--gray-chroma': tweaked?.grayChroma ? grayChroma : '',
}">
{% include 'breadcrumbs.njk' %} {% include 'breadcrumbs.njk' %}
@@ -36,6 +40,9 @@
<div class="block-info"> <div class="block-info">
<code class="class">.wa-palette-{{ paletteId }}</code> <code class="class">.wa-palette-{{ paletteId }}</code>
{% include '../_includes/status.njk' %} {% include '../_includes/status.njk' %}
{% if not isPro %}
<wa-badge class="pro" v-if="tweaked">PRO</wa-badge>
{% endif %}
</div> </div>
{% if description %} {% if description %}
<p class="summary"> <p class="summary">
@@ -48,18 +55,20 @@
{% set maxChroma = 0 %} {% set maxChroma = 0 %}
<wa-callout size="small" class="tweaked-callout" variant="brand"> <wa-callout size="small" class="tweaked-callout" variant="warning">
<wa-icon name="sliders-simple" slot="icon" variant="regular"></wa-icon> <wa-icon name="sliders-simple" slot="icon" variant="regular"></wa-icon>
This palette has been tweaked. This palette has been tweaked.
<wa-tag v-for="tweakHumanReadable, param in tweaksHumanReadable" removable @wa-remove="removeTweak(param)">{% raw %}{{ tweakHumanReadable }}{% endraw %}</wa-tag> <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"> <wa-button @click="reset()" appearance="outlined" variant="danger">
<span slot="prefix" class="icon-modifier"> <span slot="prefix" class="icon-modifier">
<wa-icon name="circle-xmark" variant="regular"></wa-icon> <wa-icon name="circle-xmark" variant="regular"></wa-icon>
</span> </span>
Reset Reset
</wa-button> </wa-button>
<wa-button v-if="!saved" @click="save"> <wa-button v-if="!saved" @click="save" variant="success">
<span slot="prefix" class="icon-modifier"> <span slot="prefix" class="icon-modifier">
<wa-icon name="sidebar" variant="regular"></wa-icon> <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> <wa-icon name="circle-plus" class="modifier" style="color: light-dark(var(--wa-color-green-70), var(--wa-color-green-60));"></wa-icon>
@@ -81,25 +90,67 @@
{# Initialize to last hue before gray #} {# Initialize to last hue before gray #}
{%- set hueBefore = hues[hues|length - 2] -%} {%- set hueBefore = hues[hues|length - 2] -%}
{% for hue in hues -%} {% for hue in hues -%}
{%- set coreColor = palettes[paletteId][hue][palettes[paletteId][hue].maxChromaTint] -%} {% set coreTint = palettes[paletteId][hue].maxChromaTint %}
{%- set coreColor = palettes[paletteId][hue][coreTint] -%}
{%- set maxChroma = coreColor.c if coreColor.c > maxChroma else maxChroma -%} {%- set maxChroma = coreColor.c if coreColor.c > maxChroma else maxChroma -%}
<tr data-hue="{{ hue }}" class="color-scale" :class="{tweaking: tweaking.{{ hue }}, tweaked: hueShifts.{{ hue }} }" {% 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 }} || '' }"> :style="{ '--hue-shift': hueShifts.{{ hue }} || '' }">
{% endif %}
<th> <th>
{{ hue | capitalize }} {{ hue | capitalize }}
</th> </th>
<td class="core-column" style="--color: var(--wa-color-{{ hue }})"> <td class="core-column"
{% if hue !== 'gray' %} style="--color: var(--wa-color-{{ hue }})"
{%- set hueAfter = hues[loop.index0 + 1] -%} :style="{
{%- set hueAfter = hues[0] if hueAfter == 'gray' else hueAfter -%} '--color-tweaked': colors.{{ hue }}[{{ coreTint }}],
{%- set minShift = hueRanges[hue].min - coreColor.h | round -%} '--color-gray-undertone': colors[grayColor][{{coreTint}}],
{%- set maxShift = hueRanges[hue].max - coreColor.h | round -%} '--color-tweaked-no-gray-chroma': colorsMinusGrayChroma.{{ hue }}[{{ coreTint }}],
}">
<wa-dropdown> <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' }});"> <div slot="trigger" id="core-{{ hue }}-swatch" data-tint="core" class="color swatch"
{{ palettes[paletteId][hue].maxChromaTint }} style="background-color: var(--wa-color-{{ hue }}); color: var(--wa-color-{{ hue }}-{{ '05' if palettes[paletteId][hue].maxChromaTint > 60 else '95' }});"
<wa-icon name="sliders-simple" class="tweak-icon"></wa-icon> >
</div> {{ palettes[paletteId][hue].maxChromaTint }}
<div class="popup"> <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 }};"> <div class="decorated-slider hue-shift-slider" style="--min: {{ minShift }}; --max: {{ maxShift }};">
<wa-slider name="{{ hue }}-shift" v-model="hueShifts.{{ hue }}" value="0" <wa-slider name="{{ hue }}-shift" v-model="hueShifts.{{ hue }}" value="0"
min="{{ minShift }}" max="{{ maxShift }}" step="1" min="{{ minShift }}" max="{{ maxShift }}" step="1"
@@ -113,23 +164,23 @@
<div class="label-min">More {{hueBefore}}</div> <div class="label-min">More {{hueBefore}}</div>
<div class="label-max">More {{hueAfter}}</div> <div class="label-max">More {{hueAfter}}</div>
</div> </div>
{%- set hueBefore = hue -%}
{% endif %}
<div class="wa-gap-s"> <div class="wa-gap-s">
<code>--wa-color-{{ hue }}</code> <code>--wa-color-{{ hue }}</code>
<wa-copy-button value="--wa-color-{{ hue }}" copy-label="--wa-color-{{ hue }}"></wa-copy-button> <wa-copy-button value="--wa-color-{{ hue }}" copy-label="--wa-color-{{ hue }}"></wa-copy-button>
</div> </div>
</div>` </div>`
</wa-dropdown> </wa-dropdown>
{%- set hueBefore = hue -%}
{% else %}
<div id="core-{{ hue }}-swatch" class="color swatch" data-tint="core" 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 }}
</div>
{% endif %}
</td> </td>
{% for tint in tints -%} {% for tint in tints -%}
{%- set color = palettes[paletteId][hue][tint] -%} {%- set color = palettes[paletteId][hue][tint] -%}
<td data-tint="{{ tint }}" style="--color: var(--wa-color-{{ hue }}-{{ tint }})"> <td data-tint="{{ tint }}" style="--color: var(--wa-color-{{ hue }}-{{ tint }})"
<div class="color swatch" style="background-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> <wa-copy-button value="--wa-color-{{ hue }}-{{ tint }}" copy-label="--wa-color-{{ hue }}-{{ tint }}"></wa-copy-button>
</div> </div>
</td> </td>
@@ -144,7 +195,8 @@
<div class="decorated-slider chroma-scale-slider wa-palette-{{ paletteId }}" <div class="decorated-slider chroma-scale-slider wa-palette-{{ paletteId }}"
:class="{ tweaked: chromaScale !== 1 }" :class="{ tweaked: chromaScale !== 1 }"
style="--min: {{ chromaScaleBounds[0] }}; --max: {{ chromaScaleBounds[1] }};"> style="--min: {{ chromaScaleBounds[0] }}; --max: {{ chromaScaleBounds[1] }};">
<wa-slider name="chroma-scale" v-model="chromaScale" value="1" step="0.01" <wa-slider name="chroma-scale" ref="chromaScaleSlider"
v-model="chromaScale" value="1" step="0.01"
min="{{ chromaScaleBounds[0] }}" max="{{ chromaScaleBounds[1] }}" min="{{ chromaScaleBounds[0] }}" max="{{ chromaScaleBounds[1] }}"
@input="tweaking.chroma = true" @input="tweaking.chroma = true"
@change="tweaking.chroma = false"> @change="tweaking.chroma = false">

View File

@@ -68,7 +68,7 @@ wa_data.palettes = {
<wa-option label="{{ palette.data.title }}" value="{{ palette.fileSlug if not currentPalette }}" {{ (palette.fileSlug if currentPalette) | attr('data-id') }}> <wa-option label="{{ palette.data.title }}" value="{{ palette.fileSlug if not currentPalette }}" {{ (palette.fileSlug if currentPalette) | attr('data-id') }}>
<wa-card with-header> <wa-card with-header>
<div slot="header"> <div slot="header">
{% include "svgs/" + (palette.data.icon or "thumbnail-placeholder") + ".njk" %} {% include "svgs/" + (palette.data.icon or "thumbnail-placeholder") + ".njk" ignore missing %}
</div> </div>
<span class="page-name"> <span class="page-name">
{{ palette.data.title }} {{ palette.data.title }}

View File

@@ -29,6 +29,9 @@ function getCollection(name) {
} }
export function getCollectionItemFromUrl(url, collection) { export function getCollectionItemFromUrl(url, collection) {
if (!url) {
return null;
}
collection ??= getCollection.call(this, 'all') || []; collection ??= getCollection.call(this, 'all') || [];
return collection.find(item => item.url === url); return collection.find(item => item.url === url);
} }
@@ -42,35 +45,33 @@ export function split(text, separator) {
return (text + '').split(separator).filter(Boolean); return (text + '').split(separator).filter(Boolean);
} }
export function breadcrumbs(url, { withCurrent = false } = {}) { export function ancestors(url, { withCurrent = false, withRoot = false } = {}) {
const parts = split(url, '/'); let ret = [];
const ret = []; let currentUrl = url;
let currentItem = getCollectionItemFromUrl.call(this, url);
while (parts.length) { if (!currentItem) {
let partialUrl = '/' + parts.join('/') + '/'; // Might have eleventyExcludeFromCollections, jump to parent
let item = getCollectionItemFromUrl.call(this, partialUrl); let parentUrl = this.ctx.parentUrl;
if (parentUrl) {
if (item && (partialUrl !== url || withCurrent)) { url = parentUrl;
let title = item.data.title;
if (title) {
ret.unshift({ url: partialUrl, title });
}
}
parts.pop();
if (item?.data.parent) {
let parentURL = item.data.parent;
if (!item.data.parent.startsWith('/')) {
// Parent is in the same directory
parts.push(item.data.parent);
parentURL = '/' + parts.join('/') + '/';
}
let parentBreadcrumbs = breadcrumbs.call(this, parentURL, { withCurrent: true });
return [...parentBreadcrumbs, ...ret];
} }
} }
for (let item; (item = getCollectionItemFromUrl.call(this, url)); url = item.data.parentUrl) {
ret.unshift(item);
}
if (!withRoot && ret[0]?.page.url === '/') {
// Remove root
ret.shift();
}
if (!withCurrent && ret.at(-1)?.page.url === currentUrl) {
// Remove current page
ret.pop();
}
return ret; return ret;
} }
@@ -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. * Group an 11ty collection (or any array of objects with a `data.tags` property) by certain tags.
* @param {object[]} collection * @param {object[]} collection
* @param { Object<string, string> | (string | Object<string, string>)[]} [tags] The tags to group by. If not provided/empty, defaults to grouping by all tags. * @param { Object<string, string> | string[]} [options] Options object or array of tags to group by.
* @returns { Object.<string, object[]> } An object with keys for each tag, and an array of items for each tag. * @param {string[] | true} [options.tags] Tags to group by. If true, groups by all tags.
* If not provided/empty, defaults to grouping by page hierarchy, with any pages with more than 1 children becoming groups.
* @param {string[]} [options.groups] The groups to use if only a subset or a specific order is desired. Defaults to `options.tags`.
* @param {string[]} [options.titles] Any title overrides for groups.
* @param {string | false} [options.other="Other"] The title to use for the "Other" group. If `false`, the "Other" group is removed..
* @returns { Object.<string, object[]> } An object of group ids to arrays of page objects.
*/ */
export function groupByTags(collection, tags) { export function groupPages(collection, options = {}, page) {
if (!collection) { if (!collection) {
console.error(`Empty collection passed to groupByTags() to group by ${JSON.stringify(tags)}`); console.error(`Empty collection passed to groupPages() to group by ${JSON.stringify(options)}`);
}
if (!tags) {
// Default to grouping by union of all tags
tags = Array.from(new Set(collection.flatMap(item => item.data.tags)));
} else if (Array.isArray(tags)) {
// May contain objects of one-off tag -> label mappings
tags = tags.map(tag => (typeof tag === 'object' ? Object.keys(tag)[0] : tag));
} else if (typeof tags === 'object') {
// tags is an object of tags to labels, so we just want the keys
tags = Object.keys(tags);
} }
let ret = Object.fromEntries(tags.map(tag => [tag, []])); if (Array.isArray(options)) {
ret.other = []; options = { tags: options };
}
let { tags, groups, titles = {}, other = 'Other', 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) { for (let item of collection) {
let categorized = false; let url = item.page.url;
let parentUrl = item.data.parentUrl;
for (let tag of tags) { byUrl[url] = item;
if (item.data.tags.includes(tag)) {
ret[tag].push(item);
categorized = true;
}
}
if (!categorized) { if (parentUrl) {
ret.other.push(item); byParentUrl[parentUrl] ??= [];
byParentUrl[parentUrl].push(item);
} }
} }
// Remove empty categories let urlToGroups = {};
for (let category in ret) {
if (ret[category].length === 0) { for (let item of collection) {
delete ret[category]; let url = item.page.url;
let parentUrl = item.data.parentUrl;
if (grouping.isGroup(item)) {
continue;
}
let parentItem = byUrl[parentUrl];
if (parentItem && !grouping.isGroup(parentItem)) {
// Their parent is also here and is not a group
continue;
}
let candidateGroups = grouping.getCandidateGroups(item);
if (groups) {
candidateGroups = candidateGroups.filter(group => groups.includes(group));
}
urlToGroups[url] ??= [];
for (let group of candidateGroups) {
urlToGroups[url].push(group);
}
}
let ret = {};
for (let url in urlToGroups) {
let groups = urlToGroups[url];
let item = byUrl[url];
if (groups.length === 0) {
// Not filtered out but also not categorized
groups = ['other'];
}
for (let group of groups) {
ret[group] ??= [];
ret[group].push(item);
if (!ret[group].meta) {
if (group === 'other') {
ret[group].meta = { title: other };
} else {
ret[group].meta = grouping.getGroupMeta(group);
ret[group].meta.title = titles[group] ?? ret[group].meta.title ?? capitalize(group);
}
}
}
}
if (other === false) {
delete ret.other;
}
// Sort
let sortedGroups = groups ?? grouping.sortGroups?.(Object.keys(ret));
if (sortedGroups) {
ret = sortObject(ret, sortedGroups);
} 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; return ret;
} }
export function getCategoryTitle(category, categories) { function capitalize(str) {
let title; str += '';
if (Array.isArray(categories)) { return str.charAt(0).toUpperCase() + str.slice(1);
// Find relevant entry
// [{id: "Title"}, id2, ...]
title = categories.find(entry => typeof entry === 'object' && entry?.[category])?.[category];
} else if (typeof categories === 'object') {
// {id: "Title", id2: "Title 2", ...}
title = categories[category];
}
if (title) {
return title;
}
// Capitalized
return category.charAt(0).toUpperCase() + category.slice(1);
} }
const IDENTITY = x => x; const IDENTITY = x => x;

View File

@@ -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 // 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()); clone.querySelectorAll('[data-no-outline]').forEach(el => el.remove());
// Generate the link // Generate the link

View File

@@ -2,6 +2,7 @@
import { mkdir, writeFile } from 'fs/promises'; import { mkdir, writeFile } from 'fs/promises';
import lunr from 'lunr'; import lunr from 'lunr';
import { parse } from 'node-html-parser'; import { parse } from 'node-html-parser';
import * as path from 'path';
import { dirname, join } from 'path'; import { dirname, join } from 'path';
function collapseWhitespace(string) { function collapseWhitespace(string) {
@@ -52,8 +53,9 @@ export function searchPlugin(options = {}) {
return content; return content;
}); });
eleventyConfig.on('eleventy.after', ({ dir }) => { eleventyConfig.on('eleventy.after', ({ directories }) => {
const outputFilename = join(dir.output, 'search.json'); const { output } = directories;
const outputFilename = path.resolve(join(output, 'search.json'));
const map = []; const map = [];
const searchIndex = lunr(async function () { const searchIndex = lunr(async function () {
let index = 0; let index = 0;

View File

@@ -0,0 +1,171 @@
/**
* 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>
*/
import { discover } from '/dist/webawesome.js';
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();
discover(this.shadowRoot);
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;
}

View File

@@ -17,7 +17,7 @@ document.addEventListener('click', event => {
const code = codeExample.querySelector('code'); const code = codeExample.querySelector('code');
const cdnUrl = document.documentElement.dataset.cdnUrl; const cdnUrl = document.documentElement.dataset.cdnUrl;
const html = const html =
`<script type="module" src="${cdnUrl}webawesome.loader.js"></script>\n` + `<script data-fa-kit-code="b10bfbde90" type="module" src="${cdnUrl}webawesome.loader.js"></script>\n` +
`<link rel="stylesheet" href="${cdnUrl}styles/themes/default.css">\n` + `<link rel="stylesheet" href="${cdnUrl}styles/themes/default.css">\n` +
`<link rel="stylesheet" href="${cdnUrl}styles/webawesome.css">\n` + `<link rel="stylesheet" href="${cdnUrl}styles/webawesome.css">\n` +
`<link rel="stylesheet" href="${cdnUrl}styles/utilities.css">\n\n` + `<link rel="stylesheet" href="${cdnUrl}styles/utilities.css">\n\n` +

View File

@@ -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) { function updateResults(input) {
const filter = input.value.toLowerCase().trim(); const filter = input.value.toLowerCase().trim();
let filtered = Boolean(filter); let filtered = Boolean(filter);
@@ -18,8 +26,10 @@ function updateResults(input) {
} }
} }
const debouncedUpdateResults = debounce(updateResults, 300);
document.documentElement.addEventListener('input', e => { document.documentElement.addEventListener('input', e => {
if (e.target?.matches('#block-filter wa-input')) { if (e.target?.matches('#block-filter wa-input')) {
updateResults(e.target); debouncedUpdateResults(e.target);
} }
}); });

View File

@@ -13,7 +13,9 @@ sidebar.palettes = {
sidebar.updateCurrent(); sidebar.updateCurrent();
}, },
saved: localStorage.savedPalettes ? JSON.parse(localStorage.savedPalettes) : [], updateSaved() {
this.saved = localStorage.savedPalettes ? JSON.parse(localStorage.savedPalettes) : [];
},
save(saved = this.saved) { save(saved = this.saved) {
this.saved = saved ?? []; this.saved = saved ?? [];
@@ -26,6 +28,9 @@ sidebar.palettes = {
}, },
}; };
sidebar.palettes.updateSaved();
addEventListener('storage', event => sidebar.palettes.updateSaved());
sidebar.palette = { sidebar.palette = {
getUid() { getUid() {
let savedPalettes = sidebar.palettes.saved; let savedPalettes = sidebar.palettes.saved;
@@ -36,7 +41,7 @@ sidebar.palette = {
} }
// Find first available number // Find first available number
for (let i = 1; i < savedPalettes.length + 1; i++) { for (let i = 1; i <= savedPalettes.length + 1; i++) {
if (!uids.has(i)) { if (!uids.has(i)) {
return i; return i;
} }
@@ -94,7 +99,7 @@ sidebar.palette = {
sidebar.palettes.save(savedPalettes); sidebar.palettes.save(savedPalettes);
if (sidebar.palette.equals(globalThis.paletteApp?.saved, palette)) { if (sidebar.palette.equals(globalThis.paletteApp?.saved, palette)) {
paletteApp.saved = null; paletteApp.postDelete();
} }
}, },
@@ -184,18 +189,52 @@ sidebar.updateCurrent = function () {
// We want to start from the longest prefix // We want to start from the longest prefix
prefixes.reverse(); prefixes.reverse();
let candidates;
let matchingPrefix;
for (let prefix of prefixes) { for (let prefix of prefixes) {
let a = document.querySelector(`#sidebar a[href^="${prefix}"]`); candidates = document.querySelectorAll(`#sidebar a[href^="${prefix}"]`);
if (a) { if (candidates.length > 0) {
for (let current of document.querySelectorAll('#sidebar a.current')) { matchingPrefix = prefix;
current.classList.remove('current');
}
a.classList.add('current');
break; 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 () { sidebar.render = function () {
@@ -204,3 +243,12 @@ sidebar.render = function () {
sidebar.render(); sidebar.render();
window.addEventListener('turbo: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;
}

View File

@@ -1,12 +1,32 @@
let initialPageLoadComplete = document.readyState === 'complete';
if (!initialPageLoadComplete) {
window.addEventListener('load', () => {
initialPageLoadComplete = true;
});
}
// Helper for view transitions // Helper for view transitions
export function domChange(fn, { behavior = 'smooth' } = {}) { export function domChange(fn, { behavior = 'smooth', ignoreInitialLoad = true } = {}) {
const canUseViewTransitions = const canUseViewTransitions =
document.startViewTransition && !window.matchMedia('(prefers-reduced-motion: reduce)').matches; 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') { 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 { } else {
fn(true); fn(false);
return null;
} }
} }
@@ -100,6 +120,7 @@ const colorScheme = new ThemeAspect({
domChange(() => { domChange(() => {
let dark = this.computedValue === 'dark'; let dark = this.computedValue === 'dark';
document.documentElement.classList.toggle(`wa-dark`, dark); document.documentElement.classList.toggle(`wa-dark`, dark);
document.documentElement.dispatchEvent(new CustomEvent('wa-color-scheme-change', { detail: { dark } }));
}); });
}, },
}); });

View File

@@ -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___) { if (!window.___turboScrollPositions___) {
window.___turboScrollPositions___ = {}; window.___turboScrollPositions___ = {};
} }
@@ -70,3 +73,4 @@ function fixDSD(e) {
window.addEventListener('turbo:before-cache', saveScrollPosition); window.addEventListener('turbo:before-cache', saveScrollPosition);
window.addEventListener('turbo:before-render', restoreScrollPosition); window.addEventListener('turbo:before-render', restoreScrollPosition);
window.addEventListener('turbo:render', restoreScrollPosition); window.addEventListener('turbo:render', restoreScrollPosition);
preventTurboFouce();

View File

@@ -1,6 +1,6 @@
/** /**
* Get import code for remixed themes and tweaked palettes. * Get import code for remixed themes and tweaked palettes.
*/ */
export { theme as getThemeCode } from './tweak/code.js'; export { getThemeCode } from './tweak/code.js';
export { cdnUrl, hueRanges, hues, selectors, tints, urls } from './tweak/data.js'; export { cdnUrl, hueRanges, hues, selectors, tints, urls } from './tweak/data.js';
export { default as Permalink } from './tweak/permalink.js'; export { default as Permalink } from './tweak/permalink.js';

View File

@@ -25,18 +25,23 @@ export function cssLiteral(value, options = {}) {
} }
} }
export function theme(base, params, options) { // Params in correct order
export const themeParams = ['colors', 'palette', 'brand', 'typography'];
export function getThemeCode(base, params, options) {
let ret = []; let ret = [];
if (base) { if (base) {
ret.push(urls.theme(base)); ret.push(urls.theme(base));
} }
ret.push( for (let aspect of themeParams) {
...Object.entries(params) let value = params[aspect];
.filter(([aspect, id]) => Boolean(id))
.map(([aspect, id]) => urls[aspect](id)), if (value) {
); ret.push(urls[aspect](value));
}
}
return ret.map(url => cssImport(url, options)).join('\n'); return ret.map(url => cssImport(url, options)).join('\n');
} }

View File

@@ -29,6 +29,45 @@ export const hueRanges = {
pink: { min: 320, max: 365 }, // 45 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 hues = Object.keys(hueRanges);
export const tints = ['05', '10', '20', '30', '40', '50', '60', '70', '80', '90', '95']; export const tints = ['05', '10', '20', '30', '40', '50', '60', '70', '80', '90', '95'];

View 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;
}

View File

@@ -4,6 +4,7 @@
@import 'outline.css'; @import 'outline.css';
@import 'search.css'; @import 'search.css';
@import 'cera_typeface.css'; @import 'cera_typeface.css';
@import 'theme-icons.css';
:root { :root {
--wa-brand-orange: #f36944; --wa-brand-orange: #f36944;
@@ -370,10 +371,22 @@ wa-page > main:has(> .index-grid) {
.index-grid { .index-grid {
display: 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); gap: var(--wa-space-2xl);
margin-block-end: var(--wa-space-3xl); 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 { a {
border-radius: var(--wa-border-radius-l); border-radius: var(--wa-border-radius-l);
text-decoration: none; text-decoration: none;
@@ -400,7 +413,6 @@ wa-page > main:has(> .index-grid) {
&::part(header) { &::part(header) {
background-color: var(--header-background, var(--wa-color-neutral-fill-quiet)); background-color: var(--header-background, var(--wa-color-neutral-fill-quiet));
border-bottom: none;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View File

@@ -7,8 +7,9 @@
margin: 0 auto; margin: 0 auto;
overflow: hidden; overflow: hidden;
&::part(base) { &::part(dialog) {
margin-block: 10rem; margin-block-start: 10vh;
margin-block-end: 0;
} }
&::part(body) { &::part(body) {
@@ -23,20 +24,20 @@
@media screen and (max-width: 900px) { @media screen and (max-width: 900px) {
max-width: calc(100% - 2rem); max-width: calc(100% - 2rem);
&::part(base) { &::part(dialog) {
margin-block: 1rem; margin-block: 1rem;
} }
#site-search-container {
max-height: none;
}
} }
} }
#site-search-container { #site-search-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-height: calc(100vh - 20rem); max-height: calc(100vh - 18rem);
@media screen and (max-width: 900px) {
max-height: calc(100dvh - 2rem);
}
} }
/* Header */ /* Header */

View File

@@ -1,3 +1,61 @@
wa-card:has(> .theme-icon-host, > [slot='header'] > .theme-icon-host) {
&::part(header) {
/* We want to add a background color, so any spacing needs to go on .theme-icon */
flex: 1;
padding: 0;
min-block-size: 0;
}
[slot='header'] {
width: 100%;
}
}
.theme-icon-host,
.palette-icon-host {
flex: 1;
border-radius: inherit;
&[slot='header'],
[slot='header']:has(&) {
flex: 1;
border-radius: inherit;
}
}
.palette-icon {
display: grid;
grid-template-columns: repeat(var(--hues, 9), 1fr);
gap: var(--wa-space-3xs);
min-width: 20ch;
min-height: 9ch;
align-content: center;
.swatch {
height: 0.7em;
background: var(--color);
border-radius: var(--wa-border-radius-s);
&[data-suffix=''] {
height: 1.1em;
}
}
}
.theme-icon {
min-width: 18ch;
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 { .theme-color-icon {
display: grid; display: grid;
gap: var(--wa-space-xs); gap: var(--wa-space-xs);
@@ -25,10 +83,50 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--wa-space-xs); gap: var(--wa-space-xs);
}
h3, .theme-overall-icon {
p { display: flex;
margin-block: 0; flex-flow: column;
padding: 0; 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;
}
}
} }
} }

View File

@@ -15,27 +15,17 @@ icon: card
<strong>Mittens</strong><br /> <strong>Mittens</strong><br />
This kitten is as cute as he is playful. Bring him home today!<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-button variant="brand" pill>More Info</wa-button>
<wa-rating></wa-rating> <wa-rating label="Rating"></wa-rating>
</div> </div>
</wa-card> </wa-card>
<style> <style>
.card-overview { .card-overview {
max-width: 300px; width: 300px;
}
.card-overview small {
color: var(--wa-color-text-quiet);
}
.card-overview [slot='footer'] {
display: flex;
justify-content: space-between;
align-items: center;
} }
</style> </style>
``` ```
@@ -65,9 +55,9 @@ If using SSR, you need to also use the `with-header` attribute to add a header t
```html {.example} ```html {.example}
<wa-card with-header class="card-header"> <wa-card with-header class="card-header">
<div slot="header"> <div slot="header" class="wa-split">
Header Title 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> </div>
This card has a header. You can put all sorts of things in it! 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; max-width: 300px;
} }
.card-header [slot='header'] {
display: flex;
align-items: center;
justify-content: space-between;
}
.card-header h3 { .card-header h3 {
margin: 0; margin: 0;
} }
.card-header wa-icon-button {
font-size: var(--wa-font-size-m);
}
</style> </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"> <wa-card with-footer class="card-footer">
This card has a footer. You can put all sorts of things in it! 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-rating></wa-rating>
<wa-button variant="brand">Preview</wa-button> <wa-button variant="brand">Preview</wa-button>
</div> </div>
@@ -113,12 +93,6 @@ If using SSR, you need to also use the `with-footer` attribute to add a footer t
.card-footer { .card-footer {
max-width: 300px; max-width: 300px;
} }
.card-footer [slot='footer'] {
display: flex;
justify-content: space-between;
align-items: center;
}
</style> </style>
``` ```
@@ -153,7 +127,7 @@ Use the `size` attribute to change a card's size.
<wa-card with-footer size="small"> <wa-card with-footer size="small">
This is a small card. 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-button variant="brand" pill>More Info</wa-button>
<wa-rating></wa-rating> <wa-rating></wa-rating>
</footer> </footer>
@@ -162,7 +136,7 @@ Use the `size` attribute to change a card's size.
<wa-card with-footer size="medium"> <wa-card with-footer size="medium">
This is a medium card (default). 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-button variant="brand" pill>More Info</wa-button>
<wa-rating></wa-rating> <wa-rating></wa-rating>
</footer> </footer>
@@ -171,14 +145,39 @@ Use the `size` attribute to change a card's size.
<wa-card with-footer size="large"> <wa-card with-footer size="large">
This is a large card. 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-button variant="brand" pill>More Info</wa-button>
<wa-rating></wa-rating> <wa-rating></wa-rating>
</footer> </footer>
</wa-card> </wa-card>
</div> </div>
``` ```
<style> ### Appearance
</style>
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>
```

View File

@@ -77,6 +77,31 @@ The details component automatically adapts to right-to-left languages:
</wa-details> </wa-details>
``` ```
### Appearance
Use the `appearance` attribute to change the elements 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 ### 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. 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.

View File

@@ -2,13 +2,10 @@
title: Components title: Components
description: Components are the essential building blocks to create intuitive, cohesive experiences. Browse the library of customizable, framework-friendly web components included in Web Awesome. description: Components are the essential building blocks to create intuitive, cohesive experiences. Browse the library of customizable, framework-friendly web components included in Web Awesome.
layout: overview layout: overview
categories:
- actions
- feedback: 'Feedback & Status'
- imagery
- inputs
- navigation
- organization
- helpers: 'Utilities'
override:tags: [] override:tags: []
categories:
tags: [actions, feedback, imagery, inputs, navigation, organization, helpers]
titles:
feedback: 'Feedback & Status'
helpers: 'Utilities'
--- ---

View File

@@ -1,10 +1,80 @@
/**
* Global data for all pages
*/
import { sort } from '../_utils/filters.js';
export default { export default {
eleventyComputed: { eleventyComputed: {
children(data) { // Default parent. Can be overridden by explicitly setting parent in the data.
let mainTag = data.tags?.[0]; // parent can refer to either an ancestor page in the URL or another page in the same directory
let collection = data.collections[mainTag] ?? []; parent(data) {
let { parent, page } = data;
return collection.filter(item => item.data.parent === data.page.fileSlug); if (parent) {
return parent;
}
return page.url.split('/').filter(Boolean).at(-2);
},
parentUrl(data) {
let { parent, page } = data;
return getParentUrl(page.url, parent);
},
parentItem(data) {
let { parentUrl } = data;
return data.collections.all.find(item => item.url === parentUrl);
},
children(data) {
let { collections, page, parentOf } = data;
if (parentOf) {
return collections[parentOf];
}
let collection = collections.all ?? [];
let url = page.url;
let ret = collection.filter(item => {
return item.data.parentUrl === url;
});
sort(ret);
return ret;
}, },
}, },
}; };
function getParentUrl(url, parent) {
let parts = url.split('/').filter(Boolean);
let ancestorIndex = parts.findLastIndex(part => part === parent);
let retParts = parts.slice();
if (ancestorIndex > -1) {
// parent is an ancestor
retParts.splice(ancestorIndex + 1);
} else {
// parent is a sibling in the same directory
retParts.splice(-1, 1, parent);
}
let ret = retParts.join('/');
if (url.startsWith('/')) {
ret = '/' + ret;
}
if (!retParts.at(-1).includes('.') && !ret.endsWith('/')) {
// If no extension, make sure to end with a slash
ret += '/';
}
if (ret === '/docs/') {
ret = '/';
}
return ret;
}

View File

@@ -1,13 +1,10 @@
--- ---
title: Clamped brand tokens title: Clamped Color Tokens
layout: block layout: block
--- ---
{% set tints = ['40-max', '50-max', '60-max', '40-min', '50-min', '60-min'] %} {% set tints = ['max-50', 'max-60', 'max-70', 'min-50', 'min-60', 'min-70'] %}
{% set hues = ['red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'indigo', 'purple', 'pink', 'gray'] %}
{% for hue in hues %}
<link href="/dist/styles/brand/{{ hue }}.css" rel="stylesheet">
{% endfor %}
<table class="colors"> <table class="colors">
<thead> <thead>
@@ -20,18 +17,18 @@ layout: block
</tr> </tr>
</thead> </thead>
{% for hue in hues -%} {% for hue in hues -%}
<tr class="wa-brand-{{ hue }}"> <tr class="wa-color-{{ hue }}">
<th>{{ hue | capitalize }}</th> <th>{{ hue | capitalize }}</th>
<td class="core-column"> <td class="core-column">
<div class="color swatch" style="background-color: var(--wa-color-brand); color: var(--wa-color-brand-on);"> <div class="color swatch" style="background-color: var(--wa-color-{{ hue }}); color: var(--wa-color-{{ hue }}-on); --key: var(--wa-color-{{ hue }}-key);">
{{ palettes[paletteId][hue].maxChromaTint }} {{ palettes[paletteId][hue].maxChromaTint }}
<wa-copy-button value="--wa-color-brand" copy-label="--wa-color-brand"></wa-copy-button> <wa-copy-button value="--wa-color-{{ hue }}" copy-label="--wa-color-{{ hue }}"></wa-copy-button>
</div> </div>
</td> </td>
{% for tint in tints -%} {% for tint in tints -%}
<td> <td>
<div class="color swatch" style="background-color: var(--wa-color-brand-{{ tint }})"> <div class="color swatch" style="background-color: var(--wa-color-{{ hue }}-{{ tint }})">
<wa-copy-button value="--wa-color-brand-{{ tint }}" copy-label="--wa-color-brand-{{ tint }}"></wa-copy-button> <wa-copy-button value="--wa-color-{{ hue }}-{{ tint }}" copy-label="--wa-color-{{ hue }}-{{ tint }}"></wa-copy-button>
</div> </div>
</td> </td>
{%- endfor -%} {%- endfor -%}
@@ -41,7 +38,7 @@ layout: block
<style> <style>
.core-column .color.swatch::before { .core-column .color.swatch::before {
counter-reset: key var(--wa-color-brand-key); counter-reset: key var(--key);
content: counter(key); content: counter(key);
} }
</style> </style>

View File

@@ -37,10 +37,6 @@ This snippet includes three parts:
Now you can [start using Web Awesome!](/docs/usage) 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 ## Using Font Awesome Kit Codes

View File

@@ -2,6 +2,7 @@
title: Layout title: Layout
description: Layout components and utility classes help you organize content that can adapt to any device or screen size. See the [installation instructions](#installation) to use Web Awesome's layout tools in your project. description: Layout components and utility classes help you organize content that can adapt to any device or screen size. See the [installation instructions](#installation) to use Web Awesome's layout tools in your project.
layout: overview layout: overview
parentOf: layout
categories: ["components", "utilities"] categories: ["components", "utilities"]
override:tags: [] override:tags: []
--- ---
@@ -22,4 +23,4 @@ Or, you can choose to import _only_ the utilities:
```html ```html
<link rel="stylesheet" href="{% cdnUrl 'styles/utilities.css' %}" /> <link rel="stylesheet" href="{% cdnUrl 'styles/utilities.css' %}" />
``` ```
{% endmarkdown %} {% endmarkdown %}

View File

@@ -33,7 +33,7 @@ Use the [variant utility classes](../utilities/color.md) to set the button's sem
### Appearance ### 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} ```html {.example}
<div style="margin-block-end: 1rem;"> <div style="margin-block-end: 1rem;">

View File

@@ -57,7 +57,7 @@ Use the [variant utility classes](../utilities/color.md) to set the callout's co
### Appearance ### 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} ```html {.example}
<article class="wa-callout wa-brand wa-outlined wa-accent"> <article class="wa-callout wa-brand wa-outlined wa-accent">

View File

@@ -19,6 +19,35 @@ file: styles/native/details.css
## Examples ## 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 ### Right-to-Left Languages
The details styling automatically adapts to right-to-left languages: The details styling automatically adapts to right-to-left languages:

View File

@@ -42,6 +42,14 @@ wa-code-demo::part(preview) {
<wa-input label="WA Input (url)" type="url"></wa-input> <wa-input label="WA Input (url)" type="url"></wa-input>
``` ```
## Pill shaped text fields
Add the `wa-pill` class to an `<input>` to make it pill-shaped.
```html {.example}
<label>Input <input type="text" placeholder="placeholder" class="wa-pill"></label>
```
## Color Picker ## Color Picker
Basic: Basic:

View File

@@ -5,6 +5,6 @@ layout: overview
override:tags: [] override:tags: []
forTag: palette forTag: palette
categories: categories:
tags: [other, pro]
other: Free other: Free
pro: Pro
--- ---

View File

@@ -25,9 +25,16 @@ wa-dropdown > .color.swatch {
--track-color-inactive: transparent; --track-color-inactive: transparent;
--track-color-active: transparent; --track-color-active: transparent;
--thumb-color: var(--color-tweaked, var(--color)); --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) { &::part(base) {
background: linear-gradient(to right in oklch, var(--color-1), var(--color-2)); background: linear-gradient(to right in var(--color-interpolation-space, oklab), var(--color-1), var(--color-2));
} }
} }
@@ -63,13 +70,20 @@ wa-dropdown > .color.swatch {
.hue-shift-slider { .hue-shift-slider {
--color-1: oklch(from var(--color) l c calc(h + var(--min, 0))); --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-2: oklch(from var(--color) l c calc(h + var(--max, 0)));
--color-interpolation-space: oklch;
} }
.chroma-scale-slider { .chroma-scale-slider {
--color: var(--wa-color-brand); --color: var(--wa-color-brand);
--color-1: oklch(from var(--color) l calc(c * var(--min)) h); --color-1: oklch(from var(--color) l calc(c * var(--min)) h);
--color-2: oklch(from var(--color) l calc(c * var(--max)) h); --color-2: oklch(from var(--color) l calc(c * var(--max)) h);
--color-tweaked: oklch(from var(--color) l calc(c * var(--chroma-scale)) 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 { .popup {
@@ -91,13 +105,13 @@ wa-dropdown > .color.swatch {
td:not([data-hue='gray'] *) { td:not([data-hue='gray'] *) {
--tweak-c: calc(c * var(--chroma-scale, 1)); --tweak-c: calc(c * var(--chroma-scale, 1));
--tweak-h: calc(h + var(--hue-shift, 0)); --tweak-h: calc(h + var(--hue-shift, 0));
--color-tweaked: oklch(from var(--color) l var(--tweak-c) var(--tweak-h));
--color-tweaked-no-chroma-scale: oklch(from var(--color) l c var(--tweak-h)); --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); --color-tweaked-no-hue-shift: oklch(from var(--color) l var(--tweak-c) h);
&:is([data-tint='90'], [data-tint='95']) { &:is([data-tint='90'], [data-tint='95']) {
/* Work around https://bugs.webkit.org/show_bug.cgi?id=287637 */ /* Work around https://bugs.webkit.org/show_bug.cgi?id=287637 */
--color-tweaked: lch(from var(--color) l var(--tweak-c) var(--tweak-h));
--color-tweaked-no-chroma-scale: lch(from var(--color) l c var(--tweak-h)); --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); --color-tweaked-no-hue-shift: lch(from var(--color) l var(--tweak-c) h);
@@ -111,14 +125,18 @@ wa-dropdown > .color.swatch {
&:is(.tweaking *) { &:is(.tweaking *) {
--color-2-height: 70%; --color-2-height: 70%;
}
&:is(.tweaking-chroma *) { &:is(.tweaking-chroma *) {
--color: var(--color-tweaked-no-chroma-scale); --color: var(--color-tweaked-no-chroma-scale);
} }
&:is(.tweaking-hue *) { &:is(.tweaking-hue *) {
--color: var(--color-tweaked-no-hue-shift); --color: var(--color-tweaked-no-hue-shift);
} }
&:is(.tweaking-gray-chroma *) {
--color: var(--color-tweaked-no-gray-chroma);
} }
} }
@@ -159,6 +177,29 @@ wa-dropdown > .color.swatch {
} }
} }
[v-if='saved'] { /* Better UI before Vue initializes */
[v-if='saved'],
[v-if^='tweaked'] {
display: none; 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);
}
}

View File

@@ -3,7 +3,8 @@ import Color from 'https://colorjs.io/dist/color.js';
import { createApp, nextTick } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.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 { cdnUrl, hueRanges, hues, Permalink, tints } from '../../assets/scripts/tweak.js';
import { cssImport, cssLiteral, cssRule } from '../../assets/scripts/tweak/code.js'; import { cssImport, cssLiteral, cssRule } from '../../assets/scripts/tweak/code.js';
import { selectors, urls } from '../../assets/scripts/tweak/data.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'; import Prism from '/assets/scripts/prism.js';
await Promise.all(['wa-slider'].map(tag => customElements.whenDefined(tag))); await Promise.all(['wa-slider'].map(tag => customElements.whenDefined(tag)));
@@ -34,6 +35,8 @@ for (let palette in allPalettes) {
} }
} }
const percentFormatter = value => value.toLocaleString(undefined, { style: 'percent' });
let paletteAppSpec = { let paletteAppSpec = {
data() { data() {
let appRoot = document.querySelector('#palette-app'); let appRoot = document.querySelector('#palette-app');
@@ -49,12 +52,17 @@ let paletteAppSpec = {
hueRanges, hueRanges,
hueShifts: Object.fromEntries(hues.map(hue => [hue, 0])), hueShifts: Object.fromEntries(hues.map(hue => [hue, 0])),
chromaScale: 1, chromaScale: 1,
grayChroma: undefined,
grayColor: undefined,
tweaking: {}, tweaking: {},
saved: null, saved: null,
}; };
}, },
created() { created() {
// Non-reactive variables to expose
Object.assign(this, { moreHue });
// Read URL params and apply them. This facilitates permalinks. // Read URL params and apply them. This facilitates permalinks.
this.permalink.mapObject(this.hueShifts, { this.permalink.mapObject(this.hueShifts, {
keyTo: key => key.replace(/-shift$/, ''), keyTo: key => key.replace(/-shift$/, ''),
@@ -63,30 +71,49 @@ let paletteAppSpec = {
valueTo: value => (!value ? 0 : Number(value)), valueTo: value => (!value ? 0 : Number(value)),
}); });
this.grayChroma = this.originalGrayChroma;
this.grayColor = this.originalGrayColor;
if (location.search) { if (location.search) {
// Update from URL // Update from URL
this.permalink.writeTo(this.hueShifts); this.permalink.writeTo(this.hueShifts);
if (this.permalink.has('chroma-scale')) { for (let param of ['chroma-scale', 'gray-color', 'gray-chroma']) {
this.chromaScale = Number(this.permalink.get('chroma-scale') || 1); 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')) { if (this.permalink.has('uid')) {
this.uid = Number(this.permalink.get('uid')); this.uid = Number(this.permalink.get('uid'));
} }
let palette = { id: this.paletteId, uid: this.uid, search: location.search }; this.saved = sidebar.palette.getSaved(this.getPalette());
this.saved = sidebar.palette.getSaved(palette); }
},
mounted() {
for (let ref in this.$refs) {
this.$refs[ref].tooltipFormatter = percentFormatter;
} }
}, },
computed: { computed: {
global() {
return globalThis;
},
tweaks() { tweaks() {
return { hueShifts: this.hueShifts, chromaScale: this.chromaScale }; return {
hueShifts: this.hueShifts,
chromaScale: this.chromaScale,
grayColor: this.grayColor,
grayChroma: this.grayChroma,
};
}, },
isTweaked() { isTweaked() {
@@ -96,7 +123,7 @@ let paletteAppSpec = {
code() { code() {
let ret = {}; let ret = {};
for (let language of ['html', 'css']) { for (let language of ['html', 'css']) {
let code = getPaletteCode(this.paletteId, this.tweaks, { language, cdnUrl }); let code = getPaletteCode(this.paletteId, this.colors, this.tweaked, { language, cdnUrl });
ret[language] = { ret[language] = {
raw: code, raw: code,
highlighted: Prism.highlight(code, Prism.languages[language], language), highlighted: Prism.highlight(code, Prism.languages[language], language),
@@ -107,47 +134,46 @@ let paletteAppSpec = {
}, },
colors() { colors() {
let ret = {}; return applyTweaks.call(this, this.originalColors, this.tweaks, this.tweaked);
},
for (let hue in this.originalColors) { colorsMinusChromaScale() {
let originalScale = this.originalColors[hue]; let tweaked = { ...this.tweaked, chromaScale: false };
let scale = (ret[hue] = {}); return applyTweaks.call(this, this.originalColors, this.tweaks, tweaked);
let descriptors = Object.getOwnPropertyDescriptors(originalScale); },
Object.defineProperties(scale, {
maxChromaTint: { ...descriptors.maxChromaTint, enumerable: false },
maxChromaTintRaw: { ...descriptors.maxChromaTintRaw, enumerable: false },
});
for (let tint of tints) { colorsMinusHueShifts() {
let oklch = originalScale[tint].coords.slice(); let tweaked = { ...this.tweaked, hue: false };
return applyTweaks.call(this, this.originalColors, this.tweaks, tweaked);
},
if (this.hueShifts[hue]) { colorsMinusGrayChroma() {
oklch[2] += this.hueShifts[hue]; let tweaked = { ...this.tweaked, grayChroma: false };
} return applyTweaks.call(this, this.originalColors, this.tweaks, tweaked);
if (this.chromaScale !== 1) {
oklch[1] *= this.chromaScale;
}
scale[tint] = new Color('oklch', oklch);
}
}
return ret;
}, },
tweaked() { tweaked() {
return { let anyHueTweaked = Object.values(this.hueShifts).some(Boolean);
chroma: this.chromaScale !== 1, let hue = anyHueTweaked
hue: Object.values(this.hueShifts).some(Boolean), ? 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() { tweaksHumanReadable() {
let ret = {}; let ret = {};
if (this.chromaScale !== 1) { if (this.chromaScale !== 1) {
ret.chromaScale = 'more ' + (this.chromaScale > 1 ? 'vibrant' : 'muted'); ret.chromaScale = 'More ' + (this.chromaScale > 1 ? 'vibrant' : 'muted');
} }
for (let hue in this.hueShifts) { for (let hue in this.hueShifts) {
@@ -158,63 +184,99 @@ let paletteAppSpec = {
} }
let relHue = shift < 0 ? arrayPrevious(hues, hue) : arrayNext(hues, hue); let relHue = shift < 0 ? arrayPrevious(hues, hue) : arrayNext(hues, hue);
let hueTweak = let hueTweak = moreHue[relHue] ?? relHue + 'er';
{
red: 'redder',
orange: 'oranger',
indigo: 'more indigo',
}[relHue] ?? relHue + 'er';
ret[hue] = hueTweak + ' ' + hue + 's'; 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; return ret;
}, },
originalContrasts() { originalContrasts() {
return getContrasts(this.originalColors);
},
contrasts() {
return getContrasts(this.colors, this.originalContrasts);
},
originalCoreColors() {
let ret = {}; let ret = {};
for (let hue in this.originalColors) { for (let hue in this.originalColors) {
ret[hue] = {}; let maxChromaTintRaw = this.originalColors[hue].maxChromaTintRaw;
ret[hue] = this.originalColors[hue][maxChromaTintRaw];
}
return ret;
},
for (let tintBg of tints) { coreColors() {
ret[hue][tintBg] = {}; let ret = {};
let bgColor = this.originalColors[hue][tintBg]; for (let hue in this.colors) {
let maxChromaTintRaw = this.colors[hue].maxChromaTintRaw;
if (!bgColor || !bgColor.contrast) { ret[hue] = this.colors[hue][maxChromaTintRaw];
continue;
}
for (let tintFg of tints) {
let contrast = bgColor.contrast(this.originalColors[hue][tintFg], 'WCAG21');
ret[hue][tintBg][tintFg] = contrast;
}
}
} }
return ret; return ret;
}, },
contrasts() { originalGrayColor() {
let ret = {}; let grayHue = this.originalCoreColors.gray.get('h');
let minDistance = Infinity;
let closestHue = null;
for (let hue in this.colors) { for (let name in this.originalCoreColors) {
ret[hue] = {}; if (name === 'gray') {
continue;
}
for (let tintBg in this.colors[hue]) { let hue = this.originalCoreColors[name].get('h');
ret[hue][tintBg] = {}; let distance = Math.abs(subtractAngles(hue, grayHue));
let bgColor = this.colors[hue][tintBg]; if (distance < minDistance) {
minDistance = distance;
for (let tintFg in this.colors[hue]) { closestHue = name;
let fgColor = this.colors[hue][tintFg];
let value = bgColor.contrast(fgColor, 'WCAG21');
let original = this.originalContrasts[hue][tintBg][tintFg];
ret[hue][tintBg][tintFg] = { value, original, bgColor, fgColor };
}
} }
} }
return ret; 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;
}, },
}, },
@@ -230,6 +292,14 @@ let paletteAppSpec = {
this.permalink.set('chroma-scale', this.chromaScale, 1); 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: { tweaks: {
deep: true, deep: true,
async handler(value, oldValue) { async handler(value, oldValue) {
@@ -246,6 +316,10 @@ let paletteAppSpec = {
}, },
methods: { methods: {
getPalette() {
return { id: this.paletteId, uid: this.uid, search: location.search };
},
save({ silent } = {}) { save({ silent } = {}) {
let title = silent let title = silent
? (this.saved?.title ?? this.paletteTitle) ? (this.saved?.title ?? this.paletteTitle)
@@ -258,13 +332,15 @@ let paletteAppSpec = {
let uid = this.uid; let uid = this.uid;
if (!uid) { if (!uid) {
// First time saving
this.uid = uid = sidebar.palette.getUid(); this.uid = uid = sidebar.palette.getUid();
this.permalink.set('uid', uid); this.permalink.set('uid', uid);
this.permalink.updateLocation(); this.permalink.updateLocation();
} }
let palette = { title, id: this.paletteId, uid, search: location.search }; let palette = { ...this.getPalette(), uid, title };
sidebar.palette.save(palette, this.saved); sidebar.palette.save(palette, this.saved);
this.saved = palette; this.saved = palette;
}, },
@@ -286,21 +362,38 @@ let paletteAppSpec = {
deleteSaved() { deleteSaved() {
sidebar.palette.delete(this.saved); sidebar.palette.delete(this.saved);
},
postDelete() {
this.saved = null; this.saved = null;
this.permalink.delete('uid');
this.uid = undefined;
this.permalink.updateLocation();
}, },
reset() { /**
for (let hue in this.hueShifts) { * Remove a specific tweak or all tweaks
this.hueShifts[hue] = 0; * @param {string} [param] - The tweak to remove. If not provided, all tweaks are removed.
} */
this.chromaScale = 1; reset(param) {
}, if (!param || param === 'chromaScale') {
removeTweak(param) {
if (param === 'chromaScale') {
this.chromaScale = 1; this.chromaScale = 1;
} else { }
if (param in this.hueShifts) {
this.hueShifts[param] = 0; 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;
} }
}, },
}, },
@@ -336,16 +429,20 @@ let paletteAppSpec = {
}; };
function init() { function init() {
let paletteAppContainer = document.querySelector('#palette-app');
globalThis.paletteApp?.unmount?.(); globalThis.paletteApp?.unmount?.();
globalThis.paletteApp = createApp(paletteAppSpec).mount('#palette-app');
if (!paletteAppContainer) {
return;
}
globalThis.paletteApp = createApp(paletteAppSpec).mount(paletteAppContainer);
} }
init(); init();
addEventListener('turbo:render', init); addEventListener('turbo:render', init);
export function getPaletteCode(paletteId, tweaks, options) { export function getPaletteCode(paletteId, colors, tweaked, options) {
let palette = allPalettes[paletteId].colors;
let imports = []; let imports = [];
if (paletteId) { if (paletteId) {
@@ -353,37 +450,27 @@ export function getPaletteCode(paletteId, tweaks, options) {
} }
let css = ''; let css = '';
let declarations = [];
if (tweaks) { if (tweaked) {
let { hueShifts, chromaScale = 1 } = tweaks; for (let hue in colors) {
let declarations = []; if (hue === 'orange') {
continue;
if (hueShifts || chromaScale !== 1) { } else if (hue === 'gray') {
for (let hue in hueShifts) { if (!tweaked.grayChroma && !tweaked.grayColor) {
let shift = hueShifts[hue];
if ((!shift && chromaScale === 1) || hue === 'orange') {
continue; continue;
} }
} else if (!tweaked.chromaScale && !tweaked.hue?.[hue]) {
let scale = palette[hue]; continue;
for (let tint of ['05', '10', '20', '30', '40', '50', '60', '70', '80', '90', '95']) {
let color = scale[tint];
if (Array.isArray(color)) {
color = new Color('oklch', coords);
} else {
color = color.clone();
}
color.set({ h: h => h + shift, c: c => c * chromaScale });
let stringified = color.toString({ format: color.inGamut('srgb') ? 'hex' : undefined });
declarations.push(`--wa-color-${hue}-${tint}: ${stringified};`);
}
declarations.push('');
} }
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) { if (declarations.length > 0) {
@@ -409,3 +496,85 @@ function arrayPrevious(array, element) {
let index = array.indexOf(element); let index = array.indexOf(element);
return array[(index - 1 + array.length) % array.length]; 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;
}

View File

@@ -2,7 +2,5 @@
title: Patterns title: Patterns
description: Patterns are reusable solutions to common design problems. description: Patterns are reusable solutions to common design problems.
layout: overview layout: overview
categories: ["e-commerce"]
listChildren: true
override:tags: [] override:tags: []
--- ---

View File

@@ -14,11 +14,45 @@ During the alpha period, things might break! We take breaking changes very serio
## Next ## Next
- 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
- Fixed a bug that caused dropdowns inside button groups to show with a vertical misalignment
- Revert `<wa-dialog>` structure and CSS to fix clipped content in dialogs (WA-A #123) and light dismiss in iOS Safari (WA-A #201)
- Fixed a bug in `<wa-progress>` that prevented Safari from animation progress changes
- Fixed the missing indeterminate icon in native checkbox styles
- Fixed a bug where changing a `<wa-option>` label wouldn't update the display label in `<wa-select>`
- Fixed a bug in `<wa-radio>` where elements would stack instead of display inline
### 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
- Added default spacing to icons slotted into `<wa-tab>`
- Fixed `wa-pill` class for text fields
### 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
- Fixed a bug in `<wa-radio-button>` that prevented the pill effect from applying properly
- Fixed a bug in `<wa-radio-button>` that prevented active buttons from receiving the correct styles
- Fixed a bug in `<wa-button>` that prevented the focus ring from showing in Safari
## 3.0.0-alpha.11
### Color Palettes ### Color Palettes
- Color palette tweaking UI. Tweak hue, grays, overall colorfulness, save or share the results.
- Added a `pink` scale to all color palettes - Added a `pink` scale to all color palettes
- Fixed an incorrect CSS value in `<wa-select>`'s expand icon
- Tweaked hues of all color palettes to make them more distinct and make their hues more intentional - Tweaked hues of all color palettes to make them more distinct and make their hues more intentional
- Dropped `violet` and `teal`, instead using `purple` and `cyan` (this is not just a renaming, the colors have been adjusted too).
- Fixed a bug in `<wa-switch>` that caused tooltips to work incorrectly when toggling the switch
### Design Tokens ### Design Tokens
@@ -27,32 +61,54 @@ You can find them in the first column of each color palette.
### Themes ### 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. - 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.
- You can now override the brand color of any theme with any of the 9 hues supported.
- Improved UI for theme remixing, with previews and generated copyable code snippets.
### Components ### Components
- Various `<wa-radio>` improvements: #### `<wa-radio>`
- Dropped the `base` part. It can now be styled by directly applying CSS to the element itself.
- Added `hint` attribute and corresponding slot.
- Various `<wa-select>` improvements:
- Added the `tag` part (and associated exported parts) to `<wa-select>` to allow targeting the tag that shows when more than the max number of visible items have been selected
- Fixed a bug that prevented the placeholder color from being customized with the `--wa-form-control-placeholder-color` token
- Dropped the `base` part from `<wa-option>` for easier styling. CSS can now be applied directly to the element itself.
- Various `<wa-card>` improvements:
- Fixed a bug where child elements did not have correct rounding when headers and footers were absent.
- Re-introduced `--border-color` so that the card itself can have a different border color than its inner borders.
- Fixed a bug that prevented slots from showing automatically without `with-` attributes
- Fixed a bug in `<wa-select>` that prevented the description from being read by screen readers
- Dropped the `base` part. It can now be styled by directly applying CSS to the element itself.
- Added `hint` attribute and corresponding slot.
#### `<wa-select>`
- Added the `tag` part (and associated exported parts) to `<wa-select>` to allow targeting the tag that shows when more than the max number of visible items have been selected
- Fixed a bug that prevented the placeholder color from being customized with the `--wa-form-control-placeholder-color` token
- Fixed an incorrect CSS value in the expand icon
- Fixed a bug that prevented the description from being read by screen readers
#### `<wa-option>`
- `label` attribute to override the generated label (useful for rich content)
- `defaultLabel` property
- Dropped `getTextLabel()` method (if you need dynamic labels, just set the `label` attribute dynamically)
- Dropped `base` part for easier styling. CSS can now be applied directly to the element itself.
#### `<wa-menu-item>`
- `label` attribute to override the generated label (useful for rich content)
- `defaultLabel` property
- Dropped `getTextLabel()` method (if you need dynamic labels, just set the `label` attribute dynamically)
#### `<wa-card>`
- Fixed a bug where child elements did not have correct rounding when headers and footers were absent.
- Re-introduced `--border-color` so that the card itself can have a different border color than its inner borders.
- Fixed a bug that prevented slots from showing automatically without `with-` attributes
#### `<wa-tab>`
- Fixed a bug that caused `document.createElement('wa-tab')` to fail (which also meant it could not be used in VueJS and other frameworks)
### Docs ### Docs
- Added an orientation example to the native radio docs - Added an orientation example to the native radio docs
- Fixed a number of broken event listeners throughout the docs - Fixed a number of broken event listeners throughout the docs
## 3.0.0-alpha.10 ## 3.0.0-alpha.10
- 🚨 BREAKING: updated all components to use native events instead of `wa-` prefixed events. This will allow components to work more like native elements in your code, frameworks, third-party plugins, etc. To update your code, simply remove the prefix from your event listeners for the following events. - 🚨 BREAKING: updated all components to use native events instead of `wa-` prefixed events. This will allow components to work more like native elements in your code, frameworks, third-party plugins, etc. To update your code, simply remove the prefix from your event listeners for the following events.

View File

@@ -31,8 +31,7 @@ If you're customizing the default dark styles, scope your styles to the followin
```css ```css
.wa-dark, .wa-dark,
.wa-invert, .wa-invert {
:is(:host-context(.wa-dark)) {
/* your custom styles here */ /* your custom styles here */
} }
``` ```

View File

@@ -10,15 +10,17 @@ override:tags: []
eleventyComputed: eleventyComputed:
forceTheme: "{{ theme.fileSlug }}" forceTheme: "{{ theme.fileSlug }}"
--- ---
{% set isPro = theme.data.isPro %}
{% set status = theme.data.status %}
{% set since = theme.data.since %}
<link rel="stylesheet" href="/docs/themes/showcase.css" /> <link rel="stylesheet" href="/docs/themes/showcase.css" />
{% set content %} {% set content %}
<header> <header>
{% include 'breadcrumbs.njk' %} {% include 'breadcrumbs.njk' %}
<h1 class="title">{{ theme.data.title }}</h1> <h1 class="title">{{ theme.data.title }}</h1>
<p id="mix_and_match" hidden class="wa-size-s"></p> <p id="mix_and_match" class="wa-size-s"></p>
<p>{% include 'status.njk' %}</p> <p id="theme-status">{% include 'status.njk' %}</p>
<p id="theme-showcase-description">{{ theme.data.description | inlineMarkdown | safe }}</p> <p id="theme-showcase-description">{{ theme.data.description | inlineMarkdown | safe }}</p>
</header> </header>
{% include 'theme-showcase.njk' %} {% include 'theme-showcase.njk' %}
@@ -34,30 +36,18 @@ eleventyComputed:
</wa-image-comparer> </wa-image-comparer>
<script type="module"> <script type="module">
import { urls as stylesheetURLs } from "/assets/scripts/tweak/data.js"; import { urls as stylesheetURLs, docsURLs, icons } from "/assets/scripts/tweak/data.js";
import { theme as getThemeCode } from "/assets/scripts/tweak/code.js"; import { getThemeCode } from "/assets/scripts/tweak/code.js";
function updateTheme() { function updateTheme() {
let params = new URLSearchParams(window.location.search); let params = new URLSearchParams(window.location.search);
params = Object.fromEntries(params.entries()); params = Object.fromEntries(params.entries());
const docsURLs = {
colors: '/docs/themes/',
palette: '/docs/palettes/',
typography: '/docs/themes/'
};
const icons = {
colors: 'palette',
palette: 'swatchbook',
brand: 'droplet',
typography: 'font-case'
};
for (let link of document.querySelectorAll('link.mix-and-match')) { for (let link of document.querySelectorAll('link.mix-and-match')) {
link.remove(); link.remove();
} }
let msgs = []; let tweaks = [];
let code = getThemeCode("{{ theme.fileSlug }}", params, {attributes: 'class="mix-and-match"'}); let code = getThemeCode("{{ theme.fileSlug }}", params, {attributes: 'class="mix-and-match"'});
document.head.insertAdjacentHTML('beforeend', code); document.head.insertAdjacentHTML('beforeend', code);
@@ -72,18 +62,29 @@ function updateTheme() {
} }
let icon = icons[name]; let icon = icons[name];
msgs.push(`<wa-icon name="${icon}" variant="regular"></wa-icon> ${ title }`); tweaks.push(`<wa-icon name="${icon}" variant="regular"></wa-icon> ${ title }`);
} }
} }
for (let p of mix_and_match) { let isRemixed = tweaks.length > 0;
p.hidden = msgs.length === 0; document.documentElement.classList.toggle('is-remixed', isRemixed);
if (msgs.length) {
let icon = if (isRemixed) {
p.innerHTML = `<strong><wa-icon name="arrows-rotate"></wa-icon> Remixed</strong> ` + msgs.map(msg => `<wa-badge appearance=outlined> for (let p of document.querySelectorAll("#theme-status")) {
${ msg }</wa-badge>`).join(' '); let proBadge = p.querySelector(".pro");
if (!proBadge) {
p.insertAdjacentHTML('beforeend', '<wa-badge class="pro">PRO</wa-badge>');
}
}
for (let p of mix_and_match) {
if (tweaks.length) {
p.innerHTML = `<strong><wa-icon name="arrows-rotate"></wa-icon> Remixed</strong> ` + tweaks.map(msg => `<wa-badge appearance=outlined>
${ msg }</wa-badge>`).join(' ');
}
} }
} }
} }
updateTheme(); updateTheme();
</script> </script>

View File

@@ -1,13 +1,13 @@
--- ---
title: Themes title: Themes
description: Themes are collections of design tokens that thread through every Web Awesome component and pattern. description: Themes are collections of design tokens that thread through every Web Awesome component and pattern.
Themes play a crucial role in [customizing Web Awesome](/docs/customizing). Themes play a crucial role in [customizing Web Awesome](/docs/customizing).
layout: overview layout: overview
override:tags: [] override:tags: []
forTag: theme forTag: theme
categories: categories:
tags: [other, pro]
other: Free other: Free
pro: Pro
--- ---
<div class="max-line-length"> <div class="max-line-length">
@@ -30,7 +30,7 @@ In pre-made themes, we use a light color scheme by default.
Additionally, styles may be scoped to the `:root` selector to be activated automatically. Additionally, styles may be scoped to the `:root` selector to be activated automatically.
For pre-made themes, *all* custom properties are scoped to `:root`, the theme class, and `wa-light`. For pre-made themes, *all* custom properties are scoped to `:root`, the theme class, and `wa-light`.
Finally, we scope themes to `:host` and `:host-context()` to ensure the styles are applied to the shadow roots of custom elements. Finally, we scope themes to `:host` to ensure the styles are applied to the shadow roots of custom elements.
For example, the default theme is set up like this: For example, the default theme is set up like this:
@@ -44,8 +44,7 @@ For example, the default theme is set up like this:
} }
.wa-dark, .wa-dark,
.wa-invert, .wa-invert {
:host-context(.wa-dark) {
/* subset of CSS custom properties for a dark color scheme */ /* subset of CSS custom properties for a dark color scheme */
} }
``` ```

View File

@@ -107,6 +107,10 @@ function setDefault(select, value) {
} }
function render(changedAspect) { function render(changedAspect) {
if (!globalThis.demo) {
return;
}
let url = new URL(demo.src); let url = new URL(demo.src);
if (!changedAspect || changedAspect === 'colors') { if (!changedAspect || changedAspect === 'colors') {

View File

@@ -12,6 +12,11 @@ body,
#mix_and_match { #mix_and_match {
font-weight: var(--wa-font-weight-semibold); font-weight: var(--wa-font-weight-semibold);
color: var(--wa-color-text-quiet); color: var(--wa-color-text-quiet);
margin-block-end: var(--wa-space-xs);
html:not(.is-remixed) {
display: none;
}
wa-icon { wa-icon {
vertical-align: -0.15em; vertical-align: -0.15em;

View File

@@ -3,6 +3,7 @@
"wide": true, "wide": true,
"tags": ["themes", "theme"], "tags": ["themes", "theme"],
"brand": "blue", "brand": "blue",
"icon": "theme",
"eleventyComputed": { "eleventyComputed": {
"file": "styles/themes/{{ page.fileSlug }}.css" "file": "styles/themes/{{ page.fileSlug }}.css"
} }

View File

@@ -3,4 +3,5 @@ title: Design Tokens
description: A theme is a collection of predefined CSS custom properties that control global styles from color to shadows. These custom properties thread through all Web Awesome components for a consistent look and feel. description: A theme is a collection of predefined CSS custom properties that control global styles from color to shadows. These custom properties thread through all Web Awesome components for a consistent look and feel.
layout: overview layout: overview
override:tags: [] override:tags: []
categories: {tags: true}
--- ---

View File

@@ -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. 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 ## 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. 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.
@@ -88,49 +112,6 @@ For example, `<button>` and `<wa-button>` both have a `type` attribute, but the
**Don't make assumptions about a component's API!** To prevent unexpected behaviors, please take the time to review the documentation and make sure you understand what each attribute, property, method, and event is intended to do. **Don't make assumptions about a component's API!** To prevent unexpected behaviors, please take the time to review the documentation and make sure you understand what each attribute, property, method, and event is intended to do.
::: :::
## Waiting for Components to Load
Web components are registered with JavaScript, so depending on how and when you load Web Awesome, you may notice a [Flash of Undefined Custom Elements (FOUCE)](https://www.abeautifulsite.net/posts/flash-of-undefined-custom-elements/) when the page loads. There are a couple ways to prevent this, both of which are described in the linked article.
One option is to use the [`:defined`](https://developer.mozilla.org/en-US/docs/Web/CSS/:defined) CSS pseudo-class to "hide" custom elements that haven't been registered yet. You can scope it to specific tags or you can hide all undefined custom elements as shown below.
```css
:not(:defined) {
visibility: hidden;
}
```
As soon as a custom element is registered, it will immediately appear with all of its styles, effectively eliminating FOUCE. Note the use of `visibility: hidden` instead of `display: none` to reduce shifting as elements are registered. The drawback to this approach is that custom elements can potentially appear one by one instead of all at the same time.
Another option is to use [`customElements.whenDefined()`](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/whenDefined), which returns a promise that resolves when the specified element gets registered. You'll probably want to use it with [`Promise.allSettled()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled) in case an element fails to load for some reason.
A clever way to use this method is to hide the `<body>` with `opacity: 0` and add a class that fades it in as soon as all your custom elements are defined.
```html
<style>
body {
opacity: 0;
}
body.ready {
opacity: 1;
transition: 0.25s opacity;
}
</style>
<script type="module">
await Promise.allSettled([
customElements.whenDefined('wa-button'),
customElements.whenDefined('wa-card'),
customElements.whenDefined('wa-rating')
]);
// Button, card, and rating are registered now! Add
// the `ready` class so the UI fades in.
document.body.classList.add('ready');
</script>
```
## Component Rendering and Updating ## Component Rendering and Updating
Web Awesome components are built with [Lit](https://lit.dev/), a tiny library that makes authoring custom elements easier, more maintainable, and a lot of fun! As a Web Awesome user, here is some helpful information about rendering and updating you should probably be aware of. Web Awesome components are built with [Lit](https://lit.dev/), a tiny library that makes authoring custom elements easier, more maintainable, and a lot of fun! As a Web Awesome user, here is some helpful information about rendering and updating you should probably be aware of.

View 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();
```
:::

View File

@@ -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 its 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 %}

View File

@@ -4,8 +4,6 @@ description: Build better with Web Awesome, the open source library of web compo
layout: page layout: page
--- ---
<style> <style>
.title, .title,
.anchor-heading a, .anchor-heading a,
@@ -387,4 +385,4 @@ layout: page
&copy; Fonticons, Inc. &copy; Fonticons, Inc.
</div> </div>
</footer> </footer>
</div> </div>

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "@shoelace-style/webawesome", "name": "@shoelace-style/webawesome",
"version": "3.0.0-alpha.10", "version": "3.0.0-alpha.11",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@shoelace-style/webawesome", "name": "@shoelace-style/webawesome",
"version": "3.0.0-alpha.10", "version": "3.0.0-alpha.11",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ctrl/tinycolor": "^4.1.0", "@ctrl/tinycolor": "^4.1.0",

View File

@@ -1,7 +1,7 @@
{ {
"name": "@shoelace-style/webawesome", "name": "@shoelace-style/webawesome",
"description": "A forward-thinking library of web components.", "description": "A forward-thinking library of web components.",
"version": "3.0.0-alpha.10", "version": "3.0.0-alpha.11",
"homepage": "https://webawesome.com/", "homepage": "https://webawesome.com/",
"author": "Web Awesome", "author": "Web Awesome",
"license": "MIT", "license": "MIT",
@@ -52,8 +52,8 @@
"start:alpha": "node scripts/build.js --alpha --develop", "start:alpha": "node scripts/build.js --alpha --develop",
"publish-alpha-cdn": "./publish-alpha-cdn.sh", "publish-alpha-cdn": "./publish-alpha-cdn.sh",
"create": "plop --plopfile scripts/plop/plopfile.js", "create": "plop --plopfile scripts/plop/plopfile.js",
"test": "web-test-runner --group default", "test": "CSR_ONLY=\"true\" web-test-runner --group default",
"test:component": "web-test-runner -- --watch --group", "test:component": "CSR_ONLY=\"true\" web-test-runner -- --watch --group",
"test:contrast": "cd src/styles/color && node contrast.test.js", "test:contrast": "cd src/styles/color && node contrast.test.js",
"test:watch": "web-test-runner --watch --group default", "test:watch": "web-test-runner --watch --group default",
"prettier": "prettier --check --log-level=warn .", "prettier": "prettier --check --log-level=warn .",

View File

@@ -4,6 +4,7 @@ import { execSync } from 'child_process';
import { deleteAsync } from 'del'; import { deleteAsync } from 'del';
import esbuild from 'esbuild'; import esbuild from 'esbuild';
import { replace } from 'esbuild-plugin-replace'; import { replace } from 'esbuild-plugin-replace';
import { mkdir, readFile } from 'fs/promises'; import { mkdir, readFile } from 'fs/promises';
import getPort, { portNumbers } from 'get-port'; import getPort, { portNumbers } from 'get-port';
import { globby } from 'globby'; import { globby } from 'globby';
@@ -12,7 +13,6 @@ import { dirname, join, relative } from 'path';
import process from 'process'; import process from 'process';
import copy from 'recursive-copy'; import copy from 'recursive-copy';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { preprocessStylesheet } from './preprocess-css.js';
import { cdnDir, distDir, docsDir, rootDir, runScript, siteDir } from './utils.js'; import { cdnDir, distDir, docsDir, rootDir, runScript, siteDir } from './utils.js';
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -37,7 +37,6 @@ async function buildAll() {
await generateManifest(); await generateManifest();
await generateReactWrappers(); await generateReactWrappers();
await generateTypes(); await generateTypes();
await preprocessStyles();
await generateStyles(); await generateStyles();
// copy everything to unbundled before we generate bundles. // copy everything to unbundled before we generate bundles.
@@ -107,23 +106,6 @@ function generateReactWrappers() {
return Promise.resolve(); return Promise.resolve();
} }
/**
* Generate preprocessed CSS
*/
async function preprocessStyles() {
const preprocessedCSSFiles = await globby(join(rootDir, 'src/styles/**/*.css.njk'));
if (preprocessedCSSFiles.length > 0) {
spinner.start('Preprocessing stylesheets');
for (let filePath of preprocessedCSSFiles) {
await preprocessStylesheet(filePath);
}
spinner.succeed();
}
}
/** /**
* Copies theme stylesheets to the dist. * Copies theme stylesheets to the dist.
*/ */
@@ -285,6 +267,13 @@ async function regenerateBundle() {
* Generates the documentation site. * Generates the documentation site.
*/ */
async function generateDocs() { async function generateDocs() {
/**
* Used by the webawesome-app to skip doc generation since it will do its own.
*/
if (process.env.SKIP_ELEVENTY === 'true') {
return;
}
spinner.start('Writing the docs'); spinner.start('Writing the docs');
const args = []; const args = [];
@@ -390,7 +379,6 @@ if (isDeveloping) {
try { try {
const isTestFile = filename.includes('.test.ts'); const isTestFile = filename.includes('.test.ts');
const isCssStylesheet = filename.includes('.css'); const isCssStylesheet = filename.includes('.css');
const isPreprocessedStylesheet = filename.endsWith('.css.njk');
const isComponent = const isComponent =
filename.includes('components/') && filename.includes('.ts') && !isCssStylesheet && !isTestFile; filename.includes('components/') && filename.includes('.ts') && !isCssStylesheet && !isTestFile;
@@ -401,10 +389,6 @@ if (isDeveloping) {
await regenerateBundle(); await regenerateBundle();
if (isPreprocessedStylesheet || filename.endsWith('src/styles/data.js')) {
await preprocessStyles();
}
// Copy stylesheets when CSS files change // Copy stylesheets when CSS files change
if (isCssStylesheet) { if (isCssStylesheet) {
await generateStyles(); await generateStyles();

View File

@@ -1,61 +0,0 @@
import { readFile, writeFile } from 'fs/promises';
import nunjucks from 'nunjucks';
import * as prettier from 'prettier';
import prettierConfig from '../prettier.config.js';
import * as globalData from '../src/styles/data.js';
const prelude = inputFilename => `/* DO NOT EDIT THIS FILE. It is generated from "${inputFilename}" */`;
let env = nunjucks.configure({ autoescape: false });
let filenameVariables = Object.keys(globalData)
.filter(key => key.endsWith('s'))
.map(key => key.slice(0, -1));
let filenameVariablesRegex = RegExp(`\\{\\{\\s*(${filenameVariables.join('|')})\\s*\\}\\}`, 'g');
export async function preprocessStylesheet(inputPath) {
const content = await readFile(inputPath, 'utf-8');
let outputPath = inputPath.replace(/\.njk$/, '');
let filename = outputPath.split('/').pop();
if (filenameVariablesRegex.test(filename)) {
// NOTE only supports a single variable right now
filenameVariablesRegex.lastIndex = 0;
let ret = [];
for (let match of filename.matchAll(filenameVariablesRegex)) {
let variable = match[1];
let values = globalData[variable + 's'];
for (let value of values) {
let data = { [variable]: value };
let css = await renderCSS(content, { data, inputPath, outputPath });
// Now use Nunjucks *again*, to render the actual filename
let localOutputFilePath = env.renderString(outputPath, data);
ret.push(writeFile(localOutputFilePath, css, 'utf-8'));
}
}
return Promise.all(ret);
} else {
let css = await renderCSS(content, { inputPath, outputPath });
return writeFile(outputPath, css, 'utf-8');
}
// TODO add generated files to .gitignore?
}
export async function renderCSS(content, { data, inputPath, outputPath } = {}) {
let inputFilename = inputPath.split('/').pop();
data = data ? { ...globalData, ...data } : globalData;
let css = env.renderString(content, data);
if (prelude) {
css = prelude(inputFilename) + '\n' + css;
}
css = await prettier.format(css, { ...prettierConfig, filepath: outputPath });
return css;
}

View File

@@ -52,6 +52,7 @@ import styles from './button.css';
@customElement('wa-button') @customElement('wa-button')
export default class WaButton extends WebAwesomeFormAssociatedElement { export default class WaButton extends WebAwesomeFormAssociatedElement {
static shadowStyle = [variantStyles, appearanceStyles, sizeStyles, nativeStyles, styles]; static shadowStyle = [variantStyles, appearanceStyles, sizeStyles, nativeStyles, styles];
static rectProxy = 'button';
static get validators() { static get validators() {
return [...super.validators, MirrorValidator()]; return [...super.validators, MirrorValidator()];
@@ -108,7 +109,7 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
@property({ reflect: true }) value: string | null = null; @property({ reflect: true }) value: string | null = null;
/** When set, the underlying button will be rendered as an `<a>` with this `href` instead of a `<button>`. */ /** When set, the underlying button will be rendered as an `<a>` with this `href` instead of a `<button>`. */
@property() href = ''; @property({ reflect: true }) href = null;
/** Tells the browser where to open the link. Only used when `href` is present. */ /** Tells the browser where to open the link. Only used when `href` is present. */
@property() target: '_blank' | '_parent' | '_self' | '_top'; @property() target: '_blank' | '_parent' | '_self' | '_top';
@@ -224,17 +225,6 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
this.button.blur(); this.button.blur();
} }
getBoundingClientRect(): DOMRect {
let rect = super.getBoundingClientRect();
let buttonRect = this.button.getBoundingClientRect();
if (rect.width === 0 && buttonRect.width > 0) {
return buttonRect;
}
return rect;
}
render() { render() {
const isLink = this.isLink(); const isLink = this.isLink();
const tag = isLink ? literal`a` : literal`button`; const tag = isLink ? literal`a` : literal`button`;

View File

@@ -5,15 +5,16 @@
--spacing: var(--wa-space); --spacing: var(--wa-space);
--border-width: var(--wa-panel-border-width); --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); --border-radius: var(--wa-panel-border-radius);
--inner-border-radius: calc(var(--border-radius) - var(--border-width)); --inner-border-radius: calc(var(--border-radius) - var(--border-width));
--inner-border-color: var(--outlined-border-color);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: var(--wa-color-surface-default); background-color: var(--background-color, var(--wa-color-surface-default));
border-color: var(--border-color); border-color: var(--border-color, var(--wa-color-surface-border));
border-radius: var(--border-radius); border-radius: var(--border-radius);
border-style: var(--wa-panel-border-style); border-style: var(--wa-panel-border-style);
box-shadow: var(--wa-shadow-s); box-shadow: var(--wa-shadow-s);
@@ -21,6 +22,20 @@
color: var(--wa-color-text-normal); 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 */ /* Take care of top and bottom radii */
.image, .image,
:host(:not([with-image])) .header, :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 { .header {
display: block; display: block;
border-block-end-style: inherit; 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); border-block-end-width: var(--border-width);
padding: calc(var(--spacing) / 2) var(--spacing); padding: calc(var(--spacing) / 2) var(--spacing);
} }
@@ -62,7 +86,7 @@
.footer { .footer {
display: block; display: block;
border-block-start-style: inherit; 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); border-block-start-width: var(--border-width);
padding: var(--spacing); padding: var(--spacing);
} }

View File

@@ -2,6 +2,7 @@ import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
import { HasSlotController } from '../../internal/slot.js'; import { HasSlotController } from '../../internal/slot.js';
import WebAwesomeElement from '../../internal/webawesome-element.js'; import WebAwesomeElement from '../../internal/webawesome-element.js';
import appearanceStyles from '../../styles/utilities/appearance.css';
import sizeStyles from '../../styles/utilities/size.css'; import sizeStyles from '../../styles/utilities/size.css';
import styles from './card.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. * @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-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 [--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. * @cssproperty [--spacing=var(--wa-space)] - The amount of space around and between sections of the card. Expects a single value.
*/ */
@customElement('wa-card') @customElement('wa-card')
export default class WaCard extends WebAwesomeElement { export default class WaCard extends WebAwesomeElement {
static shadowStyle = [sizeStyles, styles]; static shadowStyle = [sizeStyles, appearanceStyles, styles];
private readonly hasSlotController = new HasSlotController(this, 'footer', 'header', 'image'); private readonly hasSlotController = new HasSlotController(this, 'footer', 'header', 'image');
/** The component's size. Will be inherited by any descendants with a `size` attribute. */ /** 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'; @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. */ /** Renders the card with a header. Only needed for SSR, otherwise is automatically added. */
@property({ attribute: 'with-header', type: Boolean, reflect: true }) withHeader = false; @property({ attribute: 'with-header', type: Boolean, reflect: true }) withHeader = false;

View File

@@ -278,11 +278,11 @@
} }
/* /*
* Color dropdown * Color dropdown
*/ */
.color-dropdown { .color-dropdown {
display: flex; display: contents;
} }
.color-dropdown::part(panel) { .color-dropdown::part(panel) {

View File

@@ -9,6 +9,7 @@ import { getTargetElement, waitForEvent } from '../../internal/event.js';
import { watch } from '../../internal/watch.js'; import { watch } from '../../internal/watch.js';
import WebAwesomeElement from '../../internal/webawesome-element.js'; import WebAwesomeElement from '../../internal/webawesome-element.js';
import nativeStyles from '../../styles/native/details.css'; import nativeStyles from '../../styles/native/details.css';
import appearanceStyles from '../../styles/utilities/appearance.css';
import { LocalizeController } from '../../utilities/localize.js'; import { LocalizeController } from '../../utilities/localize.js';
import '../icon/icon.js'; import '../icon/icon.js';
import styles from './details.css'; import styles from './details.css';
@@ -45,7 +46,7 @@ import styles from './details.css';
*/ */
@customElement('wa-details') @customElement('wa-details')
export default class WaDetails extends WebAwesomeElement { export default class WaDetails extends WebAwesomeElement {
static shadowStyle = [nativeStyles, styles]; static shadowStyle = [appearanceStyles, nativeStyles, styles];
private detailsObserver: MutationObserver; private detailsObserver: MutationObserver;
private readonly localize = new LocalizeController(this); 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. */ /** Disables the details so it can't be toggled. */
@property({ type: Boolean, reflect: true }) disabled = false; @property({ type: Boolean, reflect: true }) disabled = false;
/** The element's visual appearance. */
@property({ reflect: true }) appearance: 'filled' | 'outlined' | 'plain' = 'outlined';
firstUpdated() { firstUpdated() {
this.body.style.height = this.open ? 'auto' : '0'; this.body.style.height = this.open ? 'auto' : '0';
if (this.open) { if (this.open) {

View File

@@ -1,24 +1,89 @@
:host { :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); --spacing: var(--wa-space-xl);
--show-duration: 200ms;
--hide-duration: 200ms;
display: contents;
}
:host(:not([open])) {
display: none; display: none;
} }
dialog { :host([open]) {
width: inherit; display: block;
max-width: inherit; }
max-height: inherit;
background-color: inherit; .dialog {
border-radius: inherit; display: flex;
border: inherit; flex-direction: column;
box-shadow: inherit; top: 0;
padding: inherit; 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; 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 { .header-actions {
@@ -28,13 +93,81 @@ dialog {
flex-wrap: wrap; flex-wrap: wrap;
justify-content: end; justify-content: end;
gap: var(--wa-space-2xs); gap: var(--wa-space-2xs);
margin-inline-start: auto; padding-inline-start: var(--spacing);
}
wa-icon-button, .header-actions wa-icon-button,
::slotted(wa-icon-button) { .header-actions ::slotted(wa-icon-button) {
flex: 0 0 auto; flex: 0 0 auto;
display: flex; display: flex;
align-items: center; align-items: center;
font-size: var(--wa-font-size-m); 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;
} }
} }

View File

@@ -1,5 +1,6 @@
import { html, isServer } from 'lit'; 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 { WaAfterHideEvent } from '../../events/after-hide.js';
import { WaAfterShowEvent } from '../../events/after-show.js'; import { WaAfterShowEvent } from '../../events/after-show.js';
import { WaHideEvent } from '../../events/hide.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 { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll.js';
import { watch } from '../../internal/watch.js'; import { watch } from '../../internal/watch.js';
import WebAwesomeElement from '../../internal/webawesome-element.js'; import WebAwesomeElement from '../../internal/webawesome-element.js';
import dialogStyles from '../../styles/native/dialog.css';
import { LocalizeController } from '../../utilities/localize.js'; import { LocalizeController } from '../../utilities/localize.js';
import '../icon-button/icon-button.js'; import '../icon-button/icon-button.js';
import styles from './dialog.css'; import styles from './dialog.css';
@@ -35,7 +35,6 @@ import styles from './dialog.css';
* behavior such as data loss. * behavior such as data loss.
* @event wa-after-hide - Emitted after the dialog closes and all animations are complete. * @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 - 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 header-actions - Optional actions to add to the header. Works best with `<wa-icon-button>`.
* @csspart title - The dialog's title. * @csspart title - The dialog's title.
@@ -44,16 +43,22 @@ import styles from './dialog.css';
* @csspart body - The dialog's body. * @csspart body - The dialog's body.
* @csspart footer - The dialog's footer. * @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') @customElement('wa-dialog')
export default class WaDialog extends WebAwesomeElement { export default class WaDialog extends WebAwesomeElement {
static shadowStyle = [dialogStyles, styles]; static shadowStyle = styles;
private readonly localize = new LocalizeController(this); private readonly localize = new LocalizeController(this);
private originalTrigger: HTMLElement | null; 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 * 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. */ /** When enabled, the dialog will be closed when the user clicks outside of it. */
@property({ attribute: 'light-dismiss', type: Boolean }) lightDismiss = false; @property({ attribute: 'light-dismiss', type: Boolean }) lightDismiss = false;
@state()
hasOpened = this.open;
firstUpdated() { firstUpdated() {
if (this.open) { if (this.open) {
this.addOpenListeners(); this.addOpenListeners();
@@ -100,12 +102,14 @@ export default class WaDialog extends WebAwesomeElement {
if (waHideEvent.defaultPrevented) { if (waHideEvent.defaultPrevented) {
this.open = true; this.open = true;
animateWithClass(this.dialog, 'wa-dialog-pulse'); animateWithClass(this.dialog, 'pulse');
return; return;
} }
this.removeOpenListeners(); this.removeOpenListeners();
await animateWithClass(this.dialog, 'hide');
this.open = false; this.open = false;
this.dialog.close(); this.dialog.close();
unlockBodyScrolling(this); unlockBodyScrolling(this);
@@ -120,16 +124,7 @@ export default class WaDialog extends WebAwesomeElement {
} }
private addOpenListeners() { private addOpenListeners() {
if ('CloseWatcher' in window) { document.addEventListener('keydown', this.handleDocumentKeyDown);
this.closeWatcher?.destroy();
this.closeWatcher = new CloseWatcher();
this.closeWatcher.onclose = () => {
this.requestClose(this.dialog);
};
} else {
this.closeWatcher?.destroy();
document.addEventListener('keydown', this.handleDocumentKeyDown);
}
} }
private removeOpenListeners() { private removeOpenListeners() {
@@ -161,7 +156,7 @@ export default class WaDialog extends WebAwesomeElement {
if (this.lightDismiss) { if (this.lightDismiss) {
this.requestClose(this.dialog); this.requestClose(this.dialog);
} else { } 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.addOpenListeners();
this.originalTrigger = document.activeElement as HTMLElement; this.originalTrigger = document.activeElement as HTMLElement;
this.open = true; this.open = true;
this.hasOpened = true;
this.dialog.showModal(); this.dialog.showModal();
lockBodyScrolling(this); lockBodyScrolling(this);
@@ -211,20 +205,28 @@ export default class WaDialog extends WebAwesomeElement {
} }
}); });
await animateWithClass(this.dialog, 'show');
this.dispatchEvent(new WaAfterShowEvent()); this.dispatchEvent(new WaAfterShowEvent());
} }
render() { render() {
return html` return html`
<dialog <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} @cancel=${this.handleDialogCancel}
@click=${this.handleDialogClick} @click=${this.handleDialogClick}
@pointerdown=${this.handleDialogPointerDown} @pointerdown=${this.handleDialogPointerDown}
> >
${this.withHeader ${this.withHeader
? html` ? html`
<header part="header"> <header part="header" class="header">
<h2 part="title" class="title" id="title"> <h2 part="title" class="title" id="title">
<!-- If there's no label, use an invisible character to prevent the header from collapsing --> <!-- 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> <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 ${this.withFooter
? html` ? html`
<footer part="footer"> <footer part="footer" class="footer">
<slot name="footer"></slot> <slot name="footer"></slot>
</footer> </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 // Ugly, but it fixes light dismiss in Safari: https://bugs.webkit.org/show_bug.cgi?id=267688
if (!isServer) { if (!isServer) {
document.body.addEventListener('pointerdown', () => { document.addEventListener('pointerdown', () => {
/* empty */ /* empty */
}); });
} }

View File

@@ -62,7 +62,6 @@ export default class WaDrawer extends WebAwesomeElement {
private readonly localize = new LocalizeController(this); private readonly localize = new LocalizeController(this);
private originalTrigger: HTMLElement | null; private originalTrigger: HTMLElement | null;
private closeWatcher: CloseWatcher | null;
@query('.drawer') drawer: HTMLDialogElement; @query('.drawer') drawer: HTMLDialogElement;
@@ -136,16 +135,7 @@ export default class WaDrawer extends WebAwesomeElement {
} }
private addOpenListeners() { private addOpenListeners() {
if ('CloseWatcher' in window) { document.addEventListener('keydown', this.handleDocumentKeyDown);
this.closeWatcher?.destroy();
this.closeWatcher = new CloseWatcher();
this.closeWatcher.onclose = () => {
this.requestClose(this.drawer);
};
} else {
this.closeWatcher?.destroy();
document.addEventListener('keydown', this.handleDocumentKeyDown);
}
} }
private removeOpenListeners() { private removeOpenListeners() {

View File

@@ -50,8 +50,6 @@ export default class WaDropdown extends WebAwesomeElement {
@query('#trigger') trigger: HTMLSlotElement; @query('#trigger') trigger: HTMLSlotElement;
@query('.panel') panel: 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 * 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. * 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) => { private handleKeyDown = (event: KeyboardEvent) => {
// Close when escape is pressed inside an open dropdown. We need to listen on the panel itself and stop propagation // 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. // 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(); event.stopPropagation();
this.hide(); this.hide();
this.focusOnTrigger(); this.focusOnTrigger();
@@ -344,16 +342,7 @@ export default class WaDropdown extends WebAwesomeElement {
addOpenListeners() { addOpenListeners() {
this.panel.addEventListener('wa-select', this.handlePanelSelect); this.panel.addEventListener('wa-select', this.handlePanelSelect);
if ('CloseWatcher' in window) { this.panel.addEventListener('keydown', this.handleKeyDown);
this.closeWatcher?.destroy();
this.closeWatcher = new CloseWatcher();
this.closeWatcher.onclose = () => {
this.hide();
this.focusOnTrigger();
};
} else {
this.panel.addEventListener('keydown', this.handleKeyDown);
}
document.addEventListener('keydown', this.handleDocumentKeyDown); document.addEventListener('keydown', this.handleDocumentKeyDown);
document.addEventListener('mousedown', this.handleDocumentMouseDown); document.addEventListener('mousedown', this.handleDocumentMouseDown);
} }
@@ -365,7 +354,6 @@ export default class WaDropdown extends WebAwesomeElement {
} }
document.removeEventListener('keydown', this.handleDocumentKeyDown); document.removeEventListener('keydown', this.handleDocumentKeyDown);
document.removeEventListener('mousedown', this.handleDocumentMouseDown); document.removeEventListener('mousedown', this.handleDocumentMouseDown);
this.closeWatcher?.destroy();
} }
@watch('open', { waitUntilFirstUpdate: true }) @watch('open', { waitUntilFirstUpdate: true })

View File

@@ -1,5 +1,8 @@
:host { :host {
--background-color-hover: var(--wa-color-neutral-fill-quiet); --background-color-hover: var(--wa-color-neutral-fill-quiet);
--text-color-hover: color-mix(in oklab, currentColor, var(--wa-color-mix-hover));
--background-color-active: transparent;
--text-color-active: color-mix(in oklab, currentColor, var(--wa-color-mix-active));
display: inline-block; display: inline-block;
color: var(--wa-color-text-quiet); color: var(--wa-color-text-quiet);
@@ -22,12 +25,13 @@
:host(:not([disabled])) .icon-button:hover, :host(:not([disabled])) .icon-button:hover,
:host(:not([disabled])) .icon-button:focus-visible { :host(:not([disabled])) .icon-button:focus-visible {
background-color: var(--wa-color-neutral-fill-quiet); background-color: var(--background-color-hover);
color: color-mix(in oklab, currentColor, var(--wa-color-mix-hover)); color: var(--text-color-hover);
} }
:host(:not([disabled])) .icon-button:active { :host(:not([disabled])) .icon-button:active {
color: color-mix(in oklab, currentColor, var(--wa-color-mix-active)); background-color: var(--background-color-active);
color: var(--text-color-active);
} }
.icon-button:focus { .icon-button:focus {

View File

@@ -17,7 +17,10 @@ import styles from './icon-button.css';
* @event blur - Emitted when the icon button loses focus. * @event blur - Emitted when the icon button loses focus.
* @event focus - Emitted when the icon button gains focus. * @event focus - Emitted when the icon button gains focus.
* *
* @cssproperty --background-color-hover - The color of the button's background on hover. * @cssproperty [--background-color-hover=var(--wa-color-neutral-fill-quiet)] - The color of the button's background on hover.
* @cssproperty [--background-color-active=var(--wa-color-neutral-fill-quiet)] - The color of the button's background on `:active`.
* @cssproperty --text-color-hover - The color of the button's background on hover.
* @cssproperty --text-color-active - The color of the button's background on `:active`.
* *
* @csspart base - The component's base wrapper. * @csspart base - The component's base wrapper.
*/ */

View File

@@ -106,6 +106,11 @@ export default class WaOption extends WebAwesomeElement {
} }
private handleDefaultSlotChange() { private handleDefaultSlotChange() {
// Tell the controller to update the label
if (customElements.get('wa-select')) {
this.closest('wa-select')?.selectionChanged();
}
this.updateDefaultLabel(); this.updateDefaultLabel();
if (this.isInitialized) { if (this.isInitialized) {
@@ -123,7 +128,7 @@ export default class WaOption extends WebAwesomeElement {
private handleHover = (event: Event) => { private handleHover = (event: Event) => {
// We need this because Safari doesn't honor :hover styles while dragging // We need this because Safari doesn't honor :hover styles while dragging
// Testcase: https://codepen.io/leaverou/pen/VYZOOjy // Test case: https://codepen.io/leaverou/pen/VYZOOjy
if (event.type === 'mouseenter') { if (event.type === 'mouseenter') {
this.toggleCustomState('hover', true); this.toggleCustomState('hover', true);
} else if (event.type === 'mouseleave') { } else if (event.type === 'mouseleave') {

View File

@@ -16,7 +16,7 @@
} }
.indicator { .indicator {
width: calc(var(--value, 0) * 1%); width: var(--percentage);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -26,9 +26,7 @@
overflow: hidden; overflow: hidden;
line-height: 1; line-height: 1;
font-weight: var(--wa-font-weight-semibold); font-weight: var(--wa-font-weight-semibold);
transition: var(--wa-transition-slow, 200ms) var(--wa-transition-easing, ease); transition: all var(--wa-transition-slow, 200ms) var(--wa-transition-easing, ease);
transition: 1s;
/* transition-property: width, background; */
user-select: none; user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
} }

View File

@@ -38,8 +38,9 @@ describe('<wa-progress-bar>', () => {
expect(base.getAttribute('aria-valuenow')).to.equal('25'); expect(base.getAttribute('aria-valuenow')).to.equal('25');
}); });
it('uses the value parameter to set the custom property on the indicator', () => { it('uses the value parameter to set the custom property on the indicator', async () => {
expect(indicator.style.getPropertyValue('--value')).to.equal('25'); await new Promise(requestAnimationFrame);
expect(el.style.getPropertyValue('--percentage')).to.equal('25%');
}); });
}); });

View File

@@ -1,6 +1,8 @@
import type { PropertyValues } from 'lit';
import { html } from 'lit'; import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js'; import { ifDefined } from 'lit/directives/if-defined.js';
import { clamp } from '../../internal/math.js';
import WebAwesomeElement from '../../internal/webawesome-element.js'; import WebAwesomeElement from '../../internal/webawesome-element.js';
import { LocalizeController } from '../../utilities/localize.js'; import { LocalizeController } from '../../utilities/localize.js';
import styles from './progress-bar.css'; import styles from './progress-bar.css';
@@ -33,6 +35,16 @@ export default class WaProgressBar extends WebAwesomeElement {
/** A custom label for assistive devices. */ /** A custom label for assistive devices. */
@property() label = ''; @property() label = '';
updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has('value')) {
// Wait a cycle before setting it so Safari animates it.
// https://github.com/shoelace-style/webawesome/issues/356
requestAnimationFrame(() => {
this.style.setProperty('--percentage', `${clamp(this.value, 0, 100)}%`);
});
}
}
render() { render() {
return html` return html`
<div <div
@@ -45,7 +57,7 @@ export default class WaProgressBar extends WebAwesomeElement {
aria-valuemax="100" aria-valuemax="100"
aria-valuenow=${this.indeterminate ? '0' : this.value} aria-valuenow=${this.indeterminate ? '0' : this.value}
> >
<div part="indicator" class="indicator" style="--value: ${this.value}"> <div part="indicator" class="indicator">
${!this.indeterminate ? html` <slot part="label" class="label"></slot> ` : ''} ${!this.indeterminate ? html` <slot part="label" class="label"></slot> ` : ''}
</div> </div>
</div> </div>

View File

@@ -32,7 +32,6 @@
/* /*
* Checked buttons * Checked buttons
*/ */
:host([checked]) { :host([checked]) {
--indicator-color: var(--wa-form-control-activated-color); --indicator-color: var(--wa-form-control-activated-color);
--indicator-width: var(--wa-border-width-s); --indicator-width: var(--wa-border-width-s);
@@ -43,3 +42,41 @@
--border-color: var(--indicator-color); --border-color: var(--indicator-color);
} }
} }
/*
* Active buttons
*/
button:active {
--text-color-active: var(--wa-form-control-activated-color);
--border-color-active: var(--wa-form-control-activated-color);
}
/* Horizontal radio pill buttons */
:host([data-wa-radio-horizontal][data-wa-radio-first]:not([data-wa-radio-last])) .wa-pill {
border-start-end-radius: 0;
border-end-end-radius: 0;
}
:host([data-wa-radio-horizontal][data-wa-radio-inner]) .wa-pill {
border-radius: 0;
}
:host([data-wa-radio-horizontal][data-wa-radio-last]:not([data-wa-radio-first])) .wa-pill {
border-start-start-radius: 0;
border-end-start-radius: 0;
}
/* Vertical radio pill buttons */
:host([data-wa-radio-vertical][data-wa-radio-first]:not([data-wa-radio-last])) .wa-pill {
border-end-start-radius: 0;
border-end-end-radius: 0;
}
:host([data-wa-radio-vertical][data-wa-radio-inner]) .wa-pill {
border-radius: 0;
}
:host([data-wa-radio-vertical][data-wa-radio-last]:not([data-wa-radio-first])) .wa-pill {
border-start-start-radius: 0;
border-start-end-radius: 0;
}

View File

@@ -9,7 +9,6 @@ import nativeStyles from '../../styles/native/button.css';
import appearanceStyles from '../../styles/utilities/appearance.css'; import appearanceStyles from '../../styles/utilities/appearance.css';
import sizeStyles from '../../styles/utilities/size.css'; import sizeStyles from '../../styles/utilities/size.css';
import variantStyles from '../../styles/utilities/variants.css'; import variantStyles from '../../styles/utilities/variants.css';
import buttonStyles from '../button/button.css';
import styles from './radio-button.css'; import styles from './radio-button.css';
/** /**
@@ -49,7 +48,8 @@ import styles from './radio-button.css';
*/ */
@customElement('wa-radio-button') @customElement('wa-radio-button')
export default class WaRadioButton extends WebAwesomeFormAssociatedElement { export default class WaRadioButton extends WebAwesomeFormAssociatedElement {
static shadowStyle = [variantStyles, appearanceStyles, sizeStyles, nativeStyles, buttonStyles, styles]; static shadowStyle = [variantStyles, appearanceStyles, sizeStyles, nativeStyles, styles];
static rectProxy = 'input';
private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix'); private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix');

View File

@@ -38,8 +38,6 @@ import styles from './radio-group.css';
* @csspart form-control-input - The input's wrapper. * @csspart form-control-input - The input's wrapper.
* @csspart radios - The wrapper than surrounds radio items, styled as a flex container by default. * @csspart radios - The wrapper than surrounds radio items, styled as a flex container by default.
* @csspart hint - The hint's wrapper. * @csspart hint - The hint's wrapper.
* @csspart button-group - The button group that wraps radio buttons.
* @csspart button-group__base - The button group's `base` part.
*/ */
@customElement('wa-radio-group') @customElement('wa-radio-group')
export default class WaRadioGroup extends WebAwesomeFormAssociatedElement { export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
@@ -65,7 +63,6 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
@query('slot:not([name])') defaultSlot: HTMLSlotElement; @query('slot:not([name])') defaultSlot: HTMLSlotElement;
@state() private hasButtonGroup = false;
@state() private hasRadioButtons = false; @state() private hasRadioButtons = false;
/** /**
@@ -150,7 +147,7 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
clickedRadio.checked = true; clickedRadio.checked = true;
const radios = this.getAllRadios(); const radios = this.getAllRadios();
const hasButtonGroup = radios.some(radio => radio.tagName.toLowerCase() === 'wa-radio-button'); const hasRadioButtons = radios.some(radio => radio.tagName.toLowerCase() === 'wa-radio-button');
for (const radio of radios) { for (const radio of radios) {
if (clickedRadio === radio) { if (clickedRadio === radio) {
continue; continue;
@@ -158,7 +155,7 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
radio.checked = false; radio.checked = false;
if (!hasButtonGroup) { if (!hasRadioButtons) {
radio.setAttribute('tabindex', '-1'); radio.setAttribute('tabindex', '-1');
} }
} }
@@ -183,6 +180,15 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
// Detect the presence of radio buttons // Detect the presence of radio buttons
this.hasRadioButtons = radios.some(radio => radio.localName === 'wa-radio-button'); this.hasRadioButtons = radios.some(radio => radio.localName === 'wa-radio-button');
// Add data attributes to support styling
radios.forEach((radio, index) => {
radio.toggleAttribute('data-wa-radio-horizontal', this.orientation !== 'vertical');
radio.toggleAttribute('data-wa-radio-vertical', this.orientation === 'vertical');
radio.toggleAttribute('data-wa-radio-first', index === 0);
radio.toggleAttribute('data-wa-radio-inner', index !== 0 && index !== radios.length - 1);
radio.toggleAttribute('data-wa-radio-last', index === radios.length - 1);
});
await Promise.all( await Promise.all(
// Sync the checked state and size // Sync the checked state and size
radios.map(async radio => { radios.map(async radio => {
@@ -196,10 +202,8 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
}), }),
); );
this.hasButtonGroup = radios.some(radio => radio.tagName.toLowerCase() === 'wa-radio-button');
if (radios.length > 0 && !radios.some(radio => radio.checked)) { if (radios.length > 0 && !radios.some(radio => radio.checked)) {
if (this.hasButtonGroup) { if (this.hasRadioButtons) {
const buttonRadio = radios[0].shadowRoot?.querySelector('button'); const buttonRadio = radios[0].shadowRoot?.querySelector('button');
if (buttonRadio) { if (buttonRadio) {
@@ -210,7 +214,7 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
} }
} }
if (this.hasButtonGroup) { if (this.hasRadioButtons) {
const buttonGroup = this.shadowRoot?.querySelector('wa-button-group'); const buttonGroup = this.shadowRoot?.querySelector('wa-button-group');
if (buttonGroup) { if (buttonGroup) {
@@ -276,12 +280,12 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
index = 0; index = 0;
} }
const hasButtonGroup = radios.some(radio => radio.tagName.toLowerCase() === 'wa-radio-button'); const hasRadioButtons = radios.some(radio => radio.tagName.toLowerCase() === 'wa-radio-button');
this.getAllRadios().forEach(radio => { this.getAllRadios().forEach(radio => {
radio.checked = false; radio.checked = false;
if (!hasButtonGroup) { if (!hasRadioButtons) {
radio.setAttribute('tabindex', '-1'); radio.setAttribute('tabindex', '-1');
} }
}); });
@@ -289,7 +293,7 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
this.value = radios[index].value; this.value = radios[index].value;
radios[index].checked = true; radios[index].checked = true;
if (!hasButtonGroup) { if (!hasRadioButtons) {
radios[index].setAttribute('tabindex', '0'); radios[index].setAttribute('tabindex', '0');
radios[index].focus(); radios[index].focus();
} else { } else {
@@ -351,7 +355,7 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
<slot <slot
part="form-control-input" part="form-control-input"
class=${classMap({ class=${classMap({
'wa-button-group': this.hasButtonGroup, 'wa-button-group': this.hasRadioButtons,
'wa-button-group-vertical': this.orientation === 'vertical', 'wa-button-group-vertical': this.orientation === 'vertical',
})} })}
@slotchange=${this.syncRadioElements} @slotchange=${this.syncRadioElements}

View File

@@ -23,6 +23,10 @@
opacity: 0; opacity: 0;
} }
[part~='label'] {
display: inline;
}
[part~='hint'] { [part~='hint'] {
grid-column: 2; grid-column: 2;
margin-block-start: var(--wa-space-3xs); margin-block-start: var(--wa-space-3xs);

View File

@@ -208,13 +208,13 @@
&::slotted(wa-divider) { &::slotted(wa-divider) {
--spacing: var(--wa-space-xs); --spacing: var(--wa-space-xs);
} }
}
&::slotted(small) {
display: block; slot:not([name])::slotted(small) {
font-size: var(--wa-font-size-s); display: block;
font-weight: var(--wa-font-weight-semibold); font-size: var(--wa-font-size-s);
color: var(--wa-color-text-quiet); font-weight: var(--wa-font-weight-semibold);
padding-block: var(--wa-space-xs); color: var(--wa-color-text-quiet);
padding-inline: var(--wa-space-xl); padding-block: var(--wa-space-xs);
} padding-inline: var(--wa-space-xl);
} }

View File

@@ -647,6 +647,7 @@ describe('<wa-select>', () => {
); );
const el = form.querySelector<WaSelect>('wa-select')!; const el = form.querySelector<WaSelect>('wa-select')!;
expect(el.defaultValue).to.equal('option-1');
expect(el.value).to.equal(''); expect(el.value).to.equal('');
expect(new FormData(form).get('select')).equal(''); expect(new FormData(form).get('select')).equal('');
@@ -657,6 +658,7 @@ describe('<wa-select>', () => {
await aTimeout(10); await aTimeout(10);
await el.updateComplete; await el.updateComplete;
expect(el.optionValues ? [...el.optionValues] : []).to.have.members(['option-1']);
expect(el.value).to.equal('option-1'); expect(el.value).to.equal('option-1');
expect(new FormData(form).get('select')).equal('option-1'); expect(new FormData(form).get('select')).equal('option-1');
}); });
@@ -745,6 +747,8 @@ describe('<wa-select>', () => {
); );
const el = form.querySelector<WaSelect>('wa-select')!; const el = form.querySelector<WaSelect>('wa-select')!;
expect(el.optionValues ? [...el.optionValues] : []).to.have.members(['bar', 'baz']);
expect(el.optionValues?.size).to.equal(2);
expect(el.value).to.have.members(['bar', 'baz']); expect(el.value).to.have.members(['bar', 'baz']);
expect(el.value!.length).to.equal(2); expect(el.value!.length).to.equal(2);
expect(new FormData(form).getAll('select')).to.have.members(['bar', 'baz']); expect(new FormData(form).getAll('select')).to.have.members(['bar', 'baz']);
@@ -760,6 +764,36 @@ describe('<wa-select>', () => {
expect(new FormData(form).getAll('select')).to.have.members(['foo', 'bar', 'baz']); expect(new FormData(form).getAll('select')).to.have.members(['foo', 'bar', 'baz']);
}); });
}); });
describe('With setting the value via JS', () => {
it('Should preserve value even if not returned', async () => {
const form = await fixture<HTMLFormElement>(
html` <form>
<wa-select name="select">
<wa-option value="bar">Bar</wa-option>
<wa-option value="baz">Baz</wa-option>
</wa-select>
</form>`,
);
const el = form.querySelector<WaSelect>('wa-select')!;
expect(el.value).to.equal('');
el.value = 'foo';
await aTimeout(10);
await el.updateComplete;
expect(el.value).to.equal('');
const option = document.createElement('wa-option');
option.value = 'foo';
option.innerText = 'Foo';
el.append(option);
await aTimeout(10);
await el.updateComplete;
expect(el.value).to.equal('foo');
});
});
}); });
}); });
} }

View File

@@ -22,6 +22,7 @@ import appearanceStyles from '../../styles/utilities/appearance.css';
import sizeStyles from '../../styles/utilities/size.css'; import sizeStyles from '../../styles/utilities/size.css';
import { LocalizeController } from '../../utilities/localize.js'; import { LocalizeController } from '../../utilities/localize.js';
import '../icon/icon.js'; import '../icon/icon.js';
import '../option/option.js';
import type WaOption from '../option/option.js'; import type WaOption from '../option/option.js';
import '../popup/popup.js'; import '../popup/popup.js';
import type WaPopup from '../popup/popup.js'; import type WaPopup from '../popup/popup.js';
@@ -37,6 +38,7 @@ import styles from './select.css';
* @dependency wa-icon * @dependency wa-icon
* @dependency wa-popup * @dependency wa-popup
* @dependency wa-tag * @dependency wa-tag
* @dependency wa-option
* *
* @slot - The listbox options. Must be `<wa-option>` elements. You can use `<wa-divider>` to group items visually. * @slot - The listbox options. Must be `<wa-option>` elements. You can use `<wa-divider>` to group items visually.
* @slot label - The input's label. Alternatively, you can use the `label` attribute. * @slot label - The input's label. Alternatively, you can use the `label` attribute.
@@ -102,7 +104,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
private readonly localize = new LocalizeController(this); private readonly localize = new LocalizeController(this);
private typeToSelectString = ''; private typeToSelectString = '';
private typeToSelectTimeout: number; private typeToSelectTimeout: number;
private closeWatcher: CloseWatcher | null;
@query('.select') popup: WaPopup; @query('.select') popup: WaPopup;
@query('.combobox') combobox: HTMLSlotElement; @query('.combobox') combobox: HTMLSlotElement;
@@ -118,6 +119,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
@state() displayLabel = ''; @state() displayLabel = '';
@state() currentOption: WaOption; @state() currentOption: WaOption;
@state() selectedOptions: WaOption[] = []; @state() selectedOptions: WaOption[] = [];
@state() optionValues: Set<string> | undefined;
/** The name of the select, submitted as a name/value pair with form data. */ /** The name of the select, submitted as a name/value pair with form data. */
@property() name = ''; @property() name = '';
@@ -158,7 +160,47 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
return val; return val;
} }
@property({ attribute: false }) value: string | string[] | null = null; private _value: string[] | undefined;
@property({ attribute: false })
set value(val: string | string[]) {
let oldValue = this.value;
if (!Array.isArray(val)) {
val = val.split(' ');
}
if (!this._value || this._value.join(' ') !== val.join(' ')) {
this._value = val;
let newValue = this.value;
if (newValue != oldValue) {
this.requestUpdate('value', oldValue);
}
}
}
get value() {
let value = this._value ?? this.defaultValue;
value = Array.isArray(value) ? value : [value];
let optionsChanged = !this.optionValues;
if (optionsChanged) {
this.optionValues = new Set(
this.getAllOptions()
.filter(option => !option.disabled)
.map(option => option.value),
);
}
// Drop values not in the DOM
let ret: string | string[] = value.filter(v => this.optionValues!.has(v));
ret = this.multiple ? ret : (ret[0] ?? '');
if (optionsChanged) {
this.requestUpdate('value');
}
return ret;
}
/** The select's size. */ /** The select's size. */
@property({ reflect: true, initial: 'medium' }) size: 'small' | 'medium' | 'large' | 'inherit' = 'inherit'; @property({ reflect: true, initial: 'medium' }) size: 'small' | 'medium' | 'large' | 'inherit' = 'inherit';
@@ -250,7 +292,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
?pill=${this.pill} ?pill=${this.pill}
size=${this.size} size=${this.size}
removable removable
@wa-remove=${(event: WaRemoveEvent) => this.handleTagRemove(event, option)}
> >
${option.label} ${option.label}
</wa-tag> </wa-tag>
@@ -280,17 +321,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
if (this.getRootNode() !== document) { if (this.getRootNode() !== document) {
this.getRootNode().addEventListener('focusin', this.handleDocumentFocusIn); 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() { private removeOpenListeners() {
@@ -301,8 +331,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
if (this.getRootNode() !== document) { if (this.getRootNode() !== document) {
this.getRootNode().removeEventListener('focusin', this.handleDocumentFocusIn); this.getRootNode().removeEventListener('focusin', this.handleDocumentFocusIn);
} }
this.closeWatcher?.destroy();
} }
private handleFocus() { private handleFocus() {
@@ -538,21 +566,41 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
} }
const allOptions = this.getAllOptions(); const allOptions = this.getAllOptions();
const val = this.valueHasChanged ? this.value : this.defaultValue; this.optionValues = undefined; // dirty the value so it gets recalculated
const value = Array.isArray(val) ? val : [val];
const values: string[] = [];
// Check for duplicate values in menu items const value = this.value;
allOptions.forEach(option => values.push(option.value));
// Select only the options that match the new value // Select only the options that match the new value
this.setSelectedOptions(allOptions.filter(el => value.includes(el.value))); this.setSelectedOptions(allOptions.filter(el => value.includes(el.value)));
} }
private handleTagRemove(event: WaRemoveEvent, option: WaOption) { private handleTagRemove(event: WaRemoveEvent, directOption?: WaOption) {
event.stopPropagation(); event.stopPropagation();
if (!this.disabled) { if (this.disabled) return;
// Use the directly provided option if available (from getTag method)
let option = directOption;
// If no direct option was provided, find the option from the event path
if (!option) {
const tagElement = (event.target as Element).closest('wa-tag[part~=tag]');
if (tagElement) {
// Find the index of this tag among all tags
const tagsContainer = this.shadowRoot?.querySelector('[part="tags"]');
if (tagsContainer) {
const allTags = Array.from(tagsContainer.children);
const index = allTags.indexOf(tagElement as HTMLElement);
if (index >= 0 && index < this.selectedOptions.length) {
option = this.selectedOptions[index];
}
}
}
}
if (option) {
this.toggleOptionSelection(option, false); this.toggleOptionSelection(option, false);
// Emit after updating // Emit after updating
@@ -565,6 +613,9 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
// Gets an array of all `<wa-option>` elements // Gets an array of all `<wa-option>` elements
private getAllOptions() { private getAllOptions() {
if (!this?.querySelectorAll) {
return [];
}
return [...this.querySelectorAll<WaOption>('wa-option')]; return [...this.querySelectorAll<WaOption>('wa-option')];
} }
@@ -621,18 +672,31 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
this.selectionChanged(); this.selectionChanged();
} }
// This method must be called whenever the selection changes. It will update the selected options cache, the current // @internal This method must be called whenever the selection changes. It will update the selected options cache, the
// value, and the display value // current value, and the display value. The option component uses it internally to update labels as they change.
private selectionChanged() { public selectionChanged() {
const options = this.getAllOptions(); const options = this.getAllOptions();
// Update selected options cache // Update selected options cache
this.selectedOptions = options.filter(el => el.selected); this.selectedOptions = options.filter(el => el.selected);
let selectedValues = new Set(this.selectedOptions.map(el => el.value));
// Toggle values present in the DOM from this.value, while preserving options NOT present in the DOM (for lazy loading)
// Note that options NOT present in the DOM will be moved to the end after this
if (selectedValues.size > 0 || this._value) {
if (!this._value) {
// First time it's set
let value = this.defaultValue ?? [];
this._value = Array.isArray(value) ? value : [value];
}
// Filter out values that are in the DOM
this._value = this._value.filter(value => !this.optionValues?.has(value));
this._value.unshift(...selectedValues);
}
// Update the value and display label // Update the value and display label
if (this.multiple) { if (this.multiple) {
this.value = this.selectedOptions.map(el => el.value);
if (this.placeholder && this.value.length === 0) { if (this.placeholder && this.value.length === 0) {
// When no items are selected, keep the value empty so the placeholder shows // When no items are selected, keep the value empty so the placeholder shows
this.displayLabel = ''; this.displayLabel = '';
@@ -641,7 +705,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
} }
} else { } else {
const selectedOption = this.selectedOptions[0]; const selectedOption = this.selectedOptions[0];
this.value = selectedOption?.value ?? '';
this.displayLabel = selectedOption?.label ?? ''; this.displayLabel = selectedOption?.label ?? '';
} }
@@ -650,14 +713,13 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
this.updateValidity(); this.updateValidity();
}); });
} }
protected get tags() { protected get tags() {
return this.selectedOptions.map((option, index) => { return this.selectedOptions.map((option, index) => {
if (index < this.maxOptionsVisible || this.maxOptionsVisible <= 0) { if (index < this.maxOptionsVisible || this.maxOptionsVisible <= 0) {
const tag = this.getTag(option, index); const tag = this.getTag(option, index);
// Wrap so we can handle the remove if (!tag) return null;
return html`<div @wa-remove=${(e: WaRemoveEvent) => this.handleTagRemove(e, option)}> return typeof tag === 'string' ? unsafeHTML(tag) : tag;
${typeof tag === 'string' ? unsafeHTML(tag) : tag}
</div>`;
} else if (index === this.maxOptionsVisible) { } else if (index === this.maxOptionsVisible) {
// Hit tag limit // Hit tag limit
return html` return html`
@@ -673,7 +735,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
> >
`; `;
} }
return html``; return null;
}); });
} }
@@ -873,7 +935,9 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
/> />
<!-- Tags need to wait for first hydration before populating otherwise it will create a hydration mismatch. --> <!-- Tags need to wait for first hydration before populating otherwise it will create a hydration mismatch. -->
${this.multiple && this.hasUpdated ? html`<div part="tags" class="tags">${this.tags}</div>` : ''} ${this.multiple && this.hasUpdated
? html`<div part="tags" class="tags" @wa-remove=${this.handleTagRemove}>${this.tags}</div>`
: ''}
<input <input
class="value-input" class="value-input"

View File

@@ -49,6 +49,7 @@ import styles from './switch.css';
*/ */
@customElement('wa-switch') @customElement('wa-switch')
export default class WaSwitch extends WebAwesomeFormAssociatedElement { export default class WaSwitch extends WebAwesomeFormAssociatedElement {
static shadowRootOptions = { ...WebAwesomeFormAssociatedElement.shadowRootOptions, delegatesFocus: true };
static shadowStyle = [formControlStyles, sizeStyles, styles]; static shadowStyle = [formControlStyles, sizeStyles, styles];
static get validators() { static get validators() {

View File

@@ -15,6 +15,14 @@
-webkit-user-select: none; -webkit-user-select: none;
cursor: pointer; cursor: pointer;
transition: color var(--wa-transition-fast) var(--wa-transition-easing); transition: color var(--wa-transition-fast) var(--wa-transition-easing);
::slotted(wa-icon:first-child) {
margin-inline-end: var(--wa-space-xs);
}
::slotted(wa-icon:last-child) {
margin-inline-start: var(--wa-space-xs);
}
} }
:host(:hover:not([disabled])) .tab { :host(:hover:not([disabled])) .tab {

View File

@@ -4,10 +4,16 @@
--max-width: 30ch; --max-width: 30ch;
--padding: var(--wa-space-2xs) var(--wa-space-xs); --padding: var(--wa-space-2xs) var(--wa-space-xs);
/** These styles are added so we don't interfere in the DOM. */
display: inline-block; display: inline-block;
position: absolute; position: absolute;
/** These styles are added so we dont interfere in the DOM. */ /** Defaults for inherited CSS properties */
color: var(--wa-tooltip-content-color);
font-size: var(--wa-tooltip-font-size);
line-height: var(--wa-tooltip-line-height);
text-align: start;
white-space: normal;
} }
.tooltip { .tooltip {
@@ -41,12 +47,6 @@
max-width: var(--max-width); max-width: var(--max-width);
border-radius: var(--border-radius); border-radius: var(--border-radius);
background-color: var(--background-color); background-color: var(--background-color);
font: inherit;
color: var(--wa-tooltip-content-color);
font-size: var(--wa-tooltip-font-size);
line-height: var(--wa-tooltip-line-height);
text-align: start;
white-space: normal;
padding: var(--padding); padding: var(--padding);
user-select: none; user-select: none;
-webkit-user-select: none; -webkit-user-select: none;

View File

@@ -35,11 +35,8 @@ import styles from './tooltip.css';
* *
* @cssproperty --background-color - The tooltip's background color. * @cssproperty --background-color - The tooltip's background color.
* @cssproperty --border-radius - The radius of the tooltip's corners. * @cssproperty --border-radius - The radius of the tooltip's corners.
* @cssproperty --text-color - The color of the tooltip's content.
* @cssproperty --max-width - The maximum width of the tooltip before its content will wrap. * @cssproperty --max-width - The maximum width of the tooltip before its content will wrap.
* @cssproperty --padding - The padding within the tooltip. * @cssproperty --padding - The padding within the tooltip.
* @cssproperty --hide-delay - The amount of time to wait before hiding the tooltip when hovering.
* @cssproperty --show-delay - The amount of time to wait before showing the tooltip when hovering.
*/ */
@customElement('wa-tooltip') @customElement('wa-tooltip')
export default class WaTooltip extends WebAwesomeElement { export default class WaTooltip extends WebAwesomeElement {
@@ -47,7 +44,6 @@ export default class WaTooltip extends WebAwesomeElement {
static dependencies = { 'wa-popup': WaPopup }; static dependencies = { 'wa-popup': WaPopup };
private hoverTimeout: number; private hoverTimeout: number;
private closeWatcher: CloseWatcher | null;
@query('slot:not([name])') defaultSlot: HTMLSlotElement; @query('slot:not([name])') defaultSlot: HTMLSlotElement;
@query('.body') body: HTMLElement; @query('.body') body: HTMLElement;
@@ -130,7 +126,6 @@ export default class WaTooltip extends WebAwesomeElement {
super.disconnectedCallback(); super.disconnectedCallback();
// Cleanup this event in case the tooltip is removed while open // Cleanup this event in case the tooltip is removed while open
this.closeWatcher?.destroy();
document.removeEventListener('keydown', this.handleDocumentKeyDown); document.removeEventListener('keydown', this.handleDocumentKeyDown);
this.eventController.abort(); this.eventController.abort();
@@ -215,15 +210,7 @@ export default class WaTooltip extends WebAwesomeElement {
return; return;
} }
if ('CloseWatcher' in window) { document.addEventListener('keydown', this.handleDocumentKeyDown, { signal: this.eventController.signal });
this.closeWatcher?.destroy();
this.closeWatcher = new CloseWatcher();
this.closeWatcher.onclose = () => {
this.hide();
};
} else {
document.addEventListener('keydown', this.handleDocumentKeyDown, { signal: this.eventController.signal });
}
this.body.hidden = false; this.body.hidden = false;
this.popup.active = true; this.popup.active = true;
@@ -240,7 +227,6 @@ export default class WaTooltip extends WebAwesomeElement {
return; return;
} }
this.closeWatcher?.destroy();
document.removeEventListener('keydown', this.handleDocumentKeyDown); document.removeEventListener('keydown', this.handleDocumentKeyDown);
await animateWithClass(this.popup.popup, 'hide-with-scale'); await animateWithClass(this.popup.popup, 'hide-with-scale');

View File

@@ -204,6 +204,32 @@ export default class WebAwesomeElement extends LitElement {
); );
} }
getBoundingClientRect(): DOMRect {
let rect = super.getBoundingClientRect();
if (rect.width === 0 && rect.height === 0) {
let Self = this.constructor as typeof WebAwesomeElement;
if (Self.rectProxy) {
let element = this[Self.rectProxy as keyof this];
if (element instanceof Element) {
let childRect = element.getBoundingClientRect();
if (childRect.width > 0 || childRect.height > 0) {
return childRect;
}
}
}
}
return rect;
}
/**
* If getBoundingClientRect() returns an empty rect,
* should we check another element?
*/
static rectProxy: undefined | string;
static createProperty(name: PropertyKey, options?: PropertyDeclaration): void { static createProperty(name: PropertyKey, options?: PropertyDeclaration): void {
if (options) { if (options) {
if (options.initial !== undefined && options.default === undefined) { if (options.initial !== undefined && options.default === undefined) {

View File

@@ -1,80 +0,0 @@
/* DO NOT EDIT THIS FILE. It is generated from "base.css.njk" */
:where(:root),
:host,
:where([class^='wa-theme-'], [class*=' wa-theme-']),
:where([class^='wa-palette-'], [class*=' wa-palette-']),
:where([class^='wa-brand-'], [class*=' wa-brand-']) {
/**
* Conditional tokens for use in color-mix()
* --wa-color-brand-if-lt-N ➡️ 100% if key < N, 0% otherwise
* --wa-color-brand-if-gte-N ➡️ 100% if key >= N, 0% otherwise
*/
--wa-color-brand-if-lt-40: calc(clamp(0, 40 - var(--wa-color-brand-key), 1) * 100%);
--wa-color-brand-if-gte-40: calc(100% - var(--wa-color-brand-if-lt-40));
--wa-color-brand-if-lt-50: calc(clamp(0, 50 - var(--wa-color-brand-key), 1) * 100%);
--wa-color-brand-if-gte-50: calc(100% - var(--wa-color-brand-if-lt-50));
--wa-color-brand-if-lt-60: calc(clamp(0, 60 - var(--wa-color-brand-key), 1) * 100%);
--wa-color-brand-if-gte-60: calc(100% - var(--wa-color-brand-if-lt-60));
--wa-color-brand-if-lt-70: calc(clamp(0, 70 - var(--wa-color-brand-key), 1) * 100%);
--wa-color-brand-if-gte-70: calc(100% - var(--wa-color-brand-if-lt-70));
--wa-color-brand-if-lt-80: calc(clamp(0, 80 - var(--wa-color-brand-key), 1) * 100%);
--wa-color-brand-if-gte-80: calc(100% - var(--wa-color-brand-if-lt-80));
/*
* Convenience tokens for common tint cutoffs
* --wa-color-brand-N-max ➡️ var(--color-brand) if key <= N, var(--color-brand-N) otherwise
* --wa-color-brand-N-min ➡️ var(--color-brand) if key >= N, var(--color-brand-N) otherwise
*/
--wa-color-brand-40-max: color-mix(
in oklab,
var(--wa-color-brand) var(--wa-color-brand-if-lt-40),
var(--wa-color-brand-40)
);
--wa-color-brand-40-min: color-mix(
in oklab,
var(--wa-color-brand) var(--wa-color-brand-if-gte-40),
var(--wa-color-brand-40)
);
--wa-color-brand-50-max: color-mix(
in oklab,
var(--wa-color-brand) var(--wa-color-brand-if-lt-50),
var(--wa-color-brand-50)
);
--wa-color-brand-50-min: color-mix(
in oklab,
var(--wa-color-brand) var(--wa-color-brand-if-gte-50),
var(--wa-color-brand-50)
);
--wa-color-brand-60-max: color-mix(
in oklab,
var(--wa-color-brand) var(--wa-color-brand-if-lt-60),
var(--wa-color-brand-60)
);
--wa-color-brand-60-min: color-mix(
in oklab,
var(--wa-color-brand) var(--wa-color-brand-if-gte-60),
var(--wa-color-brand-60)
);
--wa-color-brand-70-max: color-mix(
in oklab,
var(--wa-color-brand) var(--wa-color-brand-if-lt-70),
var(--wa-color-brand-70)
);
--wa-color-brand-70-min: color-mix(
in oklab,
var(--wa-color-brand) var(--wa-color-brand-if-gte-70),
var(--wa-color-brand-70)
);
/* Text color: white if key < 60, brand-10 otherwise */
--wa-color-brand-on: color-mix(in oklab, var(--wa-color-brand-10) var(--wa-color-brand-if-gte-60), white);
}

View File

@@ -1,36 +0,0 @@
:where(:root),
:host,
:where([class^='wa-theme-'], [class*=' wa-theme-']),
:where([class^='wa-palette-'], [class*=' wa-palette-']),
:where([class^='wa-brand-'], [class*=' wa-brand-']) {
/**
* Conditional tokens for use in color-mix()
* --wa-color-brand-if-lt-N ➡️ 100% if key < N, 0% otherwise
* --wa-color-brand-if-gte-N ➡️ 100% if key >= N, 0% otherwise
*/
{% for tint in ['40', '50', '60', '70', '80'] %}
--wa-color-brand-if-lt-{{ tint }}: calc(clamp(0, {{ tint }} - var(--wa-color-brand-key), 1) * 100%);
--wa-color-brand-if-gte-{{ tint }}: calc(100% - var(--wa-color-brand-if-lt-{{ tint }}));
{% endfor %}
/*
* Convenience tokens for common tint cutoffs
* --wa-color-brand-N-max ➡️ var(--color-brand) if key <= N, var(--color-brand-N) otherwise
* --wa-color-brand-N-min ➡️ var(--color-brand) if key >= N, var(--color-brand-N) otherwise
*/
{% for tint in ['40', '50', '60', '70'] %}
--wa-color-brand-{{ tint }}-max: color-mix(
in oklab,
var(--wa-color-brand) var(--wa-color-brand-if-lt-{{ tint }}),
var(--wa-color-brand-{{ tint }})
);
--wa-color-brand-{{ tint }}-min: color-mix(
in oklab,
var(--wa-color-brand) var(--wa-color-brand-if-gte-{{ tint }}),
var(--wa-color-brand-{{ tint }})
);
{% endfor %}
/* Text color: white if key < 60, brand-10 otherwise */
--wa-color-brand-on: color-mix(in oklab, var(--wa-color-brand-10) var(--wa-color-brand-if-gte-60), white);
}

View File

@@ -1,6 +1,3 @@
/* DO NOT EDIT THIS FILE. It is generated from "{{ hue }}.css.njk" */
@import url('base.css');
:where(:root), :where(:root),
:host, :host,
:where([class^='wa-theme-'], [class*=' wa-theme-']), :where([class^='wa-theme-'], [class*=' wa-theme-']),
@@ -18,5 +15,22 @@
--wa-color-brand-10: var(--wa-color-blue-10); --wa-color-brand-10: var(--wa-color-blue-10);
--wa-color-brand-05: var(--wa-color-blue-05); --wa-color-brand-05: var(--wa-color-blue-05);
--wa-color-brand: var(--wa-color-blue); --wa-color-brand: var(--wa-color-blue);
--wa-color-brand-key: var(--wa-color-blue-key);
--wa-color-brand-lt-50: var(--wa-color-blue-lt-50);
--wa-color-brand-gte-50: var(--wa-color-blue-gte-50);
--wa-color-brand-lt-60: var(--wa-color-blue-lt-60);
--wa-color-brand-gte-60: var(--wa-color-blue-gte-60);
--wa-color-brand-lt-70: var(--wa-color-blue-lt-70);
--wa-color-brand-gte-70: var(--wa-color-blue-gte-70);
--wa-color-brand-max-50: var(--wa-color-blue-max-50);
--wa-color-brand-min-50: var(--wa-color-blue-min-50);
--wa-color-brand-max-60: var(--wa-color-blue-max-60);
--wa-color-brand-min-60: var(--wa-color-blue-min-60);
--wa-color-brand-max-70: var(--wa-color-blue-max-70);
--wa-color-brand-min-70: var(--wa-color-blue-min-70);
--wa-color-brand-on: var(--wa-color-blue-on);
} }

View File

@@ -1,6 +1,3 @@
/* DO NOT EDIT THIS FILE. It is generated from "{{ hue }}.css.njk" */
@import url('base.css');
:where(:root), :where(:root),
:host, :host,
:where([class^='wa-theme-'], [class*=' wa-theme-']), :where([class^='wa-theme-'], [class*=' wa-theme-']),
@@ -18,5 +15,22 @@
--wa-color-brand-10: var(--wa-color-cyan-10); --wa-color-brand-10: var(--wa-color-cyan-10);
--wa-color-brand-05: var(--wa-color-cyan-05); --wa-color-brand-05: var(--wa-color-cyan-05);
--wa-color-brand: var(--wa-color-cyan); --wa-color-brand: var(--wa-color-cyan);
--wa-color-brand-key: var(--wa-color-cyan-key);
--wa-color-brand-lt-50: var(--wa-color-cyan-lt-50);
--wa-color-brand-gte-50: var(--wa-color-cyan-gte-50);
--wa-color-brand-lt-60: var(--wa-color-cyan-lt-60);
--wa-color-brand-gte-60: var(--wa-color-cyan-gte-60);
--wa-color-brand-lt-70: var(--wa-color-cyan-lt-70);
--wa-color-brand-gte-70: var(--wa-color-cyan-gte-70);
--wa-color-brand-max-50: var(--wa-color-cyan-max-50);
--wa-color-brand-min-50: var(--wa-color-cyan-min-50);
--wa-color-brand-max-60: var(--wa-color-cyan-max-60);
--wa-color-brand-min-60: var(--wa-color-cyan-min-60);
--wa-color-brand-max-70: var(--wa-color-cyan-max-70);
--wa-color-brand-min-70: var(--wa-color-cyan-min-70);
--wa-color-brand-on: var(--wa-color-cyan-on);
} }

View File

@@ -1,6 +1,3 @@
/* DO NOT EDIT THIS FILE. It is generated from "{{ hue }}.css.njk" */
@import url('base.css');
:where(:root), :where(:root),
:host, :host,
:where([class^='wa-theme-'], [class*=' wa-theme-']), :where([class^='wa-theme-'], [class*=' wa-theme-']),
@@ -18,5 +15,22 @@
--wa-color-brand-10: var(--wa-color-gray-10); --wa-color-brand-10: var(--wa-color-gray-10);
--wa-color-brand-05: var(--wa-color-gray-05); --wa-color-brand-05: var(--wa-color-gray-05);
--wa-color-brand: var(--wa-color-gray); --wa-color-brand: var(--wa-color-gray);
--wa-color-brand-key: var(--wa-color-gray-key);
--wa-color-brand-lt-50: var(--wa-color-gray-lt-50);
--wa-color-brand-gte-50: var(--wa-color-gray-gte-50);
--wa-color-brand-lt-60: var(--wa-color-gray-lt-60);
--wa-color-brand-gte-60: var(--wa-color-gray-gte-60);
--wa-color-brand-lt-70: var(--wa-color-gray-lt-70);
--wa-color-brand-gte-70: var(--wa-color-gray-gte-70);
--wa-color-brand-max-50: var(--wa-color-gray-max-50);
--wa-color-brand-min-50: var(--wa-color-gray-min-50);
--wa-color-brand-max-60: var(--wa-color-gray-max-60);
--wa-color-brand-min-60: var(--wa-color-gray-min-60);
--wa-color-brand-max-70: var(--wa-color-gray-max-70);
--wa-color-brand-min-70: var(--wa-color-gray-min-70);
--wa-color-brand-on: var(--wa-color-gray-on);
} }

View File

@@ -1,6 +1,3 @@
/* DO NOT EDIT THIS FILE. It is generated from "{{ hue }}.css.njk" */
@import url('base.css');
:where(:root), :where(:root),
:host, :host,
:where([class^='wa-theme-'], [class*=' wa-theme-']), :where([class^='wa-theme-'], [class*=' wa-theme-']),
@@ -18,5 +15,22 @@
--wa-color-brand-10: var(--wa-color-green-10); --wa-color-brand-10: var(--wa-color-green-10);
--wa-color-brand-05: var(--wa-color-green-05); --wa-color-brand-05: var(--wa-color-green-05);
--wa-color-brand: var(--wa-color-green); --wa-color-brand: var(--wa-color-green);
--wa-color-brand-key: var(--wa-color-green-key);
--wa-color-brand-lt-50: var(--wa-color-green-lt-50);
--wa-color-brand-gte-50: var(--wa-color-green-gte-50);
--wa-color-brand-lt-60: var(--wa-color-green-lt-60);
--wa-color-brand-gte-60: var(--wa-color-green-gte-60);
--wa-color-brand-lt-70: var(--wa-color-green-lt-70);
--wa-color-brand-gte-70: var(--wa-color-green-gte-70);
--wa-color-brand-max-50: var(--wa-color-green-max-50);
--wa-color-brand-min-50: var(--wa-color-green-min-50);
--wa-color-brand-max-60: var(--wa-color-green-max-60);
--wa-color-brand-min-60: var(--wa-color-green-min-60);
--wa-color-brand-max-70: var(--wa-color-green-max-70);
--wa-color-brand-min-70: var(--wa-color-green-min-70);
--wa-color-brand-on: var(--wa-color-green-on);
} }

View File

@@ -1,6 +1,3 @@
/* DO NOT EDIT THIS FILE. It is generated from "{{ hue }}.css.njk" */
@import url('base.css');
:where(:root), :where(:root),
:host, :host,
:where([class^='wa-theme-'], [class*=' wa-theme-']), :where([class^='wa-theme-'], [class*=' wa-theme-']),
@@ -18,5 +15,22 @@
--wa-color-brand-10: var(--wa-color-indigo-10); --wa-color-brand-10: var(--wa-color-indigo-10);
--wa-color-brand-05: var(--wa-color-indigo-05); --wa-color-brand-05: var(--wa-color-indigo-05);
--wa-color-brand: var(--wa-color-indigo); --wa-color-brand: var(--wa-color-indigo);
--wa-color-brand-key: var(--wa-color-indigo-key);
--wa-color-brand-lt-50: var(--wa-color-indigo-lt-50);
--wa-color-brand-gte-50: var(--wa-color-indigo-gte-50);
--wa-color-brand-lt-60: var(--wa-color-indigo-lt-60);
--wa-color-brand-gte-60: var(--wa-color-indigo-gte-60);
--wa-color-brand-lt-70: var(--wa-color-indigo-lt-70);
--wa-color-brand-gte-70: var(--wa-color-indigo-gte-70);
--wa-color-brand-max-50: var(--wa-color-indigo-max-50);
--wa-color-brand-min-50: var(--wa-color-indigo-min-50);
--wa-color-brand-max-60: var(--wa-color-indigo-max-60);
--wa-color-brand-min-60: var(--wa-color-indigo-min-60);
--wa-color-brand-max-70: var(--wa-color-indigo-max-70);
--wa-color-brand-min-70: var(--wa-color-indigo-min-70);
--wa-color-brand-on: var(--wa-color-indigo-on);
} }

View File

@@ -1,5 +1,3 @@
@import url('base.css');
:where(:root), :where(:root),
:host, :host,
:where([class^='wa-theme-'], [class*=' wa-theme-']), :where([class^='wa-theme-'], [class*=' wa-theme-']),
@@ -17,5 +15,22 @@
--wa-color-brand-10: var(--wa-color-orange-10); --wa-color-brand-10: var(--wa-color-orange-10);
--wa-color-brand-05: var(--wa-color-orange-05); --wa-color-brand-05: var(--wa-color-orange-05);
--wa-color-brand: var(--wa-color-orange); --wa-color-brand: var(--wa-color-orange);
--wa-color-brand-key: var(--wa-color-orange-key);
--wa-color-brand-lt-50: var(--wa-color-orange-lt-50);
--wa-color-brand-gte-50: var(--wa-color-orange-gte-50);
--wa-color-brand-lt-60: var(--wa-color-orange-lt-60);
--wa-color-brand-gte-60: var(--wa-color-orange-gte-60);
--wa-color-brand-lt-70: var(--wa-color-orange-lt-70);
--wa-color-brand-gte-70: var(--wa-color-orange-gte-70);
--wa-color-brand-max-50: var(--wa-color-orange-max-50);
--wa-color-brand-min-50: var(--wa-color-orange-min-50);
--wa-color-brand-max-60: var(--wa-color-orange-max-60);
--wa-color-brand-min-60: var(--wa-color-orange-min-60);
--wa-color-brand-max-70: var(--wa-color-orange-max-70);
--wa-color-brand-min-70: var(--wa-color-orange-min-70);
--wa-color-brand-on: var(--wa-color-orange-on);
} }

Some files were not shown because too many files have changed in this diff Show More