Compare commits

..

26 Commits

Author SHA1 Message Date
Cory LaViska
15c77082a4 remove outdated section 2025-03-28 10:49:56 -04:00
Cory LaViska
5161936cb0 add option as a dep of select 2025-03-28 10:48:11 -04:00
Cory LaViska
f6e754c1b9 Merge branch 'next' into bug-fixes 2025-03-28 10:43:05 -04:00
Cory LaViska
8ad1a58914 revert example 2025-03-28 10:38:29 -04:00
Cory LaViska
5b1ef2717c fix radio labels (ALPHA-211) 2025-03-28 09:59:18 -04:00
Cory LaViska
30302fe632 update display labels when changed; fixes #702 2025-03-28 09:47:31 -04:00
Cory LaViska
22c0fd2301 fix whitespace 2025-03-28 09:41:34 -04:00
Cory LaViska
d9b3df2bea remove wa-cloak after components load 2025-03-28 09:24:46 -04:00
Cory LaViska
2c047c2843 add FA kit code for codepen 🤞🏻 2025-03-28 09:24:29 -04:00
Cory LaViska
9d3c0142b8 update test 2025-03-27 15:12:21 -04:00
Cory LaViska
acf02a7a03 stop running SSR tests locally 2025-03-27 15:07:38 -04:00
Cory LaViska
50034527e7 add comment 2025-03-27 14:13:31 -04:00
Cory LaViska
96e8ce8bf6 fix native checkbox indeterminate icon; closes #386 2025-03-27 14:08:24 -04:00
Cory LaViska
b8dd847b4b fix progress animation in Safari; closes #356 2025-03-27 13:20:55 -04:00
Cory LaViska
95fd134104 fix dropdown alignment in button group; closes #374 2025-03-27 12:52:02 -04:00
Cory LaViska
c6ccb3280d Merge branch 'next' into bug-fixes 2025-03-27 12:16:22 -04:00
Cory LaViska
aeb1c57be0 revert 2025-03-27 11:51:36 -04:00
Cory LaViska
486cf3d408 revert 2025-03-26 16:14:25 -04:00
Cory LaViska
4f81a432df Revert "improve details styles; fixes #685"
This reverts commit 8151872d22.
2025-03-26 16:09:39 -04:00
Cory LaViska
bef6ee8503 update examples 2025-03-26 15:52:49 -04:00
Cory LaViska
8151872d22 improve details styles; fixes #685 2025-03-26 15:22:29 -04:00
Cory LaViska
d78b984bd0 fix focus ring in Safari; fixes #745 2025-03-26 15:01:03 -04:00
Cory LaViska
2615fc8ab9 fixes #840 2025-03-26 14:38:33 -04:00
Cory LaViska
0d8d8d4055 remove redundant styles 2025-03-26 14:38:24 -04:00
Cory LaViska
be3cfc3420 fix radio button pill styles; fixes #759 2025-03-26 13:46:30 -04:00
Cory LaViska
f1149d22a0 add default icon spacing in tab; fixes #779 2025-03-26 13:05:57 -04:00
25 changed files with 444 additions and 948 deletions

View File

@@ -36,16 +36,10 @@ const globalData = {
},
};
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.
*/

View File

@@ -1,7 +1,7 @@
{# Getting started #}
<h2>Getting Started</h2>
<ul>
<li><a href="/docs/">Installation</a></li>
<li><a href="/docs/installation">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

@@ -21,8 +21,8 @@
</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>
<wa-input value="Input" size="small"></wa-input>
<wa-button size="small" variant="brand">Go</wa-button>
</div>
</div>
</template>

View File

@@ -30,21 +30,12 @@
{% include 'breadcrumbs.njk' %}
<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 v-if="saved" class="title">
{% raw %}{{ saved.title }}{% endraw %}
<wa-icon-button name="pencil" label="Rename palette" @click="rename"></wa-icon-button>
<wa-icon-button class="delete" name="trash" label="Delete palette" @click="deleteSaved"></wa-icon-button>
</h1>
<h1 v-if="!saved" class="title">{{ title }}</h1>
<div class="block-info">
<code class="class">.wa-palette-{{ paletteId }}</code>
@@ -68,7 +59,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)" v-content="tweakHumanReadable"></wa-tag>
<wa-tag v-for="tweakHumanReadable, param in tweaksHumanReadable" removable @wa-remove="reset(param)">{% raw %}{{ tweakHumanReadable }}{% endraw %}</wa-tag>
</div>
<wa-button @click="reset()" appearance="outlined" variant="danger">
@@ -77,6 +68,13 @@
</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

@@ -1,167 +0,0 @@
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,119 +1,254 @@
import my from '/assets/scripts/my.js';
const sidebar = (globalThis.sidebar = {});
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);
// 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');
}
a.classList.add('current');
}
return a;
},
removeLink(a) {
if (!a || !a.isConnected) {
// Link doesn't exist or is already removed
sidebar.palettes = {
render() {
if (this.saved.length === 0) {
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();
for (let palette of this.saved) {
sidebar.palette.render(palette);
}
if (a.classList.contains('current')) {
// If the deleted palette was the current one, the current one is now the parent
parentA.classList.add('current');
}
sidebar.updateCurrent();
},
findEntity(entity) {
return document.querySelector(`#sidebar a[href^="${entity.baseUrl}"][data-uid="${entity.uid}"]`);
updateSaved() {
this.saved = localStorage.savedPalettes ? JSON.parse(localStorage.savedPalettes) : [];
},
renderEntity(entity) {
let { url, parentUrl } = entity;
save(saved = this.saved) {
this.saved = saved ?? [];
// Find parent
let parentA = document.querySelector(`#sidebar a[href="${parentUrl}"]`);
let parentLi = parentA?.closest('li');
if (!parentLi) {
throw new Error(`Cannot find parent url ${parentUrl}`);
}
// Find existing
let a = this.findEntity(entity);
let alreadyExisted = !!a;
a ??= document.createElement('a');
a.textContent = entity.title;
a.href = url;
if (!alreadyExisted) {
a.dataset.uid = entity.uid;
a = sidebar.addChild(a, parentA);
// This is mainly to port Pro badges
let badges = Array.from(parentLi.querySelectorAll('wa-badge'), badge => badge.cloneNode(true));
let append = [...badges];
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);
}
}
},
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);
}
if (saved.length > 0) {
localStorage.savedPalettes = JSON.stringify(saved);
} else {
delete localStorage.savedPalettes;
}
},
};
globalThis.sidebar = sidebar;
sidebar.palettes.updateSaved();
addEventListener('storage', event => sidebar.palettes.updateSaved());
// 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.palette = {
getUid() {
let savedPalettes = sidebar.palettes.saved;
let uids = new Set(savedPalettes.map(p => p.uid));
if (savedPalettes.length === 0) {
return 1;
}
// Find first available number
for (let i = 1; i <= savedPalettes.length + 1; i++) {
if (!uids.has(i)) {
return i;
}
}
},
equals(p1, p2) {
if (!p1 || !p2) {
return false;
}
return p1.id === p2.id && p1.uid === p2.uid;
},
delete(palette) {
let savedPalettes = sidebar.palettes.saved;
let count = savedPalettes.length;
if (count === 0) {
return;
}
// TODO improve UX of this
if (!confirm(`Are you sure you want to delete palette “${palette.title}”?`)) {
return;
}
savedPalettes = savedPalettes.filter(p => !sidebar.palette.equals(palette, p));
if (savedPalettes.length === count) {
// Nothing was removed
return;
}
// Update UI
let pathname = `/docs/palettes/${palette.id}/`;
let url = pathname + palette.search;
let uls = new Set();
for (let a of document.querySelectorAll(`#sidebar a[href="${url}"]`)) {
let li = a.closest('li');
let ul = li.closest('ul');
uls.add(ul);
li.remove();
}
// Remove empty lists
for (let ul of uls) {
if (!ul.children.length) {
ul.remove();
}
}
sidebar.updateCurrent();
sidebar.palettes.save(savedPalettes);
if (sidebar.palette.equals(globalThis.paletteApp?.saved, palette)) {
paletteApp.postDelete();
}
},
getSaved(palette, savedPalettes = sidebar.palettes.saved) {
return savedPalettes.find(p => sidebar.palette.equals(p, palette));
},
render(palette) {
// Find existing <a>
let { title, id, search, uid } = palette;
for (let a of document.querySelectorAll(`#sidebar a[href^="/docs/palettes/${id}/"][data-uid="${uid}"]`)) {
// Palette already in sidebar, just update it
a.textContent = palette.title;
a.href = `/docs/palettes/${id}/${search}`;
return;
}
let pathname = `/docs/palettes/${id}/`;
let url = pathname + search;
let parentA = document.querySelector(`a[href="${pathname}"]`);
let parentLi = parentA?.closest('li');
let a;
if (parentLi) {
a = Object.assign(document.createElement('a'), { href: url, textContent: title });
a.dataset.uid = uid;
let badges = [...parentLi.querySelectorAll('wa-badge')].map(badge => badge.cloneNode(true));
let ul = parentLi.querySelector('ul') ?? parentLi.appendChild(document.createElement('ul'));
let li = document.createElement('li');
let deleteButton = Object.assign(document.createElement('wa-icon-button'), {
name: 'trash',
label: 'Delete',
className: 'delete',
});
deleteButton.addEventListener('click', () => {
let palette = { id, uid, title: a.textContent, search: a.search };
sidebar.palette.delete(palette);
});
li.append(a, ' ', ...badges, deleteButton);
ul.appendChild(li);
}
},
save(palette, saved) {
let savedPalettes = sidebar.palettes.saved;
let existing = this.getSaved(saved ?? palette, savedPalettes);
let oldValues;
if (existing) {
// Rename
oldValues = { ...existing };
Object.assign(existing, palette);
} else {
savedPalettes.push(palette);
}
this.render(palette, oldValues);
sidebar.updateCurrent();
sidebar.palettes.save(savedPalettes);
},
};
sidebar.updateCurrent = function () {
// Find the sidebar link with the longest shared prefix with the current URL
let pathParts = location.pathname.split('/').filter(Boolean);
let prefixes = [];
if (pathParts.length === 1) {
// If at /docs/ we just use that, otherwise we want at least two parts (/docs/xxx/)
prefixes.push('/' + pathParts[0] + '/');
} else {
for (let i = 2; i <= pathParts.length; i++) {
prefixes.push('/' + pathParts.slice(0, i).join('/') + '/');
}
}
// Last prefix includes the search too (if any)
if (location.search) {
let params = new URLSearchParams(location.search);
params.sort();
prefixes.push(prefixes.at(-1) + location.search);
}
// We want to start from the longest prefix
prefixes.reverse();
let candidates;
let matchingPrefix;
for (let prefix of prefixes) {
candidates = document.querySelectorAll(`#sidebar a[href^="${prefix}"]`);
if (candidates.length > 0) {
matchingPrefix = prefix;
break;
}
}
if (!matchingPrefix) {
// Abort mission
return;
}
if (matchingPrefix === pathParts.at(-1)) {
// Full path matches, check search
if (location.search) {
candidates = [...candidates];
let searchParams = new URLSearchParams(location.search);
if (searchParams.has('uid')) {
// Only consider candidates with the same uid
candidates = candidates.filter(a => {
let params = new URLSearchParams(a.search);
return params.get('uid') === searchParams.get('uid');
});
} else {
// Sort candidates based on how many params they have in common, in descending order
candidates = candidates.sort((a, b) => {
return countSharedSearchParams(searchParams, b.search) - countSharedSearchParams(searchParams, a.search);
});
}
}
}
if (candidates.length > 0) {
for (let current of document.querySelectorAll('#sidebar a.current')) {
current.classList.remove('current');
}
candidates[0].classList.add('current');
}
};
sidebar.render = function () {
this.palettes.render();
};
sidebar.render();
window.addEventListener('turbo:render', () => sidebar.render());
function countSharedSearchParams(searchParams, search) {
if (!search || search === '?') {
return 0;
}
let params = new URLSearchParams(search);
return [...searchParams.keys()].filter(k => params.get(k) === searchParams.get(k)).length;
}

View File

@@ -13,42 +13,56 @@ export default class Permalink extends URLSearchParams {
return Object.fromEntries(this.entries());
}
set(key, value, defaultValue) {
if (equals(value, defaultValue) || equals(value, '')) {
value = null;
#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);
}
}
value ??= null; // undefined -> null
writeTo(obj) {
let mapping = this.#mappings.get(obj) ?? {};
let { keyTo = IDENTITY, valueTo = IDENTITY, canExtend = false } = mapping;
let oldValue = Array.isArray(value) ? this.getAll(key) : this.get(key);
let changed = !equals(value, oldValue);
for (let [key, value] of this) {
let mappedKey = keyTo(key);
let mappedValue = valueTo(value);
if (!changed) {
// Nothing to do here
return;
}
if (Array.isArray(value)) {
super.delete(key);
value = value.slice();
for (let v of value) {
if (v || v === 0) {
if (typeof v === 'object') {
super.append(key, JSON.stringify(v));
} else {
super.append(key, v);
}
}
if (canExtend || mappedKey in obj) {
obj[mappedKey] = mappedValue;
}
} else if (value === null) {
}
}
set(key, value, defaultValue) {
let oldValue = this.get(key);
if (!value || value == defaultValue) {
super.delete(key);
if (oldValue) {
this.changed = true;
}
} else {
super.set(key, value);
if (String(value) !== String(oldValue)) {
this.changed = true;
}
}
this.sort();
this.changed ||= changed;
}
/**
@@ -65,40 +79,3 @@ 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

@@ -1,22 +0,0 @@
// 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

@@ -1,110 +0,0 @@
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

@@ -24,86 +24,6 @@ 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

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

@@ -5,11 +5,8 @@ import { sort } from '../_utils/filters.js';
export default {
eleventyComputed: {
/**
* 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}
*/
// Default parent. Can be overridden by explicitly setting parent in the data.
// parent can refer to either an ancestor page in the URL or another page in the same directory
parent(data) {
let { parent, page } = data;
@@ -20,28 +17,16 @@ export default {
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;
@@ -63,17 +48,7 @@ export default {
},
};
/**
* 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();
@@ -89,18 +64,15 @@ function getParentUrl(url, 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 (!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 = '/';
}

View File

@@ -32,7 +32,7 @@ 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)
@@ -58,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
@@ -122,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

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

View File

@@ -12,37 +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!
:::
## 3.0.0-alpha.12
## Next
- Fixed the search dialog's styles so it doesn't jump around as you search
- Removed close watcher logic to backdrop hide animation bugs in `<wa-dialog>` and `<wa-drawer>`; this logic is already handled and we'll revisit `CloseWatcher` when browser support is better and behaviors are consistent
- Fixed a bug that caused dropdowns inside button groups to show with a vertical misalignment
- Revert `<wa-dialog>` structure and CSS to fix clipped content in dialogs (WA-A #123) and light dismiss in iOS Safari (WA-A #201)
- Fixed a bug in `<wa-progress>` that prevented Safari from animation progress changes
- Fixed the missing indeterminate icon in native checkbox styles
- Fixed a bug where changing a `<wa-option>` label wouldn't update the display label in `<wa-select>`
- Fixed a bug in `<wa-radio>` where elements would stack instead of display inline
### Enhancements
- Added `appearance` to [`<wa-details>`](/docs/components/details) and [`<wa-card>`](/docs/components/card) and support for the [appearance utilities](/docs/utilities/appearance/) in the [`<details>` native styles](/docs/native/details).
- Added an `orange` scale to all color palettes
- Added the [`.wa-cloak` utility](/docs/utilities/fouce) to prevent FOUCE
- Added the [`allDefined()` utility](/docs/usage/#all-defined) for awaiting component registration
- Added the `.wa-cloak` utility to prevent FOUCE
- Added the `allDefined()` utility for awaiting component registration
- Added default spacing to icons slotted into `<wa-tab>`
- Fixed `wa-pill` class for text fields
### Bug fixes
### Bugfixes
- Specifying inherited CSS properties on `<wa-tooltip>` now works as expected ([thanks Dennis!](https://github.com/shoelace-style/webawesome-alpha/discussions/203))
- Fixed a bug in `<wa-select>` that made it hard to use with VueJS, Svelte, and many other frameworks
- Fixed the `wa-pill` class for text fields
- Fixed `pill` style for `<wa-input>` elements
- Fixed a bug in `<wa-color-picker>` that prevented light dismiss from working when clicking immediately above the color picker dropdown
- Fixed a bug in `<wa-select multiple>` that sometimes resulted in empty `<div>` elements being output
- Fixed a bug 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 the pill effect from applying properly
- Fixed a bug in `<wa-radio-button>` that prevented active buttons from receiving the correct styles
- Fixed a bug in `<wa-button>` that prevented the focus ring from showing in Safari
- 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

@@ -1,71 +0,0 @@
---
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

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

View File

@@ -12,8 +12,6 @@ If you're new to custom elements, often referred to as "web components," this se
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
@@ -23,8 +21,6 @@ await customElements.whenDefined('wa-button');
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
@@ -36,33 +32,6 @@ 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.

View File

@@ -3,14 +3,12 @@ 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.
While convenient, autoloading can lead to a [Flash of Undefined Custom Elements](https://www.abeautifulsite.net/posts/flash-of-undefined-custom-elements/).
The [FOUCE style utility](/docs/utilities/fouce/#opting-in) (which is automatically applied if you use the [Web Awesome utilities](/docs/utilities/)) takes care of hiding custom elements until they and their contents have been registered, up to a maximum of two seconds.
In many cases, this is not enough, and you may wish to hide a broader wrapper element or even the entire page until all WA elements within it have loaded.
To do that, you can add the `wa-cloak` class to any element on the page or even apply it to the whole page by placing the class on the `<html>` element:
In many cases, this is not enough, and you may wish to hide a broader wrapper element or even the entire page until all WA elements within it have loaded. To do that, you can add the `wa-reduce-fouce` class to any element on the page or even apply it to the whole page by placing the class on the `<html>` element.
```html
<html class="wa-cloak">

13
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@shoelace-style/webawesome",
"version": "3.0.0-alpha.12",
"version": "3.0.0-alpha.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@shoelace-style/webawesome",
"version": "3.0.0-alpha.12",
"version": "3.0.0-alpha.11",
"license": "MIT",
"dependencies": {
"@ctrl/tinycolor": "^4.1.0",
@@ -15,8 +15,7 @@
"@shoelace-style/localize": "^3.2.1",
"composed-offset-position": "^0.0.6",
"lit": "^3.2.1",
"qr-creator": "^1.0.0",
"style-observer": "^0.0.7"
"qr-creator": "^1.0.0"
},
"devDependencies": {
"@11ty/eleventy": "3.0.0",
@@ -13086,12 +13085,6 @@
"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.12",
"version": "3.0.0-alpha.11",
"homepage": "https://webawesome.com/",
"author": "Web Awesome",
"license": "MIT",
@@ -73,8 +73,7 @@
"@shoelace-style/localize": "^3.2.1",
"composed-offset-position": "^0.0.6",
"lit": "^3.2.1",
"qr-creator": "^1.0.0",
"style-observer": "^0.0.7"
"qr-creator": "^1.0.0"
},
"devDependencies": {
"@11ty/eleventy": "3.0.0",

View File

@@ -17,6 +17,9 @@ 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

@@ -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({ cssProperty: '--wa-icon-name' }) name?: string;
@property({ reflect: true }) 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({ cssProperty: '--wa-icon-family' }) family: string;
@property({ reflect: true }) 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({ cssProperty: '--wa-icon-variant' }) variant: string;
@property({ reflect: true }) variant: string;
/** Draws the icon in a fixed-width both. */
@property({ attribute: 'fixed-width', type: Boolean, reflect: true }) fixedWidth: false;
@@ -80,16 +80,14 @@ export default class WaIcon extends WebAwesomeElement {
@property() label = '';
/** The name of a registered custom icon library. */
@property({ cssProperty: '--wa-icon-library', default: 'default' }) library = 'default';
@property({ reflect: true }) library = 'default';
connectedCallback() {
super.connectedCallback();
watchIcon(this);
}
firstUpdated(changedProperties: PropertyValues<this>) {
super.firstUpdated(changedProperties);
firstUpdated() {
this.initialRender = true;
this.setIcon();
}

View File

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

View File

@@ -1,9 +1,7 @@
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' {
@@ -13,11 +11,6 @@ declare module 'lit' {
*/
default?: any;
initial?: any;
/**
* Indicates whether the property should reflect to a CSS custom property.
*/
cssProperty?: string;
}
}
@@ -79,99 +72,6 @@ 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(
@@ -215,50 +115,6 @@ 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);
@@ -374,11 +230,6 @@ 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) {
@@ -405,18 +256,8 @@ 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);