Compare commits

...

63 Commits

Author SHA1 Message Date
Lea Verou
6dc81f101c Update webawesome-element.ts 2025-04-11 02:17:59 -05:00
Lea Verou
bd41693fc5 Delete test.md 2025-04-11 02:13:41 -05:00
Lea Verou
7270afe64f Update icon.md 2025-04-11 02:02:49 -05:00
Lea Verou
672c1ff9d0 Update webawesome-element.ts 2025-04-11 02:02:11 -05:00
Lea Verou
df0dcba85f Refactor logic, update to use style-observer to monitor changes dynamically 2025-04-11 01:49:21 -05:00
Lea Verou
43a9205961 Merge branch 'next' into css-attribute-properties 2025-04-09 12:41:43 -05:00
Konnor Rogers
deb9fd70b3 fix pass through copying (#860)
* fix pass through copying

* prettier
2025-04-04 13:46:41 -04:00
Lea Verou
ff3b3d6558 Remove current class from existing sidebar link before adding it to new one 2025-04-02 14:16:03 -04:00
Lea Verou
6b3edb8a56 Tiny fix in saving mixin 2025-04-02 14:16:03 -04:00
Lea Verou
6162b8b115 Move v-content directive to separate file 2025-04-02 11:51:12 -04:00
Lea Verou
cff752b600 Move CRUD logic from palette app to Vue mixin 2025-04-02 11:51:12 -04:00
Lea Verou
7892a94b9b Rewrite and generalize CRUD logic for customizable entities (palettes, themes) (#854)
* Generalize CRUD logic to more easily support themes (and other types of entities)
* Decouple data structures managing saved entities (palettes, themes), sidebar update logic, and palette app (and soon themer) by using events
* Simplify logic (a lot of it carried complexity back from the time we did not use uids and/or was overly general)
* `PersistedArray` class to encapsulate arrays persisted in localStorage
* Remove unused `palette.equals()` function
2025-04-01 16:26:25 -04:00
Lea Verou
40a58ff35f Do not rely on {% raw %}, fixes #851 2025-03-31 17:53:33 -04:00
Lea Verou
0f2950c4cc Import CRUD parts from #828 2025-03-31 17:53:33 -04:00
Cory LaViska
b334884f57 remove unused custom properties (#853) 2025-03-31 17:08:50 +00:00
Cory LaViska
734417d66b fix version 2025-03-28 13:40:28 -04:00
Lea Verou
2cfd651d2f Prevent theme icons from getting focus when tabbing
Looks like `tabindex="-1"` didn't work, need to file a separate issue for that
2025-03-28 13:27:08 -04:00
Lea Verou
3e2d1b98be Changelog fixes
- Moved fixes to bug fixes section
- Linked `allDefined()` and `.wa-cloak` to their docs
- Grouped related bugfixes together
- Moved docs bugfixes to the end (since they are of least interest to users)
2025-03-28 13:27:08 -04:00
Lea Verou
40f332f37c Expand docs on allDefined() 2025-03-28 13:27:08 -04:00
Lea Verou
bfda64f690 Fix wa-cloak docs 2025-03-28 13:27:08 -04:00
Konnor Rogers
883d6df2ef fix z-index issues on sticky-disabled elements. (#848) 2025-03-28 12:26:30 -04:00
Lea Verou
b4240fd321 Move /docs/installation to /docs/, fix parent URL logic, closes #585 (#846)
* Fix: Parent URL should be undefined if parent is falsy

* Document `docs.11tydata.js` better

* Move `docs/installation.md` to `docs/`, fixes #585

* Just in case
2025-03-28 12:12:42 -04:00
Cory LaViska
8755a834f6 3.0.0-alpha.12 2025-03-28 11:07:44 -04:00
Cory LaViska
8d905296b8 update changelog 2025-03-28 11:07:42 -04:00
Cory LaViska
8eba1e5003 Various bug fixes (#839)
* add default icon spacing in tab; fixes #779

* fix radio button pill styles; fixes #759

* remove redundant styles

* fixes #840

* fix focus ring in Safari; fixes #745

* improve details styles; fixes #685

* update examples

* Revert "improve details styles; fixes #685"

This reverts commit 8151872d22.

* revert

* revert

* fix dropdown alignment in button group; closes #374

* fix progress animation in Safari; closes #356

* fix native checkbox indeterminate icon; closes #386

* add comment

* stop running SSR tests locally

* update test

* add FA kit code for codepen 🤞🏻

* remove wa-cloak after components load

* fix whitespace

* update display labels when changed; fixes #702

* fix radio labels (ALPHA-211)

* revert example

* add option as a dep of select

* remove outdated section
2025-03-28 10:57:01 -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
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
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
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
Lea Verou
fa24c0f70e Update changelog.md 2025-03-26 13:08:44 -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
89610eb449 Merge branch 'next' into css-attribute-properties 2025-01-27 15:37:49 -05:00
Lea Verou
b7824741f6 Create benchmark.njk 2025-01-14 04:05:41 -05:00
Lea Verou
9f438e2e15 Merge branch 'next' into css-attribute-properties 2025-01-13 16:39:22 -05:00
Lea Verou
32f538f657 Merge branch 'css-attribute-properties' of https://github.com/shoelace-style/webawesome into css-attribute-properties 2025-01-13 13:21:00 -05:00
Lea Verou
8e2e1b3ef5 Only get computed style when really needed 2025-01-13 13:20:55 -05:00
Lea Verou
1840a338e5 Add dynamic docs since this is not in alpha 2025-01-13 13:20:55 -05:00
Lea Verou
1bc84e7416 CSS properties to set component attributes
- Base class abstraction
- Use in `<wa-icon>` (docs excluded from alpha)
2025-01-13 13:20:55 -05:00
Lea Verou
ddb612efa2 Only get computed style when really needed 2025-01-08 10:31:11 -05:00
Lea Verou
f601b8aaf4 Add dynamic docs since this is not in alpha 2025-01-08 10:23:45 -05:00
Lea Verou
d18edcc941 CSS properties to set component attributes
- Base class abstraction
- Use in `<wa-icon>` (docs excluded from alpha)
2025-01-07 18:25:39 -05:00
115 changed files with 2536 additions and 1144 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
# # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: SSR Tests
on:
push:
branches: [next]
# push:
# branches: [next]
workflow_dispatch:
jobs:
ssr_test:

View File

@@ -1,3 +1,4 @@
import * as path from 'node:path';
import { anchorHeadingsPlugin } from './_utils/anchor-headings.js';
import { codeExamplesPlugin } from './_utils/code-examples.js';
import { copyCodePlugin } from './_utils/copy-code.js';
@@ -6,9 +7,10 @@ import { highlightCodePlugin } from './_utils/highlight-code.js';
import { markdown } from './_utils/markdown.js';
import { removeDataAlphaElements } from './_utils/remove-data-alpha-elements.js';
// import { formatCodePlugin } from './_utils/format-code.js';
import litPlugin from '@lit-labs/eleventy-plugin-lit';
// import litPlugin from '@lit-labs/eleventy-plugin-lit';
import { readFile } from 'fs/promises';
import componentList from './_data/componentList.js';
import nunjucks from 'nunjucks';
// import componentList from './_data/componentList.js';
import * as filters from './_utils/filters.js';
import { outlinePlugin } from './_utils/outline.js';
import { replaceTextPlugin } from './_utils/replace-text.js';
@@ -16,7 +18,10 @@ import { searchPlugin } from './_utils/search.js';
import process from 'process';
const packageData = JSON.parse(await readFile('./package.json', 'utf-8'));
import * as url from 'url';
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
const packageData = JSON.parse(await readFile(path.join(__dirname, '..', 'package.json'), 'utf-8'));
const isAlpha = process.argv.includes('--alpha');
const isDev = process.argv.includes('--develop');
@@ -24,12 +29,28 @@ const globalData = {
package: packageData,
isAlpha,
layout: 'page.njk',
server: {
head: '',
loginOrAvatar: '',
flashes: '',
},
};
const passThroughExtensions = ['js', 'css', 'png', 'svg', 'jpg', 'mp4'];
const passThrough = [...passThroughExtensions.map(ext => 'docs/**/*.' + ext)];
export default function (eleventyConfig) {
/**
* If you plan to add or remove any of these extensions, make sure to let either Konnor or Cory know as these passthrough extensions
* will also need to be updated in the Web Awesome App.
*/
const passThroughExtensions = ['js', 'css', 'png', 'svg', 'jpg', 'mp4'];
const baseDir = process.env.BASE_DIR || 'docs';
const passThrough = [...passThroughExtensions.map(ext => path.join(baseDir, '**/*.' + ext))];
/**
* This is the guard we use for now to make sure our final built files dont need a 2nd pass by the server. This keeps us able to still deploy the bare HTML files on Vercel until the app is ready.
*/
const serverBuild = process.env.WEBAWESOME_SERVER === 'true';
// NOTE - alpha setting removes certain pages
if (isAlpha) {
eleventyConfig.ignores.add('**/experimental/**');
@@ -55,7 +76,38 @@ export default function (eleventyConfig) {
// Shortcodes - {% shortCode arg1, arg2 %}
eleventyConfig.addShortcode('cdnUrl', location => {
return `https://early.webawesome.com/webawesome@${packageData.version}/dist/` + location.replace(/^\//, '');
return `https://early.webawesome.com/webawesome@${packageData.version}/dist/` + (location || '').replace(/^\//, '');
});
// Turns `{% server "foo" %} into `{{ server.foo | safe }}` when the WEBAWESOME_SERVER variable is set to "true"
eleventyConfig.addShortcode('server', function (property) {
if (serverBuild) {
return `{{ server.${property} | safe }}`;
}
return '';
});
eleventyConfig.addTransform('second-nunjucks-transform', function NunjucksTransform(content) {
// For a server build, we expect a server to run the second transform.
if (serverBuild) {
return content;
}
// Only run the transform on files nunjucks would transform.
if (!this.page.inputPath.match(/.(md|html|njk)$/)) {
return content;
}
/** This largely mimics what an app would do and just stubs out what we don't care about. */
return nunjucks.renderString(content, {
// Stub the server EJS shortcodes.
server: {
head: '',
loginOrAvatar: '',
flashes: '',
},
});
});
// Paired shortcodes - {% shortCode %}content{% endShortCode %}
@@ -117,29 +169,6 @@ export default function (eleventyConfig) {
]),
);
// SSR plugin
if (!isDev) {
//
// Problematic components in SSR land:
// - animation (breaks on navigation + ssr with Turbo)
// - mutation-observer (why SSR this?)
// - resize-observer (why SSR this?)
// - tooltip (why SSR this?)
//
const omittedModules = [];
const componentModules = componentList
.filter(component => !omittedModules.includes(component.tagName.split(/wa-/)[1]))
.map(component => {
const name = component.tagName.split(/wa-/)[1];
return `./dist/components/${name}/${name}.js`;
});
eleventyConfig.addPlugin(litPlugin, {
mode: 'worker',
componentModules,
});
}
// Build the search index
eleventyConfig.addPlugin(
searchPlugin({
@@ -166,6 +195,31 @@ export default function (eleventyConfig) {
eleventyConfig.addPassthroughCopy(glob);
}
// // SSR plugin
// // Make sure this is the last thing, we don't want to run the risk of accidentally transforming shadow roots with the nunjucks 2nd transform.
// if (!isDev) {
// //
// // Problematic components in SSR land:
// // - animation (breaks on navigation + ssr with Turbo)
// // - mutation-observer (why SSR this?)
// // - resize-observer (why SSR this?)
// // - tooltip (why SSR this?)
// //
// const omittedModules = [];
// const componentModules = componentList
// .filter(component => !omittedModules.includes(component.tagName.split(/wa-/)[1]))
// .map(component => {
// const name = component.tagName.split(/wa-/)[1];
// const componentDirectory = process.env.UNBUNDLED_DIST_DIRECTORY || path.join('.', 'dist');
// return path.join(componentDirectory, 'components', name, `${name}.js`);
// });
//
// eleventyConfig.addPlugin(litPlugin, {
// mode: 'worker',
// componentModules,
// });
// }
return {
markdownTemplateEngine: 'njk',
dir: {

View File

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

View File

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

View File

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

View File

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

View File

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

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. #}
{% if collections[tag] -%}
{% set groupUrl %}/docs/{{ tag }}/{% endset %}
{% set groupItem = groupUrl | getCollectionItemFromUrl %}
{% set children = groupItem.data.children if groupItem.data.children.length > 0 else (collections[tag] | sort) %}
<wa-details {{ ((tag in (tags or [])) or (groupUrl in page.url)) | attr('open') }}>
<h2 slot="summary">
{% if groupUrl | getCollectionItemFromUrl %}
{% if groupItem %}
<a href="{{ groupUrl }}" title="Overview">{{ title or (tag | capitalize) }}
<wa-icon name="grid-2"></wa-icon>
</a>
@@ -12,10 +15,8 @@
{% endif %}
</h2>
<ul>
{% for page in collections[tag] | sort %}
{% if not page.data.parent -%}
{% for page in children %}
{% include 'sidebar-link.njk' %}
{%- endif %}
{% endfor %}
</ul>
</wa-details>

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>
<a href="{{ page.url }}">{{ page.data.title }}</a>
{% if page.data.status == 'experimental' %}<wa-icon name="flask"></wa-icon>{% endif %}

View File

@@ -1,7 +1,7 @@
{# Getting started #}
<h2>Getting Started</h2>
<ul>
<li><a href="/docs/installation">Installation</a></li>
<li><a href="/docs/">Installation</a></li>
<li><a href="/docs/usage">Usage</a></li>
<li><a href="/docs/customizing">Customizing</a></li>
<li><a href="/docs/form-controls">Form Controls</a></li>

View File

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

View File

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

View File

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

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" inert></wa-input>
<wa-button size="small" variant="brand" inert>Go</wa-button>
</div>
</div>
</template>
</wa-scoped>

View File

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

View File

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

View File

@@ -30,12 +30,21 @@
{% include 'breadcrumbs.njk' %}
<h1 v-if="saved" class="title">
{% raw %}{{ saved.title }}{% endraw %}
<wa-icon-button name="pencil" label="Rename palette" @click="rename"></wa-icon-button>
<wa-icon-button class="delete" name="trash" label="Delete palette" @click="deleteSaved"></wa-icon-button>
<h1 class="title">
<span v-content="title">{{ title }}</span>
<template v-if="saved || tweaked">
<wa-icon-button name="pencil" label="Rename palette" @click="rename"></wa-icon-button>
<wa-icon-button v-if="saved" class="delete" name="trash" label="Delete palette" @click="deleteSaved"></wa-icon-button>
<wa-button @click="save()" :disabled="!unsavedChanges"
:variant="unsavedChanges ? 'success' : 'neutral'" size="small" :appearance="unsavedChanges ? 'accent' : 'outlined'">
<span slot="prefix" class="icon-modifier">
<wa-icon name="sidebar" variant="regular"></wa-icon>
<wa-icon name="circle-plus" class="modifier" style="color: light-dark(var(--wa-color-green-70), var(--wa-color-green-60));"></wa-icon>
</span>
<span v-content="unsavedChanges ? 'Save' : 'Saved'">Save</span>
</wa-button>
</template>
</h1>
<h1 v-if="!saved" class="title">{{ title }}</h1>
<div class="block-info">
<code class="class">.wa-palette-{{ paletteId }}</code>
@@ -59,7 +68,7 @@
<wa-icon name="sliders-simple" slot="icon" variant="regular"></wa-icon>
This palette has been tweaked.
<div class="wa-cluster wa-gap-xs">
<wa-tag v-for="tweakHumanReadable, param in tweaksHumanReadable" removable @wa-remove="reset(param)">{% raw %}{{ tweakHumanReadable }}{% endraw %}</wa-tag>
<wa-tag v-for="tweakHumanReadable, param in tweaksHumanReadable" removable @wa-remove="reset(param)" v-content="tweakHumanReadable"></wa-tag>
</div>
<wa-button @click="reset()" appearance="outlined" variant="danger">
@@ -68,13 +77,6 @@
</span>
Reset
</wa-button>
<wa-button v-if="!saved" @click="save" variant="success">
<span slot="prefix" class="icon-modifier">
<wa-icon name="sidebar" variant="regular"></wa-icon>
<wa-icon name="circle-plus" class="modifier" style="color: light-dark(var(--wa-color-green-70), var(--wa-color-green-60));"></wa-icon>
</span>
Save
</wa-button>
</wa-callout>
<table class="colors main wa-palette-{{ paletteId }}">

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-card with-header>
<div slot="header">
{% include "svgs/" + (palette.data.icon or "thumbnail-placeholder") + ".njk" %}
{% include "svgs/" + (palette.data.icon or "thumbnail-placeholder") + ".njk" ignore missing %}
</div>
<span class="page-name">
{{ palette.data.title }}

View File

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

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
clone.querySelectorAll('a').forEach(a => a.remove());
clone.querySelectorAll('.wa-visually-hidden, [hidden], [aria-hidden="true"]').forEach(el => el.remove());
clone.querySelectorAll('[data-no-outline]').forEach(el => el.remove());
// Generate the link

View File

@@ -2,6 +2,7 @@
import { mkdir, writeFile } from 'fs/promises';
import lunr from 'lunr';
import { parse } from 'node-html-parser';
import * as path from 'path';
import { dirname, join } from 'path';
function collapseWhitespace(string) {
@@ -52,8 +53,9 @@ export function searchPlugin(options = {}) {
return content;
});
eleventyConfig.on('eleventy.after', ({ dir }) => {
const outputFilename = join(dir.output, 'search.json');
eleventyConfig.on('eleventy.after', ({ directories }) => {
const { output } = directories;
const outputFilename = path.resolve(join(output, 'search.json'));
const map = [];
const searchIndex = lunr(async function () {
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 cdnUrl = document.documentElement.dataset.cdnUrl;
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/webawesome.css">\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) {
const filter = input.value.toLowerCase().trim();
let filtered = Boolean(filter);
@@ -18,8 +26,10 @@ function updateResults(input) {
}
}
const debouncedUpdateResults = debounce(updateResults, 300);
document.documentElement.addEventListener('input', e => {
if (e.target?.matches('#block-filter wa-input')) {
updateResults(e.target);
debouncedUpdateResults(e.target);
}
});

167
docs/assets/scripts/my.js Normal file
View File

@@ -0,0 +1,167 @@
const my = (globalThis.my = new EventTarget());
export default my;
class PersistedArray extends Array {
constructor(key) {
super();
this.key = key;
if (this.key) {
this.fromLocalStorage();
}
// Items were updated in another tab
addEventListener('storage', event => {
if (event.key === this.key || !event.key) {
this.fromLocalStorage();
}
});
}
/**
* Update data from local storage
*/
fromLocalStorage() {
// First, empty the array
this.splice(0, this.length);
// Then, fill it with the data from local storage
let saved = localStorage[this.key] ? JSON.parse(localStorage[this.key]) : null;
if (saved) {
this.push(...saved);
}
}
/**
* Write data to local storage
*/
toLocalStorage() {
if (this.length > 0) {
localStorage[this.key] = JSON.stringify(this);
} else {
delete localStorage[this.key];
}
}
}
class SavedEntities extends EventTarget {
constructor({ key, type, url }) {
super();
this.key = key;
this.type = type;
this.url = url ?? type + 's';
this.saved = new PersistedArray(key);
let all = this;
this.entityPrototype = {
type: this.type,
baseUrl: this.baseUrl,
get url() {
return all.getURL(this);
},
get parentUrl() {
return all.getParentURL(this);
},
delete() {
all.delete(this);
},
};
}
getUid() {
if (this.saved.length === 0) {
return 1;
}
let uids = new Set(this.saved.map(p => p.uid));
// Find first available number
for (let i = 1; i <= this.saved.length + 1; i++) {
if (!uids.has(i)) {
return i;
}
}
}
get baseUrl() {
return `/docs/${this.url}/`;
}
getURL(entity) {
return this.getParentURL(entity) + entity.search;
}
getParentURL(entity) {
return this.baseUrl + entity.id + '/';
}
getObject(entity) {
let ret = Object.create(this.entityPrototype, Object.getOwnPropertyDescriptors(entity));
// debugger;
return ret;
}
/**
* Save an entity, either by updating its existing entry or creating a new one
* @param {object} entity
*/
save(entity) {
if (!entity.uid) {
// First time saving
entity.uid = this.getUid();
}
let savedPalettes = this.saved;
let existingIndex = entity.uid ? this.saved.findIndex(p => p.uid === entity.uid) : -1;
let newIndex = existingIndex > -1 ? existingIndex : savedPalettes.length;
this.saved.splice(newIndex, 1, entity);
this.saved.toLocalStorage();
this.dispatchEvent(new CustomEvent('save', { detail: this.getObject(entity) }));
return entity;
}
delete(entity) {
let count = this.saved.length;
if (count === 0 || !entity?.uid) {
// No stored entities or this entity has not been saved
return;
}
// TODO improve UX of this
if (!confirm(`Are you sure you want to delete ${this.type}${entity.title}”?`)) {
return;
}
for (let index; (index = this.saved.findIndex(p => p.uid === entity.uid)) > -1; ) {
this.saved.splice(index, 1);
}
if (this.saved.length === count) {
// Nothing was removed
return;
}
this.saved.toLocalStorage();
this.dispatchEvent(new CustomEvent('delete', { detail: this.getObject(entity) }));
}
dispatchEvent(event) {
super.dispatchEvent(event);
my.dispatchEvent(event);
}
}
my.palettes = new SavedEntities({
key: 'savedPalettes',
type: 'palette',
});

View File

@@ -1,254 +1,119 @@
const sidebar = (globalThis.sidebar = {});
import my from '/assets/scripts/my.js';
sidebar.palettes = {
render() {
if (this.saved.length === 0) {
return;
}
const sidebar = {
addChild(a, parentA) {
let parentLi = parentA.closest('li');
let ul = parentLi.querySelector(':scope > ul');
ul ??= parentLi.appendChild(document.createElement('ul'));
let li = document.createElement('li');
li.append(a);
ul.appendChild(li);
for (let palette of this.saved) {
sidebar.palette.render(palette);
}
sidebar.updateCurrent();
},
updateSaved() {
this.saved = localStorage.savedPalettes ? JSON.parse(localStorage.savedPalettes) : [];
},
save(saved = this.saved) {
this.saved = saved ?? [];
if (saved.length > 0) {
localStorage.savedPalettes = JSON.stringify(saved);
} else {
delete localStorage.savedPalettes;
}
},
};
sidebar.palettes.updateSaved();
addEventListener('storage', event => sidebar.palettes.updateSaved());
sidebar.palette = {
getUid() {
let savedPalettes = sidebar.palettes.saved;
let uids = new Set(savedPalettes.map(p => p.uid));
if (savedPalettes.length === 0) {
return 1;
}
// Find first available number
for (let i = 1; i <= savedPalettes.length + 1; i++) {
if (!uids.has(i)) {
return i;
// If we are on the same page, update the current link
let url = location.href.replace(/#.+$/, '');
if (url.startsWith(a.href)) {
// Remove existing current
for (let current of document.querySelectorAll('#sidebar a.current')) {
current.classList.remove('current');
}
}
},
equals(p1, p2) {
if (!p1 || !p2) {
return false;
a.classList.add('current');
}
return p1.id === p2.id && p1.uid === p2.uid;
return a;
},
delete(palette) {
let savedPalettes = sidebar.palettes.saved;
let count = savedPalettes.length;
if (count === 0) {
removeLink(a) {
if (!a || !a.isConnected) {
// Link doesn't exist or is already removed
return;
}
// TODO improve UX of this
if (!confirm(`Are you sure you want to delete palette “${palette.title}”?`)) {
return;
let li = a?.closest('li');
let ul = li?.closest('ul');
let parentA = ul?.closest('li')?.querySelector(':scope > a');
li?.remove();
if (ul?.children.length === 0) {
ul.remove();
}
savedPalettes = savedPalettes.filter(p => !sidebar.palette.equals(palette, p));
if (savedPalettes.length === count) {
// Nothing was removed
return;
}
// Update UI
let pathname = `/docs/palettes/${palette.id}/`;
let url = pathname + palette.search;
let uls = new Set();
for (let a of document.querySelectorAll(`#sidebar a[href="${url}"]`)) {
let li = a.closest('li');
let ul = li.closest('ul');
uls.add(ul);
li.remove();
}
// Remove empty lists
for (let ul of uls) {
if (!ul.children.length) {
ul.remove();
}
}
sidebar.updateCurrent();
sidebar.palettes.save(savedPalettes);
if (sidebar.palette.equals(globalThis.paletteApp?.saved, palette)) {
paletteApp.postDelete();
if (a.classList.contains('current')) {
// If the deleted palette was the current one, the current one is now the parent
parentA.classList.add('current');
}
},
getSaved(palette, savedPalettes = sidebar.palettes.saved) {
return savedPalettes.find(p => sidebar.palette.equals(p, palette));
findEntity(entity) {
return document.querySelector(`#sidebar a[href^="${entity.baseUrl}"][data-uid="${entity.uid}"]`);
},
render(palette) {
// Find existing <a>
let { title, id, search, uid } = palette;
renderEntity(entity) {
let { url, parentUrl } = entity;
for (let a of document.querySelectorAll(`#sidebar a[href^="/docs/palettes/${id}/"][data-uid="${uid}"]`)) {
// Palette already in sidebar, just update it
a.textContent = palette.title;
a.href = `/docs/palettes/${id}/${search}`;
return;
}
let pathname = `/docs/palettes/${id}/`;
let url = pathname + search;
let parentA = document.querySelector(`a[href="${pathname}"]`);
// Find parent
let parentA = document.querySelector(`#sidebar a[href="${parentUrl}"]`);
let parentLi = parentA?.closest('li');
let a;
if (parentLi) {
a = Object.assign(document.createElement('a'), { href: url, textContent: title });
a.dataset.uid = uid;
let badges = [...parentLi.querySelectorAll('wa-badge')].map(badge => badge.cloneNode(true));
let ul = parentLi.querySelector('ul') ?? parentLi.appendChild(document.createElement('ul'));
let li = document.createElement('li');
let deleteButton = Object.assign(document.createElement('wa-icon-button'), {
name: 'trash',
label: 'Delete',
className: 'delete',
});
deleteButton.addEventListener('click', () => {
let palette = { id, uid, title: a.textContent, search: a.search };
sidebar.palette.delete(palette);
});
li.append(a, ' ', ...badges, deleteButton);
ul.appendChild(li);
}
},
save(palette, saved) {
let savedPalettes = sidebar.palettes.saved;
let existing = this.getSaved(saved ?? palette, savedPalettes);
let oldValues;
if (existing) {
// Rename
oldValues = { ...existing };
Object.assign(existing, palette);
} else {
savedPalettes.push(palette);
if (!parentLi) {
throw new Error(`Cannot find parent url ${parentUrl}`);
}
this.render(palette, oldValues);
sidebar.updateCurrent();
// Find existing
let a = this.findEntity(entity);
let alreadyExisted = !!a;
sidebar.palettes.save(savedPalettes);
},
};
a ??= document.createElement('a');
sidebar.updateCurrent = function () {
// Find the sidebar link with the longest shared prefix with the current URL
let pathParts = location.pathname.split('/').filter(Boolean);
let prefixes = [];
a.textContent = entity.title;
a.href = url;
if (pathParts.length === 1) {
// If at /docs/ we just use that, otherwise we want at least two parts (/docs/xxx/)
prefixes.push('/' + pathParts[0] + '/');
} else {
for (let i = 2; i <= pathParts.length; i++) {
prefixes.push('/' + pathParts.slice(0, i).join('/') + '/');
}
}
if (!alreadyExisted) {
a.dataset.uid = entity.uid;
// Last prefix includes the search too (if any)
if (location.search) {
let params = new URLSearchParams(location.search);
params.sort();
prefixes.push(prefixes.at(-1) + location.search);
}
a = sidebar.addChild(a, parentA);
// We want to start from the longest prefix
prefixes.reverse();
let candidates;
let matchingPrefix;
// This is mainly to port Pro badges
let badges = Array.from(parentLi.querySelectorAll('wa-badge'), badge => badge.cloneNode(true));
let append = [...badges];
for (let prefix of prefixes) {
candidates = document.querySelectorAll(`#sidebar a[href^="${prefix}"]`);
if (candidates.length > 0) {
matchingPrefix = prefix;
break;
}
}
if (!matchingPrefix) {
// Abort mission
return;
}
if (matchingPrefix === pathParts.at(-1)) {
// Full path matches, check search
if (location.search) {
candidates = [...candidates];
let searchParams = new URLSearchParams(location.search);
if (searchParams.has('uid')) {
// Only consider candidates with the same uid
candidates = candidates.filter(a => {
let params = new URLSearchParams(a.search);
return params.get('uid') === searchParams.get('uid');
});
} else {
// Sort candidates based on how many params they have in common, in descending order
candidates = candidates.sort((a, b) => {
return countSharedSearchParams(searchParams, b.search) - countSharedSearchParams(searchParams, a.search);
if (entity.delete) {
let deleteButton = Object.assign(document.createElement('wa-icon-button'), {
name: 'trash',
label: 'Delete',
className: 'delete',
});
deleteButton.addEventListener('click', () => entity.delete());
append.push(deleteButton);
}
if (append.length > 0) {
a.closest('li').append(' ', ...append);
}
}
}
},
if (candidates.length > 0) {
for (let current of document.querySelectorAll('#sidebar a.current')) {
current.classList.remove('current');
render() {
for (let type in my) {
let controller = my[type];
if (!controller.saved) {
continue;
}
for (let entity of controller.saved) {
let object = controller.getObject(entity);
this.renderEntity(object);
}
}
candidates[0].classList.add('current');
}
},
};
sidebar.render = function () {
this.palettes.render();
};
globalThis.sidebar = sidebar;
// Update sidebar when my saved stuff changes
my.addEventListener('delete', e => sidebar.removeLink(sidebar.findEntity(e.detail)));
my.addEventListener('save', e => sidebar.renderEntity(e.detail));
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
export function domChange(fn, { behavior = 'smooth' } = {}) {
export function domChange(fn, { behavior = 'smooth', ignoreInitialLoad = true } = {}) {
const canUseViewTransitions =
document.startViewTransition && !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// Skip transitions on initial page load
if (!initialPageLoadComplete && ignoreInitialLoad) {
fn(false);
return null;
}
if (canUseViewTransitions && behavior === 'smooth') {
document.startViewTransition(fn);
const transition = document.startViewTransition(() => {
fn(true);
// Wait a brief delay before finishing the transition to prevent jumpiness
return new Promise(resolve => setTimeout(resolve, 200));
});
return transition;
} else {
fn(true);
fn(false);
return null;
}
}
@@ -100,6 +120,7 @@ const colorScheme = new ThemeAspect({
domChange(() => {
let dark = this.computedValue === 'dark';
document.documentElement.classList.toggle(`wa-dark`, dark);
document.documentElement.dispatchEvent(new CustomEvent('wa-color-scheme-change', { detail: { dark } }));
});
},
});

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

View File

@@ -13,56 +13,42 @@ export default class Permalink extends URLSearchParams {
return Object.fromEntries(this.entries());
}
#mappings = new WeakMap();
mapObject(obj, mapping = {}) {
this.#mappings.set(obj, mapping);
}
readFrom(obj) {
let mapping = this.#mappings.get(obj) ?? {};
let { keyFrom = IDENTITY, valueFrom = IDENTITY } = mapping;
for (let key in obj) {
let value = obj[key];
let mappedValue = valueFrom(value);
let mappedKey = keyFrom(key);
this.set(mappedKey, mappedValue);
}
}
writeTo(obj) {
let mapping = this.#mappings.get(obj) ?? {};
let { keyTo = IDENTITY, valueTo = IDENTITY, canExtend = false } = mapping;
for (let [key, value] of this) {
let mappedKey = keyTo(key);
let mappedValue = valueTo(value);
if (canExtend || mappedKey in obj) {
obj[mappedKey] = mappedValue;
}
}
}
set(key, value, defaultValue) {
let oldValue = this.get(key);
if (equals(value, defaultValue) || equals(value, '')) {
value = null;
}
if (!value || value == defaultValue) {
value ??= null; // undefined -> null
let oldValue = Array.isArray(value) ? this.getAll(key) : this.get(key);
let changed = !equals(value, oldValue);
if (!changed) {
// Nothing to do here
return;
}
if (Array.isArray(value)) {
super.delete(key);
value = value.slice();
if (oldValue) {
this.changed = true;
for (let v of value) {
if (v || v === 0) {
if (typeof v === 'object') {
super.append(key, JSON.stringify(v));
} else {
super.append(key, v);
}
}
}
} else if (value === null) {
super.delete(key);
} else {
super.set(key, value);
if (String(value) !== String(oldValue)) {
this.changed = true;
}
}
this.sort();
this.changed ||= changed;
}
/**
@@ -79,3 +65,40 @@ export default class Permalink extends URLSearchParams {
}
}
}
function equals(value, oldValue) {
if (Array.isArray(value) || Array.isArray(oldValue)) {
value = toArray(value);
oldValue = toArray(oldValue);
if (value.length !== oldValue.length) {
return false;
}
return value.every((v, i) => equals(v, oldValue[i]));
}
// (value ?? oldValue ?? true) returns true if they're both empty (null or undefined)
[value, oldValue] = [value, oldValue].map(v => (!v && v !== false && v !== 0 ? null : v));
return value === oldValue || String(value) === String(oldValue);
}
/**
* Convert a value to an array. `undefined` and `null` values are converted to an empty array.
* @param {*} value - The value to convert.
* @returns {any[]} The converted array.
*/
function toArray(value) {
value ??= [];
if (Array.isArray(value)) {
return value;
}
// Don't convert "foo" into ["f", "o", "o"]
if (typeof value !== 'string' && typeof value[Symbol.iterator] === 'function') {
return Array.from(value);
}
return [value];
}

View File

@@ -0,0 +1,22 @@
// Like v-text, but doesn't complain if the element has content,
// making it possible to use in a PE fashion, with the contents being the fallback
export default function content(el, { value, arg }) {
if (!el.dataset.fallback) {
// Store the original content as a fallback the first time
el.dataset.fallback = el.textContent;
}
if (value === '') {
value = el.dataset.fallback;
} else {
if (arg === 'number') {
value = Number(value).toLocaleString(undefined, { maximumSignificantDigits: 2 });
}
}
if (arg === 'html') {
el.innerHTML = value;
} else {
el.textContent = value;
}
}

View File

@@ -0,0 +1,110 @@
import my from '/assets/scripts/my.js';
import Permalink from '/assets/scripts/tweak/permalink.js';
export default {
data() {
return {
uid: undefined,
saved: null,
unsavedChanges: false,
permalink: new Permalink(),
};
},
created() {
if (this.permalink.has('uid')) {
this.uid = Number(this.permalink.get('uid'));
this.saved = this.controller.saved.find(p => p.uid === this.uid);
}
this.controller.addEventListener('delete', ({ detail: entity }) => {
if (entity.uid === this.saved?.uid) {
this.postDelete();
}
});
},
mounted() {
this.$nextTick().then(() => {
if (!location.search || this.saved) {
this.unsavedChanges = false;
}
});
},
computed: {
controller() {
return my[this.collection];
},
title() {
if (this.saved) {
return this.saved.title;
} else if (this.unsavedChanges) {
return this.defaultTitle;
} else {
return this.originalTitle;
}
},
},
watch: {
saved: {
deep: true,
handler() {
this.unsavedChanges = !this.saved;
},
},
},
methods: {
async save({ title } = {}) {
let uid = this.uid;
this.saved ??= { uid: this.uid };
this.saved.id = this.id;
if (title) {
// Renaming
this.saved.title = title;
} else {
this.saved.title ??= this.defaultTitle;
}
this.saved.search = location.search;
this.saved = this.controller.save(this.saved);
if (uid !== this.saved.uid) {
// UID changed (most likely from saving a new entity)
this.uid = this.saved.uid;
this.permalink.set('uid', this.uid);
this.permalink.updateLocation();
await this.$nextTick();
this.save(); // Save again to update the search param to include the UID
}
this.unsavedChanges = false;
},
rename() {
let newTitle = prompt('Title:', this.saved?.title ?? this.defaultTitle);
if (newTitle && newTitle !== this.saved?.title) {
this.save({ title: newTitle });
}
},
// Cannot name this delete() because Vue complains
deleteSaved() {
this.controller.delete(this.saved);
},
postDelete() {
this.saved = null;
this.permalink.delete('uid');
this.uid = undefined;
this.permalink.updateLocation();
},
},
};

View File

@@ -4,6 +4,7 @@
@import 'outline.css';
@import 'search.css';
@import 'cera_typeface.css';
@import 'theme-icons.css';
:root {
--wa-brand-orange: #f36944;
@@ -370,10 +371,22 @@ wa-page > main:has(> .index-grid) {
.index-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(22ch, 100%), 1fr));
grid-template-columns: repeat(4, 1fr);
gap: var(--wa-space-2xl);
margin-block-end: var(--wa-space-3xl);
@media screen and (max-width: 1470px) {
grid-template-columns: repeat(3, 1fr);
}
@media screen and (max-width: 960px) {
grid-template-columns: repeat(2, 1fr);
}
@media screen and (max-width: 500px) {
grid-template-columns: repeat(1, 1fr);
}
a {
border-radius: var(--wa-border-radius-l);
text-decoration: none;
@@ -400,7 +413,6 @@ wa-page > main:has(> .index-grid) {
&::part(header) {
background-color: var(--header-background, var(--wa-color-neutral-fill-quiet));
border-bottom: none;
display: flex;
align-items: center;
justify-content: center;

View File

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

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 {
display: grid;
gap: var(--wa-space-xs);
@@ -25,10 +83,50 @@
display: flex;
flex-direction: column;
gap: var(--wa-space-xs);
}
h3,
p {
margin-block: 0;
padding: 0;
.theme-overall-icon {
display: flex;
flex-flow: column;
gap: var(--wa-space-xs);
justify-content: center;
width: 100%;
min-height: 7.5rem;
box-sizing: border-box;
background: var(--wa-color-surface-lowered);
.row {
display: flex;
gap: var(--wa-space-xs);
align-items: center;
justify-content: space-between;
}
.row-2 {
display: grid;
grid-template-columns: 1fr auto;
contain: inline-size;
width: 100%;
wa-input {
min-width: 1em;
}
}
.swatches {
display: flex;
gap: var(--wa-space-3xs);
> div {
width: 1.25rem;
height: 1.25rem;
border-radius: var(--wa-border-radius-s);
background: var(--wa-color-fill-loud);
color: var(--wa-color-on-loud);
&.wa-brand {
width: 2.5rem;
}
}
}
}

View File

@@ -15,9 +15,9 @@ icon: card
<strong>Mittens</strong><br />
This kitten is as cute as he is playful. Bring him home today!<br />
<small>6 weeks old</small>
<small class="wa-caption-m">6 weeks old</small>
<div slot="footer">
<div slot="footer" class="wa-split">
<wa-button variant="brand" pill>More Info</wa-button>
<wa-rating label="Rating"></wa-rating>
</div>
@@ -27,16 +27,6 @@ icon: card
.card-overview {
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>
```
@@ -65,9 +55,9 @@ If using SSR, you need to also use the `with-header` attribute to add a header t
```html {.example}
<wa-card with-header class="card-header">
<div slot="header">
<div slot="header" class="wa-split">
Header Title
<wa-icon-button name="gear" variant="solid" label="Settings"></wa-icon-button>
<wa-icon-button name="gear" variant="solid" label="Settings" class="wa-size-m"></wa-icon-button>
</div>
This card has a header. You can put all sorts of things in it!
@@ -78,19 +68,9 @@ If using SSR, you need to also use the `with-header` attribute to add a header t
max-width: 300px;
}
.card-header [slot='header'] {
display: flex;
align-items: center;
justify-content: space-between;
}
.card-header h3 {
margin: 0;
}
.card-header wa-icon-button {
font-size: var(--wa-font-size-m);
}
</style>
```
@@ -103,7 +83,7 @@ If using SSR, you need to also use the `with-footer` attribute to add a footer t
<wa-card with-footer class="card-footer">
This card has a footer. You can put all sorts of things in it!
<div slot="footer">
<div slot="footer" class="wa-split">
<wa-rating></wa-rating>
<wa-button variant="brand">Preview</wa-button>
</div>
@@ -113,12 +93,6 @@ If using SSR, you need to also use the `with-footer` attribute to add a footer t
.card-footer {
max-width: 300px;
}
.card-footer [slot='footer'] {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
```
@@ -153,7 +127,7 @@ Use the `size` attribute to change a card's size.
<wa-card with-footer size="small">
This is a small card.
<footer slot="footer" class="wa-flank">
<footer slot="footer" class="wa-split">
<wa-button variant="brand" pill>More Info</wa-button>
<wa-rating></wa-rating>
</footer>
@@ -162,7 +136,7 @@ Use the `size` attribute to change a card's size.
<wa-card with-footer size="medium">
This is a medium card (default).
<footer slot="footer" class="wa-flank">
<footer slot="footer" class="wa-split">
<wa-button variant="brand" pill>More Info</wa-button>
<wa-rating></wa-rating>
</footer>
@@ -171,14 +145,39 @@ Use the `size` attribute to change a card's size.
<wa-card with-footer size="large">
This is a large card.
<footer slot="footer" class="wa-flank">
<footer slot="footer" class="wa-split">
<wa-button variant="brand" pill>More Info</wa-button>
<wa-rating></wa-rating>
</footer>
</wa-card>
</div>
```
<style>
</style>
### Appearance
Use the `appearance` attribute to change the card's visual appearance.
```html {.example}
<div class="wa-grid">
<wa-card>
<img
slot="image"
src="https://images.unsplash.com/photo-1559209172-0ff8f6d49ff7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=80"
alt="A kitten sits patiently between a terracotta pot and decorative grasses."
/>
<div slot="header">Outlined (default)</div>
Card content.
</wa-card>
{% for appearance in ['outlined filled', 'outlined accent', 'plain', 'filled', 'accent'] -%}
<wa-card appearance="{{ appearance }}">
<img
slot="image"
src="https://images.unsplash.com/photo-1559209172-0ff8f6d49ff7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=80"
alt="A kitten sits patiently between a terracotta pot and decorative grasses."
/>
<div slot="header">{{ appearance | capitalize }}</div>
Card content.
</wa-card>
{%- endfor %}
</div>
```

View File

@@ -77,6 +77,31 @@ The details component automatically adapts to right-to-left languages:
</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
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

@@ -24,6 +24,86 @@ Many Font Awesome Pro icon families have variants such as `thin`, `light`, `regu
<wa-icon family="brands" name="web-awesome"></wa-icon>
```
### Setting defaults via CSS
You can use certain CSS custom properties to set icon defaults, not just on the icon itself, but any ancestor.
This can be useful when you want certain parameters to vary based on context, e.g. icons inside callouts or all icons for a given theme.
:::warning
These CSS properties are intended to set **defaults**, and thus only make a difference when the corresponding attributes are not set.
In future versions of Web Awesome, we may change this behavior to allow CSS properties to override attributes if `!important` is used.
:::
For example, here is how you can use CSS custom properties to set a default icon for each type of callout:
```html {.example}
<wa-callout>
<!-- Look ma, no attributes! -->
<wa-icon slot="icon"></wa-icon>
This is a normal callout.
</wa-callout>
<wa-callout variant="danger">
<wa-icon slot="icon" name="dumpster-fire" variant="solid"></wa-icon>
This is a callout with an explicit icon, which overrides these defaults.
</wa-callout>
<wa-callout variant="warning">
<!-- Look ma, no attributes! -->
<wa-icon slot="icon"></wa-icon>
Here be dragons.
</wa-callout>
<wa-callout variant="danger">
<!-- Look ma, no attributes! -->
<wa-icon slot="icon"></wa-icon>
Here be more dragons.
</wa-callout>
<wa-callout variant="success">
<!-- Look ma, no attributes! -->
<wa-icon slot="icon"></wa-icon>
Success!
</wa-callout>
<style>
wa-callout {
--wa-icon-variant: regular;
--wa-icon-name: info-circle;
&[variant="warning"] {
--wa-icon-name: triangle-exclamation;
}
&[variant="danger"] {
--wa-icon-name: circle-exclamation;
}
&[variant="success"] {
--wa-icon-name: circle-check;
}
}
</style>
```
You can even set icons dynamically, as a response to user interaction or media queries.
For example, here's how we can change the icon on hover:
```html {.example}
<wa-button class="github" href="https://github.com/webawesome/webawesome"><wa-icon slot="prefix" fixed-width></wa-icon> GitHub Repo</wa-button>
<style>
.github {
--wa-icon-name: github;
--wa-icon-family: brands;
&:hover {
--wa-icon-name: arrow-up-right-from-square;
--wa-icon-family: classic;
}
}
</style>
```
### Colors
Icons inherit their color from the current text color. Thus, you can set the `color` property on the `<wa-icon>` element or an ancestor to change the color.

View File

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

View File

@@ -885,4 +885,4 @@ If you dont want to use [native styles](/docs/native/), you can include this
```html
<link rel="stylesheet" href="{% cdnUrl 'styles/components/page.css' %}" />
```
```

View File

@@ -1,10 +1,108 @@
/**
* Global data for all pages
*/
import { sort } from '../_utils/filters.js';
export default {
eleventyComputed: {
children(data) {
let mainTag = data.tags?.[0];
let collection = data.collections[mainTag] ?? [];
/**
* Default parent slug. Can be overridden by explicitly setting parent in the data.
* It can be either the URL slug of a page in the same directory or a parent directory.
* @returns {string | undefined}
*/
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);
},
/**
* URL of parent page
* @returns {string | undefined}
*/
parentUrl(data) {
let { parent, page } = data;
return getParentUrl(page.url, parent);
},
/**
* Collection item of parent page
* @returns {object | undefined} Parent page item
*/
parentItem(data) {
let { parentUrl } = data;
return data.collections.all.find(item => item.url === parentUrl);
},
/**
* Child pages of current page
* @returns {object[]} Array of child pages
*/
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;
},
},
};
/**
* Resolve a parent slug against a page URL
* @param {string} url - The URL of the page
* @param {string} parent - The slug of the parent page
* @returns {string} The resolved URL of the parent page
*/
function getParentUrl(url, parent) {
if (!parent) {
return undefined;
}
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('/')) {
// If the current page starts with a slash, make sure the parent does too
// This is pretty much always the case with 11ty page URLs
ret = '/' + ret;
}
if (!retParts.at(-1)?.includes('.') && !ret.endsWith('/')) {
// If no extension, make sure to end with a slash
ret += '/';
}
if (ret === '/docs/') {
// We don't want anyone's parent to be "Installation"!
ret = '/';
}
return ret;
}

View File

@@ -32,15 +32,11 @@ To get everything included in Web Awesome, add the following code to the `<head>
This snippet includes three parts:
1. **The default theme**, a stylesheet that gives a cohesive look to Web Awesome components with both light and dark modes
2. **Web Awesome styles**, an optional stylesheet that [styles native HTML elements](/docs/native) and includes [utility classes](/docs/utilities) you can use in your project
2. **Web Awesome styles**, an optional stylesheet that [styles native HTML elements](/docs/native) and includes [utility classes](/docs/utilities) you can use in your project
3. **The autoloader**, a lightweight script watches the DOM for unregistered Web Awesome elements and lazy loads them for you — even if they're added dynamically
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
@@ -62,7 +58,7 @@ Font Awesome users can set their kit code to unlock Font Awesome Pro icons. You
## Advanced Setup
The autoloader is the easiest way to use Web Awesome, but different projects (or your own preferences!) may require different installation methods.
The autoloader is the easiest way to use Web Awesome, but different projects (or your own preferences!) may require different installation methods.
### Installing via npm
@@ -126,4 +122,4 @@ Most of the magic behind assets is handled internally by Web Awesome, but if you
// Get the path to an asset, e.g. /path/to/assets/file.ext
const assetPath = getBasePath('file.ext');
</script>
```
```

View File

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

View File

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

View File

@@ -57,7 +57,7 @@ Use the [variant utility classes](../utilities/color.md) to set the callout's co
### Appearance
Use the [appearance utility classes](../utilities/appearance.md) to change the callout's visual appearance (the default is `outlined filled`).
Use the [appearance utility classes](/docs/utilities/appearance) to change the callout's visual appearance (the default is `outlined filled`).
```html {.example}
<article class="wa-callout wa-brand wa-outlined wa-accent">

View File

@@ -19,6 +19,35 @@ file: styles/native/details.css
## Examples
### Appearance
Use the [appearance utility classes](/docs/utilities/appearance) to change the element's visual appearance:
```html {.example}
<div class="wa-stack">
<details>
<summary>Outlined (default)</summary>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
</details>
<details class="wa-filled">
<summary>Filled</summary>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
</details>
<details class="wa-filled wa-outlined">
<summary>Filled + Outlined</summary>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
</details>
<details class="wa-plain">
<summary>Plain</summary>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
</details>
</div>
```
### Right-to-Left Languages
The details styling automatically adapts to right-to-left languages:

View File

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

View File

@@ -6,6 +6,8 @@ import { cssImport, cssLiteral, cssRule } from '../../assets/scripts/tweak/code.
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 content from '/assets/scripts/vue/directives/content.js';
import savedMixin from '/assets/scripts/vue/mixins/saved.js';
await Promise.all(['wa-slider'].map(tag => customElements.whenDefined(tag)));
@@ -38,24 +40,25 @@ for (let palette in allPalettes) {
const percentFormatter = value => value.toLocaleString(undefined, { style: 'percent' });
let paletteAppSpec = {
mixins: [savedMixin],
data() {
let appRoot = document.querySelector('#palette-app');
let paletteId = appRoot.dataset.paletteId;
let palette = allPalettes[paletteId];
let id = appRoot.dataset.paletteId;
let palette = allPalettes[id];
return {
uid: undefined,
paletteId,
paletteTitle: palette.title,
id,
originalTitle: palette.title,
originalColors: palette.colors,
permalink: new Permalink(),
hueRanges,
hueShifts: Object.fromEntries(hues.map(hue => [hue, 0])),
chromaScale: 1,
grayChroma: undefined,
grayColor: undefined,
tweaking: {},
saved: null,
type: 'palette',
collection: 'palettes',
};
},
@@ -63,20 +66,16 @@ let paletteAppSpec = {
// Non-reactive variables to expose
Object.assign(this, { moreHue });
// Read URL params and apply them. This facilitates permalinks.
this.permalink.mapObject(this.hueShifts, {
keyTo: key => key.replace(/-shift$/, ''),
keyFrom: key => key + '-shift',
valueFrom: value => (!value ? '' : Number(value)),
valueTo: value => (!value ? 0 : Number(value)),
});
this.grayChroma = this.originalGrayChroma;
this.grayColor = this.originalGrayColor;
if (location.search) {
// Update from URL
this.permalink.writeTo(this.hueShifts);
// Read URL params and apply them. This facilitates permalinks.
for (let hue in this.hueShifts) {
if (this.permalink.has(hue + '-shift')) {
this.hueShifts[hue] = Number(this.permalink.get(hue + '-shift'));
}
}
for (let param of ['chroma-scale', 'gray-color', 'gray-chroma']) {
if (this.permalink.has(param)) {
@@ -91,12 +90,6 @@ let paletteAppSpec = {
this[prop] = value;
}
}
if (this.permalink.has('uid')) {
this.uid = Number(this.permalink.get('uid'));
}
this.saved = sidebar.palette.getSaved(this.getPalette());
}
},
@@ -107,6 +100,11 @@ let paletteAppSpec = {
},
computed: {
/** Default palette title for saving */
defaultTitle() {
return this.originalTitle + ' (tweaked)';
},
tweaks() {
return {
hueShifts: this.hueShifts,
@@ -123,7 +121,7 @@ let paletteAppSpec = {
code() {
let ret = {};
for (let language of ['html', 'css']) {
let code = getPaletteCode(this.paletteId, this.colors, this.tweaked, { language, cdnUrl });
let code = getPaletteCode(this.id, this.colors, this.tweaked, { language, cdnUrl });
ret[language] = {
raw: code,
highlighted: Prism.highlight(code, Prism.languages[language], language),
@@ -284,7 +282,9 @@ let paletteAppSpec = {
hueShifts: {
deep: true,
handler() {
this.permalink.readFrom(this.hueShifts);
for (let hue in this.hueShifts) {
this.permalink.set(hue + '-shift', this.hueShifts[hue], 0);
}
},
},
@@ -308,69 +308,12 @@ let paletteAppSpec = {
// Update page URL
this.permalink.updateLocation();
if (this.saved) {
this.save({ silent: true });
}
this.unsavedChanges = true;
},
},
},
methods: {
getPalette() {
return { id: this.paletteId, uid: this.uid, search: location.search };
},
save({ silent } = {}) {
let title = silent
? (this.saved?.title ?? this.paletteTitle)
: prompt('Palette title:', `${this.paletteTitle} (tweaked)`);
if (!title) {
return;
}
let uid = this.uid;
if (!uid) {
// First time saving
this.uid = uid = sidebar.palette.getUid();
this.permalink.set('uid', uid);
this.permalink.updateLocation();
}
let palette = { ...this.getPalette(), uid, title };
sidebar.palette.save(palette, this.saved);
this.saved = palette;
},
rename() {
if (!this.saved) {
return;
}
let newTitle = prompt('New title:', this.saved.title);
if (!newTitle) {
return;
}
this.saved.title = newTitle;
sidebar.palette.save(this.saved);
},
deleteSaved() {
sidebar.palette.delete(this.saved);
},
postDelete() {
this.saved = null;
this.permalink.delete('uid');
this.uid = undefined;
this.permalink.updateLocation();
},
/**
* Remove a specific tweak or all tweaks
* @param {string} [param] - The tweak to remove. If not provided, all tweaks are removed.
@@ -399,28 +342,7 @@ let paletteAppSpec = {
},
directives: {
// Like v-text, but doesn't complain if the element has content,
// making it possible to use in a PE fashion, with the contents being the fallback
content(el, { value, arg }) {
if (!el.dataset.fallback) {
// Store the original content as a fallback the first time
el.dataset.fallback = el.textContent;
}
if (value === '') {
value = el.dataset.fallback;
} else {
if (arg === 'number') {
value = Number(value).toLocaleString(undefined, { maximumSignificantDigits: 2 });
}
}
if (arg === 'html') {
el.innerHTML = value;
} else {
el.textContent = value;
}
},
content,
},
compilerOptions: {

View File

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

View File

@@ -12,11 +12,37 @@ Components with the <wa-badge variant="warning" pill>Experimental</wa-badge> bad
During the alpha period, things might break! We take breaking changes very seriously, but sometimes they're necessary to make the final product that much better. We appreciate your patience!
:::
## Next
## 3.0.0-alpha.12
- Fixed `wa-pill` class for text fields
- Fixed `pill` style for `<wa-input>` elements
### 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](/docs/utilities/fouce) to prevent FOUCE
- Added the [`allDefined()` utility](/docs/usage/#all-defined) for awaiting component registration
### Bug fixes
- 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 a bug in `<wa-select multiple>` that sometimes resulted in empty `<div>` elements being output
- Fixed a bug where changing a `<wa-option>` label wouldn't update the display label in `<wa-select>`
- Added default spacing to icons slotted into `<wa-tab>`
- Lots of fixes around pill-shaped elements:
- Fixed the `wa-pill` class for text fields
- Fixed `pill` style for `<wa-input>` and `<wa-radio-button>` elements
- 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
- Fixed alignment of `<wa-dropdown>` inside button groups
- Removed close watcher logic to backdrop hide animation bugs in `<wa-dialog>` and `<wa-drawer>`; this logic is already handled and we'll revisit `CloseWatcher` when browser support is better and behaviors are consistent
- Revert `<wa-dialog>` structure and CSS to fix clipped content in dialogs (WA-A #123) and light dismiss in iOS Safari (WA-A #201)
- 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-progress>` that prevented Safari from animation progress changes
- Fixed the missing indeterminate icon in [native checkbox styles](/docs/native/checkbox)
- Fixed a bug in `<wa-radio>` where elements would stack instead of display inline
- Docs fixes:
- Fixed the search dialog's styles so it doesn't jump around as you search
- Theme cards now have icons
## 3.0.0-alpha.11

View File

@@ -0,0 +1,71 @@
---
title: CSS Properties Benchmark
unlisted: true
wide: true
---
{% set icons = {
check: '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="14" viewBox="0 0 448 512"><path d="M438.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L160 338.7 393.4 105.4c12.5-12.5 32.8-12.5 45.3 0z"/></svg>',
'chevron-down': '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><path d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z"/></svg>',
'chevron-left': '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="10" viewBox="0 0 320 512"><path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z"/></svg>',
'chevron-right': '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="10" viewBox="0 0 320 512"><path d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z"/></svg>',
circle: '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512z"/></svg>',
'eye-dropper': '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><path d="M341.6 29.2L240.1 130.8l-9.4-9.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-9.4-9.4L482.8 170.4c39-39 39-102.2 0-141.1s-102.2-39-141.1 0zM55.4 323.3c-15 15-23.4 35.4-23.4 56.6v42.4L5.4 462.2c-8.5 12.7-6.8 29.6 4 40.4s27.7 12.5 40.4 4L89.7 480h42.4c21.2 0 41.6-8.4 56.6-23.4L309.4 335.9l-45.3-45.3L143.4 411.3c-3 3-7.1 4.7-11.3 4.7H96V379.9c0-4.2 1.7-8.3 4.7-11.3L221.4 247.9l-45.3-45.3L55.4 323.3z"/></svg>',
'grip-vertical': '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="10" viewBox="0 0 320 512"><path d="M40 352l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40zm192 0l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40zM40 320c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0zM232 192l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40zM40 160c-22.1 0-40-17.9-40-40L0 72C0 49.9 17.9 32 40 32l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0zM232 32l48 0c22.1 0 40 17.9 40 40l0 48c0 22.1-17.9 40-40 40l-48 0c-22.1 0-40-17.9-40-40l0-48c0-22.1 17.9-40 40-40z"/></svg>',
indeterminate: '<svg part="indeterminate-icon" class="icon" viewBox="0 0 16 16"><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"><g stroke="currentColor" stroke-width="2"><g transform="translate(2.285714, 6.857143)"><path d="M10.2857143,1.14285714 L1.14285714,1.14285714"></path></g></g></g></svg>',
minus: '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="14" viewBox="0 0 448 512"><path d="M432 256c0 17.7-14.3 32-32 32L48 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l352 0c17.7 0 32 14.3 32 32z"/></svg>',
pause: '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="10" viewBox="0 0 320 512"><path d="M48 64C21.5 64 0 85.5 0 112V400c0 26.5 21.5 48 48 48H80c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48H48zm192 0c-26.5 0-48 21.5-48 48V400c0 26.5 21.5 48 48 48h32c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48H240z"/></svg>',
play: '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="12" viewBox="0 0 384 512"><path d="M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80V432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z"/></svg>',
star: '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="18" viewBox="0 0 576 512"><path d="M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L438.5 329 542.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z"/></svg>',
user: '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="14" viewBox="0 0 448 512"><path d="M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512H418.3c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304H178.3z"/></svg>',
xmark: '<svg xmlns="http://www.w3.org/2000/svg" height="16" width="12" viewBox="0 0 384 512"><path d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"/></svg>'
} %}
<style>
.icon-tests {
font-size: .5rem;
line-height: 1;
}
wa-icon {
transition: 1s font-size;
&:hover {
font-size: 1rem;
}
}
</style>
{% set repetitions = 200 %}
<h2>Setting everything via attributes</h2>
<div class="icon-tests">
{% for icon, svg in icons %}
{% for i in range(repetitions) %}
<wa-icon name="{{ icon }}" variant="solid" family="classic"></wa-icon>
{% endfor %}
{% endfor %}
</div>
<h2>Setting variant & family via CSS</h2>
<div class="icon-tests" style="--wa-icon-variant: regular; --wa-icon-family: classic">
{% for icon, svg in icons %}
{% for i in range(repetitions) %}
<wa-icon name="{{ icon }}"></wa-icon>
{% endfor %}
{% endfor %}
</div>
<h2>Setting name via CSS</h2>
<div class="icon-tests">
{% for icon, svg in icons %}
<span style="--wa-icon-name: {{ icon }}">
{% for i in range(repetitions) %}
<wa-icon variant="solid" family="classic"></wa-icon>
{% endfor %}
</span>
{% endfor %}
</div>

View File

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

View File

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

View File

@@ -54,8 +54,12 @@ function init() {
urlParams: new Permalink(),
};
data.urlParams.mapObject(data.params);
data.urlParams.writeTo(data.params);
// Apply params from permalink
for (let key in data.params) {
if (data.urlParams.has(key)) {
data.params[key] = data.urlParams.get(key);
}
}
if (computed.isRemixed) {
// Start with the remixing UI open if the theme has been remixed
@@ -128,7 +132,11 @@ function render(changedAspect) {
selects[aspect].value = value;
}
data.urlParams.readFrom(data.params);
for (let key in data.params) {
if (data.params[key]) {
data.urlParams.set(key, data.params[key]);
}
}
// Update demo URL
domChange(() => {

View File

@@ -3,6 +3,7 @@
"wide": true,
"tags": ["themes", "theme"],
"brand": "blue",
"icon": "theme",
"eleventyComputed": {
"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.
layout: overview
override:tags: []
categories: {tags: true}
---

View File

@@ -8,6 +8,61 @@ Web Awesome components are just regular HTML elements, or [custom elements](http
If you're new to custom elements, often referred to as "web components," this section will familiarize you with how to use them.
## Awaiting Registration
Unlike traditional frameworks, custom elements don't have a centralized initialization phase. This means you need to verify that a custom element has been properly registered before attempting to interact with its properties or methods.
### Individual components: `customElements.whenDefined()` { #when-defined}
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');
```
### All Web Awesome components: `allDefined()` { #all-defined }
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!
```
#### Advanced Usage
By default, `allDefined()` will wait for all `wa-` prefixed custom elements within the current `document` to be registered.
You can customize this behavior by passing in options:
- `root` allows you to pass in a different element to search within, or a different document entirely (defaults to `document`).
- `match` allows you to specify a custom function to determine which elements to wait for. This function should return `true` for elements you want to wait for and `false` for those you don't.
- `additionalElements` allows you to wait for custom elements to be defined that may not be present in the DOM at the time `allDefined()` is called. This can be useful for elements that are loaded dynamically via JS.
Here is an example of using `match` and `root` to await registration of Web Awesome components inside an element with an id of `sidebar`, plus a `<my-component>` element if present in the DOM, and `<wa-slider>` and `<other-slider>` elements whether present in the DOM or not:
```js
import { allDefined } from '/dist/webawesome.js';
await allDefined({
match: tagName => tagName.startsWith('wa-') || tagName === 'my-component',
root: document.getElementById('sidebar'),
additionalElements: ['wa-slider', 'other-slider']
});
```
:::warning
`additionalElements` will only wait for elements to be registered — it will not load them.
If you're using the autoloader plus custom JS to inject HTML dynamically, **you need to make sure your JS runs _before_ the `await allDefined()` call**,
otherwise you could run into a chicken and egg issue:
since the autoloader will not load elements until they are present in the DOM, the promise will never resolve and your JS to inject them will not run.
:::
## 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.
@@ -88,49 +143,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.
:::
## 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
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,34 @@
---
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
snippet: .wa-cloak
---
Often, components are shown before their logic and styles have had a chance to load, also known as a [Flash of Undefined Custom Elements](https://www.abeautifulsite.net/posts/flash-of-undefined-custom-elements/).
The FOUCE style utility (which is automatically applied if you use our [style utilities](/docs/utilities/)) automatically takes care of hiding custom elements until **both 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-cloak` 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
---
<style>
.title,
.anchor-heading a,
@@ -387,4 +385,4 @@ layout: page
&copy; Fonticons, Inc.
</div>
</footer>
</div>
</div>

13
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@shoelace-style/webawesome",
"version": "3.0.0-alpha.11",
"version": "3.0.0-alpha.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@shoelace-style/webawesome",
"version": "3.0.0-alpha.11",
"version": "3.0.0-alpha.12",
"license": "MIT",
"dependencies": {
"@ctrl/tinycolor": "^4.1.0",
@@ -15,7 +15,8 @@
"@shoelace-style/localize": "^3.2.1",
"composed-offset-position": "^0.0.6",
"lit": "^3.2.1",
"qr-creator": "^1.0.0"
"qr-creator": "^1.0.0",
"style-observer": "^0.0.7"
},
"devDependencies": {
"@11ty/eleventy": "3.0.0",
@@ -13085,6 +13086,12 @@
"node": ">=0.8.0"
}
},
"node_modules/style-observer": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/style-observer/-/style-observer-0.0.7.tgz",
"integrity": "sha512-t75H3CRy+vd5q3yqyrf/De4tkz33hPQTiCcfh0NTesI5G7kJnZ227LEYTwqjKTtaFOCJvqZcYFHpJlF8bsk3bQ==",
"license": "MIT"
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",

View File

@@ -1,7 +1,7 @@
{
"name": "@shoelace-style/webawesome",
"description": "A forward-thinking library of web components.",
"version": "3.0.0-alpha.11",
"version": "3.0.0-alpha.12",
"homepage": "https://webawesome.com/",
"author": "Web Awesome",
"license": "MIT",
@@ -52,8 +52,8 @@
"start:alpha": "node scripts/build.js --alpha --develop",
"publish-alpha-cdn": "./publish-alpha-cdn.sh",
"create": "plop --plopfile scripts/plop/plopfile.js",
"test": "web-test-runner --group default",
"test:component": "web-test-runner -- --watch --group",
"test": "CSR_ONLY=\"true\" web-test-runner --group default",
"test:component": "CSR_ONLY=\"true\" web-test-runner -- --watch --group",
"test:contrast": "cd src/styles/color && node contrast.test.js",
"test:watch": "web-test-runner --watch --group default",
"prettier": "prettier --check --log-level=warn .",
@@ -73,7 +73,8 @@
"@shoelace-style/localize": "^3.2.1",
"composed-offset-position": "^0.0.6",
"lit": "^3.2.1",
"qr-creator": "^1.0.0"
"qr-creator": "^1.0.0",
"style-observer": "^0.0.7"
},
"devDependencies": {
"@11ty/eleventy": "3.0.0",

View File

@@ -4,6 +4,7 @@ import { execSync } from 'child_process';
import { deleteAsync } from 'del';
import esbuild from 'esbuild';
import { replace } from 'esbuild-plugin-replace';
import { mkdir, readFile } from 'fs/promises';
import getPort, { portNumbers } from 'get-port';
import { globby } from 'globby';
@@ -266,6 +267,13 @@ async function regenerateBundle() {
* Generates the documentation site.
*/
async function generateDocs() {
/**
* Used by the webawesome-app to skip doc generation since it will do its own.
*/
if (process.env.SKIP_ELEVENTY === 'true') {
return;
}
spinner.start('Writing the docs');
const args = [];

View File

@@ -17,9 +17,6 @@ import styles from './badge.css';
*
* @cssproperty --background-color - The badge's background color.
* @cssproperty --border-color - The color of the badge's border.
* @cssproperty --border-radius - The radius of the badge's corners.
* @cssproperty --border-style - The style of the badge's border.
* @cssproperty --border-width - The width of the badge's border.
* @cssproperty --text-color - The color of the badge's content.
*/
@customElement('wa-badge')

View File

@@ -109,7 +109,7 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
@property({ reflect: true }) value: string | null = null;
/** When set, the underlying button will be rendered as an `<a>` with this `href` instead of a `<button>`. */
@property() href = '';
@property({ reflect: true }) href = null;
/** Tells the browser where to open the link. Only used when `href` is present. */
@property() target: '_blank' | '_parent' | '_self' | '_top';

View File

@@ -5,15 +5,16 @@
--spacing: var(--wa-space);
--border-width: var(--wa-panel-border-width);
--border-color: var(--wa-color-surface-border);
--outlined-background-color: var(--wa-color-surface-default);
--outlined-border-color: var(--wa-color-surface-border);
--border-radius: var(--wa-panel-border-radius);
--inner-border-radius: calc(var(--border-radius) - var(--border-width));
--inner-border-color: var(--outlined-border-color);
display: flex;
flex-direction: column;
background-color: var(--wa-color-surface-default);
border-color: var(--border-color);
background-color: var(--background-color, var(--wa-color-surface-default));
border-color: var(--border-color, var(--wa-color-surface-border));
border-radius: var(--border-radius);
border-style: var(--wa-panel-border-style);
box-shadow: var(--wa-shadow-s);
@@ -21,6 +22,20 @@
color: var(--wa-color-text-normal);
}
:host(:is([appearance~='accent'], .wa-accent)) {
color: var(--text-color, var(--wa-color-text-normal));
}
:host([appearance~='filled']),
:host(.wa-filled) {
--inner-border-color: oklab(from var(--outlined-border-color) l a b / 65%);
}
:host([appearance='plain']) {
--inner-border-color: transparent;
box-shadow: none;
}
/* Take care of top and bottom radii */
.image,
:host(:not([with-image])) .header,
@@ -46,10 +61,19 @@
}
}
/* Round all corners for plain appearance */
:host([appearance='plain']) .image {
border-radius: var(--inner-border-radius);
&::slotted(img) {
border-radius: inherit !important;
}
}
.header {
display: block;
border-block-end-style: inherit;
border-block-end-color: var(--border-color);
border-block-end-color: var(--inner-border-color);
border-block-end-width: var(--border-width);
padding: calc(var(--spacing) / 2) var(--spacing);
}
@@ -62,7 +86,7 @@
.footer {
display: block;
border-block-start-style: inherit;
border-block-start-color: var(--border-color);
border-block-start-color: var(--inner-border-color);
border-block-start-width: var(--border-width);
padding: var(--spacing);
}

View File

@@ -2,6 +2,7 @@ import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { HasSlotController } from '../../internal/slot.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import appearanceStyles from '../../styles/utilities/appearance.css';
import sizeStyles from '../../styles/utilities/size.css';
import styles from './card.css';
@@ -22,19 +23,24 @@ import styles from './card.css';
* @csspart footer - The container that wraps the card's footer.
*
* @cssproperty [--border-radius=var(--wa-panel-border-radius)] - The radius for the card's corners. Expects a single value.
* @cssproperty [--border-color=var(--wa-color-surface-border)] - The color of the card's borders, including inner borders. Expects a single value.
* @cssproperty [--border-color=var(--wa-color-surface-border)] - The color of the card's borders. Expects a single value.
* @cssproperty [--inner-border-color=var(--wa-color-surface-border)] - The color of the card's inner borders, e.g. those separating headers and footers from the main content. Expects a single value.
* @cssproperty [--border-width=var(--wa-panel-border-width)] - The width of the card's borders. Expects a single value.
* @cssproperty [--spacing=var(--wa-space)] - The amount of space around and between sections of the card. Expects a single value.
*/
@customElement('wa-card')
export default class WaCard extends WebAwesomeElement {
static shadowStyle = [sizeStyles, styles];
static shadowStyle = [sizeStyles, appearanceStyles, styles];
private readonly hasSlotController = new HasSlotController(this, 'footer', 'header', 'image');
/** The component's size. Will be inherited by any descendants with a `size` attribute. */
@property({ reflect: true, initial: 'medium' }) size: 'small' | 'medium' | 'large' | 'inherit' = 'inherit';
/** The card's visual appearance. */
@property({ reflect: true })
appearance: 'accent' | 'filled' | 'outlined' | 'plain' = 'outlined';
/** Renders the card with a header. Only needed for SSR, otherwise is automatically added. */
@property({ attribute: 'with-header', type: Boolean, reflect: true }) withHeader = false;

View File

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

View File

@@ -1,24 +1,89 @@
:host {
--background-color: var(--wa-color-surface-raised);
--border-radius: var(--wa-panel-border-radius);
--box-shadow: var(--wa-shadow-l);
--width: 31rem;
--spacing: var(--wa-space-xl);
--show-duration: 200ms;
--hide-duration: 200ms;
display: contents;
}
:host(:not([open])) {
display: none;
}
dialog {
width: inherit;
max-width: inherit;
max-height: inherit;
background-color: inherit;
border-radius: inherit;
border: inherit;
box-shadow: inherit;
padding: inherit;
:host([open]) {
display: block;
}
.dialog {
display: flex;
flex-direction: column;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: var(--width);
max-width: calc(100% - var(--wa-space-2xl));
max-height: calc(100% - var(--wa-space-2xl));
background-color: var(--background-color);
border-radius: var(--border-radius);
border: none;
box-shadow: var(--box-shadow);
padding: 0;
margin: auto;
transition: inherit;
&.show {
animation: show-dialog var(--show-duration) ease;
&::backdrop {
animation: show-backdrop var(--show-duration, 200ms) ease;
}
}
&.hide {
animation: show-dialog var(--hide-duration) ease reverse;
&::backdrop {
animation: show-backdrop var(--hide-duration, 200ms) ease reverse;
}
}
&.pulse {
animation: pulse 250ms ease;
}
}
.dialog:focus {
outline: none;
}
/* Ensure there's enough vertical padding for phones that don't update vh when chrome appears (e.g. iPhone) */
@media screen and (max-width: 420px) {
.dialog {
max-height: 80vh;
}
}
.dialog--open {
display: flex;
opacity: 1;
}
.header {
flex: 0 0 auto;
display: flex;
flex-wrap: nowrap;
padding: var(--spacing);
padding-block-end: 0;
}
.title {
align-self: center;
flex: 1 1 auto;
font-family: inherit;
font-size: var(--wa-font-size-l);
font-weight: var(--wa-font-weight-heading);
line-height: var(--wa-line-height-condensed);
margin: 0;
}
.header-actions {
@@ -28,13 +93,81 @@ dialog {
flex-wrap: wrap;
justify-content: end;
gap: var(--wa-space-2xs);
margin-inline-start: auto;
padding-inline-start: var(--spacing);
}
wa-icon-button,
::slotted(wa-icon-button) {
flex: 0 0 auto;
display: flex;
align-items: center;
font-size: var(--wa-font-size-m);
.header-actions wa-icon-button,
.header-actions ::slotted(wa-icon-button) {
flex: 0 0 auto;
display: flex;
align-items: center;
font-size: var(--wa-font-size-m);
}
.body {
flex: 1 1 auto;
display: block;
padding: var(--spacing);
overflow: auto;
-webkit-overflow-scrolling: touch;
}
.footer {
flex: 0 0 auto;
display: flex;
flex-wrap: wrap;
gap: var(--wa-space-xs);
justify-content: end;
padding: var(--spacing);
padding-block-start: 0;
}
.footer ::slotted(wa-button:not(:first-of-type)) {
margin-inline-start: var(--wa-spacing-xs);
}
.dialog::backdrop {
/*
NOTE: the ::backdrop element doesn't inherit properly in Safari yet, but it will in 17.4! At that time, we can
remove the fallback values here.
*/
background-color: var(--wa-color-overlay-modal, rgb(0 0 0 / 0.25));
}
@keyframes pulse {
0% {
scale: 1;
}
50% {
scale: 1.02;
}
100% {
scale: 1;
}
}
@keyframes show-dialog {
from {
opacity: 0;
scale: 0.8;
}
to {
opacity: 1;
scale: 1;
}
}
@keyframes show-backdrop {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@media (forced-colors: active) {
.dialog {
border: solid 1px white;
}
}

View File

@@ -1,5 +1,6 @@
import { html, isServer } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { customElement, property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { WaAfterHideEvent } from '../../events/after-hide.js';
import { WaAfterShowEvent } from '../../events/after-show.js';
import { WaHideEvent } from '../../events/hide.js';
@@ -8,7 +9,6 @@ import { animateWithClass } from '../../internal/animate.js';
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll.js';
import { watch } from '../../internal/watch.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import dialogStyles from '../../styles/native/dialog.css';
import { LocalizeController } from '../../utilities/localize.js';
import '../icon-button/icon-button.js';
import styles from './dialog.css';
@@ -35,7 +35,6 @@ import styles from './dialog.css';
* behavior such as data loss.
* @event wa-after-hide - Emitted after the dialog closes and all animations are complete.
*
* @csspart base - The inner `<dialog>` used to render this component.
* @csspart header - The dialog's header. This element wraps the title and header actions.
* @csspart header-actions - Optional actions to add to the header. Works best with `<wa-icon-button>`.
* @csspart title - The dialog's title.
@@ -44,16 +43,22 @@ import styles from './dialog.css';
* @csspart body - The dialog's body.
* @csspart footer - The dialog's footer.
*
* @cssproperty --background-color - The dialog's background color.
* @cssproperty --border-radius - The radius of the dialog's corners.
* @cssproperty --box-shadow - The shadow effects around the edges of the dialog.
* @cssproperty --spacing - The amount of space around and between the dialog's content.
* @cssproperty --width - The preferred width of the dialog. Note that the dialog will shrink to accommodate smaller screens.
* @cssproperty [--show-duration=200ms] - The animation duration when showing the dialog.
* @cssproperty [--hide-duration=200ms] - The animation duration when hiding the dialog.
*/
@customElement('wa-dialog')
export default class WaDialog extends WebAwesomeElement {
static shadowStyle = [dialogStyles, styles];
static shadowStyle = styles;
private readonly localize = new LocalizeController(this);
private originalTrigger: HTMLElement | null;
private closeWatcher: CloseWatcher | null;
@query('dialog') dialog: HTMLDialogElement;
@query('.dialog') dialog: HTMLDialogElement;
/**
* Indicates whether or not the dialog is open. You can toggle this attribute to show and hide the dialog, or you can
@@ -76,9 +81,6 @@ export default class WaDialog extends WebAwesomeElement {
/** When enabled, the dialog will be closed when the user clicks outside of it. */
@property({ attribute: 'light-dismiss', type: Boolean }) lightDismiss = false;
@state()
hasOpened = this.open;
firstUpdated() {
if (this.open) {
this.addOpenListeners();
@@ -100,12 +102,14 @@ export default class WaDialog extends WebAwesomeElement {
if (waHideEvent.defaultPrevented) {
this.open = true;
animateWithClass(this.dialog, 'wa-dialog-pulse');
animateWithClass(this.dialog, 'pulse');
return;
}
this.removeOpenListeners();
await animateWithClass(this.dialog, 'hide');
this.open = false;
this.dialog.close();
unlockBodyScrolling(this);
@@ -120,16 +124,7 @@ export default class WaDialog extends WebAwesomeElement {
}
private addOpenListeners() {
if ('CloseWatcher' in window) {
this.closeWatcher?.destroy();
this.closeWatcher = new CloseWatcher();
this.closeWatcher.onclose = () => {
this.requestClose(this.dialog);
};
} else {
this.closeWatcher?.destroy();
document.addEventListener('keydown', this.handleDocumentKeyDown);
}
document.addEventListener('keydown', this.handleDocumentKeyDown);
}
private removeOpenListeners() {
@@ -161,7 +156,7 @@ export default class WaDialog extends WebAwesomeElement {
if (this.lightDismiss) {
this.requestClose(this.dialog);
} else {
await animateWithClass(this.dialog, 'wa-dialog-pulse');
await animateWithClass(this.dialog, 'pulse');
}
}
}
@@ -198,7 +193,6 @@ export default class WaDialog extends WebAwesomeElement {
this.addOpenListeners();
this.originalTrigger = document.activeElement as HTMLElement;
this.open = true;
this.hasOpened = true;
this.dialog.showModal();
lockBodyScrolling(this);
@@ -211,20 +205,28 @@ export default class WaDialog extends WebAwesomeElement {
}
});
await animateWithClass(this.dialog, 'show');
this.dispatchEvent(new WaAfterShowEvent());
}
render() {
return html`
<dialog
part="base"
part="dialog"
class=${classMap({
dialog: true,
'dialog--open': this.open,
'dialog--with-header': this.withHeader,
'dialog--with-footer': this.withFooter,
})}
@cancel=${this.handleDialogCancel}
@click=${this.handleDialogClick}
@pointerdown=${this.handleDialogPointerDown}
>
${this.withHeader
? html`
<header part="header">
<header part="header" class="header">
<h2 part="title" class="title" id="title">
<!-- If there's no label, use an invisible character to prevent the header from collapsing -->
<slot name="label"> ${this.label.length > 0 ? this.label : String.fromCharCode(65279)} </slot>
@@ -246,11 +248,11 @@ export default class WaDialog extends WebAwesomeElement {
`
: ''}
<slot part="body" class="body"></slot>
<div part="body" class="body"><slot></slot></div>
${this.withFooter
? html`
<footer part="footer">
<footer part="footer" class="footer">
<slot name="footer"></slot>
</footer>
`
@@ -262,7 +264,7 @@ export default class WaDialog extends WebAwesomeElement {
// Ugly, but it fixes light dismiss in Safari: https://bugs.webkit.org/show_bug.cgi?id=267688
if (!isServer) {
document.body.addEventListener('pointerdown', () => {
document.addEventListener('pointerdown', () => {
/* empty */
});
}

View File

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

View File

@@ -50,8 +50,6 @@ export default class WaDropdown extends WebAwesomeElement {
@query('#trigger') trigger: HTMLSlotElement;
@query('.panel') panel: HTMLSlotElement;
private closeWatcher: CloseWatcher | null;
/**
* Indicates whether or not the dropdown is open. You can toggle this attribute to show and hide the dropdown, or you
* can use the `show()` and `hide()` methods and this attribute will reflect the dropdown's open state.
@@ -148,7 +146,7 @@ export default class WaDropdown extends WebAwesomeElement {
private handleKeyDown = (event: KeyboardEvent) => {
// Close when escape is pressed inside an open dropdown. We need to listen on the panel itself and stop propagation
// in case any ancestors are also listening for this key.
if (this.open && event.key === 'Escape' && !this.closeWatcher) {
if (this.open && event.key === 'Escape') {
event.stopPropagation();
this.hide();
this.focusOnTrigger();
@@ -344,16 +342,7 @@ export default class WaDropdown extends WebAwesomeElement {
addOpenListeners() {
this.panel.addEventListener('wa-select', this.handlePanelSelect);
if ('CloseWatcher' in window) {
this.closeWatcher?.destroy();
this.closeWatcher = new CloseWatcher();
this.closeWatcher.onclose = () => {
this.hide();
this.focusOnTrigger();
};
} else {
this.panel.addEventListener('keydown', this.handleKeyDown);
}
this.panel.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('keydown', this.handleDocumentKeyDown);
document.addEventListener('mousedown', this.handleDocumentMouseDown);
}
@@ -365,7 +354,6 @@ export default class WaDropdown extends WebAwesomeElement {
}
document.removeEventListener('keydown', this.handleDocumentKeyDown);
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
this.closeWatcher?.destroy();
}
@watch('open', { waitUntilFirstUpdate: true })

View File

@@ -48,21 +48,21 @@ export default class WaIcon extends WebAwesomeElement {
@state() private svg: SVGElement | HTMLTemplateResult | null = null;
/** The name of the icon to draw. Available names depend on the icon library being used. */
@property({ reflect: true }) name?: string;
@property({ cssProperty: '--wa-icon-name' }) name?: string;
/**
* The family of icons to choose from. For Font Awesome Free (default), valid options include `classic` and `brands`.
* For Font Awesome Pro subscribers, valid options include, `classic`, `sharp`, `duotone`, and `brands`. Custom icon
* libraries may or may not use this property.
*/
@property({ reflect: true }) family: string;
@property({ cssProperty: '--wa-icon-family' }) family: string;
/**
* The name of the icon's variant. For Font Awesome, valid options include `thin`, `light`, `regular`, and `solid` for
* the `classic` and `sharp` families. Some variants require a Font Awesome Pro subscription. Custom icon libraries
* may or may not use this property.
*/
@property({ reflect: true }) variant: string;
@property({ cssProperty: '--wa-icon-variant' }) variant: string;
/** Draws the icon in a fixed-width both. */
@property({ attribute: 'fixed-width', type: Boolean, reflect: true }) fixedWidth: false;
@@ -80,14 +80,16 @@ export default class WaIcon extends WebAwesomeElement {
@property() label = '';
/** The name of a registered custom icon library. */
@property({ reflect: true }) library = 'default';
@property({ cssProperty: '--wa-icon-library', default: 'default' }) library = 'default';
connectedCallback() {
super.connectedCallback();
watchIcon(this);
}
firstUpdated() {
firstUpdated(changedProperties: PropertyValues<this>) {
super.firstUpdated(changedProperties);
this.initialRender = true;
this.setIcon();
}

View File

@@ -106,6 +106,11 @@ export default class WaOption extends WebAwesomeElement {
}
private handleDefaultSlotChange() {
// Tell the controller to update the label
if (customElements.get('wa-select')) {
this.closest('wa-select')?.selectionChanged();
}
this.updateDefaultLabel();
if (this.isInitialized) {
@@ -123,7 +128,7 @@ export default class WaOption extends WebAwesomeElement {
private handleHover = (event: Event) => {
// 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') {
this.toggleCustomState('hover', true);
} else if (event.type === 'mouseleave') {

View File

@@ -97,6 +97,7 @@ slot:not([name]) {
:host([disable-sticky~='menu']) [part~='menu'] {
position: static;
overflow: unset;
z-index: unset;
}
:host([disable-sticky~='aside']) [part~='aside'],

View File

@@ -16,7 +16,7 @@
}
.indicator {
width: calc(var(--value, 0) * 1%);
width: var(--percentage);
display: flex;
align-items: center;
justify-content: center;
@@ -26,9 +26,7 @@
overflow: hidden;
line-height: 1;
font-weight: var(--wa-font-weight-semibold);
transition: var(--wa-transition-slow, 200ms) var(--wa-transition-easing, ease);
transition: 1s;
/* transition-property: width, background; */
transition: all var(--wa-transition-slow, 200ms) var(--wa-transition-easing, ease);
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');
});
it('uses the value parameter to set the custom property on the indicator', () => {
expect(indicator.style.getPropertyValue('--value')).to.equal('25');
it('uses the value parameter to set the custom property on the indicator', async () => {
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 { customElement, property } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { clamp } from '../../internal/math.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import { LocalizeController } from '../../utilities/localize.js';
import styles from './progress-bar.css';
@@ -33,6 +35,16 @@ export default class WaProgressBar extends WebAwesomeElement {
/** A custom label for assistive devices. */
@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() {
return html`
<div
@@ -45,7 +57,7 @@ export default class WaProgressBar extends WebAwesomeElement {
aria-valuemax="100"
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> ` : ''}
</div>
</div>

View File

@@ -32,7 +32,6 @@
/*
* Checked buttons
*/
:host([checked]) {
--indicator-color: var(--wa-form-control-activated-color);
--indicator-width: var(--wa-border-width-s);
@@ -43,3 +42,41 @@
--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 sizeStyles from '../../styles/utilities/size.css';
import variantStyles from '../../styles/utilities/variants.css';
import buttonStyles from '../button/button.css';
import styles from './radio-button.css';
/**
@@ -49,7 +48,7 @@ import styles from './radio-button.css';
*/
@customElement('wa-radio-button')
export default class WaRadioButton extends WebAwesomeFormAssociatedElement {
static shadowStyle = [variantStyles, appearanceStyles, sizeStyles, nativeStyles, buttonStyles, styles];
static shadowStyle = [variantStyles, appearanceStyles, sizeStyles, nativeStyles, styles];
static rectProxy = 'input';
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 radios - The wrapper than surrounds radio items, styled as a flex container by default.
* @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')
export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
@@ -65,7 +63,6 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
@query('slot:not([name])') defaultSlot: HTMLSlotElement;
@state() private hasButtonGroup = false;
@state() private hasRadioButtons = false;
/**
@@ -150,7 +147,7 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
clickedRadio.checked = true;
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) {
if (clickedRadio === radio) {
continue;
@@ -158,7 +155,7 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
radio.checked = false;
if (!hasButtonGroup) {
if (!hasRadioButtons) {
radio.setAttribute('tabindex', '-1');
}
}
@@ -183,6 +180,15 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
// Detect the presence of radio buttons
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(
// Sync the checked state and size
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 (this.hasButtonGroup) {
if (this.hasRadioButtons) {
const buttonRadio = radios[0].shadowRoot?.querySelector('button');
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');
if (buttonGroup) {
@@ -276,12 +280,12 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
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 => {
radio.checked = false;
if (!hasButtonGroup) {
if (!hasRadioButtons) {
radio.setAttribute('tabindex', '-1');
}
});
@@ -289,7 +293,7 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
this.value = radios[index].value;
radios[index].checked = true;
if (!hasButtonGroup) {
if (!hasRadioButtons) {
radios[index].setAttribute('tabindex', '0');
radios[index].focus();
} else {
@@ -351,7 +355,7 @@ export default class WaRadioGroup extends WebAwesomeFormAssociatedElement {
<slot
part="form-control-input"
class=${classMap({
'wa-button-group': this.hasButtonGroup,
'wa-button-group': this.hasRadioButtons,
'wa-button-group-vertical': this.orientation === 'vertical',
})}
@slotchange=${this.syncRadioElements}

View File

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

View File

@@ -22,6 +22,7 @@ import appearanceStyles from '../../styles/utilities/appearance.css';
import sizeStyles from '../../styles/utilities/size.css';
import { LocalizeController } from '../../utilities/localize.js';
import '../icon/icon.js';
import '../option/option.js';
import type WaOption from '../option/option.js';
import '../popup/popup.js';
import type WaPopup from '../popup/popup.js';
@@ -37,6 +38,7 @@ import styles from './select.css';
* @dependency wa-icon
* @dependency wa-popup
* @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 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 typeToSelectString = '';
private typeToSelectTimeout: number;
private closeWatcher: CloseWatcher | null;
@query('.select') popup: WaPopup;
@query('.combobox') combobox: HTMLSlotElement;
@@ -291,7 +292,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
?pill=${this.pill}
size=${this.size}
removable
@wa-remove=${(event: WaRemoveEvent) => this.handleTagRemove(event, option)}
>
${option.label}
</wa-tag>
@@ -321,17 +321,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
if (this.getRootNode() !== document) {
this.getRootNode().addEventListener('focusin', this.handleDocumentFocusIn);
}
if ('CloseWatcher' in window) {
this.closeWatcher?.destroy();
this.closeWatcher = new CloseWatcher();
this.closeWatcher.onclose = () => {
if (this.open) {
this.hide();
this.displayInput.focus({ preventScroll: true });
}
};
}
}
private removeOpenListeners() {
@@ -342,8 +331,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
if (this.getRootNode() !== document) {
this.getRootNode().removeEventListener('focusin', this.handleDocumentFocusIn);
}
this.closeWatcher?.destroy();
}
private handleFocus() {
@@ -587,10 +574,33 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
this.setSelectedOptions(allOptions.filter(el => value.includes(el.value)));
}
private handleTagRemove(event: WaRemoveEvent, option: WaOption) {
private handleTagRemove(event: WaRemoveEvent, directOption?: WaOption) {
event.stopPropagation();
if (!this.disabled) {
if (this.disabled) return;
// Use the directly provided option if available (from getTag method)
let option = directOption;
// If no direct option was provided, find the option from the event path
if (!option) {
const tagElement = (event.target as Element).closest('wa-tag[part~=tag]');
if (tagElement) {
// Find the index of this tag among all tags
const tagsContainer = this.shadowRoot?.querySelector('[part="tags"]');
if (tagsContainer) {
const allTags = Array.from(tagsContainer.children);
const index = allTags.indexOf(tagElement as HTMLElement);
if (index >= 0 && index < this.selectedOptions.length) {
option = this.selectedOptions[index];
}
}
}
}
if (option) {
this.toggleOptionSelection(option, false);
// Emit after updating
@@ -662,9 +672,9 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
this.selectionChanged();
}
// This method must be called whenever the selection changes. It will update the selected options cache, the current
// value, and the display value
private selectionChanged() {
// @internal This method must be called whenever the selection changes. It will update the selected options cache, the
// current value, and the display value. The option component uses it internally to update labels as they change.
public selectionChanged() {
const options = this.getAllOptions();
// Update selected options cache
@@ -703,14 +713,13 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
this.updateValidity();
});
}
protected get tags() {
return this.selectedOptions.map((option, index) => {
if (index < this.maxOptionsVisible || this.maxOptionsVisible <= 0) {
const tag = this.getTag(option, index);
// Wrap so we can handle the remove
return html`<div @wa-remove=${(e: WaRemoveEvent) => this.handleTagRemove(e, option)}>
${typeof tag === 'string' ? unsafeHTML(tag) : tag}
</div>`;
if (!tag) return null;
return typeof tag === 'string' ? unsafeHTML(tag) : tag;
} else if (index === this.maxOptionsVisible) {
// Hit tag limit
return html`
@@ -726,7 +735,7 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
>
`;
}
return html``;
return null;
});
}
@@ -926,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. -->
${this.multiple && this.hasUpdated ? html`<div part="tags" class="tags">${this.tags}</div>` : ''}
${this.multiple && this.hasUpdated
? html`<div part="tags" class="tags" @wa-remove=${this.handleTagRemove}>${this.tags}</div>`
: ''}
<input
class="value-input"

View File

@@ -15,6 +15,14 @@
-webkit-user-select: none;
cursor: pointer;
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 {

View File

@@ -44,7 +44,6 @@ export default class WaTooltip extends WebAwesomeElement {
static dependencies = { 'wa-popup': WaPopup };
private hoverTimeout: number;
private closeWatcher: CloseWatcher | null;
@query('slot:not([name])') defaultSlot: HTMLSlotElement;
@query('.body') body: HTMLElement;
@@ -127,7 +126,6 @@ export default class WaTooltip extends WebAwesomeElement {
super.disconnectedCallback();
// Cleanup this event in case the tooltip is removed while open
this.closeWatcher?.destroy();
document.removeEventListener('keydown', this.handleDocumentKeyDown);
this.eventController.abort();
@@ -212,15 +210,7 @@ export default class WaTooltip extends WebAwesomeElement {
return;
}
if ('CloseWatcher' in window) {
this.closeWatcher?.destroy();
this.closeWatcher = new CloseWatcher();
this.closeWatcher.onclose = () => {
this.hide();
};
} else {
document.addEventListener('keydown', this.handleDocumentKeyDown, { signal: this.eventController.signal });
}
document.addEventListener('keydown', this.handleDocumentKeyDown, { signal: this.eventController.signal });
this.body.hidden = false;
this.popup.active = true;
@@ -237,7 +227,6 @@ export default class WaTooltip extends WebAwesomeElement {
return;
}
this.closeWatcher?.destroy();
document.removeEventListener('keydown', this.handleDocumentKeyDown);
await animateWithClass(this.popup.popup, 'hide-with-scale');

View File

@@ -1,7 +1,9 @@
import type { CSSResult, CSSResultGroup, PropertyDeclaration, PropertyValues } from 'lit';
import { LitElement, defaultConverter, isServer, unsafeCSS } from 'lit';
import { property } from 'lit/decorators.js';
import { ElementStyleObserver } from 'style-observer';
import componentStyles from '../styles/shadow/component.css';
import { getComputedStyle } from './computedStyle.js';
// Augment Lit's module
declare module 'lit' {
@@ -11,6 +13,11 @@ declare module 'lit' {
*/
default?: any;
initial?: any;
/**
* Indicates whether the property should reflect to a CSS custom property.
*/
cssProperty?: string;
}
}
@@ -72,6 +79,99 @@ export default class WebAwesomeElement extends LitElement {
internals: ElementInternals;
/** Metadata about CSS-settable props on this element */
private cssProps: Record<PropertyKey, { setVia?: 'css' | 'attribute' | 'js'; updating?: boolean }> = {};
private computedStyle: CSSStyleDeclaration | null = null;
private styleObserver: ElementStyleObserver | null = null;
connectedCallback(): void {
super.connectedCallback();
// Set the initial computed styles
const Self = this.constructor as typeof WebAwesomeElement;
let cssProps = Object.keys(Self.cssProps);
if (cssProps.length > 0) {
let properties: string[] = [];
if (Object.keys(this.cssProps).length === 0) {
// First time connected, initialize
this.cssProps = Object.fromEntries(
cssProps.map(property => {
let setVia = this.getSetVia(property);
return [property, { setVia }];
}),
);
}
for (let property in this.cssProps) {
let setVia = this.cssProps[property].setVia;
if (!setVia || setVia === 'css') {
// No attribute set, observe CSS property
properties.push(property);
}
}
this.handleCSSPropertyChange(properties);
this.styleObserver ??= new ElementStyleObserver(this, (records: object[]) => {
let cssProperties = new Set(records.map((record: { property: string }) => record.property));
// Map CSS properties to prop names
let properties = cssProps.filter(property => {
let cssProperty = Self.cssProps[property].cssProperty as string;
return cssProperties.has(cssProperty);
});
this.handleCSSPropertyChange(properties);
});
this.styleObserver.unobserve();
this.styleObserver.observe(properties.map(property => Self.cssProps[property].cssProperty as string));
}
}
/**
* Respond to CSS property changes for CSS properties reflecting props
* @param [properties] - Prop names. Defaults to all CSS-reflected props.
* @void
*/
handleCSSPropertyChange(properties?: PropertyKey | PropertyKey[]) {
const Self = this.constructor as typeof WebAwesomeElement;
properties ??= Object.keys(Self.cssProps);
properties = Array.isArray(properties) ? properties : [properties];
if (properties.length === 0) {
return;
}
this.computedStyle ??= getComputedStyle(this);
for (let property of properties) {
let propOptions = Self.cssProps[property];
let cssProperty = propOptions?.cssProperty;
let meta = this.cssProps[property];
if (!cssProperty || (meta.setVia && meta.setVia !== 'css')) {
continue;
}
const value = this.computedStyle?.getPropertyValue(cssProperty);
// if (property === 'variant' && !value) debugger;
if (value) {
meta.setVia = 'css';
meta.updating = true;
// @ts-ignore
this[property] = value.trim();
this.updateComplete.then(() => {
meta.updating = false;
});
}
}
}
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {
if (!this.#hasRecordedInitialProperties) {
(this.constructor as typeof WebAwesomeElement).elementProperties.forEach(
@@ -115,6 +215,50 @@ export default class WebAwesomeElement extends LitElement {
}
}
/**
* Get how a prop was set
* @param property - The property to check
*/
private getSetVia(property: PropertyKey): 'css' | 'js' | 'attribute' | undefined {
let Self = this.constructor as typeof WebAwesomeElement;
let setVia;
let propOptions = Self.cssProps[property];
let attribute = typeof propOptions.attribute === 'string' ? propOptions.attribute : (property as string);
if (propOptions.attribute !== false && this.hasAttribute(attribute)) {
setVia = 'attribute';
} else {
// @ts-ignore
let value = this[property as PropertyKey];
if (value !== undefined && value !== propOptions.default) {
setVia = 'js';
}
}
return setVia as 'attribute' | 'js' | 'css' | undefined;
}
protected updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
let Self = this.constructor as typeof WebAwesomeElement;
let cssProps = Object.keys(Self.cssProps);
if (cssProps.length === 0) {
return;
}
for (let [property] of changedProperties) {
let meta = this.cssProps[property];
if (meta && typeof property === 'string' && !(meta.setVia === 'css' && meta.updating)) {
// A prop is being set via JS or an attribute that was previously set via CSS
// and it's not because we're in the middle of an update
meta.setVia = this.getSetVia(property);
}
}
}
protected update(changedProperties: PropertyValues<this>): void {
try {
super.update(changedProperties);
@@ -230,6 +374,11 @@ export default class WebAwesomeElement extends LitElement {
*/
static rectProxy: undefined | string;
/**
* Props that can be set via CSS custom properties
*/
static cssProps: Record<PropertyKey, PropertyDeclaration> = {};
static createProperty(name: PropertyKey, options?: PropertyDeclaration): void {
if (options) {
if (options.initial !== undefined && options.default === undefined) {
@@ -256,8 +405,18 @@ export default class WebAwesomeElement extends LitElement {
super.createProperty(name, options);
// Wrap the default accessor with logic to return the default value if the value is null
if (options) {
if (options.cssProperty) {
// Add to the set of CSS-settable props
if (this.cssProps === WebAwesomeElement.cssProps) {
// Each class needs its own, otherwise they'd share the same object
this.cssProps = {};
}
this.cssProps[name] = options;
}
// Wrap the default accessor with logic to return the default value if the value is null
if (options.default !== undefined) {
const descriptor = Object.getOwnPropertyDescriptor(this.prototype, name as string);

View File

@@ -18,17 +18,17 @@
--wa-color-red: var(--wa-color-red-70);
--wa-color-red-key: 70;
--wa-color-orange-95: oklch(96.462% 0.02077 52.138);
--wa-color-orange-90: oklch(92.556% 0.04363 51.242);
--wa-color-orange-80: #fdbb96 /* oklch(84.396% 0.09052 50.397) */;
--wa-color-orange-70: #eb9c74 /* oklch(76.151% 0.10953 47.299) */;
--wa-color-orange-60: #cf8162 /* oklch(67.86% 0.10698 42.148) */;
--wa-color-orange-50: #aa6248 /* oklch(57.281% 0.1014 40.415) */;
--wa-color-orange-40: #864834 /* oklch(47.233% 0.08958 39.166) */;
--wa-color-orange-30: #6b3727 /* oklch(39.974% 0.07776 38.975) */;
--wa-color-orange-20: #50271a /* oklch(32.519% 0.0649 38.022) */;
--wa-color-orange-10: #32160e /* oklch(23.873% 0.04713 38.423) */;
--wa-color-orange-05: #210c06 /* oklch(18.614% 0.03797 38.589) */;
--wa-color-orange-95: #ffefe8 /* oklch(96.287% 0.01978 45.262) */;
--wa-color-orange-90: #ffe0d2 /* oklch(92.854% 0.03927 45.816) */;
--wa-color-orange-80: #fbbc9e /* oklch(84.557% 0.08373 46.569) */;
--wa-color-orange-70: #f3976f /* oklch(76.236% 0.12363 43.995) */;
--wa-color-orange-60: #d67e59 /* oklch(68.046% 0.12088 43.174) */;
--wa-color-orange-50: #ae6140 /* oklch(57.433% 0.11041 43.333) */;
--wa-color-orange-40: #89472c /* oklch(47.39% 0.09837 42.278) */;
--wa-color-orange-30: #6f351e /* oklch(40.084% 0.08944 41.51) */;
--wa-color-orange-20: #542513 /* oklch(32.696% 0.07578 40.822) */;
--wa-color-orange-10: #341408 /* oklch(23.762% 0.05577 40.619) */;
--wa-color-orange-05: #230b04 /* oklch(18.679% 0.04417 39.815) */;
--wa-color-orange: var(--wa-color-orange-70);
--wa-color-orange-key: 70;

View File

@@ -22,13 +22,13 @@
--wa-color-orange-90: oklch(92.395% 0.07984 53.06);
--wa-color-orange-80: oklch(84.389% 0.12224 47.981);
--wa-color-orange-70: oklch(76.55% 0.16521 42.512);
--wa-color-orange-60: #ea7237 /* oklch(68.444% 0.16501 44.349) */;
--wa-color-orange-50: #c0561a /* oklch(57.844% 0.15254 45.085) */;
--wa-color-orange-40: #963e05 /* oklch(47.639% 0.13153 45.898) */;
--wa-color-orange-30: oklch(40.376% 0.11554 45.517);
--wa-color-orange-20: oklch(32.94% 0.09927 45.913);
--wa-color-orange-10: oklch(24.083% 0.07743 46.027);
--wa-color-orange-05: oklch(18.817% 0.06098 48.455);
--wa-color-orange-60: #e97331 /* oklch(68.357% 0.16507 46.504) */;
--wa-color-orange-50: #bf5712 /* oklch(57.834% 0.15197 47.326) */;
--wa-color-orange-40: oklch(47.62% 0.132 48.51);
--wa-color-orange-30: oklch(40.38% 0.11554 50);
--wa-color-orange-20: oklch(32.94% 0.09927 52);
--wa-color-orange-10: oklch(24.083% 0.07743 54);
--wa-color-orange-05: oklch(18.817% 0.06098 55);
--wa-color-orange: var(--wa-color-orange-70);
--wa-color-orange-key: 70;

View File

@@ -6,45 +6,45 @@
.wa-palette-classic {
--wa-color-red-95: #fff0ef /* oklch(96.667% 0.01632 22.08) */;
--wa-color-red-90: #ffdedc /* oklch(92.735% 0.03679 21.966) */;
--wa-color-red-80: #ffb8b4 /* oklch(84.753% 0.08313 22.598) */;
--wa-color-red-70: #fd908e /* oklch(76.913% 0.13219 21.705) */;
--wa-color-red-60: #f46766 /* oklch(68.945% 0.17423 23.179) */;
--wa-color-red-50: #df2f2e /* oklch(58.922% 0.21141 26.911) */;
--wa-color-red-40: #b60000 /* oklch(48.747% 0.20003 29.234) */;
--wa-color-red-30: #910000 /* oklch(41.235% 0.16921 29.234) */;
--wa-color-red-20: #6d0000 /* oklch(33.581% 0.1378 29.234) */;
--wa-color-red-10: #450000 /* oklch(24.517% 0.1006 29.234) */;
--wa-color-red-05: #2f0000 /* oklch(19.165% 0.07864 29.234) */;
--wa-color-red-80: #ffb8b5 /* oklch(84.778% 0.083 21.686) */;
--wa-color-red-70: #fe8f8d /* oklch(76.859% 0.13466 21.762) */;
--wa-color-red-60: #f56667 /* oklch(68.982% 0.17631 22.472) */;
--wa-color-red-50: #e02c2b /* oklch(58.861% 0.21461 27.156) */;
--wa-color-red-40: #b5051a /* oklch(48.833% 0.19611 25.68) */;
--wa-color-red-30: #900015 /* oklch(41.172% 0.16676 24.609) */;
--wa-color-red-20: #6c000d /* oklch(33.479% 0.1356 24.617) */;
--wa-color-red-10: #450005 /* oklch(24.598% 0.09968 24.835) */;
--wa-color-red-05: #2f0002 /* oklch(19.218% 0.07801 25.517) */;
--wa-color-red: var(--wa-color-red-50);
--wa-color-red-key: 50;
--wa-color-orange-95: oklch(96.245% 0.04153 59.224);
--wa-color-orange-90: oklch(92.468% 0.07656 58.647);
--wa-color-orange-80: oklch(84.375% 0.11283 54.179);
--wa-color-orange-70: #fb945a /* oklch(76.369% 0.14454 48.621) */;
--wa-color-orange-60: #f26b31 /* oklch(68.509% 0.18046 41.503) */;
--wa-color-orange-50: #cf4812 /* oklch(58.288% 0.18026 38.576) */;
--wa-color-orange-40: oklch(48.175% 0.16316 38.526);
--wa-color-orange-30: oklch(40.779% 0.13925 37.899);
--wa-color-orange-20: oklch(33.129% 0.11288 38.58);
--wa-color-orange-10: oklch(24.259% 0.0831 38.502);
--wa-color-orange-05: oklch(18.969% 0.06527 38.137);
--wa-color-orange-95: #fff0e4 /* oklch(96.374% 0.0228 61.238) */;
--wa-color-orange-90: #ffe0c8 /* oklch(92.611% 0.04689 59.917) */;
--wa-color-orange-80: #ffbb89 /* oklch(84.386% 0.10217 57.161) */;
--wa-color-orange-70: #ff9342 /* oklch(76.486% 0.15964 54.102) */;
--wa-color-orange-60: #f36d00 /* oklch(68.715% 0.18774 47.79) */;
--wa-color-orange-50: #c94e00 /* oklch(58.068% 0.17131 43.217) */;
--wa-color-orange-40: #9d3800 /* oklch(47.924% 0.14534 41.739) */;
--wa-color-orange-30: #7e2900 /* oklch(40.53% 0.1259 40.51) */;
--wa-color-orange-20: #5e1c00 /* oklch(32.874% 0.1027 40.228) */;
--wa-color-orange-10: #3b0f00 /* oklch(24.125% 0.07446 40.837) */;
--wa-color-orange-05: #280700 /* oklch(18.837% 0.05933 39.827) */;
--wa-color-orange: var(--wa-color-orange-60);
--wa-color-orange-key: 60;
--wa-color-yellow-95: #fef2bf /* oklch(95.823% 0.06674 96.369) */;
--wa-color-yellow-90: #fde588 /* oklch(92.2% 0.11633 95.327) */;
--wa-color-yellow-80: #f4c34e /* oklch(83.998% 0.14252 85.76) */;
--wa-color-yellow-80: #f5c24b /* oklch(83.879% 0.14445 85.083) */;
--wa-color-yellow-70: #e9a010 /* oklch(75.825% 0.15689 75.537) */;
--wa-color-yellow-60: #de7c00 /* oklch(68.073% 0.15991 59.827) */;
--wa-color-yellow-50: #bc5908 /* oklch(57.654% 0.1491 50.241) */;
--wa-color-yellow-40: #934107 /* oklch(47.603% 0.12628 47.819) */;
--wa-color-yellow-30: #763104 /* oklch(40.324% 0.1093 46.564) */;
--wa-color-yellow-20: #572301 /* oklch(32.677% 0.08796 47.926) */;
--wa-color-yellow-10: #371300 /* oklch(24.001% 0.06559 47.77) */;
--wa-color-yellow-05: #250a00 /* oklch(18.773% 0.0519 47.039) */;
--wa-color-yellow: var(--wa-color-yellow-60);
--wa-color-yellow-key: 60;
--wa-color-yellow-60: #d78000 /* oklch(67.839% 0.15287 64.455) */;
--wa-color-yellow-50: #b26000 /* oklch(57.324% 0.13672 58.338) */;
--wa-color-yellow-40: #8a4700 /* oklch(47.183% 0.11461 56.655) */;
--wa-color-yellow-30: #6e3700 /* oklch(40.03% 0.0976 56.323) */;
--wa-color-yellow-20: #522700 /* oklch(32.54% 0.07981 55.802) */;
--wa-color-yellow-10: #331600 /* oklch(23.846% 0.05834 56.02) */;
--wa-color-yellow-05: #210d00 /* oklch(18.619% 0.04431 58.553) */;
--wa-color-yellow: var(--wa-color-yellow-70);
--wa-color-yellow-key: 70;
--wa-color-green-95: #d4fce1 /* oklch(95.554% 0.05477 155.71) */;
--wa-color-green-90: #a4f8c2 /* oklch(91.11% 0.1107 155.35) */;

View File

@@ -18,19 +18,19 @@
--wa-color-red: var(--wa-color-red-50);
--wa-color-red-key: 50;
--wa-color-orange-95: oklch(96.494% 0.0335 57.914);
--wa-color-orange-90: oklch(92.556% 0.06963 56.631);
--wa-color-orange-80: oklch(84.494% 0.12276 53.381);
--wa-color-orange-70: oklch(76.375% 0.17194 46.091);
--wa-color-orange-60: #eb713f /* oklch(68.398% 0.16422 41.446) */;
--wa-color-orange-50: #cb4b1d /* oklch(58.153% 0.17174 38.404) */;
--wa-color-orange-40: #a2310c /* oklch(48.028% 0.15488 36.538) */;
--wa-color-orange-30: #7f2810 /* oklch(40.591% 0.12506 35.663) */;
--wa-color-orange-20: #5d1d0e /* oklch(32.908% 0.09683 34.387) */;
--wa-color-orange-10: #3a1005 /* oklch(24.088% 0.06954 35.613) */;
--wa-color-orange-05: #270803 /* oklch(18.801% 0.05509 34.149) */;
--wa-color-orange: var(--wa-color-orange-70);
--wa-color-orange-key: 70;
--wa-color-orange-95: #fff0e6 /* oklch(96.426% 0.02105 56.133) */;
--wa-color-orange-90: #ffdfca /* oklch(92.468% 0.04529 55.325) */;
--wa-color-orange-80: #ffbb94 /* oklch(84.588% 0.09454 50.876) */;
--wa-color-orange-70: #ff9266 /* oklch(76.744% 0.14429 42.309) */;
--wa-color-orange-60: #f46a45 /* oklch(68.848% 0.17805 35.951) */;
--wa-color-orange-50: #cd491c /* oklch(58.195% 0.17597 37.577) */;
--wa-color-orange-40: #9f3501 /* oklch(47.889% 0.14981 39.957) */;
--wa-color-orange-30: #802700 /* oklch(40.637% 0.1298 39.149) */;
--wa-color-orange-20: #601b00 /* oklch(33.123% 0.10587 39.117) */;
--wa-color-orange-10: #3c0d00 /* oklch(24.043% 0.07768 38.607) */;
--wa-color-orange-05: #280600 /* oklch(18.644% 0.0607 38.252) */;
--wa-color-orange: var(--wa-color-orange-60);
--wa-color-orange-key: 60;
--wa-color-yellow-95: #fef3cd /* oklch(96.322% 0.05069 93.748) */;
--wa-color-yellow-90: #ffe495 /* oklch(92.377% 0.10246 91.296) */;

View File

@@ -18,17 +18,17 @@
--wa-color-red: var(--wa-color-red-40);
--wa-color-red-key: 40;
--wa-color-orange-95: oklch(96.238% 0.02664 61.788);
--wa-color-orange-90: #fbe1cc /* oklch(92.512% 0.04019 60.45) */;
--wa-color-orange-80: #edc1a0 /* oklch(84.097% 0.06661 58.236) */;
--wa-color-orange-70: #dda178 /* oklch(75.734% 0.09064 55.123) */;
--wa-color-orange-60: #cd8351 /* oklch(67.646% 0.1134 53.172) */;
--wa-color-orange-50: #b65d22 /* oklch(57.437% 0.13446 49.881) */;
--wa-color-orange-40: oklch(47.576% 0.13426 46.452);
--wa-color-orange-30: oklch(40.382% 0.12087 47.003);
--wa-color-orange-20: oklch(32.846% 0.0965 46.227);
--wa-color-orange-10: oklch(24.06% 0.06873 45.849);
--wa-color-orange-05: #260900 /* oklch(18.727% 0.05359 44.791) */;
--wa-color-orange-95: #ffefe1 /* oklch(96.105% 0.02545 63.746) */;
--wa-color-orange-90: #fbe0cb /* oklch(92.334% 0.04096 60.142) */;
--wa-color-orange-80: #efc19f /* oklch(84.313% 0.06973 58.09) */;
--wa-color-orange-70: #e1a173 /* oklch(76.013% 0.09772 56.368) */;
--wa-color-orange-60: #d1824d /* oklch(67.937% 0.11938 53.195) */;
--wa-color-orange-50: #b65d22 /* oklch(57.543% 0.13444 49.914) */;
--wa-color-orange-40: #934107 /* oklch(47.603% 0.12628 47.819) */;
--wa-color-orange-30: #773001 /* oklch(40.317% 0.11198 46.326) */;
--wa-color-orange-20: #592100 /* oklch(32.692% 0.09239 45.689) */;
--wa-color-orange-10: #381200 /* oklch(24.015% 0.06777 45.771) */;
--wa-color-orange-05: #260900 /* oklch(18.789% 0.05427 44.405) */;
--wa-color-orange: var(--wa-color-orange-50);
--wa-color-orange-key: 50;

View File

@@ -18,19 +18,19 @@
--wa-color-red: var(--wa-color-red-40);
--wa-color-red-key: 40;
--wa-color-orange-95: oklch(96.126% 0.05417 66.333);
--wa-color-orange-90: oklch(92.413% 0.07898 62.545);
--wa-color-orange-80: #f9bd86 /* oklch(84.088% 0.09891 63.847) */;
--wa-color-orange-70: #e2a05f /* oklch(75.707% 0.11352 64.057) */;
--wa-color-orange-60: #d18228 /* oklch(67.572% 0.13809 64.146) */;
--wa-color-orange-50: oklch(57.202% 0.13583 64.309);
--wa-color-orange-40: oklch(47.462% 0.13789 64.939);
--wa-color-orange-30: oklch(40.049% 0.12025 65.207);
--wa-color-orange-20: oklch(32.552% 0.09777 64.859);
--wa-color-orange-10: oklch(23.884% 0.07141 64.246);
--wa-color-orange-05: oklch(18.698% 0.05597 65.589);
--wa-color-orange: var(--wa-color-orange-60);
--wa-color-orange-key: 60;
--wa-color-orange-95: #fff0e8 /* oklch(96.479% 0.01949 50.157) */;
--wa-color-orange-90: #ffdfc8 /* oklch(92.42% 0.04692 57.822) */;
--wa-color-orange-80: #ffbb84 /* oklch(84.3% 0.10585 59.641) */;
--wa-color-orange-70: #e79f64 /* oklch(76.075% 0.1143 59.207) */;
--wa-color-orange-60: #ca854c /* oklch(67.612% 0.11209 58.789) */;
--wa-color-orange-50: #a56732 /* oklch(57.133% 0.10488 58.75) */;
--wa-color-orange-40: #824c1a /* oklch(47.091% 0.09621 59.168) */;
--wa-color-orange-30: #6a3907 /* oklch(39.832% 0.09012 58.688) */;
--wa-color-orange-20: #512800 /* oklch(32.57% 0.07814 57.787) */;
--wa-color-orange-10: #331600 /* oklch(23.846% 0.05834 56.02) */;
--wa-color-orange-05: #220c00 /* oklch(18.585% 0.04625 54.588) */;
--wa-color-orange: var(--wa-color-orange-70);
--wa-color-orange-key: 70;
--wa-color-yellow-95: #fff4b3 /* oklch(96.064% 0.08319 99.657) */;
--wa-color-yellow-90: #fee58c /* oklch(92.346% 0.11242 94.205) */;

View File

@@ -18,17 +18,17 @@
--wa-color-red: var(--wa-color-red-50);
--wa-color-red-key: 50;
--wa-color-orange-95: #f8f0ec /* oklch(96.084% 0.01043 54.557) */;
--wa-color-orange-90: #f2e3d8 /* oklch(92.485% 0.02211 56.694) */;
--wa-color-orange-80: #e4c4ad /* oklch(84.166% 0.04799 57.553) */;
--wa-color-orange-70: #d3a583 /* oklch(75.674% 0.07148 57.481) */;
--wa-color-orange-60: #bc8a65 /* oklch(67.319% 0.08062 57.054) */;
--wa-color-orange-50: #9e6940 /* oklch(56.757% 0.08845 56.746) */;
--wa-color-orange-40: #7e4d27 /* oklch(46.949% 0.08447 57.382) */;
--wa-color-orange-30: #673a17 /* oklch(39.774% 0.0793 55.768) */;
--wa-color-orange-20: #4f2906 /* oklch(32.45% 0.0725 57.629) */;
--wa-color-orange-10: #311702 /* oklch(23.759% 0.05361 57.126) */;
--wa-color-orange-05: #200d01 /* oklch(18.517% 0.04211 57.178) */;
--wa-color-orange-95: #f9f2ed /* oklch(96.52% 0.01008 58.217) */;
--wa-color-orange-90: #f4e3d6 /* oklch(92.595% 0.02556 59.903) */;
--wa-color-orange-80: #e6c3aa /* oklch(84.087% 0.05242 57.474) */;
--wa-color-orange-70: #d3a685 /* oklch(75.824% 0.06976 57.703) */;
--wa-color-orange-60: #bf8962 /* oklch(67.428% 0.08515 56.553) */;
--wa-color-orange-50: #a26839 /* oklch(57.025% 0.09733 58.071) */;
--wa-color-orange-40: #824b20 /* oklch(46.959% 0.09366 55.634) */;
--wa-color-orange-30: #683a17 /* oklch(39.855% 0.08077 54.814) */;
--wa-color-orange-20: #4d2a0e /* oklch(32.469% 0.06563 55.984) */;
--wa-color-orange-10: #301705 /* oklch(23.621% 0.04968 54.306) */;
--wa-color-orange-05: #1f0e03 /* oklch(18.572% 0.03671 56.067) */;
--wa-color-orange: var(--wa-color-orange-50);
--wa-color-orange-key: 50;

View File

@@ -18,19 +18,19 @@
--wa-color-red: var(--wa-color-red-50);
--wa-color-red-key: 50;
--wa-color-orange-95: oklch(96.488% 0.04965 58.92);
--wa-color-orange-90: oklch(92.244% 0.08759 57.789);
--wa-color-orange-80: oklch(84.32% 0.12702 56.232);
--wa-color-orange-70: oklch(76.31% 0.17967 50.95);
--wa-color-orange-60: #e87431 /* oklch(68.352% 0.16354 47.083) */;
--wa-color-orange-50: oklch(58.23% 0.17947 43.3);
--wa-color-orange-40: oklch(48.089% 0.16071 40.798);
--wa-color-orange-30: oklch(40.708% 0.13699 39.616);
--wa-color-orange-20: oklch(33.124% 0.1121 39.599);
--wa-color-orange-10: oklch(24.257% 0.08204 39.602);
--wa-color-orange-05: oklch(18.966% 0.06414 39.606);
--wa-color-orange: var(--wa-color-orange-70);
--wa-color-orange-key: 70;
--wa-color-orange-95: #fff1e4 /* oklch(96.567% 0.02297 65.466) */;
--wa-color-orange-90: #ffdfc4 /* oklch(92.327% 0.0504 62.301) */;
--wa-color-orange-80: #ffbc80 /* oklch(84.418% 0.10871 62.363) */;
--wa-color-orange-70: #ff9427 /* oklch(76.467% 0.1689 59.353) */;
--wa-color-orange-60: #f16e00 /* oklch(68.563% 0.18516 48.551) */;
--wa-color-orange-50: #c75003 /* oklch(58.025% 0.16741 44.08) */;
--wa-color-orange-40: #9e3702 /* oklch(47.974% 0.14693 40.809) */;
--wa-color-orange-30: #802700 /* oklch(40.637% 0.1298 39.149) */;
--wa-color-orange-20: #601a00 /* oklch(32.994% 0.10679 38.524) */;
--wa-color-orange-10: #3d0d00 /* oklch(24.253% 0.07888 38.298) */;
--wa-color-orange-05: #290600 /* oklch(18.868% 0.06197 37.848) */;
--wa-color-orange: var(--wa-color-orange-60);
--wa-color-orange-key: 60;
--wa-color-yellow-95: #fff5b4 /* oklch(96.281% 0.08306 100.4) */;
--wa-color-yellow-90: #fde572 /* oklch(91.915% 0.13738 97.724) */;

View File

@@ -18,19 +18,19 @@
--wa-color-red: var(--wa-color-red-50);
--wa-color-red-key: 50;
--wa-color-orange-95: oklch(96.355% 0.05982 62.448);
--wa-color-orange-90: oklch(92.371% 0.10134 60.314);
--wa-color-orange-80: oklch(84.228% 0.13101 54.157);
--wa-color-orange-70: oklch(76.275% 0.16839 51.143);
--wa-color-orange-60: #e67530 /* oklch(68.22% 0.16179 47.997) */;
--wa-color-orange-50: oklch(58.011% 0.16819 44.953);
--wa-color-orange-40: #9b390d /* oklch(47.739% 0.14004 40.585) */;
--wa-color-orange-30: #7a2b17 /* oklch(40.323% 0.11475 35.602) */;
--wa-color-orange-20: #5c1e0f /* oklch(32.963% 0.09552 34.666) */;
--wa-color-orange-10: #3a0f06 /* oklch(24.042% 0.07066 34.715) */;
--wa-color-orange-05: #270803 /* oklch(18.867% 0.05444 34.696) */;
--wa-color-orange: var(--wa-color-orange-70);
--wa-color-orange-key: 70;
--wa-color-orange-95: #fff1de /* oklch(96.415% 0.02927 75.692) */;
--wa-color-orange-90: #fee0bc /* oklch(92.25% 0.05764 72.084) */;
--wa-color-orange-80: #fdbe7a /* oklch(84.485% 0.11179 67.463) */;
--wa-color-orange-70: #fb9641 /* oklch(76.472% 0.15504 57.097) */;
--wa-color-orange-60: #f56b11 /* oklch(68.77% 0.18951 45.673) */;
--wa-color-orange-50: #cf480e /* oklch(58.335% 0.18077 39.019) */;
--wa-color-orange-40: #a13308 /* oklch(48.025% 0.15268 37.837) */;
--wa-color-orange-30: #802604 /* oklch(40.538% 0.12998 37.701) */;
--wa-color-orange-20: #601a02 /* oklch(33.012% 0.10627 37.613) */;
--wa-color-orange-10: #3d0d01 /* oklch(24.269% 0.07841 37.157) */;
--wa-color-orange-05: #290600 /* oklch(18.868% 0.06197 37.848) */;
--wa-color-orange: var(--wa-color-orange-60);
--wa-color-orange-key: 60;
--wa-color-yellow-95: #fef6ab /* oklch(96.234% 0.09455 102.83) */;
--wa-color-yellow-90: #fde761 /* oklch(92.138% 0.15325 99.997) */;

View File

@@ -81,10 +81,6 @@ input:is([type='button'], [type='reset'], [type='submit']),
* States
*/
&::-moz-focus-inner {
border: 0;
}
&:focus {
outline: none;
}
@@ -103,6 +99,11 @@ input:is([type='button'], [type='reset'], [type='submit']),
pointer-events: none;
}
}
/* Keep it last so Safari doesn't stop parsing this block */
&::-moz-focus-inner {
border: 0;
}
}
/**

View File

@@ -38,6 +38,16 @@ input[type='checkbox']:where(:not(:host *)) {
height: 100%;
width: 100%;
}
&:indeterminate::after {
background-color: currentColor;
content: '';
mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="16" width="14" viewBox="0 0 448 512"><path d="M431 256c0 17.7-14.3 32-32 32H49c-17.7 0-32-14.3-32-32s14.3-32 32-32h350c17.7 0 32 14.3 32 32z"/></svg>')
center no-repeat;
position: absolute;
height: 100%;
width: 100%;
}
}
input[type='checkbox']:where(:not(:host *)),

View File

@@ -2,10 +2,12 @@ details:where(:not(:host *)),
:host {
--icon-color: var(--wa-color-text-quiet);
--spacing: var(--wa-space-m);
--outlined-border-color: var(--wa-color-surface-border);
background-color: var(--wa-color-surface-default);
border: var(--wa-panel-border-width) var(--wa-color-surface-border) var(--wa-panel-border-style);
background-color: var(--background-color, var(--wa-color-surface-default));
border: var(--wa-panel-border-width) var(--border-color, var(--wa-color-surface-border)) var(--wa-panel-border-style);
border-radius: var(--wa-panel-border-radius);
color: var(--text-color, inherit);
padding: var(--spacing);
/* Print styles */
@@ -19,6 +21,12 @@ details:where(:not(:host *)),
}
}
details.wa-plain,
:host(wa-details[appearance='plain']),
:host(wa-details.wa-plain) {
border-radius: 0;
}
details:where(:not(:host *)) summary,
:host summary /* nesting these summary styles in the rule above breaks in Firefox and Safari */ {
display: flex;

View File

@@ -80,8 +80,7 @@
}
.wa-dark,
.wa-invert,
:is(:host-context(.wa-dark)) {
.wa-invert {
/**
* Foundational Colors
*/

View File

@@ -71,8 +71,7 @@
}
.wa-dark,
.wa-invert,
:is(:host-context(.wa-dark)) {
.wa-invert {
color-scheme: dark;
color: var(--wa-color-text-normal);

View File

@@ -78,8 +78,7 @@
}
.wa-dark,
.wa-invert,
:is(:host-context(.wa-dark)) {
.wa-invert {
color-scheme: dark;
color: var(--wa-color-text-normal);

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