Compare commits

..

147 Commits

Author SHA1 Message Date
Lea Verou
5a0b2da5a1 Merge branch 'next' into custom-palettes 2025-03-21 19:02:11 -04:00
Lea Verou
d64d75b3f4 Update generate-palette.js 2025-03-20 10:59:01 -04:00
Lea Verou
fe2829698a Update data.js 2025-03-20 10:58:52 -04:00
Lea Verou
fe2be5cbdb Patch gray bugs 2025-03-18 13:38:02 -04:00
Lea Verou
f9b932042e Swatch picker component 2025-03-18 13:32:54 -04:00
Lea Verou
8dee82a44a Fix gray chroma bugs 2025-03-18 13:18:46 -04:00
Lea Verou
0b883866d1 Spacing 2025-03-18 12:53:59 -04:00
Lea Verou
d0a60d2c30 Fix gray tweaks 2025-03-18 10:31:44 -04:00
Lea Verou
398ae15979 Present default roles differently 2025-03-17 17:38:12 -04:00
Lea Verou
0780c12adb Desaturate pro badge once I've started editing to reduce distraction 2025-03-17 12:03:15 -04:00
Lea Verou
bfafc08761 Fix generated palette code
- Only include my scales for custom palettes
- Do not include bogus role declarations
2025-03-17 11:15:03 -04:00
Lea Verou
2ac15dcda1 Less jargony sliders 2025-03-17 11:10:28 -04:00
Lea Verou
67437b719d Bugfix 2025-03-17 10:53:44 -04:00
Lea Verou
f05c8f7b84 Hide experimental badge once you start editing 2025-03-17 10:47:47 -04:00
Lea Verou
2c0ff72f0d wa-details for suggested colors 2025-03-17 10:44:49 -04:00
lindsaym-fa
672fc3a5ad Make suggested swatches smaller 2025-03-17 10:34:13 -04:00
Lea Verou
ff45ca2232 Add text for My Colors 2025-03-17 10:25:38 -04:00
Lea Verou
7dcbd7407f Suggested -> Common 2025-03-17 10:10:39 -04:00
Lea Verou
7c04550753 Focus input when added via add button 2025-03-17 10:09:21 -04:00
Lea Verou
08bf971f91 Fix bug where editing color did not update it in scales 2025-03-17 10:04:55 -04:00
Lea Verou
8245d8a40a Remove unused method 2025-03-17 10:04:43 -04:00
Lea Verou
e342f513b7 Better defaults when adding colors 2025-03-17 09:41:05 -04:00
Lea Verou
cdaa34e1bc Remove dead code 2025-03-17 09:40:54 -04:00
Lea Verou
0cca25a118 Move log to utils 2025-03-17 09:32:07 -04:00
Lea Verou
e9edc572b5 UI to override detected hue (and to communicate that a hue has been pinned) 2025-03-17 09:28:56 -04:00
Lea Verou
77da38fda3 Rotate pin icons 45deg 2025-03-17 09:28:23 -04:00
Lea Verou
cd4486cc86 Fade out tweak icon when not interacted with 2025-03-17 09:28:09 -04:00
Lea Verou
badc6c9dc2 Refactor: allHues = hues + gray 2025-03-17 04:52:10 -04:00
Lea Verou
33f3f8d4c0 Colorfulness sliders 2025-03-17 04:30:16 -04:00
Lea Verou
2bdfcae9ba Fix 2025-03-17 04:15:26 -04:00
Lea Verou
f369916f01 Ability to pin hue so that colors don't jump to another scale when pinned 2025-03-16 23:06:47 -04:00
Lea Verou
c9a1e21cdb Refactor: Move exposed properties to array 2025-03-16 23:06:23 -04:00
Lea Verou
d649d2ee3b More slider bugfixes 2025-03-16 22:31:54 -04:00
Lea Verou
44469183cb Oopsie 2025-03-16 21:12:20 -04:00
Lea Verou
d30149e718 Fix normalizeAngle(), which fixes generated pinks 2025-03-16 21:11:32 -04:00
Lea Verou
416aaee672 Maximize distance between generated hue and both hues before and after it 2025-03-16 21:11:14 -04:00
Lea Verou
e9ea0b7f1c Simplify color-slider and fix a bunch of bugs around it 2025-03-16 20:20:01 -04:00
Lea Verou
5d97db178a Remove more tweaking stuff 2025-03-16 18:35:11 -04:00
Lea Verou
45b3a8e76e More hueBefore/hueAfter to data 2025-03-16 18:32:06 -04:00
Lea Verou
550df496e1 Fix tweaking sliders for predefined palettes 2025-03-16 18:13:35 -04:00
Lea Verou
cde67b7984 Reduce visual impact of Save button when saved 2025-03-16 18:09:31 -04:00
Lea Verou
fd6e7e19f0 Remove unused tweaking classes 2025-03-16 18:06:23 -04:00
Lea Verou
c442e52c63 Easier pinning of generated colors 2025-03-16 17:47:10 -04:00
Lea Verou
9c57646f48 Fix bug where edges where unintentionally added to my colors 2025-03-16 17:37:24 -04:00
Lea Verou
344e693c8b Fix bug where saving colors changed their order 2025-03-16 17:36:43 -04:00
Lea Verou
12b2ab133a Prevent bug where edges where auto-added 2025-03-16 17:25:22 -04:00
Lea Verou
1b26bee1af Tweak edges 2025-03-16 17:12:48 -04:00
Lea Verou
27c7e56a7e Hide general colorfulness slider from custom palettes (for now) 2025-03-16 17:12:32 -04:00
Lea Verou
22e5850a3f Move delete button inside popup 2025-03-16 17:12:20 -04:00
Lea Verou
f4897dcabe Show default values, make color tweak sliders work properly 2025-03-16 16:51:38 -04:00
Lea Verou
3dd5e0e8aa Refactor 2025-03-16 15:32:48 -04:00
Lea Verou
515b48f8a5 Make seed tweak popup wider 2025-03-16 15:27:59 -04:00
Lea Verou
9f141dbc4a Fix clear button 2025-03-16 02:14:03 -04:00
Lea Verou
ca60751cb8 decorated-slider -> color-slider, move template to top 2025-03-16 01:14:16 -04:00
Lea Verou
7dfa2f6a93 Sliders to tweak key colors 2025-03-16 00:22:49 -04:00
Lea Verou
31c4dc658f Merge branch 'next' into custom-palettes 2025-03-15 15:37:28 -04:00
Lea Verou
82c34a8fe6 Merge branch 'next' into custom-palettes 2025-03-15 15:35:11 -04:00
Lea Verou
15ac2d169d Pin any color 2025-03-15 01:13:55 -04:00
Lea Verou
412670a21d Show experimental and pro badges on palette index 2025-03-15 01:13:27 -04:00
Lea Verou
c70ea3627c Prevent scales not in palette from showing up in contrast tables 2025-03-14 17:39:24 -04:00
Lea Verou
0a938d5cf3 Drop functionality where we show the old color in the swatch
Too disorienting and adds complexity
2025-03-14 17:18:57 -04:00
Lea Verou
1a9372839c Color popup 2025-03-14 17:18:57 -04:00
Lea Verou
12c5747cd2 Various changes around tweaks 2025-03-14 16:33:14 -04:00
Lea Verou
bb24db30b5 Update slider.ts 2025-03-14 16:32:53 -04:00
Lea Verou
48d7e45d30 Update custom.njk 2025-03-14 15:23:40 -04:00
Lea Verou
6dd2fbec74 Presentational 2025-03-14 15:23:26 -04:00
Lea Verou
d7dbf0f3f9 Nicer loading 2025-03-14 14:09:30 -04:00
Lea Verou
ba9d4c1f21 Button 2025-03-13 18:16:36 -04:00
Lea Verou
d4131095a8 Rework seed colors to support undoable tweaks 2025-03-13 16:44:40 -04:00
Lea Verou
1dd47557c0 Persist roles in permalink 2025-03-12 17:22:43 -04:00
Lea Verou
054058a52c Remove color from roles if its scale is deleted 2025-03-12 17:22:35 -04:00
Lea Verou
9f0d1df974 Do not wrap when a color has multiple roles 2025-03-12 17:00:54 -04:00
Lea Verou
a918c2297d Mark as experimental 2025-03-12 16:58:23 -04:00
Lea Verou
96704a2d7e Merge branch 'next' into custom-palettes 2025-03-12 16:53:10 -04:00
Lea Verou
3ae89b827f Role multiselect in seed colors 2025-03-12 16:20:43 -04:00
Lea Verou
6523925eaf Dynamic default roles 2025-03-11 22:10:12 -04:00
Lea Verou
9d6cf9efb8 Formatting 2025-03-11 19:12:39 -04:00
Lea Verou
73892da3a7 Prevent gray inadvertently showing up as tweaked 2025-03-11 18:48:57 -04:00
Lea Verou
b1a29ecf69 Take pinned colors beyond core color more into account 2025-03-11 17:29:43 -04:00
Lea Verou
089450c25e Edit seed color when input is edited
Does not yet remove tweaks though
2025-03-11 16:30:22 -04:00
Lea Verou
ed9a1280c1 Update custom.css 2025-03-11 16:28:55 -04:00
Lea Verou
b50b5983d3 Show tweaked and original color in My Colors 2025-03-11 15:20:30 -04:00
Lea Verou
748fd42d40 Refactor 2025-03-11 15:19:39 -04:00
Lea Verou
efe570f7b3 If gray is provided, use it 2025-03-11 15:08:06 -04:00
Lea Verou
110dc7da60 Formatting 2025-03-11 14:20:29 -04:00
Lea Verou
d778013667 Identify gray 2025-03-11 14:17:42 -04:00
Lea Verou
e898179802 Fix bug 2025-03-11 14:16:54 -04:00
Lea Verou
5c78e3226f Min height for add button 2025-03-11 11:22:22 -04:00
Lea Verou
daa0ccee26 Use thumbnail placeholder until I figure out palette icons 2025-03-11 11:20:46 -04:00
Lea Verou
890791f94e Define and use <color-slider> Vue component rather than ad hoc markup 2025-03-11 11:14:33 -04:00
Lea Verou
9a03cea920 Reduce duplicate calculations, pave the way for passing in custom identified hue 2025-03-10 20:53:30 -04:00
Lea Verou
fd9235fe29 Make color identification more clearly output 2025-03-10 20:01:06 -04:00
Lea Verou
df108ba346 Hide colors not in my scale from hue wheel 2025-03-10 19:45:01 -04:00
Lea Verou
510a6c4eac Include role assignments to generated CSS 2025-03-10 18:00:49 -04:00
Lea Verou
b627c9b7d5 Add core tint to generated CSS 2025-03-10 18:00:34 -04:00
Lea Verou
29aeb078b7 Fix bug 2025-03-10 17:22:44 -04:00
Lea Verou
b966f57a83 Custom class name, align new palette UX closer to sketches 2025-03-10 16:28:46 -04:00
Lea Verou
9928f77091 Hide "Used By" section for custom palettes 2025-03-10 15:35:21 -04:00
Lea Verou
baae409bfc Less cluttered default indication 2025-03-10 15:33:39 -04:00
Lea Verou
15abc6d21c Merge branch 'next' into custom-palettes 2025-03-10 15:28:08 -04:00
Lea Verou
353c053153 MVP for assigning roles to palettes, rel ##782 2025-03-07 19:43:52 -05:00
Lea Verou
ab01fbb5af Refactor: core-color-input -> color-input 2025-03-07 18:03:16 -05:00
Lea Verou
a73b3d5697 Move vue components to separate directory 2025-03-07 17:46:12 -05:00
Lea Verou
7b6b570ac9 Fix theme remixing regressions 2025-03-07 17:43:29 -05:00
Lea Verou
438ddf5ba2 Suggested colors 2025-03-07 15:27:07 -05:00
Lea Verou
5216061c39 Remove commented out Safari workaround 2025-03-07 15:26:48 -05:00
Lea Verou
e466a0aa8d Pin instead of star 2025-03-07 15:25:39 -05:00
Lea Verou
08876bbda9 Fix bug with seed color order 2025-03-07 15:25:20 -05:00
Lea Verou
a73daf9426 Orange 2025-03-07 15:25:12 -05:00
Lea Verou
af832017d3 stringifyColor() 2025-03-07 14:28:13 -05:00
Lea Verou
9244bfbe15 Orange 2025-03-07 14:27:59 -05:00
Lea Verou
1f89043040 Refactor: move color-related code to separate modules 2025-03-07 11:22:52 -05:00
Lea Verou
9865a71499 Rename palettes/edit/palettes/app/ 2025-03-07 10:26:56 -05:00
Lea Verou
f2e8a71567 Uncomment orange 2025-03-07 10:23:55 -05:00
Lea Verou
72d8058259 Merge branch 'next' into custom-palettes 2025-03-05 23:19:39 -05:00
Lea Verou
08f652f0dc Start show saved variations, rework renaming/saving UI 2025-03-05 13:18:05 -05:00
Lea Verou
48b37b05bb Ensure generated tint lightness is still within range 2025-03-04 14:08:07 -05:00
Lea Verou
a3e1cebf18 My scales filter 2025-03-04 13:26:35 -05:00
Lea Verou
9632e57fd0 Thumbtack icon to star 2025-03-04 13:22:52 -05:00
Lea Verou
5bfac00428 Fix hue shift for darker colors 2025-03-04 12:55:18 -05:00
Lea Verou
a0069c9783 Improve chroma curves 2025-03-04 12:55:04 -05:00
Lea Verou
b43a3f736a Generate palette code 2025-03-04 12:07:47 -05:00
Lea Verou
7ed3e5e92b Another attempt to improve yellows + cleanup
- Generate yellow based on most vibrant scale, even if not neighboring
- Only take seed hues into account, not generated hues (which would compound any error)
- General cleanup
2025-03-04 11:58:14 -05:00
Lea Verou
01b697d9e6 Avoid premature optimization 2025-03-04 10:25:26 -05:00
Lea Verou
fa2e35a299 Fix bug 2025-02-28 17:52:22 -05:00
Lea Verou
cb5f8433d5 Interpolate % of chroma from gamut boundary rather than absolute chroma
Produces brighter, more balanced colors overall
2025-02-28 16:22:55 -05:00
Lea Verou
1fa95f66e8 Evaluate palette lightness relative to hue, better capping of consecutive hue shifts 2025-02-28 14:34:20 -05:00
Lea Verou
f682293c38 Cap hue shift for consecutive tints 2025-02-28 12:26:43 -05:00
Lea Verou
1993182f43 Reorder 2025-02-28 09:18:55 -05:00
Lea Verou
6f39781f1f Reorder 2025-02-28 09:17:57 -05:00
Lea Verou
bc170cce15 Permalinks for seed colors 2025-02-27 19:31:17 -05:00
Lea Verou
27af62591f Simplify permalinks 2025-02-27 19:25:14 -05:00
Lea Verou
e07aecb0a7 Fix 2025-02-27 11:00:45 -05:00
Lea Verou
8caeb26957 Fix 2025-02-27 10:27:26 -05:00
Lea Verou
ae6b66a3a4 Better hue spacing 2025-02-26 22:22:49 -05:00
Lea Verou
e9389b8bd5 Hue wheel visualization for every palette 2025-02-26 21:43:37 -05:00
Lea Verou
668666e1c9 Fixes 2025-02-26 21:43:29 -05:00
Lea Verou
a679693128 First stab at generating other hues based on seed colors 2025-02-26 19:02:30 -05:00
Lea Verou
3c02ce245e Refactor 2025-02-26 13:22:45 -05:00
Lea Verou
4a5b99c60d Refactor 2025-02-26 13:08:55 -05:00
Lea Verou
1a2d9ea4f1 Fix 2025-02-26 11:12:07 -05:00
Lea Verou
91d93d83f2 Emulate other palette 2025-02-26 11:05:51 -05:00
Lea Verou
6693cafe8e Interpolate subsequent hues 2025-02-26 11:01:24 -05:00
Lea Verou
d04e3d860e Moar Vue 2025-02-26 11:01:24 -05:00
Lea Verou
65f89cff84 Fix radius 2025-02-26 11:01:24 -05:00
Lea Verou
e26af1c293 Iterate 2025-02-26 11:01:24 -05:00
Lea Verou
538e132a27 Move tweak.js and tweak.css 2025-02-24 16:25:23 -05:00
191 changed files with 8222 additions and 8308 deletions

View File

@@ -1,12 +1,11 @@
# 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]
workflow_dispatch:
push:
branches: [next]
jobs:
ssr_test:

View File

@@ -7,10 +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 nunjucks from 'nunjucks';
// import componentList from './_data/componentList.js';
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';
@@ -29,6 +29,7 @@ const globalData = {
package: packageData,
isAlpha,
layout: 'page.njk',
server: {
head: '',
loginOrAvatar: '',
@@ -36,16 +37,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.
*/
@@ -195,30 +190,30 @@ 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,
// });
// }
// SSR plugin
// Make sure this is the last thing, we dont want to run the risk of accidentally transforming shadow roots with the nunjucks 2nd transform.
if (!isDev) {
//
// Problematic components in SSR land:
// - animation (breaks on navigation + ssr with Turbo)
// - mutation-observer (why SSR this?)
// - resize-observer (why SSR this?)
// - tooltip (why SSR this?)
//
const omittedModules = [];
const componentModules = componentList
.filter(component => !omittedModules.includes(component.tagName.split(/wa-/)[1]))
.map(component => {
const name = component.tagName.split(/wa-/)[1];
const componentDirectory = process.env.UNBUNDLED_DIST_DIRECTORY || path.join('.', 'dist');
return path.join(componentDirectory, 'components', name, `${name}.js`);
});
eleventyConfig.addPlugin(litPlugin, {
mode: 'worker',
componentModules,
});
}
return {
markdownTemplateEngine: 'njk',

View File

@@ -1 +1 @@
export { hueRanges as default } from '../assets/scripts/tweak/data.js';
export { HUE_RANGES as default } from '../assets/scripts/tweak/data.js';

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" data-fa-kit-code="b10bfbde90" data-cdn-url="{% cdnUrl %}" class="wa-cloak">
<html lang="en" data-fa-kit-code="b10bfbde90" data-cdn-url="{% cdnUrl %}">
<head>
{% include 'head.njk' %}
<meta name="theme-color" content="#f36944">
@@ -20,7 +20,7 @@
</head>
<body class="layout-{{ layout | stripExtension }}{{ ' page-wide' if wide }}">
<!-- use view="desktop" as default to reduce layout jank on desktop site. -->
<wa-page view="desktop" disable-navigation-toggle="" mobile-breakpoint="1140">
<wa-page view="desktop" disable-navigation-toggle="">
<header slot="header" class="wa-split">
{# Logo #}
<div id="docs-branding">
@@ -33,13 +33,13 @@
<span class="wa-desktop-only">{% include "logo.njk" %}</span>
<span class="wa-mobile-only">{% include "logo-simple.njk" %}</span>
</a>
<small id="version-number" class="wa-desktop-only">{{ package.version }}</small>
<wa-badge variant="warning" appearance="filled" class="wa-desktop-only">Alpha</wa-badge>
<small id="version-number" class="only-desktop">{{ package.version }}</small>
<wa-badge variant="warning" appearance="filled" class="only-desktop">Alpha</wa-badge>
</div>
<div id="docs-toolbar" class="wa-cluster wa-gap-xs">
{# Desktop selectors #}
<div class="wa-desktop-only wa-cluster wa-gap-xs">
<div class="only-desktop wa-cluster wa-gap-xs">
{% include "preset-theme-selector.njk" %}
{% include "color-scheme-selector.njk" %}
</div>
@@ -48,7 +48,7 @@
<wa-button id="search-trigger" appearance="outlined" size="small" data-search>
<wa-icon slot="prefix" name="magnifying-glass"></wa-icon>
Search
<kbd slot="suffix" class="wa-desktop-only">/</kbd>
<kbd slot="suffix" class="only-desktop">/</kbd>
</wa-button>
{# Login #}

View File

@@ -12,7 +12,7 @@
</tr>
</thead>
{% for hue in hues -%}
<tr data-hue="{{ hue }}">
<tr data-hue="{{ hue }}" v-if="'{{hue}}' in paletteScales">
<th>{{ hue | capitalize }}</th>
{% for tint_bg in tints -%}
{% set color_bg = palettes[paletteId][hue][tint_bg] %}

View File

@@ -3,8 +3,8 @@
<section id="grid" class="index-grid">
{% 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 groupedPages.meta.groupCount > 1 %}
<h2 class="index-category">
{% if pages.meta.url %}<a href="{{ pages.meta.url }}">{{ pages.meta.title }}</a>
{% else %}
{{ pages.meta.title }}

View File

@@ -23,12 +23,10 @@
<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">
{# Internal components #}
<script type="module" src="/assets/components/scoped.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.10/+esm"></script>
{# Web Awesome #}
<script type="module" src="/dist/webawesome.loader.js"></script>
<script type="module" src="/dist/webawesome.ssr-loader.js"></script>
<script type="module" src="/assets/scripts/theme-picker.js"></script>
{# Preset Theme #}
@@ -50,5 +48,6 @@
<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

@@ -0,0 +1,36 @@
<table class="colors main wa-palette-{{ paletteId }} static-palette">
<thead>
<tr>
<th></th>
<th class="core-column">Core tint</th>
{% for tint in tints -%}
<th>{{ tint }}</th>
{%- endfor %}
</tr>
</thead>
<tbody>
{%- set hueBefore = hues[hues|length - 2] -%}
{% for hue in hues -%}
{% set scale = palettes[paletteId][hue] %}
{% set coreTint = scale.maxChromaTint %}
{%- set coreColor = scale[coreTint] -%}
{%- set maxChroma = coreColor.c if coreColor.c > maxChroma else maxChroma -%}
<tr data-hue="{{ hue }}" class="color-scale" style="--swatch-text-color: light-dark(var(--wa-color-{{ hue }}-10), white)">
<th>{{ hue | capitalize }}</th>
<td class="core-column" style="--color: var(--wa-color-{{ hue }})">
<div class="color swatch" style="color-scheme: {{ 'light' if scale.maxChromaTint > 60 else 'dark' }};">
{{ scale.maxChromaTint }}
</div>
</td>
{% for tint in tints -%}
{%- set color = scale[tint] -%}
<td style="--color: var(--wa-color-{{ hue }}-{{ tint }}); color-scheme: ">
<div class="color swatch" style="color-scheme: {{ 'light' if tint > 60 else 'dark' }};">
<wa-copy-button value="--wa-color-{{ hue }}-{{ tint }}" copy-label="--wa-color-{{ hue }}-{{ tint }}"></wa-copy-button>
</div>
</td>
{%- endfor -%}
</tr>
</tbody>
{% endfor %}
</table>

View File

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

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

@@ -1,22 +1,22 @@
{% if since -%}
<wa-badge variant="neutral">Since {{ since }}</wa-badge>
<wa-badge variant="neutral" class="since">Since {{ since }}</wa-badge>
{% endif -%}
{%- if status %}
{%- if status == "wip" %}
<wa-badge variant="danger">
<wa-badge variant="danger" class="status">
<wa-icon name="pickaxe"></wa-icon>
Work In Progress
</wa-badge>
{%- elif status == "experimental" %}
<wa-badge variant="warning">
<wa-badge variant="warning" class="status">
<wa-icon name="flask"></wa-icon>
Experimental
</wa-badge>
{%- elif status == "stable" %}
<wa-badge variant="brand">Stable</wa-badge>
<wa-badge variant="brand" class="status">Stable</wa-badge>
{%- else %}
<wa-badge>{{ status}}</wa-badge>
<wa-badge class="status">{{ status}}</wa-badge>
{%- endif -%}
{%- endif %}

View File

@@ -1,8 +0,0 @@
<svg width="96" height="57" viewBox="0 0 96 57" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 1H84C90.0751 1 95 5.92487 95 12V45C95 51.0751 90.0751 56 84 56H12C5.92487 56 1 51.0751 1 45V12C1 5.92487 5.92487 1 12 1Z" fill="white" stroke="#E4E5E9" stroke-width="2"/>
<rect x="7" y="39" width="50" height="11" rx="4" fill="#4895FD"/>
<rect x="7" y="8" width="41" height="5" fill="#616D8A"/>
<rect x="7" y="19.75" width="76" height="3" fill="#D9D9D9"/>
<rect x="7" y="25" width="76" height="3" fill="#D9D9D9"/>
<rect x="7" y="32" width="76" height="3" fill="#D9D9D9"/>
</svg>

Before

Width:  |  Height:  |  Size: 587 B

View File

@@ -1,20 +1,31 @@
{% 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 %}
<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">
{% 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>
<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>
{% 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>

View File

@@ -1,10 +0,0 @@
<svg width="120" height="87" viewBox="0 0 240 178" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="39" width="67" height="63" rx="12" fill="#D9D9D9"/>
<rect y="115" width="67" height="63" rx="12" fill="#D9D9D9"/>
<rect width="67" height="19" rx="6" fill="#D9D9D9"/>
<rect x="87" y="39" width="67" height="63" rx="12" fill="#D9D9D9"/>
<rect x="174" y="39" width="67" height="63" rx="12" fill="#D9D9D9"/>
<rect x="174" y="7" width="67" height="4.75" rx="2.375" fill="#D9D9D9"/>
<rect x="87" y="115" width="67" height="63" rx="12" fill="#D9D9D9"/>
<rect x="174" y="115" width="67" height="63" rx="12" fill="#D9D9D9"/>
</svg>

Before

Width:  |  Height:  |  Size: 631 B

View File

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

View File

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

View File

@@ -1,29 +0,0 @@
{% 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

@@ -255,7 +255,7 @@
{# Importing #}
<h2>Importing</h2>
<p>
The <a href="/docs/#quick-start-autoloading-via-cdn">autoloader</a> is the recommended way to import components. If you prefer to do it manually, use one of the following code snippets.
The <a href="/docs/installation/#quick-start-autoloading-via-cdn">autoloader</a> is the recommended way to import components. If you prefer to do it manually, use one of the following code snippets.
</p>
<wa-tab-group label="How would you like to import this component?">

View File

@@ -1,38 +1,39 @@
{% set hasSidebar = true %}
{% set hasOutline = true %}
{% set paletteId = page.fileSlug %}
{% set paletteId = "default" if page.fileSlug == 'custom' else page.fileSlug %}
{% set isCustom = page.fileSlug == 'custom' %}
{% set tints = ["95", "90", "80", "70", "60", "50", "40", "30", "20", "10", "05"] %}
{% extends '../_includes/base.njk' %}
{% block head %}
<style>@import url('/dist/styles/color/{{ paletteId }}.css') layer(palette.{{ paletteId }});</style>
<link href="{{ page.url }}../tweak.css" rel="stylesheet">
<script type="module" src="{{ page.url }}../tweak.js"></script>
<link href="{{ page.url }}../app/tweak.css" rel="stylesheet">
<script type="module" src="{{ page.url }}../app/tweak.js"></script>
{% endblock %}
{% block header %}
<div id="palette-app" data-palette-id="{{ paletteId }}">
<div id="palette-app" data-slug="{{ page.fileSlug }}" data-palette-id="{{ page.fileSlug }}">
<div
:class="{
tweaking: tweaking.chroma,
'tweaking-chroma': tweaking.chroma,
'tweaking-hue': tweaking.chroma,
'tweaking-gray-chroma': tweaking.grayChroma,
seeded: isSeeded,
'tweaked-chroma': tweaked?.chroma,
'tweaked-hue': tweaked?.hue,
'tweaked-any': tweaked
'tweaked-any': Object.keys(tweaksHumanReadable).length,
}"
:style="{
'--chroma-scale': chromaScale,
'--gray-chroma': tweaked?.grayChroma ? grayChroma : '',
'--gray-chroma': tweaked?.grayChroma ? grayChroma : originalGrayChroma,
'--max-c': maxChroma,
'--avg-l': L_RANGES[level].mid,
}">
<header id="palette-info">
{% include 'breadcrumbs.njk' %}
<h1 class="title">
<span v-content="title">{{ title }}</span>
<template v-if="saved || tweaked">
<span v-content="saved?.title || (step > 0 ? defaultPaletteTitle : paletteTitle)">{{ title }}</span>
<template v-if="saved || step > 0">
<wa-icon-button name="pencil" label="Rename palette" @click="rename"></wa-icon-button>
<wa-icon-button v-if="saved" class="delete" name="trash" label="Delete palette" @click="deleteSaved"></wa-icon-button>
<wa-button @click="save()" :disabled="!unsavedChanges"
@@ -46,11 +47,11 @@
</template>
</h1>
<div class="block-info">
<code class="class">.wa-palette-{{ paletteId }}</code>
<div class="block-info" v-cloak>
<code class="class" v-if="saved || !isCustom || step > 0">.wa-palette-<span v-content="slug">{{ page.fileSlug }}</span></code>
{% include '../_includes/status.njk' %}
{% if not isPro %}
<wa-badge class="pro" v-if="tweaked">PRO</wa-badge>
<wa-badge class="pro" v-if="tweaked || isCustom">PRO</wa-badge>
{% endif %}
</div>
{% if description %}
@@ -58,17 +59,32 @@
{{ description | inlineMarkdown | safe }}
</p>
{% endif %}
{% raw %}
<div class="hue-wheel" v-if="!isCustom || step > 1">
<template v-for="color, hue in coreColors">
<template v-if="!isCustom || seedHues[hue]">
<div :id="`hue-wheel-${hue}`" class="color"
:style="{
'--color': color,
'--h': color.get('oklch.h'),
'--c': color.get('oklch.c'),
'--l': color.get('oklch.l'),
}"></div>
<wa-tooltip :for="`hue-wheel-${ hue }`" hoist>{{ capitalize(hue) }} {{ coreLevels[hue] }}</wa-tooltip>
</template>
</template>
</div>
{% endraw %}
</header>
{% endblock %}
{% block afterContent %}
{% set maxChroma = 0 %}
<wa-callout size="small" class="tweaked-callout" variant="warning">
<wa-callout size="small" class="tweaked-callout" variant="warning" v-if="!isCustom">
<wa-icon name="sliders-simple" slot="icon" variant="regular"></wa-icon>
This palette has been tweaked.
<div class="wa-cluster wa-gap-xs">
<wa-tag v-for="tweakHumanReadable, param in tweaksHumanReadable" removable @wa-remove="reset(param)" 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">
@@ -79,6 +95,10 @@
</wa-button>
</wa-callout>
<h2>Scales</h2>
{% include "palette.njk" %}
<table class="colors main wa-palette-{{ paletteId }}">
<thead>
<tr>
@@ -89,128 +109,126 @@
{%- endfor %}
</tr>
</thead>
{# Initialize to last hue before gray #}
{%- set hueBefore = hues[hues|length - 2] -%}
{% for hue in hues -%}
{% set coreTint = palettes[paletteId][hue].maxChromaTint %}
{%- set coreColor = palettes[paletteId][hue][coreTint] -%}
{%- set maxChroma = coreColor.c if coreColor.c > maxChroma else maxChroma -%}
{% if hue === 'gray' %}
<tr data-hue="{{ hue }}" class="color-scale"
:class="{tweaking: tweaking.grayChroma, tweaked: tweaked.grayChroma || tweaked.grayColor }">
{% else %}
<tr data-hue="{{ hue }}" class="color-scale"
:class="{tweaking: tweaking.{{ hue }}, tweaked: hueShifts.{{ hue }} }"
:style="{ '--hue-shift': hueShifts.{{ hue }} || '' }">
{% endif %}
<th>
{{ hue | capitalize }}
</th>
<td class="core-column"
style="--color: var(--wa-color-{{ hue }})"
:style="{
'--color-tweaked': colors.{{ hue }}[{{ coreTint }}],
'--color-gray-undertone': colors[grayColor][{{coreTint}}],
'--color-tweaked-no-gray-chroma': colorsMinusGrayChroma.{{ hue }}[{{ coreTint }}],
}">
<wa-dropdown>
<div slot="trigger" id="core-{{ hue }}-swatch" data-tint="core" class="color swatch"
style="background-color: var(--wa-color-{{ hue }}); color: var(--wa-color-{{ hue }}-{{ '05' if palettes[paletteId][hue].maxChromaTint > 60 else '95' }});"
>
{{ palettes[paletteId][hue].maxChromaTint }}
<wa-icon name="sliders-simple" class="tweak-icon"></wa-icon>
</div>
<div class="popup">
{% if hue === 'gray' %}
<wa-radio-group class="core-color" orientation="horizontal" v-model="grayColor">
{% for h in hues -%}
{%- if h !== 'gray' -%}
<wa-radio-button id="gray-undertone-{{ h }}" value="{{ h }}" label="{{ h | capitalize }}" style="--color: var(--wa-color-{{ h }})"></wa-radio-button>
<wa-tooltip for="gray-undertone-{{ h }}">
{{ h | capitalize }}
</wa-tooltip>
{%- endif -%}
{%- endfor -%}
<div slot="label">
Gray undertone
</div>
</wa-radio-group>
<div class="decorated-slider gray-chroma-slider" :style="{'--max': maxGrayChroma}">
<wa-slider name="gray-chroma" v-model="grayChroma" ref="grayChromaSlider"
value="0" min="0" :max="maxGrayChroma" step="0.01"
@input="tweaking.grayChroma = true" @change="tweaking.grayChroma = false">
<div slot="label">
Gray colorfulness
<wa-icon-button @click="grayChroma = originalGrayChroma" class="clear-button" name="circle-xmark" library="system" variant="regular" label="Reset"></wa-icon-button>
</div>
</wa-slider>
<div class="label-min">Neutral</div>
<div class="label-max" v-content="moreHue[grayColor]">Warmer/Cooler</div>
</div>
{% else %}
{%- set hueAfter = hues[loop.index0 + 1] -%}
{%- set hueAfter = hues[0] if hueAfter == 'gray' else hueAfter -%}
{%- set minShift = hueRanges[hue].min - coreColor.h | round -%}
{%- set maxShift = hueRanges[hue].max - coreColor.h | round -%}
<div class="decorated-slider hue-shift-slider" style="--min: {{ minShift }}; --max: {{ maxShift }};">
<wa-slider name="{{ hue }}-shift" v-model="hueShifts.{{ hue }}" value="0"
min="{{ minShift }}" max="{{ maxShift }}" step="1"
@input="tweaking.hue = tweaking.{{hue}} = true"
@change="tweaking.hue = tweaking.{{ hue }} = false">
<div slot="label">
Tweak {{ hue }} hue
<wa-icon-button @click="hueShifts.{{ hue }} = 0" class="clear-button" name="circle-xmark" library="system" variant="regular" label="Reset"></wa-icon-button>
</div>
</wa-slider>
<div class="label-min">More {{hueBefore}}</div>
<div class="label-max">More {{hueAfter}}</div>
</div>
{%- set hueBefore = hue -%}
{% endif %}
<div class="wa-gap-s">
<code>--wa-color-{{ hue }}</code>
<wa-copy-button value="--wa-color-{{ hue }}" copy-label="--wa-color-{{ hue }}"></wa-copy-button>
</div>
</div>`
</wa-dropdown>
</td>
{% for tint in tints -%}
{%- set color = palettes[paletteId][hue][tint] -%}
<td data-tint="{{ tint }}" style="--color: var(--wa-color-{{ hue }}-{{ tint }})"
{% raw %}
<tbody v-cloak>
<tr v-for="hue in paletteScalesList" :data-hue="hue" :key="hue"
class="color-scale" :class="{
tweaked: hue === 'gray' ? tweaked.grayChroma || tweaked.grayColor : hueShifts[hue],
}"
:style="{
'--color-tweaked': colors.{{ hue }}[{{ tint }}],
'--color-tweaked-no-gray-chroma': colorsMinusGrayChroma.{{ hue }}[{{ tint }}],
'--swatch-text-color': `light-dark(var(--wa-color-${ hue }-10), white)`,
'--hue-shift': hueShifts[hue] || ''
}">
<div class="color swatch" style="--color: var(--wa-color-{{ hue }}-{{ tint }})">
<wa-copy-button value="--wa-color-{{ hue }}-{{ tint }}" copy-label="--wa-color-{{ hue }}-{{ tint }}"></wa-copy-button>
</div>
</td>
{%- endfor -%}
</tr>
{%- endfor %}
<th>
{{ capitalize(hue) }}
<info-tip v-if="isCustom && !seedHues[hue]">
<wa-icon name="sparkles" style="color: var(--wa-color-gray-50)"></wa-icon>
<template #content>Generated scale</template>
</info-tip>
</th>
<td class="core-column" :style="{'--original-color': `var(--wa-color-${ hue })`, '--color': colors[hue][coreLevels[hue]]}">
<color-popup :title="capitalize(hue) + ' (core)'" :token="`--wa-color-${ hue }`" :color="coreColors[hue]"
:pinned="!!seedColors[colorToIndex[hue].core]"
:deletable="isCustom" @delete="deleteColor(colorToIndex[hue].core)"
:pinnable="isCustom" @pin="addColor(coreColors[hue] + '')">
<div slot="trigger" :id="`core-${ hue }-swatch`" class="color swatch" :style="{colorScheme: coreLevels[hue] > 60 ? 'light' : 'dark'}">
<span v-content="coreLevels[hue]"></span>
<wa-icon name="sliders-simple" class="tweak-icon"></wa-icon>
</div>
<template #content>
<template v-if="hue === 'gray'">
<color-swatch-picker :model-value="computedGrayColor" @update:model-value="grayColor = $event" label="Gray undertone" :colors="coreColors"></color-swatch-picker>
</template>
<template v-else>
<color-slider v-if="isCustom && seedColors[colorToIndex[hue].core]"
coord="h" type="shift"
v-model:color="seedColors[colorToIndex[hue].core].color"
:default-value="seedColors[colorToIndex[hue].core].inputColor.oklch.h"
:min="HUE_RANGES[hue].min + 1" :max="HUE_RANGES[hue].max"
label="Adjust hue" :label-min="moreHue[hueBefore[hue]]" :label-max="moreHue[hueAfter[hue]]"
></color-slider>
<color-slider v-if="!isCustom && baseCoreColors[hue]"
coord="h" type="shift"
v-model="hueShifts[hue]"
:default-color="baseCoreColors[hue]"
:min="HUE_RANGES[hue].min + 1" :max="HUE_RANGES[hue].max"
label="Adjust hue" :label-min="moreHue[hueBefore[hue]]" :label-max="moreHue[hueAfter[hue]]"
></color-slider>
</template>
<color-slider v-if="hue === 'gray'" coord="c" type="scale"
:model-value="computedGrayChroma"
@update:model-value="grayChroma = $event"
:default-color="baseCoreColors[computedGrayColor]"
:base-value="baseCoreColors[originalGrayColor].oklch.c"
:default-value-relative="originalGrayChroma"
:min="0" :max-relative="maxGrayChroma" :step="0.00001"
label="Gray colorfulness" label-min="Neutral" :label-max="moreHue[computedGrayColor]"
></color-slider>
<color-slider v-else-if="isCustom" v-model:color="seedColors[colorToIndex[hue].core].color"
:default-value="seedColors[colorToIndex[hue].core].inputColor?.oklch.c"
coord="c"
:min="Math.max(coreColors.gray.oklch.c, ...Object.keys(seedHues[hue]).filter(t => t !== coreLevels[hue]).map(t => seedHues[hue][t].oklch.c))"
:max="getMaxChroma(colors[hue].core?.oklch.l, colors[hue].core?.oklch.h) - 0.001" :step="0.00001"
label="Adjust colorfulness" label-min="More muted" label-max="More vibrant"
label-default="Entered color"
format-type="scale"
></color-slider>
</template>
</color-popup>
</td>
<td v-for="tint in tints.toReversed()" :data-tint="tint" :style="{'--original-color': `var(--wa-color-${ hue }-${tint})`, '--color': colors[hue][tint] }">
<color-popup :title="capitalize(hue) + ' ' + tint" :token="`--wa-color-${ hue }-${ tint }`" :color="colors[hue][tint]"
:pinned="!!seedColors[colorToIndex[hue][tint]]"
:deletable="isCustom" @delete="deleteColor(colorToIndex[hue][tint])"
:pinnable="isCustom" @pin="addColor({hue, pinnedHue: hue, level: tint})">
<div slot="trigger" class="color swatch" :style="{ colorScheme: tint > 60 ? 'light' : 'dark' }">
<wa-icon class="pinned-icon" name="thumbtack" variant="regular" v-if="seedColors[colorToIndex[hue][tint]]"></wa-icon>
<wa-icon name="sliders-simple" class="tweak-icon"></wa-icon>
</div>
<template #content v-if="isCustom && seedHues[hue] && (tint == '95' || tint == '05' || seedColors[colorToIndex[hue][tint]]) && tweakBase[hue][tint]">
<color-slider v-if="HUE_RANGES[hue]" v-model:color="colors[hue][tint]"
:default-value="colors[hue][tweakBase[hue][tint]].oklch.h"
@input="!seedColors[colorToIndex[hue][tint]] ? addColor({hue, pinnedHue: hue, level: tint}) : null"
@update:color="seedColors[colorToIndex[hue][tint]] ? seedColors[colorToIndex[hue][tint]].color = $event : null"
coord="h"
:min="HUE_RANGES[hue].mid - 70" :max="HUE_RANGES[hue].mid + 70" :step="1"
label="Hue shift" :label-min="moreHue[hueBefore[hue]]" :label-max="moreHue[hueAfter[hue]]"
:label-default="`${capitalize(hue)} ${tweakBase[hue][tint]}`"
format-type="shift"
></color-slider>
<color-slider v-if="hue != 'gray'" v-model:color="colors[hue][tint]"
:default-value="colors[hue][tweakBase[hue][tint]].oklch.c"
@input="!seedColors[colorToIndex[hue][tint]] ? addColor({hue, pinnedHue: hue, level: tint}) : null"
@update:color="seedColors[colorToIndex[hue][tint]] ? seedColors[colorToIndex[hue][tint]].color = $event : null"
coord="c"
:min="coreColors.gray.oklch.c + 0.001"
:max="tint == coreLevels[hue] ? maxChroma(colors[hue][tweakBase[hue][tint]].oklch.l, colors[hue][tweakBase[hue][tint]].oklch.h) : coreColors[hue].oklch.c - 0.001" :step="0.001"
label="Colorfulness" label-min="More muted" label-max="More vibrant"
format-type="scale"
:label-default="`${capitalize(hue)} ${tweakBase[hue][tint]}`"
></color-slider>
</template>
</color-popup>
</td>
</tr>
{% endraw %}
</tbody>
</table>
{% set chromaScaleBounds = [
(0.08 / maxChroma) | number({maximumFractionDigits: 2}),
(0.3 / maxChroma]) | number({maximumFractionDigits: 2}) -%}
<div class="decorated-slider chroma-scale-slider wa-palette-{{ paletteId }}"
:class="{ tweaked: chromaScale !== 1 }"
style="--min: {{ chromaScaleBounds[0] }}; --max: {{ chromaScaleBounds[1] }};">
<wa-slider name="chroma-scale" ref="chromaScaleSlider"
v-model="chromaScale" value="1" step="0.01"
min="{{ chromaScaleBounds[0] }}" max="{{ chromaScaleBounds[1] }}"
@input="tweaking.chroma = true"
@change="tweaking.chroma = false">
<div slot="label">
Overall colorfulness
<wa-icon-button @click="chromaScale = 1" class="clear-button" name="circle-xmark" library="system" variant="regular" label="Reset"></wa-icon-button>
</div>
</wa-slider>
<div class="label-min">More muted</div>
<div class="label-max">More vibrant</div>
</div>
<color-slider v-if="!isCustom" :class="{ tweaked: chromaScale !== 1 }"
type="scale"
v-model="chromaScale"
coord="c"
:default-color="baseMaxChromaColor"
:default-value="baseMaxChroma"
:min="MAX_CHROMA_BOUNDS.min" :max="MAX_CHROMA_BOUNDS.max" :step="0.01"
label="Overall colorfulness" label-min="More muted" label-max="More vibrant"
></color-slider>
{% if page.fileSlug != 'custom' %}
<h2>Used By</h2>
<section class="index-grid">
@@ -220,6 +238,7 @@ style="--min: {{ chromaScaleBounds[0] }}; --max: {{ chromaScaleBounds[1] }};">
{%- endif -%}
{% endfor %}
</section>
{% endif %}
{% markdown %}
## Color Contrast
@@ -312,8 +331,22 @@ Add the following code at the top of your CSS file:
</wa-tab-panel>
</wa-tab-group>
{% endmarkdown %}
<section id="saved" class="index-grid" v-if="savedVariations?.length">
<h2 class="index-category">Saved {{ 'custom palettes' if page.fileSlug == 'custom' else title + ' variations' }}</h2>
<a v-for="palette of savedVariations" :href="'/docs/palettes/' + palette.id">
<wa-card with-header>
<div slot="header">
{# {% include "svgs/palette.njk" %} #}
{% include "svgs/thumbnail-placeholder.njk" %}
</div>
<span class="page-name" v-text="palette.title"></span>
</wa-card>
</a>
</section>
</div></div> {# end palette app #}
{% endblock %}

View File

@@ -1,5 +0,0 @@
{% extends '../_layouts/block.njk' %}
{% block head %}
<link href="/docs/patterns/patterns.css" rel="stylesheet">
{% endblock %}

View File

@@ -25,8 +25,8 @@ wa_data.palettes = {
{% endfor %}
};
</script>
<link href="/docs/themes/remix.css" rel="stylesheet">
<script src="/docs/themes/remix.js" type="module"></script>
<link href="{{ page.url }}../remix.css" rel="stylesheet">
<script src="{{ page.url }}../remix.js" type="module"></script>
{% endblock %}
{% block header %}
@@ -81,7 +81,7 @@ wa_data.palettes = {
{% set palette = defaultPalette %}
</wa-select>
<wa-select name="brand" label="Brand color" value="" clearable>
<wa-select class="color-select" name="brand" label="Brand color" value="" clearable>
<div class="selected-swatch" slot="prefix"></div>
{% for hue in hues %}
{% set currentBrand = hue == brand %}

View File

@@ -33,7 +33,7 @@ export function copyCodePlugin(eleventyConfig, options = {}) {
// Add a copy button
pre.innerHTML += `<wa-icon-button href="#${preId}" class="block-link-icon" name="link"></wa-icon-button>
<wa-copy-button from="${codeId}" class="copy-button"></wa-copy-button>`;
<wa-copy-button from="${codeId}" class="copy-button" hoist></wa-copy-button>`;
});
return doc.toString();

View File

@@ -178,10 +178,6 @@ 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
@@ -202,7 +198,7 @@ export function groupPages(collection, options = {}, page) {
options = { tags: options };
}
let { tags, groups, titles = {}, other = 'Other', filter = show } = options;
let { tags, groups, titles = {}, other = 'Other' } = options;
if (groups === undefined && Array.isArray(tags)) {
groups = tags;
@@ -241,10 +237,6 @@ export function groupPages(collection, options = {}, page) {
let byUrl = {};
let byParentUrl = {};
if (filter) {
collection = collection.filter(filter);
}
for (let item of collection) {
let url = item.page.url;
let parentUrl = item.data.parentUrl;
@@ -321,13 +313,6 @@ export function groupPages(collection, options = {}, page) {
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', {

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

View File

@@ -2,7 +2,6 @@
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) {
@@ -53,9 +52,8 @@ export function searchPlugin(options = {}) {
return content;
});
eleventyConfig.on('eleventy.after', ({ directories }) => {
const { output } = directories;
const outputFilename = path.resolve(join(output, 'search.json'));
eleventyConfig.on('eleventy.after', ({ dir }) => {
const outputFilename = join(dir.output, 'search.json');
const map = [];
const searchIndex = lunr(async function () {
let index = 0;

View File

@@ -1,171 +0,0 @@
/**
* 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 data-fa-kit-code="b10bfbde90" type="module" src="${cdnUrl}webawesome.loader.js"></script>\n` +
`<script 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,11 +1,3 @@
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);
@@ -26,10 +18,8 @@ function updateResults(input) {
}
}
const debouncedUpdateResults = debounce(updateResults, 300);
document.documentElement.addEventListener('input', e => {
if (e.target?.matches('#block-filter wa-input')) {
debouncedUpdateResults(e.target);
updateResults(e.target);
}
});

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,269 @@
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}"]`);
saved: [],
/**
* Update saved palettes from local storage
*/
fromLocalStorage() {
// Replace contents of array without breaking references
let saved = localStorage.savedPalettes ? JSON.parse(localStorage.savedPalettes) : [];
this.saved.splice(0, this.saved.length, ...saved);
},
renderEntity(entity) {
let { url, parentUrl } = entity;
// 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);
}
/**
* Write palettes to local storage
*/
toLocalStorage() {
if (this.saved.length > 0) {
localStorage.savedPalettes = JSON.stringify(this.saved);
} else {
delete localStorage.savedPalettes;
}
},
};
globalThis.sidebar = sidebar;
sidebar.palettes.fromLocalStorage();
// 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));
// Palettes were updated in another tab
addEventListener('storage', () => sidebar.palettes.fromLocalStorage());
sidebar.palette = {
getUid() {
let savedPalettes = sidebar.palettes.saved;
let uids = new Set(savedPalettes.map(p => p.uid));
if (savedPalettes.length === 0) {
return 1;
}
// Find first available number
for (let i = 1; i <= savedPalettes.length + 1; i++) {
if (!uids.has(i)) {
return i;
}
}
},
equals(p1, p2) {
if (!p1 || !p2) {
return false;
}
return p1.id === p2.id && p1.uid === p2.uid;
},
delete(palette) {
let savedPalettes = sidebar.palettes.saved;
let count = savedPalettes.length;
if (count === 0 || !palette.uid) {
// No stored palettes or this palette has not been saved
return;
}
// TODO improve UX of this
if (!confirm(`Are you sure you want to delete palette “${palette.title}”?`)) {
return;
}
for (let index; index > -1; index = savedPalettes.findIndex(p => p.uid === palette.uid)) {
savedPalettes.splice(index, 1);
}
if (savedPalettes.length === count) {
// Nothing was removed
return;
}
// Update UI
let pathname = `/docs/palettes/${palette.id}/`;
let url = pathname + palette.search;
let uls = new Set();
for (let a of document.querySelectorAll(`#sidebar a[href="${url}"]`)) {
let li = a.closest('li');
let ul = li.closest('ul');
uls.add(ul);
li.remove();
}
// Remove empty lists
for (let ul of uls) {
if (!ul.children.length) {
ul.remove();
}
}
sidebar.updateCurrent();
sidebar.palettes.toLocalStorage();
if (globalThis.paletteApp?.saved?.uid === palette.uid) {
// We deleted the currently active palette
paletteApp.postDelete();
}
},
render(palette) {
// Find existing <a>
let { title, id, search, uid } = palette;
for (let a of document.querySelectorAll(`#sidebar a[href^="/docs/palettes/${id}/"][data-uid="${uid}"]`)) {
// Palette already in sidebar, just update it
a.textContent = palette.title;
a.href = `/docs/palettes/${id}/${search}`;
return;
}
let pathname = `/docs/palettes/${id}/`;
let url = pathname + search;
let parentA = document.querySelector(`a[href="${pathname}"]`);
let parentLi = parentA?.closest('li');
let a;
if (parentLi) {
a = Object.assign(document.createElement('a'), { href: url, textContent: title });
a.dataset.uid = uid;
let badges = [...parentLi.querySelectorAll('wa-badge')].map(badge => badge.cloneNode(true));
let ul = parentLi.querySelector('ul') ?? parentLi.appendChild(document.createElement('ul'));
let li = document.createElement('li');
let deleteButton = Object.assign(document.createElement('wa-icon-button'), {
name: 'trash',
label: 'Delete',
className: 'delete',
});
deleteButton.addEventListener('click', () => {
let palette = { id, uid, title: a.textContent, search: a.search };
sidebar.palette.delete(palette);
});
li.append(a, ' ', ...badges, deleteButton);
ul.appendChild(li);
}
},
/**
* Save a palette, either by updating its existing entry or creating a new one
* @param {object} palette
*/
save(palette) {
if (!palette.uid) {
// First time saving
palette.uid = this.getUid();
}
let savedPalettes = sidebar.palettes.saved;
let existingIndex = palette.uid ? sidebar.palettes.saved.findIndex(p => p.uid === palette.uid) : -1;
let newIndex = existingIndex > -1 ? existingIndex : savedPalettes.length;
let [oldValues] = sidebar.palettes.saved.splice(newIndex, 1, palette);
this.render(palette, oldValues);
sidebar.updateCurrent();
sidebar.palettes.toLocalStorage();
return palette;
},
};
sidebar.updateCurrent = function () {
// Find the sidebar link with the longest shared prefix with the current URL
let pathParts = location.pathname.split('/').filter(Boolean);
let prefixes = [];
if (pathParts.length === 1) {
// If at /docs/ we just use that, otherwise we want at least two parts (/docs/xxx/)
prefixes.push('/' + pathParts[0] + '/');
} else {
for (let i = 2; i <= pathParts.length; i++) {
prefixes.push('/' + pathParts.slice(0, i).join('/') + '/');
}
}
// Last prefix includes the search too (if any)
if (location.search) {
let params = new URLSearchParams(location.search);
params.sort();
prefixes.push(prefixes.at(-1) + location.search);
}
// We want to start from the longest prefix
prefixes.reverse();
let candidates;
let matchingPrefix;
for (let prefix of prefixes) {
candidates = document.querySelectorAll(`#sidebar a[href^="${prefix}"]`);
if (candidates.length > 0) {
matchingPrefix = prefix;
break;
}
}
if (!matchingPrefix) {
// Abort mission
return;
}
if (matchingPrefix === pathParts.at(-1)) {
// Full path matches, check search
if (location.search) {
candidates = [...candidates];
let searchParams = new URLSearchParams(location.search);
if (searchParams.has('uid')) {
// Only consider candidates with the same uid
candidates = candidates.filter(a => {
let params = new URLSearchParams(a.search);
return params.get('uid') === searchParams.get('uid');
});
} else {
// Sort candidates based on how many params they have in common, in descending order
candidates = candidates.sort((a, b) => {
return countSharedSearchParams(searchParams, b.search) - countSharedSearchParams(searchParams, a.search);
});
}
}
}
if (candidates.length > 0) {
for (let current of document.querySelectorAll('#sidebar a.current')) {
current.classList.remove('current');
}
candidates[0].classList.add('current');
}
};
sidebar.render = function () {
this.palettes.render();
};
sidebar.render();
window.addEventListener('turbo:render', () => sidebar.render());
function countSharedSearchParams(searchParams, search) {
if (!search || search === '?') {
return 0;
}
let params = new URLSearchParams(search);
return [...searchParams.keys()].filter(k => params.get(k) === searchParams.get(k)).length;
}

View File

@@ -1,5 +1,14 @@
import { domChange } from './util/dom-change.js';
export { domChange };
// Helper for view transitions
export function domChange(fn, { behavior = 'smooth' } = {}) {
const canUseViewTransitions =
document.startViewTransition && !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (canUseViewTransitions && behavior === 'smooth') {
document.startViewTransition(fn);
} else {
fn(true);
}
}
export function nextFrame() {
return new Promise(resolve => requestAnimationFrame(resolve));
@@ -91,7 +100,6 @@ 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,6 +1,3 @@
import 'https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.10/+esm';
import { preventTurboFouce } from '/dist/webawesome.js';
if (!window.___turboScrollPositions___) {
window.___turboScrollPositions___ = {};
}
@@ -73,4 +70,3 @@ function fixDSD(e) {
window.addEventListener('turbo:before-cache', saveScrollPosition);
window.addEventListener('turbo:before-render', restoreScrollPosition);
window.addEventListener('turbo:render', restoreScrollPosition);
preventTurboFouce();

View File

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

View File

@@ -12,49 +12,6 @@ export const urls = {
typography: id => `styles/themes/${id}/typography.css`,
};
export const selectors = {
palette: id =>
[':where(:root)', ':host', ":where([class^='wa-theme-'], [class*=' wa-theme-'])", `.wa-palette-${id}`].join(',\n'),
};
export const hueRanges = {
red: { min: 5, max: 35 }, // 30
orange: { min: 35, max: 60 }, // 25
yellow: { min: 60, max: 112 }, // 45
green: { min: 112, max: 170 }, // 55
cyan: { min: 170, max: 220 }, // 50
blue: { min: 220, max: 265 }, // 45
indigo: { min: 265, max: 290 }, // 25
purple: { min: 290, max: 320 }, // 30
pink: { min: 320, max: 365 }, // 45
};
export const moreHue = {
red: 'Redder',
orange: 'More orange', // https://www.reddit.com/r/grammar/comments/u9n0uo/is_it_oranger_or_more_orange/
yellow: 'Yellower',
green: 'Greener',
cyan: 'More cyan',
blue: 'Bluer',
indigo: 'More indigo',
pink: 'Pinker',
};
/**
* Max gray chroma (% of chroma of undertone) per hue
*/
export const maxGrayChroma = {
red: 0.2,
orange: 0.2,
yellow: 0.25,
green: 0.25,
cyan: 0.3,
blue: 0.35,
indigo: 0.35,
purple: 0.3,
pink: 0.25,
};
export const docsURLs = {
colors: '/docs/themes/',
palette: '/docs/palettes/',
@@ -68,6 +25,189 @@ export const icons = {
typography: 'font-case',
};
export const hues = Object.keys(hueRanges);
export const selectors = {
palette: id =>
[':where(:root)', ':host', ":where([class^='wa-theme-'], [class*=' wa-theme-'])", `.wa-palette-${id}`].join(',\n'),
};
export const HUE_RANGES = {
red: { min: 15, max: 35 }, // 20
orange: { min: 35, max: 75 }, // 40
yellow: { min: 75, max: 110 }, // 35
green: { min: 115, max: 170 }, // 55
cyan: { min: 170, max: 220 }, // 50
blue: { min: 220, max: 265 }, // 45
indigo: { min: 265, max: 290 }, // 25
purple: { min: 290, max: 320 }, // 30
pink: { min: 320, max: 375 }, // 55
};
export const hues = Object.keys(HUE_RANGES);
export const allHues = [...hues, 'gray'];
export const tints = ['05', '10', '20', '30', '40', '50', '60', '70', '80', '90', '95'];
export const L_RANGES = {
'05': { min: 0.18, max: 0.2 },
10: { min: 0.23, max: 0.25 },
20: { min: 0.31, max: 0.35 },
30: { min: 0.38, max: 0.43 },
40: { min: 0.45, max: 0.5 },
50: { min: 0.55, max: 0.6 },
60: { min: 0.65, max: 0.7 },
70: { min: 0.73, max: 0.78 },
80: { min: 0.82, max: 0.85 },
90: { min: 0.91, max: 0.93 },
95: { min: 0.95, max: 0.97 },
};
for (let range of [HUE_RANGES, L_RANGES]) {
for (let key in range) {
range[key].mid = (range[key].min + range[key].max) / 2;
}
}
/**
* Most common tint per hue.
* Largely the statistical mode, but also informed by the average and median.
*/
export const HUE_TOP_TINT = {
red: 50,
orange: 70,
yellow: 80,
green: 80,
cyan: 70,
blue: 50,
indigo: 40,
purple: 50,
pink: 50,
gray: 40,
};
/*
┌─────────┬──────┬──────┬────────┬──────┬────────┬───────┐
│ (index) │ min │ max │ median │ avg │ stddev │ count │
├─────────┼──────┼──────┼────────┼──────┼────────┼───────┤
│ red │ 0.74 │ 1 │ 0.92 │ 0.88 │ 0.085 │ 9 │
│ yellow │ 0.72 │ 1 │ 0.98 │ 0.92 │ 0.11 │ 8 │
│ green │ 0.55 │ 0.93 │ 0.75 │ 0.75 │ 0.1 │ 8 │
│ cyan │ 0.7 │ 0.88 │ 0.82 │ 0.81 │ 0.053 │ 8 │
│ blue │ 0.54 │ 1 │ 0.83 │ 0.82 │ 0.15 │ 9 │
│ indigo │ 0.63 │ 1 │ 0.87 │ 0.86 │ 0.13 │ 8 │
│ purple │ 0.58 │ 0.99 │ 0.86 │ 0.84 │ 0.11 │ 8 │
│ pink │ 0.74 │ 1 │ 0.93 │ 0.89 │ 0.089 │ 8 │
└─────────┴──────┴──────┴────────┴──────┴────────┴───────┘
*/
/** Max(Average, Median) % of max P3 chroma per hue, relative to palette maximum and capped to 0.8 */
export const HUE_CHROMA_SCALE = {
red: 0.92,
orange: 0.96, // interpolated
yellow: 1,
green: 0.7,
cyan: 0.81,
blue: 0.83,
indigo: 0.87,
purple: 0.86,
pink: 0.92,
};
export const CHROMA_SCALE_LIGHTEST = {
95: 1,
90: 0.8,
80: 0.5,
70: 0.2,
60: 0.2,
50: 0.15,
40: 0.1,
};
export const MAX_CHROMA_BY_TINT = {
95: 0.11,
};
/**
* Chroma levels to identify gray.
* First number: below this we identify as gray regardless
* Second number: below this we identify as gray if it's also in the bottom 25% of colors when sorted by chroma
*/
export const GRAY_CHROMA_BY_TINT = {
'05': [0.03, 0.05],
10: [0.035, 0.06],
20: [0.045, 0.06],
30: [0.05, 0.06],
40: [0.05, 0.06],
50: [0.04, 0.06],
60: [0.03, 0.05],
70: [0.02, 0.04],
80: [0.015, 0.03],
90: [0.007, 0.01],
95: [0.004, 0.005],
};
export const moreHue = {
red: 'Redder',
orange: 'More orange', // https://www.reddit.com/r/grammar/comments/u9n0uo/is_it_oranger_or_more_orange/
yellow: 'Yellower',
green: 'Greener',
cyan: 'More cyan',
blue: 'Bluer',
indigo: 'More indigo',
purple: 'Purpler',
pink: 'Pinker',
};
export const hueBefore = {};
export const hueAfter = {};
for (let i = 0; i < hues.length; i++) {
hueBefore[hues[i]] = hues[i - 1] ?? hues.at(-1);
hueAfter[hues[i]] = hues[i + 1] ?? hues[0];
}
export const HUE_SHIFTS = [
// Reds
{ range: [0, 25], peak: [10, 25], shift: { dark: 15, light: -18 }, maxConsecutive: { dark: 4, light: -2 } },
// Yellows
{ range: [30, 112], peak: [70, 100], shift: { dark: -48, light: 16 }, maxConsecutive: { dark: -20, light: 4 } },
// Greens
{ range: [140, 160], peak: [145, 155], shift: { dark: 15, light: -5 }, maxConsecutive: { dark: 7, light: -5 } },
// Blues
{ range: [240, 265], peak: [245, 260], shift: { dark: -3, light: -15 }, maxConsecutive: { dark: -3, light: -4 } },
];
export const CHROMA_CURVES = {
50: { dark: 0.9, light: 0.8 },
60: { dark: 1, light: 1.2 },
70: { light: 1.2 },
80: { dark: 1.1, light: 2 },
90: { dark: 3, light: 2 },
};
export const MAX_CHROMA_BOUNDS = { min: 0.08, max: 0.3 };
/**
* Max gray chroma (% of chroma of undertone) per hue
*/
export const MAX_GRAY_CHROMA_SCALE = {
red: 0.2,
orange: 0.2,
yellow: 0.25,
green: 0.25,
cyan: 0.3,
blue: 0.35,
indigo: 0.35,
purple: 0.3,
pink: 0.25,
};
/** Default accent tint if all chromas are 0, but also the tint accent colors will be nudged towards (see chromaTolerance) */
export const DEFAULT_ACCENT = 60;
/** Min and max allowed tints */
export const MIN_ACCENT = 40;
export const MAX_ACCENT = 90;
/** Chroma tolerance: Chroma will need to differ more than this to gravitate away from defaultAccent */
export const CHROMA_TOLERANCE = 0.000001;
export const ROLES = ['brand', 'neutral', 'success', 'warning', 'danger'];

View File

@@ -1,36 +1,304 @@
// https://lea.verou.me/blog/2016/12/resolve-promises-externally-with-this-one-weird-trick/
export function promise() {
let res, rej;
let promise = new Promise((resolve, reject) => {
res = resolve;
rej = reject;
});
return Object.assign(promise, { resolve: res, reject: rej });
}
export function normalizeAngles(angles) {
// First, normalize
angles = angles.map(h => ((h % 360) + 360) % 360);
// First, normalize each angle individually
let normalizedAngles = angles.map(h => ((h % 360) + 360) % 360);
// Remove top and bottom 25% and find average
let averageHue =
angles
.toSorted((a, b) => a - b)
.slice(angles.length / 4, -angles.length / 4)
.reduce((a, b) => a + b, 0) / angles.length;
for (let i = 0; i < angles.length; i++) {
let h = angles[i];
let prevHue = angles[i - 1];
let delta = h - prevHue;
for (let i = 1; i < angles.length; i++) {
let angle = normalizedAngles[i];
let prevAngle = normalizedAngles[i - 1];
let delta = angle - prevAngle;
if (Math.abs(delta) > 180) {
let equivalent = [h + 360, h - 360];
// Offset hue to minimize difference in the direction that brings it closer to the average
let delta = h - averageHue;
let equivalent = [angle + 360, angle - 360];
if (Math.abs(equivalent[0] - prevHue) <= Math.abs(equivalent[1] - prevHue)) {
angles[i] = equivalent[0];
} else {
angles[i] = equivalent[1];
}
// Offset hue to minimize difference in the direction that brings it closer to the previous hue
let deltas = equivalent.map(e => Math.abs(e - prevAngle));
normalizedAngles[i] = equivalent[deltas[0] < deltas[1] ? 0 : 1];
}
}
return angles;
return normalizedAngles;
}
export function subtractAngles(θ1, θ2) {
let [a, b] = normalizeAngles([θ1, θ2]);
return a - b;
}
/**
* Given an object of keys to ranges, find the closest range.
* Ranges are assumed to be mutually exclusive.
* @param {Object<string, {min: number, max: number}>} ranges
* @param {number} value
* @param {object} options
* @param {"angle" | undefined} options.type
* @param {number} [options.tolerance=Infinity] If value is not within any range, how close can it be?
* @param {(range: {min: number, max: number}) => {min: number, max: number}} options.getRange
* @returns {{key: string, distance: number}} The key of the closest range. Distance is 0 if the value is within the range, negative if below, positive if above.
*/
export function getRange(ranges, value, options) {
let { type } = options || {};
let keys = Object.keys(ranges);
let closest = { key: keys[0], distance: Infinity };
for (let key of keys) {
let range = ranges[key];
if (options?.getRange) {
range = options.getRange(range);
}
let { min, max } = range;
if (Array.isArray(range)) {
[min, max] = range;
}
let deltaMin = type === 'angle' ? subtractAngles(value, min) : value - min;
let deltaMax = type === 'angle' ? subtractAngles(value, max) : value - max;
if (deltaMin >= 0 && deltaMax <= 0) {
return { key, distance: 0 };
}
if (Math.abs(deltaMin) < Math.abs(closest.distance)) {
closest = { key, distance: deltaMin };
}
if (deltaMax > 0 && Math.abs(deltaMax) < Math.abs(closest.distance)) {
closest = { key, distance: deltaMax };
}
}
// TODO use angle functions to check tolerance against angles
if (options?.tolerance !== undefined && Math.abs(closest.distance) > options.tolerance) {
return;
}
return closest;
}
export function camelCase(str) {
return (str + '').replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
}
export function capitalize(str) {
if (!str) {
return str;
}
str = str + '';
return str[0].toUpperCase() + str.slice(1);
}
export function arrayNext(array, element) {
let index = array.indexOf(element);
return array[(index + 1) % array.length];
}
export function arrayPrevious(array, element) {
let index = array.indexOf(element);
return array[(index - 1 + array.length) % array.length];
}
export function levelToIndex(level) {
if (level === '05') {
return 0;
}
return level === '95' ? 10 : +level / 10;
}
export function indexToLevel(i) {
if (i === 0) {
return '05';
}
return (i === 10 ? 95 : i * 10) + '';
}
export function previousLevel(level) {
if (level === '05') {
return;
}
return indexToLevel(levelToIndex(level) - 1);
}
export function nextLevel(level) {
if (level === '95') {
return;
}
return indexToLevel(levelToIndex(level) + 1);
}
export function relativeLevel(level, steps) {
if (level == 100) {
// loose intentional
return relativeLevel(95, ++steps);
}
if (level == 95) {
// loose intentional
return relativeLevel(90, ++steps);
}
if (level == 0) {
// loose intentional
return relativeLevel(5, --steps);
}
if (level == 5) {
// loose intentional
return relativeLevel(10, --steps);
}
let index = clamp(0, levelToIndex(level) + steps, 10);
return indexToLevel(index);
}
/**
*
* @param {number} p Number from 0-1 where 0 is start and 1 is end
* @param {*} start Number for p=0
* @param {*} end Number for p=1
* @returns
*/
export function interpolate(p, range = [0, 1], options) {
let [start, end] = range;
if (p <= 0 || p >= 1 || range.length === 2) {
let value = start + p * (end - start);
return options?.unclamped ? value : clamp(start, value, end);
}
// If we're here, there are more points in the range
let interval = 1 / (range.length - 1);
let index = Math.floor(p / interval);
let intervalProgress = progress(p, [index * interval, (index + 1) * interval]);
return interpolate(intervalProgress, range.slice(index, index + 2), options);
}
/**
* Inverse of interpolate: given a value, find the progress between start and end.
* @param {*} value
* @param {*} range
* @returns
*/
export function progress(value, range = [0, 1], options) {
let [start, end] = range;
if (value <= start || value >= end || range.length === 2) {
let ret = (value - start) / (end - start);
return options?.unclamped ? ret : clamp(0, ret, 1);
}
// If we're here, there are more points in the range
let index = range.findIndex((v, i) => value > range[i - 1] && value <= v);
return (index - 1) / (range.length - 1);
}
export function mapRange(value, { from, to, progression }) {
let p = progress(value, from);
if (progression) {
p = progression(p);
}
return interpolate(p, to);
}
export function clamp(min, value, max) {
if (max < min) {
[min, max] = [max, min];
}
if (min !== undefined) {
value = Math.max(min, value);
}
if (max !== undefined) {
value = Math.min(max, value);
}
return value;
}
export function clampAngle(min, value, max) {
[min, value, max] = normalizeAngles([min, value, max]);
return clamp(min, value, max);
}
export function interpolateAngles(p, range) {
range = normalizeAngles(range);
return interpolate(p, range, { unclamped: true });
}
export function progressAngle(angle, range) {
[angle, ...range] = normalizeAngles([angle, ...range]);
return progress(angle, range, { unclamped: true });
}
/**
* Round a number to the nearest multiple of `roundTo` or to the closest number in an array of numbers
* @param {number} value
* @param {number | number[]} roundTo
* @returns
*/
export function roundTo(value, roundTo = 1) {
if (Array.isArray(roundTo)) {
let closest = roundTo[0];
let closestDistance = Math.abs(value - closest);
for (let candidate of roundTo) {
let distance = Math.abs(value - candidate);
if (distance < closestDistance) {
closest = candidate;
closestDistance = distance;
}
}
return closest;
}
let decimals = roundTo.toString().split('.')[1]?.length ?? 0;
let ret = Math.round(value / roundTo) * roundTo;
if (decimals > 0) {
// Eliminate IEEE 754 floating point errors
ret = +ret.toFixed(decimals);
}
return ret;
}
export function slugify(str) {
return str
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '') // Convert accented letters to ASCII
.replace(/[^\w\s-]/g, '') // Remove remaining non-ASCII characters
.trim()
.replace(/\s+/g, '-') // Convert whitespace to hyphens
.toLowerCase();
}
export function log(...args) {
console.log(...args);
return args[0];
}

View File

@@ -1,37 +0,0 @@
let initialPageLoadComplete = document.readyState === 'complete';
if (!initialPageLoadComplete) {
window.addEventListener('load', () => {
initialPageLoadComplete = true;
});
}
/**
* Helper for performing a DOM change using a view transition, wherever supported and reduced motion is not desired.
* @param {function} fn - Function to perform the DOM change. If async, must resolve when the change is complete.
* @param {object} [options] - Options for the transition
* @param {'smooth' | 'instant'} [options.behavior] - Transition behavior. Defaults to 'smooth'. 'instant' will skip the transition.
* @param {boolean} [options.ignoreInitialLoad] - If true, will skip the transition on initial page load. Defaults to true.
*/
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') {
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(false);
return null;
}
}

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

@@ -4,7 +4,6 @@
@import 'outline.css';
@import 'search.css';
@import 'cera_typeface.css';
@import 'theme-icons.css';
:root {
--wa-brand-orange: #f36944;
@@ -371,22 +370,10 @@ wa-page > main:has(> .index-grid) {
.index-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(auto-fit, minmax(min(22ch, 100%), 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;
@@ -413,6 +400,7 @@ 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;
@@ -452,9 +440,14 @@ wa-page > main:has(> .index-grid) {
&.color {
border-color: transparent;
transition: background var(--wa-transition-slow);
background: linear-gradient(var(--color-2, transparent) 0% 100%) no-repeat border-box var(--color,);
background-position: var(--color-2-position, bottom);
background-size: var(--color-2-width, 100%) var(--color-2-height, 50%);
background:
linear-gradient(var(--color-top, transparent) 0% 100%) top no-repeat border-box,
linear-gradient(var(--color-bottom, transparent) 0% 100%) bottom no-repeat border-box var(--color,);
background-position: top, bottom;
background-size:
var(--color-top-width, 100%) var(--color-top-height, 30%),
var(--color-bottom-width, 100%) var(--color-bottom-height, 30%);
color: var(--swatch-text-color);
&.contrast-fail {
outline: 1px dashed var(--wa-color-red);
@@ -608,6 +601,13 @@ table.colors {
margin-block-end: var(--wa-flow-spacing);
}
/** mobile */
@media screen and (max-width: 768px) {
wa-page .only-desktop {
display: none;
}
}
/** desktop */
@media screen and not (max-width: 768px) {
/* Navigation sidebar */
@@ -646,3 +646,46 @@ table.colors {
max-height: 21lh;
}
}
.color-select {
&.default::part(display-input) {
opacity: 0.6;
font-style: italic;
}
> small {
margin-inline-start: var(--wa-space-xs);
padding-block: 0 var(--wa-space-xs);
}
&::part(combobox)::before,
wa-option::before {
content: '';
display: inline-block;
width: 1.2em;
aspect-ratio: 1;
margin-inline-end: var(--wa-space-xs);
flex: none;
border-radius: var(--wa-border-radius-m);
background: var(--color);
border: 1px solid var(--wa-color-surface-default);
}
wa-option {
white-space: nowrap;
&::before {
width: 1em;
margin-inline: var(--wa-space-xs);
}
&::part(checked-icon) {
order: 2;
}
}
.default-badge {
opacity: 0.6;
margin-inline-start: var(--wa-space-xs);
}
}

View File

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

View File

@@ -1,61 +1,3 @@
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);
@@ -83,50 +25,10 @@ wa-card:has(> .theme-icon-host, > [slot='header'] > .theme-icon-host) {
display: flex;
flex-direction: column;
gap: var(--wa-space-xs);
}
.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;
}
}
h3,
p {
margin-block: 0;
padding: 0;
}
}

View File

@@ -167,7 +167,7 @@ Other elements can also be placed inside button groups:
<wa-button-group label="Example Button Group">
<wa-button>Button</wa-button>
<button>Native Button</button>
<wa-dropdown>
<wa-dropdown hoist>
<wa-button slot="trigger" caret>Dropdown</wa-button>
<wa-menu>
<wa-menu-item>Item 1</wa-menu-item>
@@ -185,7 +185,7 @@ Create a split button using a button and a dropdown. Use a [visually hidden](/do
```html {.example}
<wa-button-group label="Example Button Group">
<wa-button variant="brand">Save</wa-button>
<wa-dropdown placement="bottom-end">
<wa-dropdown placement="bottom-end" hoist>
<wa-button slot="trigger" variant="brand" caret>
<span class="wa-visually-hidden">More options</span>
</wa-button>

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 class="wa-caption-m">6 weeks old</small>
<small>6 weeks old</small>
<div slot="footer" class="wa-split">
<div slot="footer">
<wa-button variant="brand" pill>More Info</wa-button>
<wa-rating label="Rating"></wa-rating>
</div>
@@ -27,6 +27,16 @@ 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>
```
@@ -55,9 +65,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" class="wa-split">
<div slot="header">
Header Title
<wa-icon-button name="gear" variant="solid" label="Settings" class="wa-size-m"></wa-icon-button>
<wa-icon-button name="gear" variant="solid" label="Settings"></wa-icon-button>
</div>
This card has a header. You can put all sorts of things in it!
@@ -68,9 +78,19 @@ 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>
```
@@ -83,7 +103,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" class="wa-split">
<div slot="footer">
<wa-rating></wa-rating>
<wa-button variant="brand">Preview</wa-button>
</div>
@@ -93,6 +113,12 @@ 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>
```
@@ -127,7 +153,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-split">
<footer slot="footer" class="wa-flank">
<wa-button variant="brand" pill>More Info</wa-button>
<wa-rating></wa-rating>
</footer>
@@ -136,7 +162,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-split">
<footer slot="footer" class="wa-flank">
<wa-button variant="brand" pill>More Info</wa-button>
<wa-rating></wa-rating>
</footer>
@@ -145,39 +171,14 @@ 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-split">
<footer slot="footer" class="wa-flank">
<wa-button variant="brand" pill>More Info</wa-button>
<wa-rating></wa-rating>
</footer>
</wa-card>
</div>
```
### 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>
```
<style>
</style>

View File

@@ -3,7 +3,6 @@ title: Code Demo
description: Code demos can be used to render code examples as inline live demos.
tags: component
noAlpha: true
isPro: true
---
```html {.example}

View File

@@ -77,31 +77,6 @@ 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

@@ -180,3 +180,38 @@ To create a submenu, nest an `<wa-menu slot="submenu">` element in a [menu item]
:::warning
As a UX best practice, avoid using more than one level of submenu when possible.
:::
### Hoisting
Dropdown panels will be clipped if they're inside a container that has `overflow: auto|hidden`. The `hoist` attribute forces the panel to use a fixed positioning strategy, allowing it to break out of the container. In this case, the panel will be positioned relative to its [containing block](https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#Identifying_the_containing_block), which is usually the viewport unless an ancestor uses a `transform`, `perspective`, or `filter`. [Refer to this page](https://developer.mozilla.org/en-US/docs/Web/CSS/position#fixed) for more details.
```html {.example}
<div class="dropdown-hoist">
<wa-dropdown>
<wa-button slot="trigger" caret>No Hoist</wa-button>
<wa-menu>
<wa-menu-item>Item 1</wa-menu-item>
<wa-menu-item>Item 2</wa-menu-item>
<wa-menu-item>Item 3</wa-menu-item>
</wa-menu>
</wa-dropdown>
<wa-dropdown hoist>
<wa-button slot="trigger" caret>Hoist</wa-button>
<wa-menu>
<wa-menu-item>Item 1</wa-menu-item>
<wa-menu-item>Item 2</wa-menu-item>
<wa-menu-item>Item 3</wa-menu-item>
</wa-menu>
</wa-dropdown>
</div>
<style>
.dropdown-hoist {
position: relative;
border: solid 2px var(--wa-color-surface-border);
padding: var(--wa-space-m);
overflow: hidden;
}
</style>
```

View File

@@ -17,93 +17,13 @@ Not sure which icon to use? [Find the perfect icon over at Font Awesome!](https:
The default icon library is Font Awesome Free, which comes with two icon families: `classic` and `brands`. Use the `family` attribute to set the icon family.
Many Font Awesome Pro icon families have variants such as `thin`, `light`, `regular`, and `solid`. Font Awesome Pro users can [provide their kit code](/docs/#using-font-awesome-kit-codes) to unlock additional families, including `sharp` and `duotone`. For these icon families, use the `variant` attribute to set the variant.
Many Font Awesome Pro icon families have variants such as `thin`, `light`, `regular`, and `solid`. Font Awesome Pro users can [provide their kit code](/docs/installation/#using-font-awesome-kit-codes) to unlock additional families, including `sharp` and `duotone`. For these icon families, use the `variant` attribute to set the variant.
```html {.example}
<wa-icon family="brands" name="font-awesome"></wa-icon>
<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.
@@ -641,4 +561,4 @@ If you want to change the icons Web Awesome uses internally, you can register an
resolver: name => `/path/to/custom/icons/${name}.svg`
});
</script>
```
```

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

@@ -468,20 +468,75 @@ Use the `sync` attribute to make the popup the same width or height as the ancho
</script>
```
### Positioning Strategy
By default, the popup is positioned using an absolute positioning strategy. However, if your anchor is fixed or exists within a container that has `overflow: auto|hidden`, the popup risks being clipped. To work around this, you can use a fixed positioning strategy by setting the `strategy` attribute to `fixed`.
The fixed positioning strategy reduces jumpiness when the anchor is fixed and allows the popup to break out containers that clip. When using this strategy, it's important to note that the content will be positioned relative to its [containing block](https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#Identifying_the_containing_block), which is usually the viewport unless an ancestor uses a `transform`, `perspective`, or `filter`. [Refer to this page](https://developer.mozilla.org/en-US/docs/Web/CSS/position#fixed) for more details.
In this example, you can see how the popup breaks out of the overflow container when it's fixed. The fixed positioning strategy tends to be less performant than absolute, so avoid using it unnecessarily.
Toggle the switch and scroll the container to see the difference.
```html {.example}
<div class="popup-strategy">
<div class="overflow">
<wa-popup placement="top" strategy="fixed" active>
<span slot="anchor"></span>
<div class="box"></div>
</wa-popup>
</div>
<wa-switch checked>Fixed</wa-switch>
</div>
<style>
.popup-strategy .overflow {
position: relative;
height: 300px;
border: solid 2px var(--wa-color-surface-border);
overflow: auto;
}
.popup-strategy span[slot='anchor'] {
display: inline-block;
width: 150px;
height: 150px;
border: dashed 2px var(--wa-color-neutral-fill-loud);
margin: 150px 50px;
}
.popup-strategy .box {
width: 100px;
height: 50px;
background: var(--wa-color-brand-fill-loud);
border-radius: var(--wa-border-radius-m);
}
.popup-strategy wa-switch {
margin-top: 1rem;
}
</style>
<script>
const container = document.querySelector('.popup-strategy');
const popup = container.querySelector('wa-popup');
const fixed = container.querySelector('wa-switch');
fixed.addEventListener('change', () => (popup.strategy = fixed.checked ? 'fixed' : 'absolute'));
</script>
```
### Flip
When the popup doesn't have enough room in its preferred placement, it can automatically flip to keep it in view and visually connected to its anchor.
To enable this, use the `flip` attribute. By default, the popup will flip to the opposite placement, but you can configure preferred fallback placements using `flip-fallback-placement` and `flip-fallback-strategy`. Additional options are available to control the flip behavior's boundary and padding.
By default, flip takes effect when the popup would overflow the viewport.
You can use `boundary="scroll"` to make the popup resize when it overflows its nearest scrollable container instead.
When the popup doesn't have enough room in its preferred placement, it can automatically flip to keep it in view. To enable this, use the `flip` attribute. By default, the popup will flip to the opposite placement, but you can configure preferred fallback placements using `flip-fallback-placement` and `flip-fallback-strategy`. Additional options are available to control the flip behavior's boundary and padding.
Scroll the container to see how the popup flips to prevent clipping.
```html {.example}
<div class="popup-flip">
<div class="overflow">
<wa-popup placement="top" flip active boundary="scroll">
<wa-popup placement="top" flip active>
<span slot="anchor"></span>
<div class="box"></div>
</wa-popup>
@@ -537,7 +592,7 @@ Scroll the container to see how the popup changes it's fallback placement to pre
```html {.example}
<div class="popup-flip-fallbacks">
<div class="overflow">
<wa-popup placement="top" flip flip-fallback-placements="right bottom" flip-fallback-strategy="initial" active boundary="scroll">
<wa-popup placement="top" flip flip-fallback-placements="right bottom" flip-fallback-strategy="initial" active>
<span slot="anchor"></span>
<div class="box"></div>
</wa-popup>
@@ -571,18 +626,14 @@ Scroll the container to see how the popup changes it's fallback placement to pre
### Shift
When a popup is longer than its anchor, it risks overflowing.
In this case, use the `shift` attribute to shift the popup along its axis and back into view. You can customize the shift behavior using `shiftBoundary` and `shift-padding`.
By default, auto-size takes effect when the popup would overflow the viewport.
You can use `boundary="scroll"` to make the popup resize when it overflows its nearest scrollable container instead.
When a popup is longer than its anchor, it risks being clipped by an overflowing container. In this case, use the `shift` attribute to shift the popup along its axis and back into view. You can customize the shift behavior using `shiftBoundary` and `shift-padding`.
Toggle the switch to see the difference.
```html {.example}
<div class="popup-shift">
<div class="overflow">
<wa-popup placement="top" shift shift-padding="10" active boundary="scroll">
<wa-popup placement="top" shift shift-padding="10" active>
<span slot="anchor"></span>
<div class="box"></div>
</wa-popup>
@@ -625,11 +676,7 @@ Toggle the switch to see the difference.
### Auto-size
Use the `auto-size` attribute to tell the popup to resize when necessary to prevent it from overflowing.
Possible values are `horizontal`, `vertical`, and `both`. You can use `autoSizeBoundary` and `auto-size-padding` to customize the behavior of this option. Auto-size works well with `flip`, but if you're using `auto-size-padding` make sure `flip-padding` is the same value.
By default, auto-size takes effect when the popup would overflow the viewport.
You can use `boundary="scroll"` to make the popup resize when it overflows its nearest scrollable container instead.
Use the `auto-size` attribute to tell the popup to resize when necessary to prevent it from getting clipped. Possible values are `horizontal`, `vertical`, and `both`. You can use `autoSizeBoundary` and `auto-size-padding` to customize the behavior of this option. Auto-size works well with `flip`, but if you're using `auto-size-padding` make sure `flip-padding` is the same value.
When using `auto-size`, one or both of `--auto-size-available-width` and `--auto-size-available-height` will be applied to the host element. These values determine the available space the popover has before clipping will occur. Since they cascade, you can use them to set a max-width/height on your popup's content and easily control its overflow.
@@ -638,7 +685,7 @@ Scroll the container to see the popup resize as its available space changes.
```html {.example}
<div class="popup-auto-size">
<div class="overflow">
<wa-popup placement="top" auto-size="both" auto-size-padding="10" active boundary="scroll">
<wa-popup placement="top" auto-size="both" auto-size-padding="10" active>
<span slot="anchor"></span>
<div class="box"></div>
</wa-popup>

View File

@@ -425,3 +425,11 @@ This can be hard to conceptualize, so heres a fairly large example showing how l
</script>
```
<script type="module">
//
// TODO - remove once we switch to the Popover API
//
customElements.whenDefined('wa-select').then(() => {
document.querySelectorAll('wa-code-demo [slot="preview"] wa-select').forEach(select => select.hoist = true);
});
</script>

View File

@@ -152,3 +152,25 @@ Use the `--max-width` custom property to change the width the tooltip can grow t
<wa-button id="wrapping-tooltip">Hover me</wa-button>
```
### Hoisting
Tooltips will be clipped if they're inside a container that has `overflow: auto|hidden|scroll`. The `hoist` attribute forces the tooltip to use a fixed positioning strategy, allowing it to break out of the container. In this case, the tooltip will be positioned relative to its [containing block](https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#Identifying_the_containing_block), which is usually the viewport unless an ancestor uses a `transform`, `perspective`, or `filter`. [Refer to this page](https://developer.mozilla.org/en-US/docs/Web/CSS/position#fixed) for more details.
```html {.example}
<div class="tooltip-hoist">
<wa-tooltip for="no-hoist">This is a tooltip</wa-tooltip>
<wa-button id="no-hoist">No Hoist</wa-button>
<wa-tooltip for="hoist" hoist>This is a tooltip</wa-tooltip>
<wa-button id="hoist">Hoist</wa-button>
</div>
<style>
.tooltip-hoist {
position: relative;
overflow: hidden;
border: solid 2px var(--wa-color-surface-border);
padding: var(--wa-space-m);
}
</style>
```

View File

@@ -3,7 +3,6 @@ title: Viewport Demo
description: Viewport demos can be used to display an iframe as a resizable, zoomable preview.
tags: component
noAlpha: true
isPro: true
---
```html {.example}

View File

@@ -10,7 +10,7 @@ You can customize the look and feel of Web Awesome at a high level with themes.
Web Awesome uses [themes](/docs/themes) to apply a cohesive look and feel across the entire library. Themes are built with a collection of predefined CSS custom properties, which we call [design tokens](/docs/tokens), and there are many premade themes you can choose from.
To use a theme, simply add a link to the theme's stylesheet to the `<head>` of your page. For example, you can replace the link to `default.css` in the [installation code](/docs/#quick-start-autoloading-via-cdn) with this snippet to use the *Awesome* theme:
To use a theme, simply add a link to the theme's stylesheet to the `<head>` of your page. For example, you can replace the link to `default.css` in the [installation code](/docs/installation/#quick-start-autoloading-via-cdn) with this snippet to use the *Awesome* theme:
```html
<link rel="stylesheet" href="{% cdnUrl 'styles/themes/awesome.css' %}" />
@@ -197,7 +197,7 @@ For example, we can give all outlined callouts a thick left border, regardless o
<style>
wa-callout:is(
[appearance~="outlined"],
[appearance~="outlined"],
.wa-outlined
) {
border-left-width: var(--wa-panel-border-radius);

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,11 +32,15 @@ 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
@@ -58,7 +62,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 +126,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

@@ -10,7 +10,7 @@ override:tags: []
{% markdown %}
## Installation
Layout components are included in Web Awesome's [autoloader](/docs/#quick-start-autoloading-via-cdn). You can also import them individually via [cherry picking](/docs/#cherry-picking).
Layout components are included in Web Awesome's [autoloader](/docs/installation/#quick-start-autoloading-via-cdn). You can also import them individually via [cherry picking](/docs/installation/#cherry-picking).
Layout utilities are bundled with all [style utilities](/docs/utilities). You can import all Web Awesome page styles (including [native styles](/docs/native/)) by including the following stylesheet in your project:

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](/docs/utilities/appearance) to change the button's visual appearance:
Use the [appearance utility classes](../utilities/appearance.md) 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](/docs/utilities/appearance) to change the callout's visual appearance (the default is `outlined filled`).
Use the [appearance utility classes](../utilities/appearance.md) 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,35 +19,6 @@ 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

@@ -0,0 +1,28 @@
import { tints } from '/assets/scripts/tweak/data.js';
export function generateGrays(colors, { grayColor, grayChroma }) {
let ret = {};
let undertoneScale = colors[grayColor];
// These will be the same, since scaling them won't change the relationship
ret.maxChromaTint = undertoneScale.maxChromaTint;
Object.defineProperty(ret, 'core', {
enumerable: false,
get() {
return this[this.maxChromaTint];
},
});
ret.maxChromaTintRaw = undertoneScale.maxChromaTintRaw;
for (let tint of tints) {
let colorUndertone = undertoneScale[tint].clone().to('oklch');
ret[tint] = colorUndertone.set({ c: c => c * grayChroma });
}
ret.maxChroma = ret[ret.maxChromaTint].get('oklch.c');
ret.maxChromaRaw = ret[ret.maxChromaTintRaw].get('oklch.c');
return ret;
}
export default generateGrays;

View File

@@ -0,0 +1,162 @@
// TODO move these to local imports
import Color from 'https://colorjs.io/dist/color.js';
import generateGrays from './generate-grays.js';
import generateScale from './generate-scale.js';
import getMaxChroma from './get-max-chroma.js';
import { getCoreTint } from './util.js';
import {
HUE_CHROMA_SCALE,
HUE_RANGES,
HUE_TOP_TINT,
L_RANGES,
MAX_ACCENT,
MIN_ACCENT,
} from '/assets/scripts/tweak/data.js';
import {
clamp,
clampAngle,
interpolate,
normalizeAngles,
progressAngle,
roundTo,
subtractAngles,
} from '/assets/scripts/tweak/util.js';
export default function generatePalette(seedHues, { huesAfter: allHuesAfter, ...options } = {}) {
let ret = {};
// Generate scales from seed hues
let firstSeedHue;
let coreLevels = {};
let seedMeta = {};
for (let hue in seedHues) {
let seedColors = seedHues[hue];
if (!seedColors) {
continue;
}
firstSeedHue ??= hue;
let coreLevel = (coreLevels[hue] = getCoreTint(seedColors));
let coreColor = seedColors[coreLevel];
let [l, c, h] = coreColor.getAll('oklch');
let lOffset = l - L_RANGES[coreLevel].mid;
let cScale = c / getMaxChroma(l, h);
let relativeCScale = cScale / HUE_CHROMA_SCALE[hue];
let levelOffset = coreLevel - HUE_TOP_TINT[hue];
seedMeta[hue] = { lOffset, cScale, relativeCScale, levelOffset };
ret[hue] = generateScale(seedColors);
}
if (!firstSeedHue) {
// No valid seed colors, abort mission
return null;
}
// Fill in remaining hues
let hueBefore = firstSeedHue;
for (let hue of allHuesAfter[firstSeedHue]) {
if (hue in ret) {
continue;
}
let huesAfter = allHuesAfter[hue];
let seedHuesAfter = huesAfter.filter(hue => seedHues[hue]);
let neighboringSeedHues = [seedHuesAfter.at(-1), seedHuesAfter[0]];
// A number from 0 to 1 indicating how close we are to each neighboring seed hue (0 if only one seed hue)
let hueProgress =
seedHuesAfter.length === 1
? 0
: progressAngle(
HUE_RANGES[hue].mid,
neighboringSeedHues.map(hue => HUE_RANGES[hue].mid),
);
// Hue of the core color of the previous seed scale
let hBefore = ret[hueBefore][ret[hueBefore].maxChromaTint].get('oklch.h');
// We start from the midpoint of the hue range
let h = HUE_RANGES[hue].mid;
// Shift if too close to seed hues
let hBeforeDelta = subtractAngles(h, hBefore);
if (Math.abs(hBeforeDelta) < 40) {
h = hBefore + 40 * Math.sign(hBeforeDelta);
}
if (seedHuesAfter.length > 1) {
let hueAfter = seedHuesAfter[0];
let hAfter = ret[hueAfter][ret[hueAfter].maxChromaTint].get('oklch.h');
[hBefore, h, hAfter] = normalizeAngles([hBefore, h, hAfter]);
let hAfterDelta = subtractAngles(hAfter, h);
if (hAfter - 40 < hBefore + 40) {
// It's not possible to have a distance of at least 40deg from both neighboring hues
// so at least maximize distance
h = (hBefore + hAfter) / 2;
} else if (hAfterDelta < 40) {
h = hAfter - 40;
}
}
// Make sure hue is still within range for this scale
h = clampAngle(HUE_RANGES[hue].min, h, HUE_RANGES[hue].max);
let coreLevelOffset = interpolate(
hueProgress,
neighboringSeedHues.map(hue => seedMeta[hue].levelOffset),
);
let coreLevel = clamp(MIN_ACCENT, roundTo(HUE_TOP_TINT[hue] + coreLevelOffset, 10), MAX_ACCENT);
coreLevels[hue] = coreLevel;
let lOffsets = neighboringSeedHues.map(hue => seedMeta[hue].lOffset);
let lOffset = interpolate(hueProgress, lOffsets);
let l = L_RANGES[coreLevel].mid + lOffset;
let cScale = 1;
if (hue === 'yellow') {
// Yellow tends to be the brighest hue in the palette
cScale = Math.max(
...Object.values(seedMeta)
.map(meta => meta.relativeCScale)
.filter(c => c > 0),
);
} else {
cScale = interpolate(
hueProgress,
neighboringSeedHues.map(neighboringHue => seedMeta[neighboringHue].relativeCScale),
);
}
cScale *= HUE_CHROMA_SCALE[hue];
let maxC = getMaxChroma(l, h);
let c = cScale * maxC;
// let c = interpolate(
// hueProgress,
// pinnedScale.map(scale => scale.maxChroma),
// );
let coreColor = new Color('oklch', [l, c, h]).toGamut('p3');
ret[hue] = generateScale(coreColor);
hueBefore = hue;
}
if ('gray' in seedHues) {
ret.gray = generateScale(seedHues.gray);
} else {
ret.gray = generateGrays(ret, options);
}
return ret;
}

View File

@@ -0,0 +1,138 @@
import { getCoreTint, getHueShift, getLightness, identifyColor } from './util.js';
import {
CHROMA_CURVES,
CHROMA_SCALE_LIGHTEST,
L_RANGES,
MAX_CHROMA_BY_TINT,
tints,
} from '/assets/scripts/tweak/data.js';
import { clamp, interpolate, progress } from '/assets/scripts/tweak/util.js';
/**
* Generate a scale of tints from one or more key colors
* @param {Color | Record<number | string, Color>} seedColors
* @returns {Record<number | string, Color>}
*/
export function generateScale(seedColors) {
if (seedColors.constructor.name === 'Color') {
// Single color given
let { level } = identifyColor(seedColors);
seedColors = { [level]: seedColors };
}
// Find core color
let coreLevel = getCoreTint(seedColors);
let coreColor = seedColors[coreLevel];
let coreChroma = coreColor.get('oklch.c');
let scale = {};
Object.defineProperties(scale, {
maxChromaTint: { value: coreLevel, enumerable: false, configurable: true },
maxChromaTintRaw: { value: coreLevel, enumerable: false, configurable: true },
maxChroma: { value: coreChroma, enumerable: false, configurable: true },
maxChromaRaw: { value: coreChroma, enumerable: false, configurable: true },
core: {
get() {
return this[this.maxChromaTint];
},
enumerable: false,
},
});
// First, add pinned colors
for (let tint in seedColors) {
scale[tint] = seedColors[tint];
}
// For finding lightest and darkest pinned colors
let pinnedTints = Object.keys(seedColors).sort((a, b) => a - b);
let chromaCurve = CHROMA_CURVES[clamp(50, coreLevel, 90)];
// Now generate the rest, starting from the edges
if (!('95' in scale)) {
let lightestPinnedTint = pinnedTints.at(-1);
let lightest = seedColors[lightestPinnedTint];
let lOffset = lightest.get('oklch.l') - L_RANGES[lightestPinnedTint].mid;
let chromaScale = CHROMA_SCALE_LIGHTEST[lightestPinnedTint];
let hueShift = getHueShift(lightest, lightestPinnedTint, '95');
let color = lightest.clone().to('oklch');
color.set({
l: getLightness(95, lOffset),
c: clamp(0, lightest.get('oklch.c') * chromaScale, MAX_CHROMA_BY_TINT[95]),
h: h => h + hueShift,
});
scale[95] = color;
}
if (!('05' in scale)) {
let darkestPinnedTint = pinnedTints[0];
let darkest = seedColors[darkestPinnedTint];
let lOffset = darkest.get('oklch.l') - L_RANGES[darkestPinnedTint].mid;
let color = darkest.clone().to('oklch');
let hueShift = getHueShift(darkest, darkestPinnedTint, '05');
color.set({
l: getLightness('05', lOffset),
// TODO c
h: h => h + hueShift,
});
scale['05'] = color;
}
let tintBefore = '05';
for (let tint of tints) {
if (tint in scale) {
// Pinned or already generated
tintBefore = tint;
continue;
}
// Generated color
// First, find closest pinned colors before and after
let tintAfter = pinnedTints.find(level => level > tint) ?? '95';
let neighboringTints = [tintBefore, tintAfter];
let neighboringColors = neighboringTints.map(t => scale[t]);
let tintProgress = progress(tint, neighboringTints);
let color = coreColor.clone().to('oklch');
// Lightness
let lOffset = interpolate(
tintProgress,
neighboringTints.map(t => scale[t].get('oklch.l') - L_RANGES[t].mid),
);
// Interpolate hue linearly and chroma with a power curve
color.set({
l: getLightness(tint, lOffset),
c: interpolate(
tintProgress,
neighboringColors.map(c => c.get('oklch.c')),
{
progression: tint > coreLevel ? p => p ** chromaCurve.light : undefined,
},
),
h: interpolate(
tintProgress,
neighboringColors.map(c => c.get('oklch.h')),
),
});
scale[tint] = color;
}
for (let tint in scale) {
if (!(tint in seedColors) && scale[tint].toGamut) {
scale[tint] = scale[tint].toGamut('p3');
}
}
return scale;
}
export default generateScale;

View File

@@ -0,0 +1,3 @@
export { generateGrays, generateGrays as grays } from './generate-grays.js';
export { generatePalette, generatePalette as palette } from './generate-palette.js';
export { generateScale, generateScale as scale } from './generate-scale.js';

View File

@@ -0,0 +1,91 @@
/**
* Memoized calculation of OKLCH gamut boundary for a given L and H
* Currently unused, but we can use it if existing code becomes too slow.
*/
import Color from 'https://colorjs.io/dist/color.js';
import { interpolate, progress, progressAngle, roundTo } from '/assets/scripts/tweak/util.js';
/** Max oklch.c per h and l (rounded to 1 significant digit) */
const maxChroma = {};
const OOG_CHROMA = 0.4; // guaranteed to be OOG for every P3 color
const C_THRESHOLD = 0.03;
const MIN_H_STEP = 0.1;
const MIN_L_STEP = 0.001;
export default function getMaxChroma(l, h) {
let { hStep, lStep, count } = calculateBoundary(l, h);
let hRounded = roundTo(h, hStep);
let lRounded = roundTo(l, lStep);
// Calculate gamut boundary around this point
let hProgress = progressAngle(h - hRounded, [-hStep, 0, hStep]);
let lProgress = progress(l - lRounded, [-lStep, 0, lStep]);
let maxChromaH = [];
for (let i of [-1, 0, 1]) {
let h = roundTo(hRounded + i * hStep, hStep);
let cs = [-1, 0, 1].map(j => {
let l = roundTo(lRounded + j * lStep, lStep);
return maxChroma[l][h];
});
maxChromaH.push(interpolate(lProgress, cs));
}
// Interpolate between the 9 points using bilinear interpolation
let c = interpolate(hProgress, maxChromaH);
return c;
}
function calculateBoundary(pointL, pointH, lStep = 0.1, hStep = 10) {
let hRounded = roundTo(pointH, hStep);
let lRounded = roundTo(pointL, lStep);
let ret = { count: 0, hStep, lStep };
for (let i of [-1, 0, 1]) {
let l = roundTo(lRounded + i * lStep, lStep);
maxChroma[l] ??= {};
for (let j of [-1, 0, 1]) {
let h = roundTo(hRounded + j * hStep, hStep);
if (maxChroma[l][h] !== undefined) {
continue;
}
let gamutBoundary = new Color('oklch', [l, OOG_CHROMA, h]).toGamut('p3', { method: 'oklch.c' });
let c = gamutBoundary.get('c');
maxChroma[l][h] = c;
ret.count++;
let tooFar = { h: false, l: false };
if (i > -1) {
let lPrev = roundTo(lRounded + (i - 1) * lStep, lStep);
let cPrev = maxChroma[lPrev][h];
tooFar.l = Math.abs(c - cPrev) > C_THRESHOLD && lStep > MIN_L_STEP;
if (tooFar.l) {
ret.lStep /= 2;
ret.count += calculateBoundary(pointL, pointH, ret.lStep, ret.hStep).count;
}
}
if (j > -1) {
let hPrev = roundTo(hRounded + (j - 1) * hStep, hStep);
let cPrev = maxChroma[l][hPrev];
tooFar.h = Math.abs(c - cPrev) > C_THRESHOLD && hStep > MIN_H_STEP;
if (tooFar.h) {
ret.hStep /= 2;
ret.count += calculateBoundary(pointL, pointH, ret.lStep, ret.hStep).count;
}
}
}
}
return ret;
}

View File

@@ -0,0 +1,83 @@
import { stringifyColor } from './util.js';
import { cssImport, cssLiteral, cssRule } from '/assets/scripts/tweak/code.js';
import { selectors, tints, urls } from '/assets/scripts/tweak/data.js';
export function getPaletteCode({ base, slug = base, colors, tweaked, roles, ...options }) {
let imports = [];
if (base && options.imports !== false && !tweaked.seedColors) {
imports.push(urls.palette(base));
}
let ret = imports.map(url => cssImport(url, options)).join('\n');
let declarations = [];
let prefix = options.prefix ?? 'wa-color';
let css = '';
if (tweaked) {
for (let hue in colors) {
if (!tweaked.seedColors) {
if (hue === 'gray') {
if (!tweaked.grayChroma && !tweaked.grayColor) {
continue;
}
} else if (!tweaked.chromaScale && !tweaked.hue?.[hue]) {
continue;
}
}
let scale = colors[hue];
for (let tint of tints) {
let color = scale[tint];
let stringified = stringifyColor(color);
declarations.push(`--${prefix}-${hue}-${tint}: ${stringified};`);
}
let coreTint = scale.maxChromaTint;
if (coreTint) {
declarations.push(
`--${prefix}-${hue}: var(--${prefix}-${hue}-${coreTint});`,
`--${prefix}-${hue}-key: ${coreTint};`,
);
}
declarations.push('');
}
}
if (roles) {
for (let role in roles) {
let hue = roles[role];
if (!hue) {
continue;
}
for (let suffix of [...tints.map(t => '-' + t), '', '-key']) {
declarations.push(`--${prefix}-${role}${suffix}: var(--${prefix}-${hue}${suffix});`);
}
declarations.push('');
}
}
if (declarations.length > 0) {
let selector = options.selector ?? selectors.palette(slug);
css += cssRule(selector, declarations);
}
if (css) {
if (imports.length) {
ret += '\n\n';
}
ret += `${cssLiteral(css, options)}`;
}
return ret;
}
export default getPaletteCode;

View File

@@ -0,0 +1,28 @@
// TODO move these to local imports
import Color from 'https://colorjs.io/dist/color.js';
import { tints } from '/assets/scripts/tweak/data.js';
let palettes = await fetch('/docs/palettes/data.json').then(r => r.json());
for (let palette in palettes) {
for (let hue in palettes[palette].colors) {
let scale = palettes[palette].colors[hue];
for (let tint of tints) {
let color = scale[tint];
if (Array.isArray(color)) {
scale[tint] = new Color('oklch', color);
}
}
Object.defineProperty(scale, 'core', {
get() {
return this[this.maxChromaTint];
},
enumerable: false,
});
}
}
globalThis.allPalettes = palettes;
export default palettes;

View File

@@ -0,0 +1,74 @@
// TODO move these to local imports
import generateGrays from './generate-grays.js';
import { tints } from '/assets/scripts/tweak/data.js';
export function tweakPalette(baseColors, tweaks, tweaked) {
let ret = {};
if (!tweaked) {
return baseColors;
}
for (let hue in baseColors) {
let originalScale = baseColors[hue];
let scale = (ret[hue] = {});
let descriptors = Object.getOwnPropertyDescriptors(originalScale);
Object.defineProperties(scale, {
maxChromaTint: { ...descriptors.maxChromaTint, enumerable: false },
maxChromaTintRaw: { ...descriptors.maxChromaTintRaw, enumerable: false },
core: {
get() {
return this[this.maxChromaTint];
},
enumerable: false,
},
});
if (hue === 'gray') {
if (tweaked.grayChroma || tweaked.grayColor) {
let grayColor = tweaks.grayColor ?? this.originalGrayColor;
let grayChroma = this.computedGrayChroma;
ret.gray = generateGrays(baseColors, { grayColor, grayChroma });
} else {
ret.gray = originalScale;
}
continue;
}
for (let tint of tints) {
scale[tint] = tweakColor(hue, originalScale[tint], tweaks, tweaked);
}
}
return ret;
}
export function tweakColor(hue, originalColor, tweaks, tweaked) {
if (!tweaked) {
return originalColor;
}
let color = originalColor;
let { hueShifts, chromaScale = 1, grayColor, grayChroma } = tweaks;
let tweak = {};
let thisTweaked = false;
if (tweaked.hue && hueShifts[hue]) {
tweak.h = h => h + hueShifts[hue];
thisTweaked = true;
}
if (tweaked.chromaScale && chromaScale !== 1) {
tweak.c = c => c * chromaScale;
thisTweaked = true;
}
if (thisTweaked) {
color = color.clone().to('oklch').set(tweak);
}
return color;
}
export default tweakPalette;

View File

@@ -0,0 +1,154 @@
import {
CHROMA_TOLERANCE,
DEFAULT_ACCENT,
GRAY_CHROMA_BY_TINT,
HUE_RANGES,
HUE_SHIFTS,
L_RANGES,
MAX_ACCENT,
MIN_ACCENT,
tints,
} from '/assets/scripts/tweak/data.js';
import { clamp, getRange, mapRange } from '/assets/scripts/tweak/util.js';
export function identifyColor(color, colors) {
let [l, c, h] = color.getAll('oklch');
let level = getRange(L_RANGES, l).key;
let hue;
// Identify grays
let grayBounds = GRAY_CHROMA_BY_TINT[level];
if (c <= grayBounds[1]) {
// Possibly gray
if (c <= grayBounds[0]) {
// Definitely gray
hue = 'gray';
} else if (colors) {
// May or may not be gray, compare to palette max chroma
// FIXME this does not take level into account, so is more likely to identify lighter colors as gray
let maxChroma = Math.max(...colors.map(color => color.get('oklch.c')));
if (c / maxChroma < 0.2) {
hue = 'gray';
}
}
}
hue ??= getRange(HUE_RANGES, h, { type: 'angle' }).key;
return { hue, level };
}
export function getLightness(level, distance) {
return clamp(L_RANGES[level].min, L_RANGES[level].mid + distance, L_RANGES[level].max);
}
/**
* How many tints are between two tints?
* E.g. `getTintDistance('90', '95')` should return `1`
* @param {number | string} tint1
* @param {number | string} tint2
* @returns {number}
*/
export function getTintDistance(tint1, tint2) {
tint1 = String(tint1);
tint2 = String(tint2);
return tints.indexOf(tint2) - tints.indexOf(tint1);
}
export function getHueShift(color, fromTint, toTint) {
let tintDistance = getTintDistance(fromTint, toTint);
let hueShift = getRange(HUE_SHIFTS, color.get('oklch.h'), {
getRange: v => v.range,
type: 'angle',
tolerance: 0,
});
if (!hueShift) {
return 0;
}
hueShift = HUE_SHIFTS[hueShift.key];
let { peak, range } = hueShift;
let h = color.get('oklch.h');
let breakpoints = [range[0], ...peak, range[1]];
let intensity = mapRange(h, breakpoints, [0, 1, 1, 0]);
let type = tintDistance < 0 ? 'dark' : 'light';
let shift = hueShift.shift[type];
let ret = shift * intensity;
let maxConsecutive = hueShift.maxConsecutive[type] ?? hueShift.maxConsecutive;
let maxShift = Math.sign(shift) * maxConsecutive * Math.abs(tintDistance);
ret = clamp(undefined, ret, maxShift);
return ret;
}
export function getCoreTint(scale) {
let tintsInScale = Object.keys(scale);
if (tintsInScale.length <= 1) {
return tintsInScale[0];
}
let ret = DEFAULT_ACCENT in scale ? DEFAULT_ACCENT : tintsInScale[Math.floor(tintsInScale.length / 2)];
let maxChroma = 0;
for (let tint in scale) {
let color = scale[tint];
let chroma = color.get('oklch.c');
if (chroma > maxChroma + CHROMA_TOLERANCE && tint >= MIN_ACCENT && tint <= MAX_ACCENT) {
ret = tint;
maxChroma = chroma;
}
}
return ret;
}
export function getContrasts(colors, originalContrasts) {
let ret = {};
for (let hue in colors) {
ret[hue] = {};
for (let tintBg of tints) {
ret[hue][tintBg] = {};
let bgColor = colors[hue][tintBg];
if (!bgColor || !bgColor.contrast) {
continue;
}
for (let tintFg of tints) {
let fgColor = colors[hue][tintFg];
let value = bgColor.contrast(fgColor, 'WCAG21');
if (originalContrasts) {
let original = originalContrasts[hue][tintBg][tintFg];
ret[hue][tintBg][tintFg] = { value, original, bgColor, fgColor };
} else {
ret[hue][tintBg][tintFg] = value;
}
}
}
}
return ret;
}
/**
* Return hex code iff a color is within sRGB, otherwise fall back to its default string representation
*
* @param {Color} color
* @returns {string}
*/
export function stringifyColor(color) {
if (color?.constructor.name !== 'Color') {
return color;
}
let format = color.inGamut('srgb') ? 'hex' : undefined;
return color.toString({ format });
}

View File

@@ -0,0 +1,187 @@
/* CSS for custom palettes only */
#seed-colors {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(22ch, 1fr));
gap: var(--wa-space-m);
> .add-button {
flex-flow: column wrap;
height: auto;
min-height: 15ch;
border: var(--wa-panel-border-width) var(--wa-panel-border-style) var(--wa-color-surface-border);
--border-color: var(--wa-color-surface-border);
border-radius: var(--wa-panel-border-radius);
background-color: var(--wa-color-surface-default);
box-shadow: var(--wa-shadow-s);
wa-icon {
font-size: 200%;
margin: 0;
margin-top: 0.35em;
}
}
> wa-card {
--spacing: var(--wa-space-s);
[slot='image'] {
position: relative;
height: 5.5rem;
width: 100%;
border-start-start-radius: var(--inner-border-radius);
border-start-end-radius: var(--inner-border-radius);
background-color: var(--color);
color: canvastext;
.tweak-icon {
position: absolute;
top: var(--wa-space-s);
right: var(--wa-space-s);
--background-color-hover: oklab(from currentColor l a b / 15%);
--text-color-hover: currentColor;
&:not(:hover, :focus, :has(+ :focus-within)) {
opacity: 50%;
}
&:is(.tweaked *) {
&::part(base) {
transition: var(--wa-transition-normal);
transition-property: padding, border, opacity;
background-color: var(--color-original);
padding: var(--wa-space-s);
border: 1px solid hsl(0 0 100 / 60%);
}
}
}
.name {
display: flex;
gap: var(--wa-space-xs);
position: absolute;
bottom: var(--wa-space-xs);
left: var(--wa-space-s);
font-weight: var(--wa-font-weight-semibold);
wa-dropdown.pin-hue {
wa-button {
--outlined-border-color: oklab(from currentColor l a b / 10%);
--outlined-background-color-hover: transparent;
--border-width: 1.5px;
--text-color: currentColor;
--wa-space: var(--wa-space-xs);
--wa-space-smaller: var(--wa-space-2xs);
}
&.pin-hue.pinned {
wa-button {
--outlined-border-color: oklab(from currentColor l a b / 40%);
font-weight: var(--wa-font-weight-bold);
}
}
wa-icon[name='thumbtack'] {
opacity: 60%;
}
}
.level {
font-weight: var(--wa-font-weight-bold);
}
}
}
wa-input {
margin-top: var(--wa-space-xs);
}
wa-icon-button {
color: light-dark(black, white);
transition: opacity var(--wa-transition-slow);
--background-color-hover: oklab(from currentColor l a b / 15%);
--text-color-hover: currentColor;
}
}
.color-to-role {
--border-width: 0;
margin-inline-start: calc(-1 * var(--wa-space-3xs));
&::part(tags) {
margin-inline-start: 0;
}
&::part(combobox) {
padding: var(--wa-space-3xs);
min-height: auto;
}
}
}
wa-icon-button.delete-button {
position: absolute;
top: var(--wa-space-s);
right: var(--wa-space-s);
--text-color-hover: var(--wa-color-danger-on-normal);
}
.pinned-icon {
opacity: 70%;
}
#suggested-colors {
margin-top: var(--wa-space-2xl);
h3 {
margin-bottom: 0;
}
&::part(content) {
padding-block-start: 0;
}
p.wa-caption-m {
margin-block: var(--wa-space-xs) var(--wa-space-m);
text-wrap: pretty;
}
.suggestions {
display: flex;
flex-wrap: wrap;
gap: var(--wa-space-s);
wa-button {
/* --background-color-hover: var(--background-color); */
height: var(--wa-form-control-height);
aspect-ratio: 1.2;
wa-icon {
transition: var(--wa-transition-normal);
}
&:not(:focus, :hover) wa-icon {
opacity: 0;
}
}
}
}
#roles {
margin-block: var(--wa-space-2xl);
> div {
display: flex;
flex-wrap: wrap;
gap: var(--wa-space-m);
> wa-select {
flex: 1;
max-width: 20ch;
}
}
}
.seed-color-tweak .popup {
min-width: clamp(0ch, 50ch, 90vw);
}

View File

@@ -0,0 +1,390 @@
/* CSS included both in predefined palettes and custom ones */
:root {
--fa-sliders-simple: '\f1de';
}
.core-column {
position: relative;
> wa-dropdown {
min-width: 100%;
}
}
wa-dropdown > .color.swatch {
cursor: pointer;
}
.color-slider {
display: grid;
grid-template-columns: auto 1fr auto;
wa-slider {
grid-column: 1 / -1;
--track-height: 1em;
--track-color-inactive: transparent;
--track-color-active: transparent;
--thumb-color: var(--color);
--thumb-shadow: 0 0 0 var(--thumb-gap) var(--wa-color-surface-default),
var(--wa-shadow-offset-x-m) var(--wa-shadow-offset-y-m) var(--wa-shadow-blur-m)
calc(var(--wa-shadow-offset-x-m) * -1 + var(--thumb-gap)) var(--wa-color-shadow);
&:active {
--thumb-size: 2em;
}
&::part(base) {
position: relative;
background: linear-gradient(to right in var(--color-interpolation-space, oklab), var(--color-1), var(--color-2));
}
.tick {
--width: 1px;
--height: 0.5em;
--tick-color: var(--wa-color-neutral-border-normal);
width: 4px;
height: 2.4em;
background: no-repeat;
background-image: linear-gradient(var(--tick-color) 0 100%), linear-gradient(var(--tick-color) 0 100%);
background-position: top, bottom;
background-size: var(--width) var(--height);
position: absolute;
left: calc(var(--default-value-progress) * 100% - (var(--default-value-progress) - 0.5) * var(--thumb-size));
translate: -50% 0;
bottom: -0.5em;
&:hover {
--tick-color: var(--wa-color-neutral-border-loud);
}
}
}
[slot='label'] {
min-height: 1.1lh;
}
.clear-button {
vertical-align: middle;
font-size: var(--wa-font-size-xs);
}
.label-min,
.label-max {
font-size: var(--wa-font-size-xs);
}
.label-min {
grid-column: 1;
margin-inline-start: 0.15em;
}
.label-max {
grid-column: 3;
margin-inline-end: 0.1em;
}
}
[data-component='h'] {
--color-interpolation-space: oklch increasing hue;
}
.popup {
display: flex;
flex-flow: column;
gap: var(--wa-space-m);
background: var(--wa-color-surface-default);
color: var(--wa-color-text-normal);
border: 1px solid var(--wa-color-surface-border);
padding: var(--wa-space-m) var(--wa-space-l);
border-radius: var(--wa-border-radius-m);
color-scheme: light;
.copyable-code {
display: flex;
gap: var(--wa-space-xs);
align-items: center;
code {
flex: 1;
max-width: 20ch;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
> legend {
/* Force legend to be rendered inside the fieldset */
float: left;
clear: all;
padding: 0;
}
.wa-heading-s {
display: flex;
gap: var(--wa-gap-xs);
align-items: center;
> :nth-child(1 of .align-end) {
margin-inline-start: auto;
}
}
}
@scope (.wa-dark) to (.wa-light) {
.popup {
color-scheme: dark;
}
}
.color-scale {
th {
white-space: nowrap;
}
.tweak-icon {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: var(--wa-space-s);
opacity: var(--tweak-icon-opacity, 0%);
}
.color.swatch:hover {
--tweak-icon-opacity: 40%;
}
&.tweaked .core-column {
--tweak-icon-opacity: 80%;
}
}
.tweaked-callout {
padding: var(--wa-space-xs);
padding-inline-start: var(--wa-space-m);
margin-block: var(--wa-space-m);
align-items: center;
&:not(.tweaked-any *) {
visibility: hidden;
}
&::part(message) {
display: flex;
align-items: center;
gap: var(--wa-space-xs);
}
wa-button:first-of-type {
margin-inline-start: auto;
}
}
/* Better UI before Vue initializes */
[v-if='saved'],
[v-if^='tweaked'],
[v-cloak] {
display: none;
}
.static-palette:has(+ .colors:not([v-cloak])) {
display: none;
}
.core-color {
wa-radio-button::part(base) {
width: 2em;
height: 2em;
padding: 0;
border-radius: var(--wa-border-radius-circle);
background: var(--color);
background-clip: border-box;
}
wa-radio-button:is([checked], :state(checked))::part(base) {
box-shadow:
inset 0 0 0 var(--indicator-width) var(--indicator-color),
inset 0 0 0 calc(var(--indicator-width) + 1.5px) var(--wa-color-surface-default);
}
&::part(form-control-input) {
gap: var(--wa-space-xs);
}
}
[data-slug='custom'] > :not(.seeded) #seed-colors ~ :not(#saved),
#outline:has(+ main > [data-slug='custom'] > :not(.seeded)) li:nth-child(n + 2) {
display: none;
}
[id='palette-info'] {
display: grid;
grid-template-columns: 1fr auto;
grid-auto-flow: column;
> * {
grid-column: 1;
}
}
.hue-wheel {
--r: clamp(2em, 6rem, 25vmin);
grid-column: 2;
grid-row: 1 / 5;
position: relative;
width: calc(var(--r) * 2);
aspect-ratio: 1;
border-radius: 50%;
--lc: var(--avg-l) var(--max-c);
--lc2: var(--avg-l) calc(var(--max-c) / 2);
margin-top: calc(var(--r) * -0.05);
background: conic-gradient(
in oklch,
oklch(var(--lc) 0),
oklch(var(--lc) 60),
oklch(var(--lc) 120),
oklch(var(--lc) 180),
oklch(var(--lc) 240),
oklch(var(--lc) 300),
oklch(var(--lc) 360)
);
&,
&::before {
--stops: oklch(var(--lc) 0), oklch(var(--lc) 60), oklch(var(--lc) 120), oklch(var(--lc) 180), oklch(var(--lc) 240),
oklch(var(--lc) 300), oklch(var(--lc) 360);
}
&::before {
content: '';
display: block;
height: 100%;
border-radius: 50%;
-webkit-mask: radial-gradient(white, transparent);
background: radial-gradient(oklch(var(--avg-l) calc(var(--gray-chroma) * var(--max-c)) 0) 5%, transparent 30%),
conic-gradient(
in oklch,
oklch(var(--lc2) 0),
oklch(var(--lc2) 60),
oklch(var(--lc2) 120),
oklch(var(--lc2) 180),
oklch(var(--lc2) 240),
oklch(var(--lc2) 300),
oklch(var(--lc2) 360)
);
}
.color {
--scale-c: calc(var(--c) / var(--max-c));
--distance: calc(var(--r) * var(--scale-c));
top: 50%;
left: 50%;
transform: translate(-50%, -50%) rotate(calc(var(--h) * 1deg - 90deg)) translateX(var(--distance));
position: absolute;
z-index: 1;
width: calc(1.2em + 0.3em * var(--scale-c));
aspect-ratio: 1;
&:hover {
--scale: 1.2;
--line-color: white;
--line-style: solid;
}
&::before {
content: '';
position: absolute;
z-index: -1;
width: 100%;
height: 0;
border-top: 2px var(--line-style, dashed) var(--line-color, var(--wa-color-gray-80));
padding-top: 100%;
top: calc(50% - 1px);
right: 50%;
width: var(--distance);
}
&::after {
content: '';
display: block;
position: relative;
height: 100%;
border-radius: 50%;
border: 2px solid white;
box-shadow: var(--wa-shadow-l);
background: var(--color);
transition: var(--wa-transition-fast);
scale: var(--scale, 1);
}
}
wa-tooltip {
/* Prevent flickering */
pointer-events: none;
}
}
.scale-filter {
wa-tab wa-icon {
margin-right: 0.4em;
}
}
.title wa-icon-button[name='pencil'] {
margin-inline-start: var(--wa-space-xs);
}
.seeded {
wa-badge.status {
display: none;
}
wa-badge.pro {
filter: grayscale(0.95);
}
}
.selected-swatch,
.color-select wa-option::before {
content: '';
display: inline-block;
width: 1.2em;
aspect-ratio: 1;
flex: none;
border-radius: var(--wa-border-radius-m);
background: var(--color);
border: 1px solid var(--wa-color-surface-default);
}
.color-select wa-option {
white-space: nowrap;
&::before {
width: 1em;
margin-inline: var(--wa-space-xs);
}
&::part(checked-icon) {
order: 2;
}
wa-icon[name='square-plus'] {
vertical-align: -0.15em;
color: var(--color-gray);
opacity: 0.6;
}
}
.color-popup {
display: block;
.popup {
min-width: 25ch;
}
}
wa-icon[name='thumbtack'],
wa-icon-button[name='thumbtack']::part(icon) {
rotate: 45deg;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,357 @@
const template = `
<wa-card size="small" class="color" :class="{tweaked}"
:style="{'--color': value, '--color-original': inputValue}">
<div slot="image" :style="{ colorScheme: level <= 60 ? 'dark' : 'light'}">
<color-popup placement="top-start" class="seed-color-tweak" :pinned=true deletable @delete="$emit('delete')" title="Edit color">
<wa-icon-button name="sliders-simple" class="tweak-icon"></wa-icon-button>
<template #content>
<color-slider label="Hue" label-default="Entered color"
coord="h" :min="0" :max="359" :step="1"
v-model:color="color" :default-value="inputLCH[2]" ></color-slider>
<color-slider label="Colorfulness" label-default="Entered color"
coord="c" :min="0" :max="maxChroma" :step="0.001"
v-model:color="color" :default-value="inputLCH[1]" format-type="scale" :format-base-value="maxChroma" ></color-slider>
<color-slider label="Lightness" label-default="Entered color"
coord="l" :min="0" :max="1" :step="0.01"
v-model:color="color" :default-value="inputLCH[0]" format-type="scale" :format-base-value="1" ></color-slider>
</template>
</color-popup>
<div class="name">
<wa-dropdown class="pin-hue" :class="{pinned: pinnedHue}">
<wa-button slot="trigger" appearance="outlined" caret>
<wa-icon name="thumbtack" v-if="pinnedHue" variant="solid" slot="prefix"></wa-icon>
{{ capitalize(hue) || 'New color' }}
</wa-button>
<wa-menu @wa-select="pinnedHue = $event.detail.item.value">
<wa-menu-item type="checkbox" :checked="pinnedHue ? null : ''">Automatic <em>({{ capitalize(detectedColorInfo.hue) }})</em></wa-menu-item>
<wa-divider></wa-divider>
<wa-menu-label>Pin to…</wa-menu-label>
<wa-menu-item v-for="hue in allHues" type="checkbox" :value="hue" :checked="pinnedHue === hue ? '' : null">{{ capitalize(hue) }}</wa-menu-item>
</wa-menu>
</wa-dropdown>
<span class="level">{{ level }}</span>
</div>
</div>
<wa-select class="color-to-role" multiple appearance="plain" placeholder="(No states)" max-options-visible="2"
ref="roles" :value.attr="Object.keys(roles).join(' ')" :value="Object.keys(roles)"
:getTag="getTag"
@input="$emit('update:roles', $event.target.value)">
<wa-option v-for="role in ROLES" :value="role" :class="{'default': !roles[role]}">{{ capitalize(role) }}</wa-option>
</wa-select>
<wa-input :value="valueRaw" @input="handleInput" @focus="inputFocused = true" @blur="inputFocused = false" ref="input"></wa-input>
</wa-card>
`;
import Color from 'https://colorjs.io/dist/color.js';
// import { nextTick } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';
import { nextTick } from 'https://cdn.jsdelivr.net/npm/vue@3/dist/vue.esm-browser.js';
import getMaxChroma from '../color/get-max-chroma.js';
import { identifyColor } from '../color/util.js';
import ColorPopup from './color-popup.js';
import ColorSlider from './color-slider.js';
import InfoTip from './info-tip.js';
import { ROLES, allHues } from '/assets/scripts/tweak/data.js';
import { capitalize } from '/assets/scripts/tweak/util.js';
await customElements.whenDefined('wa-select');
let maxUid = 0;
const expose = [
'valueRaw',
'value',
'inputValueRaw',
'inputValue',
'colorRaw',
'color',
'inputColorRaw',
'inputColor',
'hue',
'pinnedHue',
'level',
'tweaked',
];
export default {
expose,
props: {
modelValue: {
type: Object,
default(rawProps) {
return { value: '' };
},
},
otherColors: {
type: Array,
},
roles: {
type: Object,
default: {},
},
},
emits: ['update:modelValue', 'update:roles', 'delete'],
data() {
let uid = this.modelValue.uid ?? maxUid++;
if (this.modelValue.uid) {
maxUid = Math.max(maxUid, uid);
}
this.modelValue.uid = uid;
let valueRaw = this.modelValue.value;
let inputValueRaw = this.modelValue.inputValue ?? valueRaw;
let color = tryColor(this.modelValue.value);
let inputColor = tryColor(inputValueRaw);
return {
uid,
initialProps: { ...this.modelValue },
valueRaw,
value: color ? valueRaw : undefined,
color,
inputValueRaw,
inputValue: inputColor ? inputValueRaw : undefined,
inputColor,
pinnedHue: this.modelValue.pinnedHue,
editing: 0,
inputFocused: false,
watching: {},
};
},
created() {
// Non-reactive variables to expose
Object.assign(this, { ROLES, allHues });
},
async mounted() {
if (this.modelValue.editImmediately) {
let input = this.$refs.input;
await input.updateComplete;
input.focus();
input.select();
}
},
computed: {
inputLCH() {
return this.inputColor?.oklch;
},
currentLCH() {
return this.color?.oklch;
},
tweaked() {
if (this.inputFocused || this.editing > 0 || !this.inputLCH || !this.currentLCH) {
return false;
}
return this.inputLCH.some((coord, i) => coord !== this.currentLCH[i]);
},
computedValue() {
let ret = {};
for (let property of expose) {
ret[property] = this[property];
}
return ret;
},
colorRaw() {
let ret = tryColor(this.modelValue.valueRaw);
if (ret) {
this.value = this.modelValue.valueRaw;
}
return ret;
},
colorInfo() {
let ret = { ...this.detectedColorInfo };
if (this.pinnedHue) {
ret.hue = this.pinnedHue;
}
return ret;
},
detectedColorInfo() {
if (!this.color) {
return { hue: undefined, level: undefined };
}
return identifyColor(this.color, this.otherColors);
},
hue() {
return this.colorInfo.hue;
},
level() {
return this.colorInfo.level;
},
stringifiedColor() {
// return stringifyColor(this.colorRaw);
return this.color + '';
},
inputColorRaw() {
let ret = tryColor(this.inputValueRaw);
if (ret) {
this.inputValue = this.inputValueRaw;
}
return ret;
},
maxChroma() {
if (!this.color) {
return 0.4;
}
return getMaxChroma(this.color.oklch.l, this.color.oklch.h);
},
},
methods: {
capitalize,
handleInput(event) {
this.editing++;
let value = event.target.value;
// Editing the input manually also incorporates any tweaks as part of the color itself
// I.e. input color and color are now the same
this.valueRaw = this.inputValueRaw = value;
nextTick().then(() => {
if (this.colorRaw) {
this.color = this.colorRaw;
this.$refs.input.setCustomValidity('');
} else {
this.$refs.input.setCustomValidity('Invalid color');
this.$refs.input.reportValidity();
}
this.editing--;
});
},
mutateModelValue(mutator) {
if (this.watching.modelValue === null) {
// If we're not watching modelValue, it means we're reacting to a change to it
// so no point in updating it again
return;
}
if (this.watching.modelValue) {
this.watching.modelValue();
this.watching.modelValue = null;
}
mutator();
this.watching.modelValue = this.$watch('modelValue', {
deep: true,
handler() {
let computedValue = this.computedValue;
// What changed?
if (this.modelValue.value !== computedValue.value) {
this.valueRaw = this.modelValue.value;
}
if (this.modelValue.color + '' !== computedValue.color + '') {
this.color = this.modelValue.color;
}
},
});
},
getTag(option) {
let isDefault = option.classList.contains('default');
let tag = Object.assign(document.createElement('wa-tag'), {
part: `tag${isDefault ? ' default' : ''}`,
exportparts: `
base:tag__base,
content:tag__content,
remove-button:tag__remove-button,
remove-button__base:tag__remove-button__base`,
size: 'small',
removable: !isDefault,
'data-value': option.value,
id: 'tag-' + option.value,
innerHTML: option.label + ` <wa-tooltip hoist for="tag-${option.value}">Default role</wa-tooltip>`,
});
return tag;
},
},
watch: {
/** colorRaw -> color */
colorRaw: {
deep: true,
handler() {
if (this.colorRaw) {
this.color = this.colorRaw;
}
},
},
/** inputColorRaw -> inputColor */
inputColorRaw: {
deep: true,
handler() {
if (this.inputColorRaw) {
this.inputColor = this.inputColorRaw;
}
},
},
/** color -> value, valueRaw, modelValue.value */
color: {
deep: true,
handler() {
if (this.tweaked && this.color) {
// If tweaked, color is the source of truth
this.value = this.valueRaw = this.color + '';
}
},
},
/** computedValue -> modelValue */
computedValue: {
deep: true,
immediate: true,
handler() {
this.mutateModelValue(() => {
Object.assign(this.modelValue, this.computedValue);
this.$emit('update:modelValue', this.modelValue);
});
},
},
},
template,
components: { InfoTip, ColorSlider, ColorPopup },
compilerOptions: {
isCustomElement: tag => tag.startsWith('wa-'),
},
};
function tryColor(value) {
if (!value) {
return null;
}
if (value instanceof Color) {
return value;
}
try {
return new Color(value);
} catch (e) {
return null;
}
}

View File

@@ -0,0 +1,82 @@
import Color from 'https://colorjs.io/dist/color.js';
import { stringifyColor } from '../color/util.js';
import InfoTip from './info-tip.js';
export default {
props: {
title: String,
token: String,
color: Color,
deletable: Boolean,
pinnable: Boolean,
pinned: Boolean,
placement: String,
},
data() {
return {};
},
emits: ['delete', 'pin'],
mounted() {
let popup = this.$refs.popup;
if (popup) {
// Find trigger
let trigger = popup.previousElementSibling;
if (trigger) {
trigger.slot ||= 'trigger';
}
}
},
computed: {
stringifiedColor() {
return stringifyColor(this.color);
},
},
template: `
<wa-dropdown class="color-popup" :placement>
<slot></slot>
<component :is="title ? 'fieldset' : 'div'" class="popup" ref="popup">
<component :is="title ? 'legend' : 'div'" class="wa-heading-s" v-if="title || token || deletable || pinnable">
<span v-if="title">{{ title }}</span>
<wa-copy-button v-if="title && token" :value="token" :copy-label="token"></wa-copy-button>
<info-tip v-if="deletable && pinned">
<wa-button size="small" variant="danger" appearance="plain" class="delete-button align-end" @click="$emit('delete')">
<wa-icon name="trash" variant="regular"></wa-icon>
</wa-button>
<template #content>
Delete from my colors
</template>
</info-tip>
<info-tip v-if="pinnable && !pinned">
<wa-button appearance="outlined" size="small" class="pin-color align-end" @click="$emit('pin')">
<wa-icon name="thumbtack" variant="regular" slot="prefix"></wa-icon>
Pin
</wa-button>
<template #content>
Prevent this color from changing as others are edited
</template>
</info-tip>
</component>
<slot name="content"></slot>
<div class="wa-stack wa-gap-xs">
<div class="copyable-code" v-if="token && !title">
<code>{{ token }}</code>
<wa-copy-button :value="token"></wa-copy-button>
</div>
<div class="copyable-code" v-if="color">
<code>{{ stringifiedColor }}</code>
<wa-copy-button :value="stringifiedColor"></wa-copy-button>
</div>
</div>
</component>
</wa-dropdown>`,
compilerOptions: {
isCustomElement: tag => tag.startsWith('wa-'),
},
components: {
InfoTip,
},
};

View File

@@ -0,0 +1,73 @@
import { capitalize } from '/assets/scripts/tweak/util.js';
export default {
props: {
modelValue: String,
label: String,
getLabel: {
type: Function,
default: capitalize,
},
getContent: {
type: Function,
},
getColor: {
type: Function,
default: value => `var(--wa-color-${value})`,
},
values: {
type: Array,
default: [],
},
groups: {
type: Object,
},
},
emits: ['update:modelValue', 'input'],
data() {
return {};
},
computed: {
computedGroups() {
let ret = {};
if (this.values?.length) {
ret[''] = this.values;
}
if (this.groups) {
for (let group in this.groups) {
if (this.groups[group]?.length) {
ret[group] = this.groups[group];
}
}
}
return ret;
},
firstGroup() {
return Object.keys(this.computedGroups)[0];
},
},
methods: {
capitalize,
handleInput(e) {
this.$emit('input', this.modelValue);
},
},
template: `
<wa-select class="color-select" name="brand" :label="label" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)"
:style="{'--color': getColor(modelValue)}">
<template v-for="values, group in computedGroups">
<template v-if="group">
<wa-divider v-if="group !== firstGroup"></wa-divider>
<small>{{ group }}</small>
</template>
<wa-option v-if="values?.length" v-for="value of values" :label="getLabel(value)" :value="value" :style="{'--color': getColor(value)}" v-html="getContent?.(value) ?? getLabel(value)"></wa-option>
</template>
<slot></slot>
</wa-select>
`,
};

View File

@@ -0,0 +1,343 @@
const template = `
<div class="color-slider" :style="{
'--color': computedColor, '--color-1': colorMin, '--color-2': colorMax,
'--default-value-progress': defaultProgress,
}" :data-component="coord || null">
<wa-slider ref="slider" :min="computedMin" :max="computedMax" :step="step" :value="value"
@input="handleInput($event.target.value);" @change="inputEnd($event.target.value)">
<div slot="label">
{{ label }}
<wa-icon-button v-if="value !== computedDefaultValue" @click="reset" class="clear-button" name="circle-xmark" library="system" variant="regular" label="Reset"></wa-icon-button>
<info-tip>
<div class="tick"></div>
<template #content>{{ computedLabelDefault }}</template>
</info-tip>
</div>
</wa-slider>
<div class="label-min">{{ labelMin }}</div>
<div class="label-max">{{ labelMax }}</div>
</div>
`;
import Color from 'https://colorjs.io/dist/color.js';
import InfoTip from './info-tip.js';
import { capitalize, promise, roundTo } from '/assets/scripts/tweak/util.js';
export default {
props: {
coord: {
type: String,
required: true,
validator(value) {
return ['l', 'c', 'h'].includes(value);
},
},
color: Color,
defaultColor: Color,
defaultValue: Number,
defaultValueRelative: Number,
/** Used for relative types. Defaults to defaultValue if not provided. */
baseValue: Number,
/** Used for formatting only. Only specify if different from base value. */
formatBaseValue: {
type: Number,
default: undefined,
},
modelValue: {
type: Number,
},
min: Number,
max: Number,
minRelative: Number,
maxRelative: Number,
step: {
type: Number,
default: 1,
},
type: {
type: String,
default: 'raw',
},
formatType: {
type: String,
},
label: String,
labelMin: String,
labelMax: String,
labelDefault: String,
},
emits: ['update:modelValue', 'update:color', 'input'],
data() {
return {
mounted: promise(),
initialColor: this.color,
value: undefined,
tweaking: false,
};
},
created() {
if (!this.color && !this.defaultColor) {
console.warn(
`[${this.label}]`,
'<color-slider> requires at least one of the following props: color, defaultColor',
);
}
if (this.modelValue !== undefined) {
this.value = this.getAbsoluteValue(this.modelValue);
} else if (this.color) {
this.value = this.colorCoords[this.coordIndex];
}
},
mounted() {
if (this.$refs.slider) {
this.$refs.slider.tooltipFormatter = value => this.formatValue(value);
this.$refs.slider.colorSliderData = this; // for debugging
}
this.mounted.resolve();
},
beforeUnmount() {
delete this.$refs.slider?.colorSliderData;
},
computed: {
computedMin() {
if (this.minRelative !== undefined) {
return getAbsoluteValue(this.minRelative);
}
return this.min;
},
computedMax() {
if (this.maxRelative !== undefined) {
return this.getAbsoluteValue(this.maxRelative);
}
return this.max;
},
computedColor() {
return this.getColorAt(this.value);
},
computedColorCoords() {
return this.computedColor.oklch.slice();
},
colorCoords() {
let color = this.color ?? this.computedColor;
return color?.oklch.slice();
},
computedColorString() {
return `oklch(${this.computedColorCoords.join(' ')})`;
},
colorString() {
return `oklch(${this.colorCoords.join(' ')})`;
},
defaultCoords() {
if (this.defaultColor) {
return this.defaultColor.oklch.slice();
}
let ret = this.color.oklch.slice();
if (this.defaultValue !== undefined) {
ret[this.coordIndex] = this.defaultValue;
}
return ret;
},
coordIndex() {
return ['l', 'c', 'h'].indexOf(this.coord);
},
colorMin() {
return this.getColorAt(this.computedMin);
},
colorMax() {
return this.getColorAt(this.computedMax);
},
isRelative() {
return this.type && this.type !== 'raw';
},
computedBaseValue() {
if (!this.isRelative) {
return;
}
if (this.baseValue !== undefined) {
return this.baseValue;
}
return this.computedDefaultValue;
},
computedDefaultValue() {
if (this.defaultValue !== undefined) {
return this.defaultValue;
}
if (this.defaultValueRelative !== undefined) {
return this.getAbsoluteValue(this.defaultValueRelative);
}
if (this.baseValue !== undefined) {
return this.baseValue;
}
return this.defaultCoords[this.coordIndex];
},
computedDefaultColor() {
return this.defaultColor ?? this.getColorAt(this.computedDefaultValue);
},
computedLabelDefault() {
let labelDefault = this.labelDefault || 'Default value';
let formattedDefaultValue = this.formatValue(this.computedDefaultValue);
return `${labelDefault} (${formattedDefaultValue})`;
},
defaultProgress() {
return (this.computedDefaultValue - this.computedMin) / (this.computedMax - this.computedMin);
},
relativeValue() {
this.computedBaseValue;
return this.getRelativeValue(this.value);
},
},
methods: {
capitalize,
getAbsoluteValue(relativeValue) {
return getAbsoluteValue({
type: this.type,
relativeValue,
baseValue: this.baseValue ?? this.computedBaseValue,
});
},
getRelativeValue(absoluteValue) {
return getRelativeValue({
type: this.type,
absoluteValue,
baseValue: this.baseValue ?? this.computedBaseValue,
});
},
formatValue(value = this.value) {
let formatType = this.formatType ?? this.type;
let style = formatType === 'scale' ? 'percent' : undefined;
if (formatType && formatType !== 'raw') {
let baseValue = this.formatBaseValue ?? this.computedBaseValue;
value = getRelativeValue({ type: formatType, absoluteValue: value, baseValue });
}
value = roundTo(value, this.step);
return value.toLocaleString(undefined, { style });
},
getColorAt(value) {
let coords = this.defaultCoords.slice();
coords[this.coordIndex] = value;
return new Color('oklch', coords);
},
/** Called when value changes due to user interaction */
handleInput(value) {
this.value = value;
this.tweaking = true;
this.$emit('input', value);
},
inputEnd() {
this.tweaking = false;
},
reset() {
this.handleInput(this.computedDefaultValue);
this.inputEnd();
},
},
watch: {
computedColorString() {
if (this.color && this.colorString !== this.computedColorString) {
// Color changed, communicate to the outside world
this.$emit('update:color', this.computedColor);
}
},
colorString() {
if (this.color && this.colorString !== this.computedColorString) {
// Color changed in the outside world, update our internals
if (this.colorCoords[this.coordIndex] !== this.value) {
this.value = this.colorCoords[this.coordIndex];
let modelValue = this.getRelativeValue(this.value);
this.$emit('update:modelValue', modelValue);
}
}
},
relativeValue() {
this.$emit('update:modelValue', this.relativeValue);
},
},
template,
components: { InfoTip },
compilerOptions: {
isCustomElement: tag => tag.startsWith('wa-'),
},
};
function getAbsoluteValue({ type, relativeValue, baseValue }) {
if (baseValue === undefined) {
type = 'raw';
}
if (type === 'shift') {
return relativeValue + baseValue;
}
if (type === 'scale') {
return relativeValue * baseValue;
}
return relativeValue;
}
function getRelativeValue({ type, absoluteValue, baseValue }) {
if (baseValue === undefined) {
type = 'raw';
}
if (type === 'shift') {
return absoluteValue - baseValue;
}
if (type === 'scale') {
if (!absoluteValue) {
return 0;
}
return absoluteValue / baseValue;
}
return absoluteValue;
}

View File

@@ -0,0 +1,56 @@
const template = `
<wa-radio-group class="core-color" orientation="horizontal" :value="modelValue" @input="handleInput($event.target.value)">
<template v-for="h in hues">
<info-tip>
<wa-radio-button :label="capitalize(h)" :value="h" :style="{'--color': colors[h]}"></wa-radio-button>
<template #content>{{ capitalize(h) }}</template>
</info-tip>
</template>
<div slot="label">{{ label }}</div>
</wa-radio-group>
`;
import Color from 'https://colorjs.io/dist/color.js';
import InfoTip from './info-tip.js';
import { hues } from '/assets/scripts/tweak/data.js';
import { capitalize, promise, roundTo } from '/assets/scripts/tweak/util.js';
export default {
props: {
modelValue: String,
label: {
type: String,
default: 'Color',
},
colors: Object,
},
emits: ['update:modelValue', 'input'],
data() {
return {
defaultValue: this.modelValue,
};
},
created() {
Object.assign(this, { hues });
},
computed: {},
methods: {
capitalize,
/** Called when value changes due to user interaction */
handleInput(value) {
this.value = value;
this.$emit('input', value);
this.$emit('update:modelValue', value);
},
reset() {
this.handleInput(this.defaultValue);
},
},
template,
components: { InfoTip },
compilerOptions: {
isCustomElement: tag => tag.startsWith('wa-'),
},
};

View File

@@ -0,0 +1,85 @@
const template = `
<color-popup :title :token="token" :color="modelValue"
:pinned :pinnable @pin="$emit('pin')" :deletable @delete="$emit('delete')">
<div slot="trigger" class="color swatch" :style="{ '--color': modelValue, colorScheme: level > 60 ? 'light' : 'dark' }">
<wa-icon class="pinned-icon" name="thumbtack" variant="regular" v-if="pinned"></wa-icon>
<wa-icon name="sliders-simple" class="tweak-icon"></wa-icon>
</div>
<template #content>
<color-slider v-if="(isEdge || pinned) && tweakBase && HUE_RANGES[hue]"
:color="modelValue" @update:model-value="$emit('update:modelValue', $event)" :default-value="colors[hue][tweakBase].oklch.h"
@input="!pinned ? $emit('pin') : null"
coord="h" :min="HUE_RANGES[hue].min + 1" :max="HUE_RANGES[hue].max" :step="1"
label="Hue shift" :label-min="moreHue[hueBefore[hue]]" :label-max="moreHue[hueAfter[hue]]"
:label-default="\`\${capitalize(hue)} \${tweakBase}\`"
></color-slider>
</template>
</color-popup>
`;
import Color from 'https://colorjs.io/dist/color.js';
import ColorPopup from './color-popup.js';
import ColorSlider from './color-slider.js';
import InfoTip from './info-tip.js';
import { HUE_RANGES, hueAfter, hueBefore, hues, moreHue } from '/assets/scripts/tweak/data.js';
import { capitalize, clamp, promise, roundTo } from '/assets/scripts/tweak/util.js';
export default {
props: {
modelValue: Color,
hue: {
type: String,
required: true,
},
level: {
type: [String, Number],
required: true,
},
coreLevel: {
type: [String, Number],
required: true,
},
pinned: Boolean,
pinnable: Boolean,
deletable: Boolean,
colors: {
type: Object,
required: true,
},
tweakBase: [String, Number],
},
emits: ['update:modelValue', 'pin', 'delete'],
data() {
return {};
},
created() {
// Attach non-reactive data
Object.assign(this, { moreHue, HUE_RANGES });
},
computed: {
title() {
return capitalize(this.hue) + ' ' + this.level;
},
hueBefore() {
return hueBefore[this.hue];
},
hueAfter() {
return hueAfter[this.hue];
},
token() {
return `--wa-color-${this.hue}-${this.level}`;
},
isEdge() {
return this.level == '95' || this.level == '05';
},
isCore() {
return this.level == this.coreLevel;
},
},
methods: { capitalize },
template,
components: { InfoTip, ColorSlider, ColorPopup },
compilerOptions: {
isCustomElement: tag => tag.startsWith('wa-'),
},
};

View File

@@ -0,0 +1,37 @@
import Color from 'https://colorjs.io/dist/color.js';
import { stringifyColor } from '../color/util.js';
let maxUid = 0;
export default {
props: {},
data() {
let uid = ++maxUid;
return { uid, id: 'info-tip-' + uid };
},
mounted() {
let tooltip = this.$refs.tooltip;
if (tooltip) {
// Find trigger
let trigger = tooltip.previousElementSibling;
if (trigger) {
if (trigger.id) {
// Already has id
this.id = trigger.id;
} else {
trigger.id = this.id;
}
}
}
},
computed: {},
template: `
<slot>
<wa-icon-button :id="id" name="circle-question" variant="regular"></wa-icon-button>
</slot>
<wa-tooltip :for="id" ref="tooltip"><slot name="content"></slot></wa-tooltip>
`,
compilerOptions: {
isCustomElement: tag => tag.startsWith('wa-'),
},
};

View File

@@ -0,0 +1,71 @@
---
title: Custom
isPro: true
override:tags: [palettes, pro]
order: 99
description: Create your own color palette from scratch, from one or more seed colors.
status: experimental
---
<link href="{{ page.url }}../app/custom.css" rel="stylesheet">
<h2 v-if="step > 0" v-cloak>My Colors</h2>
<p v-if="step > 0" v-cloak>
Just add your colors, in any order. Well sort them out for you, generate tints, and suggest additional colors.
</p>
<div id="seed-colors">
<template v-for="color, i in seedColors" :key="color.uid ?? maxSeedUid">
<color-input v-model="seedColors[i]"
:other-colors="seedColors.filter((_, j) => j !== i).map(c => c.color)"
:roles="seedColorRoles[i]"
@update:roles="roles => setColorRole(i, roles)"
@delete="deleteColor(i)"></color-input>
</template>
<wa-button class="add-button" appearance="outlined" @click="addColor(undefined, {editImmediately: true})">
<wa-icon slot="prefix" name="plus" variant="regular"></wa-icon>
<span v-content="step > 0 ? 'Add color' : 'New palette'">New palette</span>
</wa-button>
</div>
<wa-details id="suggested-colors" v-if="step > 0" v-cloak open>
<h3 class="wa-heading-m" slot="summary">Suggestions</h3>
<p class="wa-caption-m">
Generated by our fancy-schmancy algorithm to complement your colors.
See a color you like? Grab it before its gone!
</p>
<div class="suggestions wa-cluster wa-align-items-start wa-gap-s">
<template v-for="color, hue in suggestedColors">
<info-tip>
<wa-button :style="{'--background-color': color}" @click="addColor({hue})">
<wa-icon name="plus"></wa-icon>
</wa-button>
<template #content>{% raw %}{{ capitalize(hue) }}{% endraw %}</template>
</info-tip>
</template>
</div>
</wa-details>
<section id="roles" v-if="step > 0" v-cloak>
<h2>Roles</h2>
<div>
<color-select v-for="computedRole, role in computedRoles"
:model-value="computedRoles[role]"
@update:model-value="value => setRoleColor(role, value)"
:class="{'default': !roles[role]}"
:label="capitalize(role) + ':'"
:groups="{
Dynamic: !['brand', 'neutral'].includes(role) ? ['brand', 'neutral'] : undefined,
Colors: Object.keys(paletteScales),
Common: suggestedForRole[role]
}"
:get-label="capitalize"
:get-content="value => capitalize(value) + (seedHues[value] || computedRoles[value] || value === 'gray' ? '' : ' <wa-icon name=square-plus variant=regular></wa-icon>')"
:get-color="value => coreColors[computedRoles[value] ?? value]">
{# <wa-badge class="default-badge" v-if="!roles[role]" slot="suffix" variant="neutral" appearance="outlined">Default</wa-badge> #}
</color-select>
</div>
</section>

View File

@@ -1,205 +0,0 @@
:root {
--fa-sliders-simple: '\f1de';
}
.core-column {
position: relative;
> wa-dropdown {
min-width: 100%;
}
}
wa-dropdown > .color.swatch {
cursor: pointer;
}
.decorated-slider {
display: grid;
grid-template-columns: auto 1fr auto;
margin-block-end: var(--wa-space-xl);
wa-slider {
grid-column: 1 / -1;
--track-height: 1em;
--track-color-inactive: transparent;
--track-color-active: transparent;
--thumb-color: var(--color-tweaked, var(--color));
--thumb-shadow: 0 0 0 var(--thumb-gap) var(--wa-color-surface-default),
var(--wa-shadow-offset-x-m) var(--wa-shadow-offset-y-m) var(--wa-shadow-blur-m)
calc(var(--wa-shadow-offset-x-m) * -1 + var(--thumb-gap)) var(--wa-color-shadow);
&:active {
--thumb-size: 2em;
}
&::part(base) {
background: linear-gradient(to right in var(--color-interpolation-space, oklab), var(--color-1), var(--color-2));
}
}
[slot='label'] {
min-height: 1.1lh;
}
.clear-button {
vertical-align: middle;
font-size: var(--wa-font-size-xs);
&:not(.tweaked *) {
display: none;
}
}
.label-min,
.label-max {
font-size: var(--wa-font-size-xs);
}
.label-min {
grid-column: 1;
margin-inline-start: 0.15em;
}
.label-max {
grid-column: 3;
margin-inline-end: 0.1em;
}
}
.hue-shift-slider {
--color-1: oklch(from var(--color) l c calc(h + var(--min, 0)));
--color-2: oklch(from var(--color) l c calc(h + var(--max, 0)));
--color-interpolation-space: oklch;
}
.chroma-scale-slider {
--color: var(--wa-color-brand);
--color-1: oklch(from var(--color) l calc(c * var(--min)) h);
--color-2: oklch(from var(--color) l calc(c * var(--max)) h);
}
.gray-chroma-slider {
--color: var(--wa-color-gray);
--color-1: oklch(from var(--wa-color-gray) l 0 none);
--color-2: oklch(from var(--color-gray-undertone) l calc(c * var(--max)) h);
margin-top: var(--wa-space-m);
}
.popup {
background: var(--wa-color-surface-default);
border: 1px solid var(--wa-color-surface-border);
padding: var(--wa-space-xl);
border-radius: var(--wa-border-radius-m);
code {
white-space: nowrap;
}
}
.color-scale {
th {
white-space: nowrap;
}
td:not([data-hue='gray'] *) {
--tweak-c: calc(c * var(--chroma-scale, 1));
--tweak-h: calc(h + var(--hue-shift, 0));
--color-tweaked-no-chroma-scale: oklch(from var(--color) l c var(--tweak-h));
--color-tweaked-no-hue-shift: oklch(from var(--color) l var(--tweak-c) h);
&:is([data-tint='90'], [data-tint='95']) {
/* Work around https://bugs.webkit.org/show_bug.cgi?id=287637 */
--color-tweaked-no-chroma-scale: lch(from var(--color) l c var(--tweak-h));
--color-tweaked-no-hue-shift: lch(from var(--color) l var(--tweak-c) h);
/* outline: 1px dashed red; */
}
}
.color.swatch {
--color-2: var(--color-tweaked);
--color-2-height: 100%;
&:is(.tweaking *) {
--color-2-height: 70%;
}
&:is(.tweaking-chroma *) {
--color: var(--color-tweaked-no-chroma-scale);
}
&:is(.tweaking-hue *) {
--color: var(--color-tweaked-no-hue-shift);
}
&:is(.tweaking-gray-chroma *) {
--color: var(--color-tweaked-no-gray-chroma);
}
}
.tweak-icon {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: var(--wa-space-s);
opacity: var(--tweak-icon-opacity, 0%);
}
.core-column:hover {
--tweak-icon-opacity: 40%;
}
&.tweaked .core-column {
--tweak-icon-opacity: 80%;
}
}
.tweaked-callout {
padding: var(--wa-space-xs);
padding-inline-start: var(--wa-space-m);
align-items: center;
&:not(.tweaked-any *) {
visibility: hidden;
}
&::part(message) {
display: flex;
align-items: center;
gap: var(--wa-space-xs);
}
wa-button:first-of-type {
margin-inline-start: auto;
}
}
/* Better UI before Vue initializes */
[v-if='saved'],
[v-if^='tweaked'] {
display: none;
}
.core-color {
wa-radio-button::part(base) {
width: 2em;
height: 2em;
padding: 0;
border-radius: var(--wa-border-radius-circle);
background: var(--color);
background-clip: border-box;
}
wa-radio-button:is([checked], :state(checked))::part(base) {
box-shadow:
inset 0 0 0 var(--indicator-width) var(--indicator-color),
inset 0 0 0 calc(var(--indicator-width) + 1.5px) var(--wa-color-surface-default);
}
&::part(form-control-input) {
gap: var(--wa-space-xs);
}
}

View File

@@ -1,502 +0,0 @@
// TODO move these to local imports
import Color from 'https://colorjs.io/dist/color.js';
import { createApp, nextTick } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';
import { cdnUrl, hueRanges, hues, Permalink, tints } from '../../assets/scripts/tweak.js';
import { cssImport, cssLiteral, cssRule } from '../../assets/scripts/tweak/code.js';
import { maxGrayChroma, moreHue, selectors, urls } from '../../assets/scripts/tweak/data.js';
import { subtractAngles } from '../../assets/scripts/tweak/util.js';
import Prism from '/assets/scripts/prism.js';
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)));
// // Detect https://bugs.webkit.org/show_bug.cgi?id=287637
// const SAFARI_OKLCH_BUG = (() => {
// let dummy = document.createElement('div');
// document.body.appendChild(dummy);
// dummy.style.color = 'oklch(from #d5e0e6 l c h)';
// let computedColor = getComputedStyle(dummy).color;
// dummy.remove();
// return computedColor.endsWith(' 0)');
// })();
let allPalettes = await fetch('/docs/palettes/data.json').then(r => r.json());
globalThis.allPalettes = allPalettes;
for (let palette in allPalettes) {
for (let hue in allPalettes[palette].colors) {
let scale = allPalettes[palette].colors[hue];
for (let tint of tints) {
let color = scale[tint];
if (Array.isArray(color)) {
scale[tint] = new Color('oklch', color);
}
}
}
}
const percentFormatter = value => value.toLocaleString(undefined, { style: 'percent' });
let paletteAppSpec = {
mixins: [savedMixin],
data() {
let appRoot = document.querySelector('#palette-app');
let id = appRoot.dataset.paletteId;
let palette = allPalettes[id];
return {
id,
originalTitle: palette.title,
originalColors: palette.colors,
hueRanges,
hueShifts: Object.fromEntries(hues.map(hue => [hue, 0])),
chromaScale: 1,
grayChroma: undefined,
grayColor: undefined,
tweaking: {},
type: 'palette',
collection: 'palettes',
};
},
created() {
// Non-reactive variables to expose
Object.assign(this, { moreHue });
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'));
}
}
for (let param of ['chroma-scale', 'gray-color', 'gray-chroma']) {
if (this.permalink.has(param)) {
let value = this.permalink.get(param);
if (!isNaN(value)) {
// Convert numeric values to numbers
value = Number(value);
}
let prop = camelCase(param);
this[prop] = value;
}
}
}
},
mounted() {
for (let ref in this.$refs) {
this.$refs[ref].tooltipFormatter = percentFormatter;
}
},
computed: {
/** Default palette title for saving */
defaultTitle() {
return this.originalTitle + ' (tweaked)';
},
tweaks() {
return {
hueShifts: this.hueShifts,
chromaScale: this.chromaScale,
grayColor: this.grayColor,
grayChroma: this.grayChroma,
};
},
isTweaked() {
return Object.values(this.hueShifts).some(Boolean);
},
code() {
let ret = {};
for (let language of ['html', 'css']) {
let code = getPaletteCode(this.id, this.colors, this.tweaked, { language, cdnUrl });
ret[language] = {
raw: code,
highlighted: Prism.highlight(code, Prism.languages[language], language),
};
}
return ret;
},
colors() {
return applyTweaks.call(this, this.originalColors, this.tweaks, this.tweaked);
},
colorsMinusChromaScale() {
let tweaked = { ...this.tweaked, chromaScale: false };
return applyTweaks.call(this, this.originalColors, this.tweaks, tweaked);
},
colorsMinusHueShifts() {
let tweaked = { ...this.tweaked, hue: false };
return applyTweaks.call(this, this.originalColors, this.tweaks, tweaked);
},
colorsMinusGrayChroma() {
let tweaked = { ...this.tweaked, grayChroma: false };
return applyTweaks.call(this, this.originalColors, this.tweaks, tweaked);
},
tweaked() {
let anyHueTweaked = Object.values(this.hueShifts).some(Boolean);
let hue = anyHueTweaked
? Object.fromEntries(Object.entries(this.hueShifts).map(([hue, shift]) => [hue, shift !== 0]))
: false;
let ret = {
chromaScale: this.chromaScale !== 1,
hue,
grayChroma: this.grayChroma !== this.originalGrayChroma,
grayColor: this.grayColor !== this.originalGrayColor,
};
let anyTweaked = Object.values(ret).some(Boolean);
return anyTweaked ? ret : false;
},
tweaksHumanReadable() {
let ret = {};
if (this.chromaScale !== 1) {
ret.chromaScale = 'More ' + (this.chromaScale > 1 ? 'vibrant' : 'muted');
}
for (let hue in this.hueShifts) {
let shift = this.hueShifts[hue];
if (!shift) {
continue;
}
let relHue = shift < 0 ? arrayPrevious(hues, hue) : arrayNext(hues, hue);
let hueTweak = moreHue[relHue] ?? relHue + 'er';
ret[hue] = capitalize(hueTweak + ' ' + hue + 's');
}
if (this.tweaked.grayChroma || this.tweaked.grayColor) {
if (this.tweaked.grayChroma === 0) {
ret.grayChroma = 'Achromatic grays';
} else {
if (this.tweaked.grayColor) {
ret.grayColor = capitalize(this.grayColor) + ' gray undertone';
}
if (this.tweaked.grayChroma) {
let more = this.tweaked.grayChroma > this.originalGrayChroma;
ret.grayChroma = `More ${more ? 'colorful' : 'neutral'} grays`;
}
}
}
return ret;
},
originalContrasts() {
return getContrasts(this.originalColors);
},
contrasts() {
return getContrasts(this.colors, this.originalContrasts);
},
originalCoreColors() {
let ret = {};
for (let hue in this.originalColors) {
let maxChromaTintRaw = this.originalColors[hue].maxChromaTintRaw;
ret[hue] = this.originalColors[hue][maxChromaTintRaw];
}
return ret;
},
coreColors() {
let ret = {};
for (let hue in this.colors) {
let maxChromaTintRaw = this.colors[hue].maxChromaTintRaw;
ret[hue] = this.colors[hue][maxChromaTintRaw];
}
return ret;
},
originalGrayColor() {
let grayHue = this.originalCoreColors.gray.get('h');
let minDistance = Infinity;
let closestHue = null;
for (let name in this.originalCoreColors) {
if (name === 'gray') {
continue;
}
let hue = this.originalCoreColors[name].get('h');
let distance = Math.abs(subtractAngles(hue, grayHue));
if (distance < minDistance) {
minDistance = distance;
closestHue = name;
}
}
return closestHue ?? 'indigo';
},
originalGrayChroma() {
let coreTint = this.originalColors.gray.maxChromaTint;
let grayChroma = this.originalColors.gray[coreTint].get('c');
if (grayChroma === 0 || grayChroma === null) {
return 0;
}
let grayColorChroma = this.originalColors[this.originalGrayColor][coreTint].get('c');
return grayChroma / grayColorChroma;
},
/**
* We want to preserve the original grayChroma selection so that when the user switches to another undertone
* that supports higher chromas, their selection will be there.
* This property is the gray chroma % that is actually applied.
*/
computedGrayChroma() {
return Math.min(this.grayChroma, this.maxGrayChroma);
},
maxGrayChroma() {
return maxGrayChroma[this.grayColor] ?? 0.3;
},
},
watch: {
hueShifts: {
deep: true,
handler() {
for (let hue in this.hueShifts) {
this.permalink.set(hue + '-shift', this.hueShifts[hue], 0);
}
},
},
chromaScale() {
this.permalink.set('chroma-scale', this.chromaScale, 1);
},
grayColor() {
this.permalink.set('gray-color', this.grayColor, this.originalGrayColor);
},
grayChroma() {
this.permalink.set('gray-chroma', this.grayChroma, this.originalGrayChroma);
},
tweaks: {
deep: true,
async handler(value, oldValue) {
await nextTick(); // must run after individual watchers
// Update page URL
this.permalink.updateLocation();
this.unsavedChanges = true;
},
},
},
methods: {
/**
* Remove a specific tweak or all tweaks
* @param {string} [param] - The tweak to remove. If not provided, all tweaks are removed.
*/
reset(param) {
if (!param || param === 'chromaScale') {
this.chromaScale = 1;
}
if (param in this.hueShifts) {
this.hueShifts[param] = 0;
} else if (!param) {
for (let hue in this.hueShifts) {
this.hueShifts[hue] = 0;
}
}
if (!param || param === 'grayColor') {
this.grayColor = this.originalGrayColor;
}
if (!param || param === 'grayChroma') {
this.grayChroma = this.originalGrayChroma;
}
},
},
directives: {
content,
},
compilerOptions: {
isCustomElement: tag => tag.startsWith('wa-'),
},
};
function init() {
let paletteAppContainer = document.querySelector('#palette-app');
globalThis.paletteApp?.unmount?.();
if (!paletteAppContainer) {
return;
}
globalThis.paletteApp = createApp(paletteAppSpec).mount(paletteAppContainer);
}
init();
addEventListener('turbo:render', init);
export function getPaletteCode(paletteId, colors, tweaked, options) {
let imports = [];
if (paletteId) {
imports.push(urls.palette(paletteId));
}
let css = '';
let declarations = [];
if (tweaked) {
for (let hue in colors) {
if (hue === 'orange') {
continue;
} else if (hue === 'gray') {
if (!tweaked.grayChroma && !tweaked.grayColor) {
continue;
}
} else if (!tweaked.chromaScale && !tweaked.hue?.[hue]) {
continue;
}
for (let tint of tints) {
let color = colors[hue][tint];
let stringified = color.toString({ format: color.inGamut('srgb') ? 'hex' : undefined });
declarations.push(`--wa-color-${hue}-${tint}: ${stringified};`);
}
declarations.push('');
}
if (declarations.length > 0) {
css += cssRule(selectors.palette(paletteId), declarations);
}
}
let ret = imports.map(url => cssImport(url, options)).join('\n');
if (css) {
ret += `\n\n${cssLiteral(css, options)}`;
}
return ret;
}
function arrayNext(array, element) {
let index = array.indexOf(element);
return array[(index + 1) % array.length];
}
function arrayPrevious(array, element) {
let index = array.indexOf(element);
return array[(index - 1 + array.length) % array.length];
}
function applyTweaks(originalColors, tweaks, tweaked) {
let ret = {};
let { hueShifts, chromaScale = 1, grayColor, grayChroma } = tweaks;
if (!tweaked) {
return originalColors;
}
if (tweaked.grayChroma) {
grayChroma = this.computedGrayChroma;
}
for (let hue in originalColors) {
let originalScale = originalColors[hue];
let scale = (ret[hue] = {});
let descriptors = Object.getOwnPropertyDescriptors(originalScale);
Object.defineProperties(scale, {
maxChromaTint: { ...descriptors.maxChromaTint, enumerable: false },
maxChromaTintRaw: { ...descriptors.maxChromaTintRaw, enumerable: false },
});
for (let tint of tints) {
let color = originalScale[tint].clone();
if (tweaked.hue && hueShifts[hue]) {
color.set({ h: h => h + hueShifts[hue] });
}
if (tweaked.chromaScale && chromaScale !== 1) {
color.set({ c: c => c * chromaScale });
}
if (hue === 'gray' && (tweaked.grayChroma || tweaked.grayColor)) {
let colorUndertone = originalColors[grayColor][tint].clone();
color = colorUndertone.set({ c: c => c * grayChroma });
}
scale[tint] = color;
}
}
return ret;
}
function camelCase(str) {
return (str + '').replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
}
function capitalize(str) {
return str[0].toUpperCase() + str.slice(1);
}
function getContrasts(colors, originalContrasts) {
let ret = {};
for (let hue in colors) {
ret[hue] = {};
for (let tintBg of tints) {
ret[hue][tintBg] = {};
let bgColor = colors[hue][tintBg];
if (!bgColor || !bgColor.contrast) {
continue;
}
for (let tintFg of tints) {
let fgColor = colors[hue][tintFg];
let value = bgColor.contrast(fgColor, 'WCAG21');
if (originalContrasts) {
let original = originalContrasts[hue][tintBg][tintFg];
ret[hue][tintBg][tintFg] = { value, original, bgColor, fgColor };
} else {
ret[hue][tintBg][tintFg] = value;
}
}
}
}
return ret;
}

1047
docs/docs/patterns/app.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,70 +0,0 @@
---
title: Action Panel
description: 'Help users complete tasks efficiently with quick access to key actions.'
icon: action-panel
isPro: true
---
## Simple
```html {.example}
<wa-card style="max-width: 60ch; margin: auto">
<div class="wa-stack wa-align-items-start">
<h3 class="wa-heading-m">New Dashboard</h3>
<p>Arrange your data into a single view to monitor trends and track performance.</p>
<wa-button variant="brand" size="small">Build Dashboard</wa-button>
</div>
</wa-card>
```
## With Flanked Button
```html {.example}
<wa-card style="max-width: 60ch; margin: auto">
<div class="wa-flank:end">
<div class="wa-stack wa-gap-xs">
<h3 class="wa-heading-m">Query with SQL Runner</h3>
<p>Access your database to run ad hoc queries.</p>
</div>
<wa-button variant="brand" size="small">New Query</wa-button>
</div>
</wa-card>
```
## With Switch
```html {.example}
<wa-card style="max-width: 70ch; margin: auto">
<div class="wa-stack">
<div class="wa-flank:end">
<h3 id="auto-renew-label" class="wa-heading-m">Auto-renew</h3>
<wa-switch size="large" aria-labelledby="auto-renew-label"></wa-switch>
</div>
<p class="wa-body-s">Automatically renew your subscription using your preferred payment method. We'll send you a reminder 30 days before we draft your account.</p>
</div>
</wa-card>
```
## Avatar and Quick actions
```html {.example}
<wa-card style="margin: 0 auto; max-width: 45ch;">
<div class="wa-flank">
<wa-avatar image="https://images.unsplash.com/photo-1532202802379-df93d543bac3?q=80&w=2574&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" label="profile-image"></wa-avatar>
<div class="wa-split">
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-s">Super Dog</span>
<div class="wa-caption-m wa-cluster wa-gap-xs">
<span>Online</span>
<wa-icon name="circle" style="color: var(--wa-color-green); font-size: 10px;"></wa-icon>
</div>
</div>
<div class="wa-cluster" style="font-size: var(--wa-font-size-l);">
<wa-icon-button name="microphone" label="audio-input"></wa-icon-button>
<wa-icon-button name="headphones" label="audio-output"></wa-icon-button>
<wa-icon-button name="gear" label="settings"></wa-icon-button>
</div>
</div>
</div>
</wa-card>
```

View File

@@ -1,336 +0,0 @@
---
title: Activity Log
description: 'Track and organize recent user actions or events.'
---
## Simple
```html {.example}
<div class="wa-stack" style="max-width: 60ch; margin: auto">
<article class="wa-flank:end wa-align-items-baseline" style="--flank-size: 10ch">
<div class="wa-grid">
<div class="wa-cluster">
<wa-icon name="french-fries" fixed-width></wa-icon>
<span>Fast food</span>
</div>
<wa-relative-time sync></wa-relative-time>
</div>
<wa-tag variant="danger">- $5.00</wa-tag>
</article>
<wa-divider></wa-divider>
<article class="wa-flank:end wa-align-items-baseline" style="--flank-size: 10ch">
<div class="wa-grid">
<div class="wa-cluster">
<wa-icon name="piggy-bank" fixed-width></wa-icon>
<span>Refund</span>
</div>
<wa-relative-time date="2025-03-26T09:00:00-04:00"></wa-relative-time>
</div>
<wa-tag variant="success">+ $48.99</wa-tag>
</article>
<wa-divider></wa-divider>
<article class="wa-flank:end wa-align-items-baseline" style="--flank-size: 10ch">
<div class="wa-grid">
<div class="wa-cluster">
<wa-icon name="carrot" fixed-width></wa-icon>
<span>Groceries</span>
</div>
<wa-relative-time date="2025-03-24T09:00:00-04:00"></wa-relative-time>
</div>
<wa-tag variant="danger">- $115.37</wa-tag>
</article>
<wa-divider></wa-divider>
<article class="wa-flank:end wa-align-items-baseline" style="--flank-size: 10ch">
<div class="wa-grid">
<div class="wa-cluster">
<wa-icon name="shirt" fixed-width></wa-icon>
<span>Clothing</span>
</div>
<wa-relative-time date="2025-03-15T09:00:00-04:00"></wa-relative-time>
</div>
<wa-tag variant="danger">- $220.99</wa-tag>
</article>
</div>
```
## Timeline with Icons
```html {.example}
<div class="wa-stack wa-gap-3xs" style="max-width: 60ch; margin: auto">
<article class="wa-flank" style="flex-wrap: nowrap">
<wa-avatar style="--size: 2rem">
<wa-icon slot="icon" name="acorn"></wa-icon>
</wa-avatar>
<div class="wa-flank:end wa-gap-xs">
<span>Buried by <strong>squirrel</strong></span>
<wa-format-date date="2025-04-01" month="short" day="numeric"></wa-format-date>
</div>
</article>
<wa-divider vertical style="height: 1em; margin-left: 1rem"></wa-divider>
<article class="wa-flank" style="flex-wrap: nowrap">
<wa-avatar style="--size: 2rem">
<wa-icon slot="icon" name="seedling"></wa-icon>
</wa-avatar>
<div class="wa-flank:end wa-gap-xs">
<span>Germinated in <strong>nutrient-rich soil</strong></span>
<wa-format-date date="2025-05-29" month="short" day="numeric"></wa-format-date>
</div>
</article>
<wa-divider vertical style="height: 1em; margin-left: 1rem"></wa-divider>
<article class="wa-flank" style="flex-wrap: nowrap">
<wa-avatar style="--size: 2rem">
<wa-icon slot="icon" name="tree-deciduous"></wa-icon>
</wa-avatar>
<div class="wa-flank:end wa-gap-xs">
<span>Matured by <strong>water</strong> and <strong>sunlight</strong></span>
<wa-format-date date="2025-09-15" month="short" day="numeric"></wa-format-date>
</div>
</article>
<wa-divider vertical style="height: 1em; margin-left: 1rem"></wa-divider>
<article class="wa-flank" style="flex-wrap: nowrap">
<wa-avatar style="--size: 2rem">
<wa-icon slot="icon" name="crate-apple"></wa-icon>
</wa-avatar>
<div class="wa-flank:end wa-gap-xs">
<span>Fruit harvested by <strong>you</strong></span>
<wa-format-date date="2025-10-18" month="short" day="numeric"></wa-format-date>
</div>
</article>
</div>
```
## With Expandable Details
```html {.example}
<wa-card style="max-width: 70ch; margin: auto">
<h3 class="wa-heading-m">Monthly Activity</h3>
<div class="wa-stack">
<wa-details>
<span class="wa-heading-m" slot="summary">
February
</span>
<div class="wa-stack">
<article class="wa-flank">
<wa-icon style="font-size: var(--wa-font-size-l)" name="envelope" fixed-width></wa-icon>
<div class="wa-split">
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">Email blasts</span>
<div class="wa-cluster wa-gap-2xs">
<a href="#">Nick Burkhart</a><span>sent to</span><a href="#">likely customers</a>
</div>
</div>
<wa-format-date date="2025-02-28" month="short" day="numeric" class="wa-caption-m"></wa-format-date>
</div>
</article>
<wa-divider></wa-divider>
<article class="wa-flank">
<wa-icon style="font-size: var(--wa-font-size-l)" name="phone" fixed-width></wa-icon>
<div class="wa-split">
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">Spoke with the Pope</span>
<div class="wa-cluster wa-gap-2xs">
<a href="#">Artur Fleck</a><span>for 1 hour</span>
</div>
</div>
<wa-format-date date="2025-02-23" month="short" day="numeric" class="wa-caption-m"></wa-format-date>
</div>
</article>
</div>
</wa-details>
<wa-details>
<span class="wa-heading-m" slot="summary">
March
</span>
<div class="wa-stack">
<article class="wa-flank">
<wa-icon style="font-size: var(--wa-font-size-l)" name="video" fixed-width></wa-icon>
<div class="wa-split">
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">Zoom Call with Northeast office</span>
<div class="wa-cluster wa-gap-2xs">
<a href="#">Axel Foley</a><span>for 47 minutes</span>
</div>
</div>
<wa-format-date date="2025-03-15" month="short" day="numeric" class="wa-caption-m"></wa-format-date>
</div>
</article>
<wa-divider></wa-divider>
<article class="wa-flank">
<wa-icon style="font-size: var(--wa-font-size-l)" name="calendar" fixed-width></wa-icon>
<div class="wa-split">
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">Scheduled birthday party</span>
<div class="wa-cluster wa-gap-2xs">
<a href="#">John Blaze</a><span>in</span><a href="#">Social Events</a>
</div>
</div>
<wa-format-date date="2025-03-03" month="short" day="numeric" class="wa-caption-m"></wa-format-date>
</div>
</article>
</div>
</wa-details>
<wa-details>
<span class="wa-heading-m" slot="summary">
April
</span>
<div class="wa-stack">
<article class="wa-flank">
<wa-icon style="font-size: var(--wa-font-size-l)" family="brands" name="intercom" fixed-width></wa-icon>
<div class="wa-split">
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">Got new lead</span>
<div class="wa-cluster wa-gap-2xs">
<a href="#">Jack Carter</a><span>on Intercom switchboard</span>
</div>
</div>
<wa-format-date date="2025-04-18" month="short" day="numeric" class="wa-caption-m"></wa-format-date>
</div>
</article>
<wa-divider></wa-divider>
<article class="wa-flank">
<wa-icon style="font-size: var(--wa-font-size-l)" name="list-check" fixed-width></wa-icon>
<div class="wa-split">
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">Completed Todo</span>
<div class="wa-cluster wa-gap-2xs">
<a href="#">Huey Freeman</a><span>marked complete on</span><a href="#">Daily Tasks</a>
</div>
</div>
<wa-format-date date="2025-04-02" month="short" day="numeric" class="wa-caption-m"></wa-format-date>
</div>
</article>
</div>
</wa-details>
</div>
</wa-card>
```
## Card Separated
```html {.example}
<div class="wa-stack" style="max-width: 45ch; margin: 0 auto;">
<div class="wa-stack">
<wa-card>
<div class="wa-flank">
<wa-avatar image="https://images.unsplash.com/photo-1559188286-a173792c8340?q=80&w=2906&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" label="profile image"></wa-avatar>
<div class="wa-stack wa-gap-0">
<div class="wa-split">
<span class="wa-heading-s">Isaiah Hamilton</span>
<wa-relative-time class="wa-caption-s" date="2025-01-15T09:17:00-04:00"></wa-relative-time>
</div>
<p>Who's on first?</p>
<a href="#" class="wa-cluster wa-gap-2xs">
<wa-icon name="reply" family="sharp" variant="regular"></wa-icon>
<span>Reply</span>
</a>
</div>
</div>
</wa-card>
<div class="wa-flank wa-gap-xl">
<wa-divider vertical style="height: auto; align-self: stretch"></wa-divider>
<ul class="wa-stack">
<li class="wa-stack wa-gap-2xs">
<wa-card>
<div class="wa-flank">
<wa-avatar image="https://images.unsplash.com/photo-1645288059073-af3e9eb62a29?q=80&w=2936&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" label="profile image"></wa-avatar>
<div class="wa-stack wa-gap-0">
<div class="wa-split">
<span class="wa-heading-s">Melvin Hurst</span>
<wa-relative-time class="wa-caption-s" date="2025-02-15T09:17:00-04:00"></wa-relative-time>
</div>
<p>What's on second?</p>
<a href="#" class="wa-cluster wa-gap-2xs">
<wa-icon name="reply" family="sharp" variant="regular"></wa-icon>
<span>Reply</span>
</a>
</div>
</div>
</wa-card>
</li>
<li class="wa-stack wa-gap-2xs">
<wa-card>
<div class="wa-flank wa-align-items-start">
<wa-avatar image="https://images.unsplash.com/photo-1674044494331-8db2ecf18d46?q=80&w=3019&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" label="profile image"></wa-avatar>
<div class="wa-stack wa-gap-xs">
<div class="wa-split">
<span class="wa-heading-s">Vanessa Wright</span>
</div>
<wa-textarea size="small" aria-label="Add Your Comment"></wa-textarea>
</div>
</div>
</wa-card>
</li>
</ul>
</div>
</div>
</div>
```
## Divider Separated
```html {.example}
<wa-card with-header style="max-width: 54ch; margin: 0 auto;">
<div slot="header" class="wa-split">
<div>
<span>Notifications</span>
<wa-badge appearance="filled" variant="success" pill>2</wa-badge>
</div>
<wa-icon name="close"></wa-icon>
</div>
<div class="wa-stack">
<article>
<div class="wa-flank wa-align-items-start">
<wa-avatar image="https://images.unsplash.com/photo-1614807547811-4174d3582092?q=80&w=2932&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" label="profile image"></wa-avatar>
<div class="wa-stack wa-gap-2xs">
<div class="wa-split">
<span><strong>Happy</strong> commented in <a href="#">Reporting Dashboard</a></span>
<wa-icon name="circle" style="color: var(--wa-color-green);"></wa-icon>
</div>
<div class="wa-split">
<span class="wa-caption-m">Friday 3:12PM</span>
<wa-relative-time class="wa-caption-m" date="2025-02-15T09:17:00-04:00"></wa-relative-time>
</div>
<wa-callout variant="neutral">
Really love this approach. I think this is the best solution for the sync issue.
</wa-callout>
</div>
</div>
<wa-divider></wa-divider>
</article>
<article>
<div class="wa-flank wa-align-items-start">
<wa-avatar image="https://images.unsplash.com/photo-1613428800237-c86372070fab?q=80&w=3017&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" label="profile image"></wa-avatar>
<div class="wa-stack wa-gap-2xs">
<div class="wa-split">
<span><strong>Charlotte</strong> followed you</span>
<wa-icon name="circle" style="color: var(--wa-color-green);"></wa-icon>
</div>
<div class="wa-split">
<span class="wa-caption-m">Friday 3:04PM</span>
<wa-relative-time class="wa-caption-m" date="2025-02-15T09:17:00-04:00"></wa-relative-time>
</div>
</div>
</div>
<wa-divider></wa-divider>
</article>
<article>
<div class="wa-flank wa-align-items-start">
<wa-avatar image="https://images.unsplash.com/photo-1645288059073-af3e9eb62a29?q=80&w=2936&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" label="Profile image"></wa-avatar>
<div class="wa-stack wa-gap-2xs">
<div class="wa-split">
<span><strong>Tavitian</strong> invited you to <a href="#">Homepage Redesign</a></span>
</div>
<div class="wa-split">
<span class="wa-caption-m">Friday 2:22PM</span>
<wa-relative-time class="wa-caption-m" date="2025-02-15T09:17:00-04:00"></wa-relative-time>
</div>
<div class="wa-cluster wa-gap-xs">
<wa-button appearance="outlined" size="small">Decline</wa-button>
<wa-button variant="brand" size="small">Accept</wa-button>
</div>
</div>
</div>
<wa-divider></wa-divider>
</article>
</div>
</wa-card>
```

View File

@@ -1,3 +0,0 @@
{
"tags": ["app"]
}

View File

@@ -1,165 +0,0 @@
---
title: Comments
description: 'Enable users to engage in discussions, provide feedback, or record their thoughts.'
isPro: true
---
## Card with Header & Footer
```html {.example}
<form style="max-width: 60ch; margin: auto">
<wa-card>
<div slot="header" id="comment-area-label">
<span class="wa-heading-s">Leave a Comment</span>
</div>
<wa-textarea aria-labelledby="comment-area-label"></wa-textarea>
<div slot="footer" class="wa-cluster" style="justify-content: flex-end">
<wa-button appearance="filled" size="small">
<wa-icon slot="prefix" name="paperclip" variant="solid"></wa-icon>
Attach a file
</wa-button>
<wa-button variant="brand" size="small">Comment</wa-button>
</div>
</wa-card>
</form>
```
## Card with Thread
```html {.example}
<wa-card style="max-width: 60ch; margin: auto">
<div class="wa-stack">
<h3 class="wa-heading-m">Comments</h3>
<wa-textarea aria-label="Comment"></wa-textarea>
<wa-button variant="brand">Add Comment</wa-button>
<wa-divider></wa-divider>
<ul class="wa-stack">
<li class="wa-stack wa-gap-2xs">
<div class="wa-flank">
<wa-avatar initials="RF" label="User avatar"></wa-avatar>
<div class="wa-cluster">
<strong>Robert Fox</strong>
<span class="wa-caption-m">commented <wa-relative-time date="2025-03-31T09:17:00-04:00"></wa-relative-time></span>
</div>
</div>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras convallis mollis nunc, vel tempor sem faucibus nec. Suspendisse potenti. Pellentesque lobortis pulvinar nulla non tempor. Interdum et malesuada fames ac ante ipsum primis in faucibus.</p>
</li>
<div class="wa-flank wa-gap-xl">
<wa-divider vertical style="height: auto; align-self: stretch"></wa-divider>
<ul class="wa-stack">
<li class="wa-stack wa-gap-2xs">
<div class="wa-flank">
<wa-avatar initials="VF" label="User avatar"></wa-avatar>
<div class="wa-cluster">
<strong>Virginia Woolf</strong>
<span class="wa-caption-m">commented <wa-relative-time date="2025-03-31T12:32:00-04:00"></wa-relative-time></span>
</div>
</div>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras convallis mollis nunc, vel tempor sem faucibus nec.</p>
</li>
<li class="wa-stack wa-gap-2xs">
<div class="wa-flank">
<wa-avatar initials="CV" label="User avatar"></wa-avatar>
<div class="wa-cluster">
<strong>Clarissa Vaughan</strong>
<span class="wa-caption-m">commented <wa-relative-time></wa-relative-time></span>
</div>
</div>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras convallis mollis nunc, vel tempor sem faucibus nec.</p>
</li>
<li class="wa-cluster">
<wa-icon name="reply"></wa-icon>
<a href="">Leave a reply</a>
</li>
</ul>
</div>
</ul>
</div>
</wa-card>
```
## With Avatar & Additional Actions
```html {.example}
<div class="wa-align-items-start wa-flank">
<wa-avatar image="https://images.unsplash.com/photo-1438761681033-6461ffad8d80?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" label="User avatar"></wa-avatar>
<div class="wa-stack wa-gap-s">
<wa-textarea placeholder="Add to the conversation..." aria-label="Add comment"></wa-textarea>
<div class="wa-split">
<div class="wa-cluster wa-gap-s">
<wa-icon-button name="paperclip" label="Attach File" id="attach-button"></wa-icon-button>
<wa-tooltip for="attach-button">Attach File</wa-tooltip>
<wa-icon-button name="face-smile" label="Add Sticker" id="sticker-button"></wa-icon-button>
<wa-tooltip for="sticker-button">Add Sticker</wa-tooltip>
</div>
<wa-button variant="brand">Comment</wa-button>
</div>
</div>
</div>
```
## Rich Card with Multiple Actions
```html {.example}
<wa-card with-header with-footer style="max-width: 60ch; margin: auto">
<div slot="header">
<h3 class="wa-heading-s">I watched...</h3>
</div>
<div class="wa-stack">
<div class="wa-flank" style="--flank-size: 3rem">
<div class="wa-frame:portrait wa-border-radius-s">
<img
src="https://images.unsplash.com/photo-1607675742178-f616ae75044b?q=80&w=3435&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="the cover image for the film"
/>
</div>
<span class="wa-heading-l">Heretic</span>
</div>
<wa-divider></wa-divider>
<dl class="wa-split">
<dt>Date</dt>
<dd>
<wa-format-date date="2025-03-13T00:00:00.000-04:00" weekday="long" month="long" day="numeric" year="numeric" class="wa-caption-m"></wa-format-date>
</dd>
</dl>
<wa-divider></wa-divider>
<div class="wa-split">
<wa-rating label="Rating"></wa-rating>
<wa-checkbox>Loved it!</wa-checkbox>
</div>
<wa-divider></wa-divider>
<wa-textarea placeholder="Add review..." aria-label="Add review"></wa-textarea>
</div>
<div slot="footer" class="wa-grid">
<wa-button appearance="outlined">Cancel</wa-button>
<wa-button variant="brand">Save</wa-button>
</div>
</wa-card>
```
## With Preview Pane
```html{.example}
<div style="max-width: 60ch; margin: 0 auto;">
<wa-card class="wa-border-radius-square" with-footer>
<h3 class="wa-heading-m">Add a comment</h3>
<wa-tab-group>
<wa-tab panel="write">Write</wa-tab>
<wa-tab panel="preview">Preview</wa-tab>
<wa-tab-panel name="write">
<div class="wa-stack">
<div class="wa-cluster wa-gap-xs" style="justify-content: flex-end;">
<wa-icon-button name="link" label="add link"></wa-icon-button>
<wa-icon-button name="at" label="mention collaborator"></wa-icon-button>
<wa-icon-button name="hashtag" label="change heading"></wa-icon-button>
</div>
<wa-textarea aria-label="Add a comment"></wa-textarea>
</div>
</wa-tab-panel>
<wa-tab-panel name="preview">Your content will render here.</wa-tab-panel>
</wa-tab-group>
<div slot="footer" class="wa-cluster" style="justify-content: flex-end;">
<wa-button appearance="outlined" size="small">Post</wa-button>
</div>
</wa-card>
</div>
```

View File

@@ -1,174 +0,0 @@
---
title: Data Display
description: 'Convey insights, metrics, and aggregate data at a glance.'
isPro: true
---
## Simple
```html {.example}
<wa-card>
<div class="wa-grid wa-gap-3xl" style="--min-column-size: 24ch;">
<div class="wa-stack">
<div class="wa-split">
<div class="wa-cluster wa-gap-xs">
<wa-icon name="sack-dollar"></wa-icon>
<span>Incomes</span>
</div>
<div class="wa-cluster wa-gap-xs" style="color: var(--wa-color-green);">
<wa-icon name="arrow-trend-up"></wa-icon>
<wa-format-number class="wa-heading-s" type="percent" value=".475"></wa-format-number>
</div>
</div>
<wa-format-number class="wa-heading-xl" type="currency" currency="USD" value="175000000" lang="en-US"></wa-format-number>
</div>
<div class="wa-stack">
<div class="wa-split">
<div class="wa-cluster wa-gap-xs">
<wa-icon name="credit-card"></wa-icon>
<span>Expenses</span>
</div>
<div class="wa-cluster wa-gap-xs" style="color: var(--wa-color-red);">
<wa-icon name="arrow-trend-down"></wa-icon>
<wa-format-number class="wa-heading-s" type="percent" value=".27"></wa-format-number>
</div>
</div>
<wa-format-number class="wa-heading-xl" class="wa-heading-xl" type="currency" currency="USD" value="289472" lang="en-US"></wa-format-number>
</div>
<div class="wa-stack">
<div class="wa-split">
<div class="wa-cluster wa-gap-xs">
<wa-icon name="seedling"></wa-icon>
<span>Investments</span>
</div>
<div class="wa-cluster wa-gap-xs" style="color: var(--wa-color-green);">
<wa-icon name="arrow-trend-up"></wa-icon>
<wa-format-number class="wa-heading-s" type="percent" value=".14"></wa-format-number>
</div>
</div>
<wa-format-number class="wa-heading-xl" class="wa-heading-xl" type="currency" currency="USD" value="569213" lang="en-US"></wa-format-number>
</div>
<div class="wa-stack">
<div class="wa-split">
<div class="wa-cluster wa-gap-xs">
<wa-icon name="landmark"></wa-icon>
<span>Mortgages & Loans</span>
</div>
</div>
<wa-format-number class="wa-heading-xl" class="wa-heading-xl" type="currency" currency="USD" value="23904" lang="en-US"></wa-format-number>
</div>
</div>
</wa-card>
```
## Cards with Avatars
```html {.example}
<div class="wa-grid" style="--min-column-size: 30ch">
<wa-card>
<div class="wa-flank wa-align-items-start">
<wa-avatar shape="rounded">
<wa-icon slot="icon" name="user-group"></wa-icon>
</wa-avatar>
<div class="wa-stack wa-gap-2xs">
<h3 class="wa-caption-m">Total Subscribers</h3>
<div class="wa-cluster wa-gap-xs">
<span class="wa-heading-l">81,779</span>
<wa-badge variant="success" appearance="filled outlined" pill>
<wa-icon fixed-width name="arrow-up" label="Up"></wa-icon>
212
</wa-badge>
</div>
</div>
</div>
</wa-card>
<wa-card>
<div class="wa-flank wa-align-items-start">
<wa-avatar shape="rounded">
<wa-icon slot="icon" name="envelope-open"></wa-icon>
</wa-avatar>
<div class="wa-stack wa-gap-2xs">
<h3 class="wa-caption-m">Open Rate</h3>
<div class="wa-cluster wa-gap-xs">
<span class="wa-heading-l">61.58%</span>
<wa-badge variant="success" appearance="filled outlined" pill>
<wa-icon fixed-width name="arrow-up" label="Up"></wa-icon>
4.5%
</wa-badge>
</div>
</div>
</div>
</wa-card>
<wa-card>
<div class="wa-flank wa-align-items-start">
<wa-avatar shape="rounded">
<wa-icon slot="icon" name="arrow-pointer"></wa-icon>
</wa-avatar>
<div class="wa-stack wa-gap-2xs">
<h3 class="wa-caption-m">Click Rate</h3>
<div class="wa-cluster wa-gap-xs">
<span class="wa-heading-l">25.74%</span>
<wa-badge variant="danger" appearance="filled outlined" pill>
<wa-icon fixed-width name="arrow-down" label="Down"></wa-icon>
2.1%
</wa-badge>
</div>
</div>
</div>
</wa-card>
</div>
```
## Condensed Card
```html {.example}
<wa-card style="max-width: 50ch; margin: auto">
<div slot="header" class="wa-split">
<h3 class="wa-heading-m"><span style="color: var(--wa-color-text-quiet)">query</span> getUser</h3>
<wa-icon-button id="go-to-query-button" name="chevron-right" label="Go to Query"></wa-icon-button>
<wa-tooltip for="go-to-query-button">Go to Query</wa-tooltip>
</div>
<div class="wa-stack wa-gap-xl">
<div class="wa-split wa-align-items-stretch">
<article class="wa-stack wa-align-items-start wa-gap-xs">
<h4 class="wa-caption-l">Cache Hit Rate</h4>
<div class="wa-cluster wa-heading-2xl">
<wa-progress-ring value="12.3" style="--size: 1em; --track-width: 0.125em"></wa-progress-ring>
<span>12.3%</span>
</div>
<wa-badge appearance="filled outlined" variant="danger"><wa-icon name="arrow-down"></wa-icon> down from 19.6%</wa-badge>
</article>
<article class="wa-stack wa-gap-xs wa-align-items-end">
<h4 class="wa-caption-l">Max CHR</h4>
<span class="wa-heading-2xl">72.6%</span>
<wa-badge appearance="filled outlined" variant="success"><wa-icon name="sparkles"></wa-icon> CHR Impact +5.4%</wa-badge>
</article>
</div>
<wa-divider></wa-divider>
<article class="wa-stack wa-gap-xl">
<div class="wa-stack wa-gap-xs">
<h4 class="wa-caption-l">Cacheable Bandwidth</h4>
<div class="wa-split">
<span class="wa-heading-2xl">90.5 GB</span>
<span class="wa-caption-xl">69.9%</span>
</div>
<wa-progress-bar value="30.1" label="Cached and non-cacheable bandwidth"></wa-progress-bar>
</div>
<dl class="wa-stack wa-caption-m">
<div class="wa-cluster">
<dt>Cached</dt>
<dd>12.8 GB (9.8%)</dd>
</div>
<div class="wa-cluster">
<dt>Non-Cacheable</dt>
<dd>26.3 GB (20.3%)</dd>
</div>
<div class="wa-cluster">
<dt>Total</dt>
<dd>129.6 GB</dd>
</div>
</dl>
</article>
</div>
</wa-card>
```

View File

@@ -1,253 +0,0 @@
---
title: Description List
description: 'Help users digest detailed information in a structured, easy-to-scan format.'
isPro: true
---
## Left Aligned
```html {.example}
<div class="wa-stack">
<h3 class="wa-heading-m">Applicant Info</h3>
<p class="wa-caption-m">Details about the applicant and attachments.</p>
<wa-divider></wa-divider>
<dl class="wa-stack wa-gap-2xl">
<div class="wa-flank" style="--flank-size: 20ch;">
<dt>Full name</dt>
<dd>Bucky Barnes</dd>
</div>
<div class="wa-flank" style="--flank-size: 20ch;">
<dt>Application for</dt>
<dd>Machine Learning Engineer</dd>
</div>
<div class="wa-flank" style="--flank-size: 20ch;">
<dt>Email address</dt>
<dd>winter_soldier@example.com</dd>
</div>
<div class="wa-flank" style="--flank-size: 20ch;">
<dt>Salary expectation</dt>
<dd>$240,000</dd>
</div>
<div class="wa-flank wa-align-items-start" style="--flank-size: 20ch;">
<dt>About</dt>
<dd>After being lost in action and brainwashed into becoming Hydra's ruthless assassin, my journey is one of redemption, healing, and reclaiming my true self. Though burdened with the weight of the past, I remain a fierce warrior, loyal to those I love, and I'm always striving to atone for those dark days as the Winter Soldier.
</dd>
</div>
<div class="wa-flank wa-align-items-start" style="--flank-size: 20ch;">
<dt>Attachments</dt>
<dd>
<wa-card>
<div class="wa-stack">
<div class="wa-flank">
<wa-icon name="paperclip"></wa-icon>
<div class="wa-split">
<span class="wa-caption-m wa-cluster">
<span>bb_resume.pdf</span>
<span>2.4mb</span>
</span>
<wa-button appearance="plain" variant="brand" size="small">Download</wa-button>
</div>
</div>
<wa-divider></wa-divider>
<div class="wa-flank">
<wa-icon name="paperclip"></wa-icon>
<div class="wa-split">
<span class="wa-caption-m wa-cluster">
<span>bb_cover_letter.pdf</span>
<span>2.4mb</span>
</span>
<wa-button appearance="plain" variant="brand" size="small">Download</wa-button>
</div>
</div>
</div>
</wa-card>
</dd>
</div>
</dl>
</div>
```
## Two Column
```html{.example}
<div class="wa-stack">
<h2 class="wa-heading-m">Applicant Info</h2>
<p class="wa-caption-m">Details about the applicant and attachments.</p>
<wa-divider></wa-divider>
<dl class="wa-grid wa-gap-2xl" style="--min-column-size: 40ch;">
<div class="wa-stack wa-gap-xs">
<dt>Full name</dt>
<dd>Bucky Barnes</dd>
</div>
<div class="wa-stack wa-gap-xs">
<dt>Application for</dt>
<dd>Machine Learning Engineer</dd>
</div>
<div class="wa-stack wa-gap-xs">
<dt>Email address</dt>
<dd>winter_soldier@example.com</dd>
</div>
<div class="wa-stack wa-gap-xs">
<dt>Salary expectation</dt>
<dd>$240,000</dd>
</div>
<div class="wa-stack wa-gap-xs wa-span-grid">
<dt>About</dt>
<dd>After being lost in action and brainwashed into becoming Hydra's ruthless assassin, my journey is one of redemption, healing, and reclaiming my true self. Though burdened with the weight of the past, I remain a fierce warrior, loyal to those I love, and I'm always striving to atone for those dark days as the Winter Soldier.
</dd>
</div>
<div class="wa-stack wa-gap-xs wa-span-grid">
<dt>Attachments</dt>
<dd>
<wa-card>
<div>
<div class="wa-flank">
<wa-icon name="paperclip"></wa-icon>
<div class="wa-split">
<span class="wa-caption-m wa-cluster">
<span>bb_resume.pdf</span>
<span>2.4mb</span>
</span>
<wa-button appearance="plain" variant="brand" size="small">Download</wa-button>
</div>
</div>
<wa-divider></wa-divider>
<div class="wa-flank">
<wa-icon name="paperclip"></wa-icon>
<div class="wa-split">
<span class="wa-caption-m wa-cluster">
<span>bb_cover_letter.pdf</span>
<span>2.4mb</span>
</span>
<wa-button appearance="plain" variant="brand" size="small">Download</wa-button>
</div>
</div>
</div>
</wa-card>
</dd>
</div>
</dl>
</div>
```
## Left Aligned with Actions
```html {.example}
<div class="wa-stack">
<h3 class="wa-heading-m">Applicant Info</h3>
<p class="wa-caption-m">Details about the applicant and attachments.</p>
<wa-divider></wa-divider>
<dl class="wa-stack wa-gap-2xl">
<div class="wa-flank" style="--flank-size: 20ch;">
<dt>Full name</dt>
<div class="wa-flank:end">
<dd>Bucky Barnes</dd>
<wa-button appearance="plain" variant="brand" size="small">Edit</wa-button>
</div>
</div>
<div class="wa-flank" style="--flank-size: 20ch;">
<dt>Application for</dt>
<div class="wa-flank:end">
<dd>Machine Learning Engineer</dd>
<wa-button appearance="plain" variant="brand" size="small">Edit</wa-button>
</div>
</div>
<div class="wa-flank" style="--flank-size: 20ch;">
<dt>Email address</dt>
<div class="wa-flank:end">
<dd>winter_soldier@example.com</dd>
<wa-button appearance="plain" variant="brand" size="small">Edit</wa-button>
</div>
</div>
<div class="wa-flank" style="--flank-size: 20ch;">
<dt>Salary expectation</dt>
<div class="wa-flank:end">
<dd>$240,000</dd>
<wa-button appearance="plain" variant="brand" size="small">Edit</wa-button>
</div>
</div>
<div class="wa-flank wa-align-items-start" style="--flank-size: 20ch;">
<dt>About</dt>
<div class="wa-flank:end">
<dd>After being lost in action and brainwashed into becoming Hydra's ruthless assassin, my journey is one of redemption, healing, and reclaiming my true self. Though burdened with the weight of the past, I remain a fierce warrior, loyal to those I love, and I'm always striving to atone for those dark days as the Winter Soldier.</dd>
<wa-button appearance="plain" variant="brand" size="small">Edit</wa-button>
</div>
</div>
<div class="wa-flank" style="--flank-size: 20ch;">
<dt>Attachments</dt>
<dd>
<wa-card>
<div class="wa-stack">
<div class="wa-flank">
<wa-icon name="paperclip"></wa-icon>
<div class="wa-split">
<span class="wa-caption-m wa-cluster">
<span>bb_resume.pdf</span>
<span>2.4mb</span>
</span>
<div class="wa-cluster wa-gap-2xs">
<wa-button appearance="plain" variant="brand" size="small">Download</wa-button>
<wa-divider vertical style="height: 1em"></wa-divider>
<wa-button appearance="plain" variant="danger" size="small">Delete</wa-button>
</div>
</div>
</div>
<wa-divider></wa-divider>
<div class="wa-flank">
<wa-icon name="paperclip"></wa-icon>
<div class="wa-split">
<span class="wa-caption-m wa-cluster">
<span>bb_cover_letter.pdf</span>
<span>2.4mb</span>
</span>
<div class="wa-cluster wa-gap-2xs">
<wa-button appearance="plain" variant="brand" size="small">Download</wa-button>
<wa-divider vertical style="height: 1em"></wa-divider>
<wa-button appearance="plain" variant="danger" size="small">Delete</wa-button>
</div>
</div>
</div>
</div>
</wa-card>
</dd>
</div>
</dl>
</div>
```
## Condensed
```html{.example}
<wa-card appearance="filled" style="max-width: 45ch; margin: auto">
<div class="wa-stack">
<div class="wa-split wa-align-items-start">
<dl class="wa-stack wa-gap-2xs">
<dt class="wa-heading-s">Amount</dt>
<dd class="wa-heading-l">$5,610.00</dd>
</dl>
<wa-badge appearance="filled outlined" variant="success">Paid</wa-badge>
</div>
<wa-divider></wa-divider>
<dl class="wa-stack">
<div class="wa-flank wa-align-items-stretch">
<dt><wa-icon name="user" label="Name" fixed-width></wa-icon></dt>
<dd>Sam Wilson</dd>
</div>
<div class="wa-flank wa-align-items-stretch">
<dt><wa-icon name="calendar-days" label="Date" fixed-width></wa-icon></dt>
<dd><wa-format-date date="2025-03-15"></wa-format-date></dd>
</div>
<div class="wa-flank wa-align-items-stretch">
<dt><wa-icon family="brands" name="cc-visa" label="Credit Card" fixed-width></wa-icon></dt>
<dd>Paid with Visa 1234</dd>
</div>
</dl>
</div>
<div slot="footer">
<a href="" class="wa-cluster wa-gap-2xs">
<span>Download Receipt</span>
<wa-icon name="arrow-right"></wa-icon>
</a>
</div>
</wa-card>
```

View File

@@ -1,207 +0,0 @@
---
title: Empty State
description: 'Guide users with helpful prompts and visuals when no content is available.'
isPro: true
---
## Simple
```html {.example}
<div class="wa-stack wa-align-items-center">
<wa-icon name="backpack" class="wa-caption-l" style="font-size: var(--wa-font-size-3xl)"></wa-icon>
<span class="wa-heading-m">No Kits</span>
<p class="wa-caption-l">Manage all of your project's icons in a kit.</p>
<wa-button>
<wa-icon slot="prefix" name="plus"></wa-icon>
Add Kit
</wa-button>
</div>
```
## With Interactive Placeholder
```html {.example}
<a href="" class="wa-stack wa-align-items-center wa-placeholder wa-link-plain" style="max-width: 60ch; margin: auto">
<wa-icon name="ufo-beam" class="wa-caption-l" family="sharp" style="font-size: var(--wa-font-size-3xl)"></wa-icon>
<p class="wa-heading-m">No Custom Icons</p>
<p style="text-align: center">Add your own icon or logo to get started.</p>
</a>
```
## With Templates
```html {.example}
<wa-card with-header with-footer style="max-width: 70ch; margin: auto">
<div slot="header" class="wa-stack wa-gap-xs">
<h2 class="wa-heading-m">Projects</h2>
</div>
<div class="wa-stack wa-gap-xl">
<p class="wa-caption-m">You havent created a project yet. Get started by selecting a template or start with a blank canvas.</p>
<div class="wa-grid wa-gap-xl" style="--min-column-size: 30ch;">
<a href="" class="wa-flank wa-align-items-start wa-link-plain">
<wa-avatar shape="rounded" style="--background-color: var(--wa-color-yellow-90);color: var(--wa-color-yellow-40);">
<wa-icon slot="icon" name="note-sticky"></wa-icon>
</wa-avatar>
<div class="wa-stack wa-gap-2xs">
<span class="wa-align-items-center wa-cluster wa-gap-xs wa-heading-s">
Create a Quick Note <wa-icon name="arrow-right"></wa-icon>
</span>
<p class="wa-caption-m">
Jot down any idea. Will it make sense later? Who knows.
</p>
</div>
</a>
<a href="" class="wa-flank wa-align-items-start wa-link-plain">
<wa-avatar shape="rounded" style="--background-color: var(--wa-color-red-90);color: var(--wa-color-red-40);">
<wa-icon slot="icon" name="list-check"></wa-icon>
</wa-avatar>
<div class="wa-stack wa-gap-2xs">
<span class="wa-align-items-center wa-cluster wa-gap-xs wa-heading-s">
Create a Checklist <wa-icon name="arrow-right"></wa-icon>
</span>
<p class="wa-caption-m">
The ultimate tool for looking busy.
</p>
</div>
</a>
<a href=""class="wa-flank wa-align-items-start wa-link-plain">
<wa-avatar shape="rounded" style="--background-color: var(--wa-color-purple-90);color: var(--wa-color-purple-40);">
<wa-icon slot="icon" name="table-cells"></wa-icon>
</wa-avatar>
<div class="wa-stack wa-gap-2xs">
<span class="wa-align-items-center wa-cluster wa-gap-xs wa-heading-s">
Create a Spreadsheet <wa-icon name="arrow-right"></wa-icon>
</span>
<p class="wa-caption-m">
Endless rows and columns of tiny, soul-crushing numbers.
</p>
</div>
</a>
<a href="" class="wa-flank wa-align-items-start wa-link-plain">
<wa-avatar shape="rounded" style="--background-color: var(--wa-color-orange-90);color: var(--wa-color-orange-40);">
<wa-icon slot="icon" name="presentation-screen"></wa-icon>
</wa-avatar>
<div class="wa-stack wa-gap-2xs">
<span class="wa-align-items-center wa-cluster wa-gap-xs wa-heading-s">
Create a Slideshow <wa-icon name="arrow-right"></wa-icon>
</span>
<p class="wa-caption-m">
Dramatic transitions make everything seem more official.
</p>
</div>
</a>
<a href="" class="wa-flank wa-align-items-start wa-link-plain">
<wa-avatar shape="rounded" style="--background-color: var(--wa-color-green-90);color: var(--wa-color-green-40);">
<wa-icon slot="icon" name="pen-field"></wa-icon>
</wa-avatar>
<div class="wa-stack wa-gap-2xs">
<span class="wa-align-items-center wa-cluster wa-gap-xs wa-heading-s">
Create a Form <wa-icon name="arrow-right"></wa-icon>
</span>
<p class="wa-caption-m">
Collect the deepest personal details and darkest secrets.
</p>
</div>
</a>
<a href="" class="wa-flank wa-align-items-start wa-link-plain">
<wa-avatar shape="rounded" style="--background-color: var(--wa-color-blue-90);color: var(--wa-color-blue-40);">
<wa-icon slot="icon" name="image"></wa-icon>
</wa-avatar>
<div class="wa-stack wa-gap-2xs">
<span class="wa-align-items-center wa-cluster wa-gap-xs wa-heading-s">
Create a Photo Album <wa-icon name="arrow-right"></wa-icon>
</span>
<p class="wa-caption-m">
Curate your best memories or most basic food pictures.
</p>
</div>
</a>
</div>
</div>
<div slot="footer">
<a href="" class="wa-cluster wa-gap-xs">
<span>Or start with a blank canvas</span>
<wa-icon name="arrow-right"></wa-icon>
</a>
</div>
</wa-card>
```
## Add people
```html {.example}
<wa-card style="max-width: 60ch; margin: 0 auto;">
<div class="wa-stack wa-gap-xs">
<h3 class="wa-heading-m">Add team members</h3>
<p>This project is awful lonely. Invite some team members to liven up the joint.</p>
<div class="wa-flank:end wa-gap-xs">
<wa-input></wa-input><wa-button>Invite</wa-button>
</div>
<div class="wa-stack">
<em class="wa-caption-l">Team members previously added to projects</em>
<wa-divider></wa-divider>
<section>
<div class="wa-flank">
<wa-avatar image="https://images.unsplash.com/photo-1614807547811-4174d3582092?q=80&w=2932&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" label="profile image"></wa-avatar>
<div class="wa-split">
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">Earl Upton</span>
<span class="wa-caption-m">DevOps</span>
</div>
<wa-button appearance="plain">
<wa-icon name="user-plus" slot="prefix"></wa-icon>
Invite
</wa-button>
</div>
</div>
<wa-divider></wa-divider>
</section>
<section>
<div class="wa-flank">
<wa-avatar image="https://images.unsplash.com/photo-1613428800237-c86372070fab?q=80&w=3017&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" lable="profile image"></wa-avatar>
<div class="wa-split">
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">Steamboat Willie</span>
<span class="wa-caption-m">Captain</span>
</div>
<wa-button appearance="plain">
<wa-icon name="user-plus" slot="prefix"></wa-icon>
Invite
</wa-button>
</div>
</div>
<wa-divider></wa-divider>
</section>
<section>
<div class="wa-flank">
<wa-avatar image="https://images.unsplash.com/photo-1678582967399-bf558533f5eb?q=80&w=3029&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" label="profile image"></wa-avatar>
<div class="wa-split">
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">Melissa Eckers</span>
<span class="wa-caption-m">Cloud Engineer</span>
</div>
<wa-button appearance="plain">
<wa-icon name="user-plus" slot="prefix"></wa-icon>
Invite
</wa-button>
</div>
</div>
<wa-divider></wa-divider>
</section><section>
<div class="wa-flank">
<wa-avatar image="https://images.unsplash.com/photo-1559188286-a173792c8340?q=80&w=2906&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" label="profile image"></wa-avatar>
<div class="wa-split">
<div class="wa-stack wa-gap-0">
<span class="wa-heading-s">Devin Shears</span>
<span class="wa-caption-m">UX Writer</span>
</div>
<wa-button appearance="plain">
<wa-icon name="user-plus" slot="prefix"></wa-icon>
Invite
</wa-button>
</div>
</div>
<wa-divider></wa-divider>
</section>
</div>
</div>
</wa-card>
```

View File

@@ -1,119 +0,0 @@
---
title: FAQ
description: 'Empower users to learn more with a structured list of questions and answers.'
isPro: true
---
## With Flanked Heading & Description
```html {.example}
<div class="wa-flank wa-align-items-start wa-gap-2xl" style="--flank-size: 35ch">
<div>
<h2>Frequently Asked Questions</h2>
<p>Cant find an answer? Reach out to your local <a href="">Operator</a>, or contact <a href="">the Oracle</a>, and enjoy a cookie. &#127850;</p>
</div>
<dl class="wa-stack wa-gap-2xl">
<div class="wa-stack wa-gap-xs">
<dt>Is Zion actually real, or just another Matrix?</dt>
<dd>Ah, the question that keeps redpills up at night. Sure, we escaped the first Matrix, but whos to say Zion isnt just another layer of the simulation?</dd>
</div>
<div class="wa-stack wa-gap-xs">
<dt>Why do the Agents always wear suits?</dt>
<dd>Because nothing says "unstoppable digital enforcer" like a generic business professional aesthetic. Also, intimidation. You ever try fighting someone in sunglasses and a tie? Its terrifying.</dd>
</div>
<div class="wa-stack wa-gap-xs">
<dt>Can I go back into the Matrix once Im out?</dt>
<dd>Technically, yes—via hacking in. Emotionally? That depends on how well you handle the knowledge that nothing around you is real.</dd>
</div>
</dl>
</div>
```
## With Expandable Answers
```html {.example}
<div class="wa-stack">
<h2>Frequently Asked Questions</h2>
<wa-details appearance="plain">
<h3 slot="summary" class="wa-heading-m" style="margin: 0">Is Zion actually real, or just another Matrix?</h3>
Ah, the question that keeps redpills up at night. Sure, we escaped the first Matrix, but whos to say Zion isnt just another layer of the simulation?
</wa-details>
<wa-divider></wa-divider>
<wa-details appearance="plain">
<h3 slot="summary" class="wa-heading-m" style="margin: 0">Why do the Agents always wear suits?</h3>
Because nothing says "unstoppable digital enforcer" like a generic business professional aesthetic. Also, intimidation. You ever try fighting someone in sunglasses and a tie? Its terrifying.
</wa-details>
<wa-divider></wa-divider>
<wa-details appearance="plain">
<h3 slot="summary" class="wa-heading-m" style="margin: 0">Can I go back into the Matrix once Im out?</h3>
Technically, yes—via hacking in. Emotionally? That depends on how well you handle the knowledge that nothing around you is real.
</wa-details>
</div>
```
## Two Column
```html {.example}
<div class="wa-stack wa-gap-2xl">
<h2>Frequently Asked Questions</h2>
<dl class="wa-stack wa-gap-2xl">
<div class="wa-grid wa-gap-xs">
<dt class="wa-heading-m">Is Zion actually real, or just another Matrix?</dt>
<dd>Ah, the question that keeps redpills up at night. Sure, we escaped the first Matrix, but whos to say Zion isnt just another layer of the simulation?</dd>
</div>
<wa-divider></wa-divider>
<div class="wa-grid wa-gap-xs">
<dt class="wa-heading-m">Why do the Agents always wear suits?</dt>
<dd>Because nothing says "unstoppable digital enforcer" like a generic business professional aesthetic. Also, intimidation. You ever try fighting someone in sunglasses and a tie? Its terrifying.</dd>
</div>
<wa-divider></wa-divider>
<div class="wa-grid wa-gap-xs">
<dt class="wa-heading-m">Can I go back into the Matrix once Im out?</dt>
<dd>Technically, yes—via hacking in. Emotionally? That depends on how well you handle the knowledge that nothing around you is real.</dd>
</div>
</dl>
</div>
```
## Multiple Columns
```html{.example}
<div>
<h2>Frequently Asked Questions</h2>
<dl class="wa-grid wa-gap-m" style="--min-column-size: 30ch;">
<div class="wa-stack wa-gap-xs">
<dt class="wa-heading-m">How often do you update your courses?</dt>
<dd>A course is updated once there is a fundamental shift in the language or librarys underlying API. You can check our <a href="#">workshop</a> list to see if a new version of a given course is on the schedule. You may also write to us as <a href="#">support@frontendmasters.com</a> with suggestions for updates.</dd>
</div>
<div class="wa-stack wa-gap-xs">
<dt class="wa-heading-m">Do you offer certificates of completion?</dt>
<dd>You can download certificates of completion from the <a href="#">Completed Courses</a> list in your Learning Library. Click the diploma icon next to the course to download the certificate in light or dark mode. A link to your Public Profile is included on each certificate if youve created one. Public Profiles showcase your learning journey and are a fantastic way to share progress with friends, co-workers, or employers. Public Profiles are available to members with an active Frontend Masters subscription who have watched ten or more hours of content. Visit the <a href="#">Public Profile</a> section in My Account to get started.</dd>
</div>
<div class="wa-stack wa-gap-xs">
<dt class="wa-heading-m">Do you offer a free trial?</dt>
<dd>
<p>We offer a free trial to first-time subscribers. You can find more about the trial here.</p>
<p>We also have the following opportunities to learn for free:</p>
<ul>
<li>The online bootcamp is a free, two-week curriculum to get you started with web development.</li>
<li>You can create a free account to gain access to five full courses for free.</li>
</ul>
</dd>
</div>
<div class="wa-stack wa-gap-xs">
<dt class="wa-heading-m">Do you have discounts for students?</dt>
<dd>We are part of the <a href="#">GitHub Student Developer Pack</a>, allowing students six months of free access to the entire platform.</dd>
</div>
<div class="wa-stack wa-gap-xs">
<dt class="wa-heading-m">How do I cancel my plan?</dt>
<dd>You can cancel your Frontend Masters subscription by visiting the <a href="#">Subscription tab</a> in your My Account area.</dd>
</div>
</dl>
</div>
```

View File

@@ -1,525 +0,0 @@
---
title: Grid List
description: 'Improve browsing and selection by organizing data in a structured grid layout.'
isPro: true
---
## Cards with Footer Actions
```html {.example}
<div class="wa-grid" style="--min-column-size: 30ch;">
<wa-card with-footer>
<div class="wa-flank:end">
<div class="wa-stack wa-gap-xs">
<div class="wa-cluster wa-gap-xs">
<h3 class="wa-heading-s">Barklia Woofington</h3 class="wa-heading-s">
<wa-badge pill>Admin</wa-badge>
</div>
<span class="wa-caption-m">Canine Executive Officer</span>
</div>
<wa-avatar image="https://images.unsplash.com/photo-1593270379182-fe1b1f6d67e5?q=80&w=2175&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" label="Avatar of black and white Border Collie"></wa-avatar>
</div>
<div slot="footer" class="wa-grid wa-gap-xs" style="--min-column-size: 10ch;">
<wa-button appearance="outlined">
<wa-icon slot="prefix" name="at"></wa-icon>
Email
</wa-button>
<wa-button appearance="outlined">
<wa-icon slot="prefix" name="phone"></wa-icon>
Phone
</wa-button>
</div>
</wa-card>
<wa-card with-footer>
<div class="wa-flank:end">
<div class="wa-stack wa-gap-xs">
<div class="wa-cluster wa-gap-xs">
<h3 class="wa-heading-s">Maggie Pawsworth</h3 class="wa-heading-s">
<wa-badge pill>Admin</wa-badge>
</div>
<span class="wa-caption-m">Canine Fetch Officer</span>
</div>
<wa-avatar image="https://images.unsplash.com/photo-1514479425649-0981aca9fe41?q=80&w=3474&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" label="Avatar of black collie mix"></wa-avatar>
</div>
<div slot="footer" class="wa-grid wa-gap-xs" style="--min-column-size: 10ch;">
<wa-button appearance="outlined">
<wa-icon slot="prefix" name="at"></wa-icon>
Email
</wa-button>
<wa-button appearance="outlined">
<wa-icon slot="prefix" name="phone"></wa-icon>
Phone
</wa-button>
</div>
</wa-card>
<wa-card with-footer>
<div class="wa-flank:end">
<div class="wa-stack wa-gap-xs">
<h3 class="wa-heading-s">Rex Tailwag</h3 class="wa-heading-s">
<span class="wa-caption-m">Head of Security</span>
</div>
<wa-avatar image="https://images.unsplash.com/photo-1610968755695-d7fcb5fd4b92?q=80&w=2848&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" label="Avatar of black and tan German Shepherd"></wa-avatar>
</div>
<div slot="footer" class="wa-grid wa-gap-xs" style="--min-column-size: 10ch;">
<wa-button appearance="outlined">
<wa-icon slot="prefix" name="at"></wa-icon>
Email
</wa-button>
<wa-button appearance="outlined">
<wa-icon slot="prefix" name="phone"></wa-icon>
Phone
</wa-button>
</div>
</wa-card>
<wa-card with-footer>
<div class="wa-flank:end">
<div class="wa-stack wa-gap-xs">
<h3 class="wa-heading-s">Luna Sniffington</h3 class="wa-heading-s">
<span class="wa-caption-m">Hound Relations</span>
</div>
<wa-avatar image="https://images.unsplash.com/photo-1526440847959-4e38e7f00b04?q=80&w=3540&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" label="Avatar of black and tan Yorkshire Terrier"></wa-avatar>
</div>
<div slot="footer" class="wa-grid wa-gap-xs" style="--min-column-size: 10ch;">
<wa-button appearance="outlined">
<wa-icon slot="prefix" name="at"></wa-icon>
Email
</wa-button>
<wa-button appearance="outlined">
<wa-icon slot="prefix" name="phone"></wa-icon>
Phone
</wa-button>
</div>
</wa-card>
<wa-card with-footer>
<div class="wa-flank:end">
<div class="wa-stack wa-gap-xs">
<h3 class="wa-heading-s">Charlie Drooler</h3 class="wa-heading-s">
<span class="wa-caption-m">Head of Sales</span>
</div>
<wa-avatar image="https://images.unsplash.com/photo-1554692844-6627ca340264?q=80&w=3540&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" label="Avatar of tan and white corgi"></wa-avatar>
</div>
<div slot="footer" class="wa-grid wa-gap-xs" style="--min-column-size: 10ch;">
<wa-button appearance="outlined">
<wa-icon slot="prefix" name="at"></wa-icon>
Email
</wa-button>
<wa-button appearance="outlined">
<wa-icon slot="prefix" name="phone"></wa-icon>
Phone
</wa-button>
</div>
</wa-card>
<wa-card with-footer>
<div class="wa-flank:end">
<div class="wa-stack wa-gap-xs">
<h3 class="wa-heading-s">Daisy Zoomley</h3 class="wa-heading-s">
<span class="wa-caption-m">IT Support</span>
</div>
<wa-avatar image="https://images.unsplash.com/photo-1544378062-0b74cc8b4713?q=80&w=3648&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" label="Avatar of gray Weimaraner"></wa-avatar>
</div>
<div slot="footer" class="wa-grid wa-gap-xs" style="--min-column-size: 10ch;">
<wa-button appearance="outlined">
<wa-icon slot="prefix" name="at"></wa-icon>
Email
</wa-button>
<wa-button appearance="outlined">
<wa-icon slot="prefix" name="phone"></wa-icon>
Phone
</wa-button>
</div>
</wa-card>
</div>
```
## Cards with Footer Actions & Large Image
```html {.example}
<div class="wa-grid" style="--min-column-size: 29ch;">
<wa-card with-footer>
<div class="wa-stack wa-align-items-center wa-gap-xs">
<div class="wa-frame wa-border-radius-circle">
<img src="https://images.unsplash.com/photo-1522075469751-3a6694fb2f61?q=80&w=2680&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" />
</div>
<h2 class="wa-heading-m">Scott Summers</h2>
<p class="wa-caption-l">DevOps</p>
</div>
<div slot="footer" class="wa-grid wa-gap-xs" style="--min-column-size: 10ch;">
<wa-button appearance="outlined">
<wa-icon slot="prefix" name="at"></wa-icon>
Email
</wa-button>
<wa-button appearance="outlined">
<wa-icon slot="prefix" name="phone"></wa-icon>
Phone
</wa-button>
</div>
</wa-card>
<wa-card with-footer>
<div class="wa-stack wa-align-items-center wa-gap-xs">
<div class="wa-frame wa-border-radius-circle">
<img src="https://images.unsplash.com/photo-1559188286-a173792c8340?q=80&w=2906&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" />
</div>
<h2 class="wa-heading-m">Kaitlin Moore</h2>
<p class="wa-caption-l">Systems Engineer</p>
</div>
<div slot="footer" class="wa-grid wa-gap-xs" style="--min-column-size: 10ch;">
<wa-button appearance="outlined">
<wa-icon slot="prefix" name="at"></wa-icon>
Email
</wa-button>
<wa-button appearance="outlined">
<wa-icon slot="prefix" name="phone"></wa-icon>
Phone
</wa-button>
</div>
</wa-card>
<wa-card with-footer>
<div class="wa-stack wa-align-items-center wa-gap-xs">
<div class="wa-frame wa-border-radius-circle">
<img src="https://images.unsplash.com/photo-1613428800237-c86372070fab?q=80&w=3017&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" />
</div>
<h2 class="wa-heading-m">Nessa Riley</h2>
<p class="wa-caption-l">Cloud Engineer</p>
</div>
<div slot="footer" class="wa-grid wa-gap-xs" style="--min-column-size: 10ch;">
<wa-button appearance="outlined">
<wa-icon slot="prefix" name="at"></wa-icon>
Email
</wa-button>
<wa-button appearance="outlined">
<wa-icon slot="prefix" name="phone"></wa-icon>
Phone
</wa-button>
</div>
</wa-card>
<wa-card with-footer>
<div class="wa-stack wa-align-items-center wa-gap-xs">
<div class="wa-frame wa-border-radius-circle">
<img src="https://images.unsplash.com/photo-1645288059073-af3e9eb62a29?q=80&w=2936&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" />
</div>
<h2 class="wa-heading-m">Veronica Staley</h2>
<p class="wa-caption-l">Machine Learning Engineer</p>
</div>
<div slot="footer" class="wa-grid wa-gap-xs" style="--min-column-size: 10ch;">
<wa-button appearance="outlined">
<wa-icon slot="prefix" name="at"></wa-icon>
Email
</wa-button>
<wa-button appearance="outlined">
<wa-icon slot="prefix" name="phone"></wa-icon>
Phone
</wa-button>
</div>
</wa-card>
</div>
```
## with Images
```html {.example}
<div class="wa-grid">
<article class="wa-stack">
<div class="wa-frame wa-border-radius-l">
<img src="https://images.unsplash.com/photo-1522075469751-3a6694fb2f61?q=80&w=2680&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" />
</div>
<div class="wa-stack wa-gap-3xs">
<span>Jeff Hanks</span>
<span>Product Designer</span>
</div>
<div class="wa-cluster wa-gap-3xs">
<wa-icon-button name="bluesky" family="brands" label="link to Blusky profile"></wa-icon-button>
<wa-icon-button name="dribbble" family="brands" label="link to Dribbble profile"></wa-icon-button>
</div>
</article>
<article class="wa-stack">
<div class="wa-frame wa-border-radius-l">
<img src="https://images.unsplash.com/photo-1674044494331-8db2ecf18d46?q=80&w=3019&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" />
</div>
<div class="wa-stack wa-gap-3xs">
<span>Allen Bryant</span>
<span>Staff Engineer</span>
</div>
<div class="wa-cluster wa-gap-3xs">
<wa-icon-button name="bluesky" family="brands" label="link to Blusky profile"></wa-icon-button>
<wa-icon-button name="dribbble" family="brands" label="link to Dribbble profile"></wa-icon-button>
</div>
</article>
<article class="wa-stack">
<div class="wa-frame wa-border-radius-l">
<img src="https://images.unsplash.com/photo-1645288059073-af3e9eb62a29?q=80&w=2936&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" />
</div>
<div class="wa-stack wa-gap-3xs">
<span>Mariah Greene</span>
<span>DevOps</span>
</div>
<div class="wa-cluster wa-gap-3xs">
<wa-icon-button name="bluesky" family="brands" label="link to Blusky profile"></wa-icon-button>
<wa-icon-button name="dribbble" family="brands" label="link to Dribbble profile"></wa-icon-button>
</div>
</article>
<article class="wa-stack">
<div class="wa-frame wa-border-radius-l">
<img src="https://images.unsplash.com/photo-1613428800237-c86372070fab?q=80&w=3017&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" />
</div>
<div class="wa-stack wa-gap-3xs">
<span>Beverly Winslow</span>
<span>Design Systems Lead</span>
</div>
<div class="wa-cluster wa-gap-3xs">
<wa-icon-button name="bluesky" family="brands" label="link to Blusky profile"></wa-icon-button>
<wa-icon-button name="dribbble" family="brands" label="link to Dribbble profile"></wa-icon-button>
</div>
</article>
<article class="wa-stack">
<div class="wa-frame wa-border-radius-l">
<img src="https://images.unsplash.com/photo-1614807547811-4174d3582092?q=80&w=2932&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" />
</div>
<div class="wa-stack wa-gap-3xs">
<span>Eric Masterson</span>
<span>Copy Writer</span>
</div>
<div class="wa-cluster wa-gap-3xs">
<wa-icon-button name="bluesky" family="brands" label="link to Blusky profile"></wa-icon-button>
<wa-icon-button name="dribbble" family="brands" label="link to Dribbble profile"></wa-icon-button>
</div>
</article>
<article class="wa-stack">
<div class="wa-frame wa-border-radius-l">
<img src="https://images.unsplash.com/photo-1559188286-a173792c8340?q=80&w=2906&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" />
</div>
<div class="wa-stack wa-gap-3xs">
<span>Stephen Coffee</span>
<span>Visual Designer</span>
</div>
<div class="wa-cluster wa-gap-3xs">
<wa-icon-button name="bluesky" family="brands" label="link to Blusky profile"></wa-icon-button>
<wa-icon-button name="dribbble" family="brands" label="link to Dribbble profile"></wa-icon-button>
</div>
</article>
</div>
```
## Linked Cards with Options Menu
```html{.example}
<div class="wa-grid" style="--min-column-size: 25ch">
<wa-card>
<div class="wa-flank:end">
<a href="" class="wa-flank wa-link-plain">
<wa-avatar shape="rounded" style="--background-color: var(--wa-color-yellow-80); --text-color: var(--wa-color-yellow-40)">
<wa-icon slot="icon" name="pancakes"></wa-icon>
</wa-avatar>
<div class="wa-gap-2xs wa-stack">
<span class="wa-heading-s">Breakfast</span>
<span class="wa-caption-m">28 Items</span>
</div>
</a>
<wa-dropdown>
<wa-icon-button id="more-actions-1" slot="trigger" name="ellipsis-vertical" label="More actions"></wa-icon-button>
<wa-menu>
<wa-menu-item>Copy link</wa-menu-item>
<wa-menu-item>Rename</wa-menu-item>
<wa-menu-item>Move to trash</wa-menu-item>
</wa-menu>
</wa-dropdown>
<wa-tooltip for="more-actions-1">More actions</wa-tooltip>
</div>
</wa-card>
<wa-card>
<div class="wa-flank:end">
<a href="" class="wa-flank wa-link-plain">
<wa-avatar shape="rounded" style="--background-color: var(--wa-color-orange-80); --text-color: var(--wa-color-orange-40)">
<wa-icon slot="icon" name="burger-cheese"></wa-icon>
</wa-avatar>
<div class="wa-gap-2xs wa-stack">
<span class="wa-heading-s">Lunch + Dinner</span>
<span class="wa-caption-m">40 Items</span>
</div>
</a>
<wa-dropdown>
<wa-icon-button id="more-actions-2" slot="trigger" name="ellipsis-vertical" label="More actions"></wa-icon-button>
<wa-menu>
<wa-menu-item>Copy link</wa-menu-item>
<wa-menu-item>Rename</wa-menu-item>
<wa-menu-item>Move to trash</wa-menu-item>
</wa-menu>
</wa-dropdown>
<wa-tooltip for="more-actions-2">More actions</wa-tooltip>
</div>
</wa-card>
<wa-card>
<div class="wa-flank:end">
<a href="" class="wa-flank wa-link-plain">
<wa-avatar shape="rounded" style="--background-color: var(--wa-color-indigo-80); --text-color: var(--wa-color-indigo-40)">
<wa-icon slot="icon" name="martini-glass-citrus"></wa-icon>
</wa-avatar>
<div class="wa-gap-2xs wa-stack">
<span class="wa-heading-s">Beverages</span>
<span class="wa-caption-m">19 Items</span>
</div>
</a>
<wa-dropdown>
<wa-icon-button id="more-actions-3" slot="trigger" name="ellipsis-vertical" label="More actions"></wa-icon-button>
<wa-menu>
<wa-menu-item>Copy link</wa-menu-item>
<wa-menu-item>Rename</wa-menu-item>
<wa-menu-item>Move to trash</wa-menu-item>
</wa-menu>
</wa-dropdown>
<wa-tooltip for="more-actions-3">More actions</wa-tooltip>
</div>
</wa-card>
<wa-card>
<div class="wa-flank:end">
<a href="" class="wa-flank wa-link-plain">
<wa-avatar shape="rounded" style="--background-color: var(--wa-color-pink-80); --text-color: var(--wa-color-pink-40)">
<wa-icon slot="icon" name="cake-slice"></wa-icon>
</wa-avatar>
<div class="wa-gap-2xs wa-stack">
<span class="wa-heading-s">Dessert</span>
<span class="wa-caption-m">11 Items</span>
</div>
</a>
<wa-dropdown>
<wa-icon-button id="more-actions-4" slot="trigger" name="ellipsis-vertical" label="More actions"></wa-icon-button>
<wa-menu>
<wa-menu-item>Copy link</wa-menu-item>
<wa-menu-item>Rename</wa-menu-item>
<wa-menu-item>Move to trash</wa-menu-item>
</wa-menu>
</wa-dropdown>
<wa-tooltip for="more-actions-4">More actions</wa-tooltip>
</div>
</wa-card>
</div>
```
## Kanban
```html {.example}
<div>
<h2>Project #487</h2>
<div class="wa-grid wa-gap-2xl">
<div class="wa-stack">
<div class="wa-cluster wa-gap-s"><span>Draft</span> <wa-badge appearance="filled outlined" variant="neutral">1</wa-badge></div>
<wa-card>
<div class="wa-flank:end">
<div class="wa-stack wa-gap-2xs">
<div class="wa-cluster wa-gap-2xs">
<span class="wa-heading-s">Unit Testing</span>
<wa-dropdown>
<wa-icon-button id="task-action-4" slot="trigger" name="ellipsis" label="More actions"></wa-icon-button>
<wa-menu>
<wa-menu-item>Copy link</wa-menu-item>
<wa-menu-item>Rename</wa-menu-item>
<wa-menu-item>Move to trash</wa-menu-item>
</wa-menu>
</wa-dropdown>
<wa-tooltip for="task-action-4">More actions</wa-tooltip>
</div>
<div class="wa-cluster wa-gap-2xs">
<wa-badge appearance="outlined" pill>DevOps</wa-badge> <wa-badge variant="neutral" appearance="outlined" pill>Priority: Low</wa-badge>
</div>
</div>
<wa-avatar image="https://images.unsplash.com/photo-1559188286-a173792c8340?q=80&w=2906&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
label="profile image"></wa-avatar>
</div>
</wa-card>
<wa-button appearance="plain">
<wa-icon name="plus"></wa-icon>
Add Task
</wa-button>
</div>
<div class="wa-stack">
<div class="wa-cluster wa-gap-s"><span>In Progress</span> <wa-badge appearance="filled outlined" variant="neutral">2</wa-badge></div>
<wa-card>
<div class="wa-flank:end">
<div class="wa-stack wa-gap-2xs">
<div class="wa-cluster wa-gap-2xs">
<span class="wa-heading-s">UX Audit</span>
<wa-dropdown>
<wa-icon-button id="task-action-2" slot="trigger" name="ellipsis" label="More actions"></wa-icon-button>
<wa-menu>
<wa-menu-item>Copy link</wa-menu-item>
<wa-menu-item>Rename</wa-menu-item>
<wa-menu-item>Move to trash</wa-menu-item>
</wa-menu>
</wa-dropdown>
<wa-tooltip for="task-action-2">More actions</wa-tooltip>
</div>
<div class="wa-cluster wa-gap-2xs">
<wa-badge appearance="outlined" pill>Design</wa-badge> <wa-badge variant="warning" appearance="outlined" pill>Priority: Medium</wa-badge>
</div>
</div>
<wa-avatar image="https://images.unsplash.com/photo-1613428800237-c86372070fab?q=80&w=3017&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
label="profile image"></wa-avatar>
</div>
</wa-card>
<wa-card>
<div class="wa-flank:end">
<div class="wa-stack wa-gap-2xs">
<div class="wa-cluster wa-gap-2xs">
<span class="wa-heading-s">Visual Testing</span>
<wa-dropdown>
<wa-icon-button id="task-action-3" slot="trigger" name="ellipsis" label="More actions"></wa-icon-button>
<wa-menu>
<wa-menu-item>Copy link</wa-menu-item>
<wa-menu-item>Rename</wa-menu-item>
<wa-menu-item>Move to trash</wa-menu-item>
</wa-menu>
</wa-dropdown>
<wa-tooltip for="task-action-3">More actions</wa-tooltip>
</div>
<div class="wa-cluster wa-gap-2xs">
<wa-badge appearance="outlined" pill>Design</wa-badge> <wa-badge variant="danger" appearance="outlined" pill>Priority: High</wa-badge>
</div>
</div>
<wa-avatar image="https://images.unsplash.com/photo-1614807547811-4174d3582092?q=80&w=2932&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" label="profile image"></wa-avatar>
</div>
</wa-card>
<wa-button appearance="plain">
<wa-icon name="plus"></wa-icon>
Add Task
</wa-button>
</div>
<div class="wa-stack">
<div class="wa-cluster wa-gap-s"><span>Ready for Review</span> <wa-badge appearance="filled outlined" variant="neutral">1</wa-badge></div>
<wa-card>
<div class="wa-flank:end">
<div class="wa-stack wa-gap-2xs">
<div class="wa-cluster wa-gap-2xs">
<span class="wa-heading-s">Deploy Bug Fixes</span>
<wa-dropdown>
<wa-icon-button id="task-action-1" slot="trigger" name="ellipsis" label="More actions"></wa-icon-button>
<wa-menu>
<wa-menu-item>Copy link</wa-menu-item>
<wa-menu-item>Rename</wa-menu-item>
<wa-menu-item>Move to trash</wa-menu-item>
</wa-menu>
</wa-dropdown>
<wa-tooltip for="task-action-1">More actions</wa-tooltip>
</div>
<div class="wa-cluster wa-gap-2xs">
<wa-badge appearance="outlined" pill>Development</wa-badge> <wa-badge variant="warning" appearance="outlined" pill>Priority: Medium</wa-badge>
</div>
</div>
<wa-avatar initials="KK" label="Avatar with initials: KK"></wa-avatar>
</div>
</wa-card>
<wa-button appearance="plain">
<wa-icon name="plus"></wa-icon>
Add Task
</wa-button>
</div>
</div>
</div>
```

View File

@@ -1,9 +0,0 @@
---
title: App
description: Pre-built action panels, data displays, and more ready to drop into your web app.
parent: patterns
layout: overview
override:tags: []
listChildren: true
isPro: false
---

View File

@@ -1,266 +0,0 @@
---
title: Leaderboard
description: 'Engage and motivate users by highlighting top performers, scores, and achievements.'
isPro: true
---
## Simple
```html {.example}
<div class="wa-stack">
<h3>Daily Crossword</h3>
<div class="wa-grid">
<wa-callout variant="warning" appearance="filled">
<wa-icon slot="icon" name="timer"></wa-icon>
<div class="wa-stack wa-gap-xs">
<span class="wa-heading-l">11h 54m 52s</span>
<span class="wa-caption-m">until play ends</span>
</div>
</wa-callout>
<wa-callout variant="neutral" appearance="filled">
<wa-icon slot="icon" name="user-group"></wa-icon>
<div class="wa-stack wa-gap-xs">
<span class="wa-heading-l">304</span>
<span class="wa-caption-m">players on this leaderboard</span>
</div>
</wa-callout>
</div>
<wa-card>
<div class="wa-stack">
<ol class="wa-stack">
<li class="wa-flank">
<div class="wa-cluster">
<wa-icon name="1" fixed-width></wa-icon>
<wa-avatar>
<wa-icon slot="icon" name="hat-wizard"></wa-icon>
</wa-avatar>
</div>
<div class="wa-split wa-gap-xs">
<span>wordwiz</span>
<small class="wa-caption-l">00:01:41</small>
</div>
</li>
<wa-divider></wa-divider>
<li class="wa-flank">
<div class="wa-cluster">
<wa-icon name="2" fixed-width></wa-icon>
<wa-avatar initials="A"></wa-avatar>
</div>
<div class="wa-split wa-gap-xs">
<span>acrossNdown</span>
<small class="wa-caption-l">00:01:58</small>
</div>
</li>
<wa-divider></wa-divider>
<li class="wa-flank">
<div class="wa-cluster">
<wa-icon name="3" fixed-width></wa-icon>
<wa-avatar initials="X"></wa-avatar>
</div>
<div class="wa-split wa-gap-xs">
<span>XwordChamp</span>
<small class="wa-caption-l">00:02:14</small>
</div>
</li>
<wa-divider></wa-divider>
<li class="wa-flank">
<div class="wa-cluster">
<wa-icon name="4" fixed-width></wa-icon>
<wa-avatar>
<wa-icon slot="icon" name="chess-knight"></wa-icon>
</wa-avatar>
</div>
<div class="wa-split wa-gap-xs">
<span>puzzlepoet</span>
<small class="wa-caption-l">00:02:16</small>
</div>
</li>
<wa-divider></wa-divider>
<li class="wa-flank">
<div class="wa-cluster">
<wa-icon name="5" fixed-width></wa-icon>
<wa-avatar initials="R"></wa-avatar>
</div>
<div class="wa-split wa-gap-xs">
<span>RiddleMeThis</span>
<small class="wa-caption-l">00:02:34</small>
</div>
</li>
</ol>
</div>
<div slot="footer">
<a href="" class="wa-cluster wa-gap-xs wa-caption-m">
<span>View all standings</span>
<wa-icon name="arrow-right"></wa-icon>
</a>
</div>
</wa-card>
</div>
```
## Two Column
```html {.example}
<div class="wa-stack">
<h3>Collective Activity for Yesterday</h3>
<div class="wa-grid">
<wa-callout variant="neutral" appearance="filled">
<wa-icon slot="icon" name="book"></wa-icon>
<div class="wa-stack wa-gap-0">
<h4 class="wa-heading-xs">Items Studied</h4>
<div class="wa-heading-2xl">482,813</div>
</div>
</wa-callout>
<wa-callout variant="brand" appearance="filled">
<wa-icon slot="icon" name="diploma"></wa-icon>
<div class="wa-stack wa-gap-0">
<h4 class="wa-heading-xs">Items Mastered</h4>
<div class="wa-heading-2xl">67,106</div>
</div>
</wa-callout>
<wa-callout variant="success" appearance="filled">
<wa-icon slot="icon" name="wand-sparkles"></wa-icon>
<div class="wa-stack wa-gap-0">
<h4 class="wa-heading-xs">Items Created</h4>
<div class="wa-heading-2xl">2,080</div>
</div>
</wa-callout>
</div>
<div class="wa-grid">
<wa-card>
<div slot="header" class="wa-flank wa-gap-xl">
<wa-icon name="graduation-cap" class="wa-heading-xl"></wa-icon>
<span class="wa-gap-2xs wa-stack">
<h4>Study Leaders</h4>
<span class="wa-caption-m">Items mastered in the last 7 days</span>
</span>
</div>
<div class="wa-stack">
<ol class="wa-stack">
<li class="wa-flank">
<div class="wa-cluster">
<wa-icon name="1" fixed-width></wa-icon>
<wa-avatar image="https://images.unsplash.com/photo-1620428268482-cf1851a36764?q=80&w=3418&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" shape="rounded"></wa-avatar>
</div>
<div class="wa-split wa-gap-xs">
<span>mitsuwo</span>
<small class="wa-caption-l">2,753</small>
</div>
</li>
<wa-divider></wa-divider>
<li class="wa-flank">
<div class="wa-cluster">
<wa-icon name="2" fixed-width></wa-icon>
<wa-avatar image="https://images.unsplash.com/photo-1639628735078-ed2f038a193e?q=80&w=3348&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" shape="rounded"></wa-avatar>
</div>
<div class="wa-split wa-gap-xs">
<span>knowledgeninja</span>
<small class="wa-caption-l">2,298</small>
</div>
</li>
<wa-divider></wa-divider>
<li class="wa-flank">
<div class="wa-cluster">
<wa-icon name="3" fixed-width></wa-icon>
<wa-avatar image="https://images.unsplash.com/photo-1638803040283-7a5ffd48dad5?q=80&w=2592&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" shape="rounded"></wa-avatar>
</div>
<div class="wa-split wa-gap-xs">
<span>NxtLvl</span>
<small class="wa-caption-l">2,008</small>
</div>
</li>
<wa-divider></wa-divider>
<li class="wa-flank">
<div class="wa-cluster">
<wa-icon name="4" fixed-width></wa-icon>
<wa-avatar image="https://images.unsplash.com/photo-1630549316063-7ae02749d2cc?q=80&w=3540&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" shape="rounded"></wa-avatar>
</div>
<div class="wa-split wa-gap-xs">
<span>brainiac</span>
<small class="wa-caption-l">1,954</small>
</div>
</li>
<wa-divider></wa-divider>
<li class="wa-flank">
<div class="wa-cluster">
<wa-icon name="5" fixed-width></wa-icon>
<wa-avatar image="https://images.unsplash.com/photo-1582845512747-e42001c95638?q=80&w=3540&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" shape="rounded"></wa-avatar>
</div>
<div class="wa-split wa-gap-xs">
<span>eduexplorer</span>
<small class="wa-caption-l">1,897</small>
</div>
</li>
</ol>
</div>
</wa-card>
<wa-card>
<div slot="header" class="wa-flank wa-gap-xl">
<wa-icon name="hat-wizard" class="wa-heading-xl"></wa-icon>
<span class="wa-gap-2xs wa-stack">
<h4>Creation Leaders</h4>
<span class="wa-caption-m">Items created in the last 7 days</span>
</span>
</div>
<div class="wa-stack">
<ol class="wa-stack">
<li class="wa-flank">
<div class="wa-cluster">
<wa-icon name="1" fixed-width></wa-icon>
<wa-avatar image="https://images.unsplash.com/photo-1630549316063-7ae02749d2cc?q=80&w=3540&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" shape="rounded"></wa-avatar>
</div>
<div class="wa-split wa-gap-xs">
<span>brainiac</span>
<small class="wa-caption-l">134</small>
</div>
</li>
<wa-divider></wa-divider>
<li class="wa-flank">
<div class="wa-cluster">
<wa-icon name="2" fixed-width></wa-icon>
<wa-avatar image="https://images.unsplash.com/photo-1546776230-bb86256870ce?q=80&w=3368&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" shape="rounded"></wa-avatar>
</div>
<div class="wa-split wa-gap-xs">
<span>LessonLegend</span>
<small class="wa-caption-l">115</small>
</div>
</li>
<wa-divider></wa-divider>
<li class="wa-flank">
<div class="wa-cluster">
<wa-icon name="3" fixed-width></wa-icon>
<wa-avatar image="https://images.unsplash.com/photo-1620428268482-cf1851a36764?q=80&w=3418&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" shape="rounded"></wa-avatar>
</div>
<div class="wa-split wa-gap-xs">
<span>mitsuwo</span>
<small class="wa-caption-l">98</small>
</div>
</li>
<wa-divider></wa-divider>
<li class="wa-flank">
<div class="wa-cluster">
<wa-icon name="4" fixed-width></wa-icon>
<wa-avatar image="https://images.unsplash.com/photo-1586374579358-9d19d632b6df?q=80&w=3387&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" shape="rounded"></wa-avatar>
</div>
<div class="wa-split wa-gap-xs">
<span>wiswiz</span>
<small class="wa-caption-l">79</small>
</div>
</li>
<wa-divider></wa-divider>
<li class="wa-flank">
<div class="wa-cluster">
<wa-icon name="5" fixed-width></wa-icon>
<wa-avatar image="https://images.unsplash.com/photo-1639628735078-ed2f038a193e?q=80&w=3348&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" shape="rounded"></wa-avatar>
</div>
<div class="wa-split wa-gap-xs">
<span>knowledgeninja</span>
<small class="wa-caption-l">77</small>
</div>
</li>
</ol>
</div>
</wa-card>
</div>
</div>
```

View File

@@ -1,52 +0,0 @@
---
title: Pagination
description: 'Improve navigation and performance by breaking long lists or content into manageable pages.'
isPro: true
---
## Simple
```html {.example}
<div class="wa-stack">
<div class="wa-placeholder"></div>
<wa-divider></wa-divider>
<div class="wa-split">
<span class="wa-caption-l">Showing 1 to 10 of 50 Results</span>
<div class="wa-cluster wa-gap-xs">
<wa-button appearance="outlined">
<wa-icon slot="prefix" name="chevron-left"></wa-icon>
Prev
</wa-button>
<wa-button appearance="outlined">
<wa-icon slot="suffix" name="chevron-right"></wa-icon>
Next
</wa-button>
</div>
</div>
</div>
```
## With Button Group
```html {.example}
<div class="wa-stack">
<div class="wa-placeholder"></div>
<wa-divider></wa-divider>
<div class="wa-split">
<span class="wa-caption-l">Showing 1 to 10 of 50 Results</span>
<wa-button-group orientation="horizontal">
<wa-button appearance="outlined">
<wa-icon name="chevron-left"></wa-icon>
</wa-button>
<wa-button appearance="accent" variant="brand">1</wa-button>
<wa-button appearance="outlined">2</wa-button>
<wa-button appearance="outlined">3</wa-button>
<wa-button appearance="outlined" disabled>...</wa-button>
<wa-button appearance="outlined">10</wa-button>
<wa-button appearance="outlined">
<wa-icon name="chevron-right"></wa-icon>
</wa-button>
</wa-button-group>
</div>
</div>
```

View File

@@ -1,229 +0,0 @@
---
title: Permissions
description: 'Permission patterns provide or restrict access to users.'
isPro: true
---
## With Form Inputs
```html{.example}
<wa-card with-header style="max-width: 72ch; margin: 0 auto;">
<div slot="header" class="wa-split">
<h2 class="wa-heading-m">Invite Team Members</h2>
<wa-icon name="close"></wa-icon>
</div>
<div class="wa-stack wa-gap-2xl">
<div class="wa-align-items-end wa-flank:end wa-gap-2xs">
<wa-input label="Email" placeholder="contact@example.com"></wa-input>
<wa-button variant="success">Send Invite</wa-button>
</div>
<div class="wa-stack">
<span class="wa-heading-s">Project Members</span>
<div class="wa-stack wa-gap-xl">
<div class="wa-flank">
<wa-avatar label="User avatar" image="https://images.unsplash.com/photo-1580489944761-15a19d654956?q=80&w=1961&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"></wa-avatar>
<div class="wa-split">
<div class="wa-stack wa-gap-0">
<span class="wa-heading-xs">Jessica Jones</span>
<span class="wa-caption-m" style="word-break: break-word">jessica.jones@example.com</span>
</div>
<wa-select value="owner">
<wa-option value="owner">Owner</wa-option>
<wa-option value="admin">Admin</wa-option>
<wa-option value="can-edit">Can Edit</wa-option>
<wa-option value="view-only">View Only</wa-option>
</wa-select>
</div>
</div>
<div class="wa-flank">
<wa-avatar label="User avatar" image="https://images.unsplash.com/photo-1566492031773-4f4e44671857?q=80&w=2487&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"></wa-avatar>
<div class="wa-split">
<div class="wa-stack wa-gap-0">
<span class="wa-heading-xs">Foggy Nelson</span>
<span class="wa-caption-m" style="word-break: break-word">foggy.nelson@example.com</span>
</div>
<wa-select value="admin">
<wa-option value="owner">Owner</wa-option>
<wa-option value="admin">Admin</wa-option>
<wa-option value="can-edit">Can Edit</wa-option>
<wa-option value="view-only">View Only</wa-option>
</wa-select>
</div>
</div>
<div class="wa-flank">
<wa-avatar label="User avatar" image="https://images.unsplash.com/photo-1494790108377-be9c29b29330?q=80&w=2487&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"></wa-avatar>
<div class="wa-split">
<div class="wa-stack wa-gap-0">
<span class="wa-heading-xs">Karen Page</span>
<span class="wa-caption-m" style="word-break: break-word">karen.page@example.com</span>
</div>
<wa-select value="can-edit">
<wa-option value="owner">Owner</wa-option>
<wa-option value="admin">Admin</wa-option>
<wa-option value="can-edit">Can Edit</wa-option>
<wa-option value="view-only">View Only</wa-option>
</wa-select>
</div>
</div>
<div class="wa-flank">
<wa-avatar label="User avatar" image="https://images.unsplash.com/photo-1599566150163-29194dcaad36?q=80&w=2487&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"></wa-avatar>
<div class="wa-split">
<div class="wa-stack wa-gap-0">
<span class="wa-heading-xs">Matt Murdock</span>
<span class="wa-caption-m" style="word-break: break-word">matt.murdock@example.com</span>
</div>
<wa-select value="view-only">
<wa-option value="owner">Owner</wa-option>
<wa-option value="admin">Admin</wa-option>
<wa-option value="can-edit">Can Edit</wa-option>
<wa-option value="view-only">View Only</wa-option>
</wa-select>
</div>
</div>
</div>
</div>
<div class="wa-align-items-end wa-flank:end wa-gap-2xs">
<wa-input label="Share Link" value="https://sharelink3435re.com" disabled></wa-input>
<wa-button variant="brand" appearance="filled outlined">
<wa-icon slot="prefix" name="link" variant="solid"></wa-icon>
Copy Link
</wa-button>
</div>
</div>
</wa-card>
```
## Link Settings
```html {.example}
<wa-card style="max-width: 45ch; margin: 0 auto;">
<div class="wa-stack">
<h2 class="wa-heading-m">Manage Link</h2>
<wa-input label="Expiration Date" type="date"></wa-input>
<wa-radio-group
label="Share Limit"
orientation="horizontal"
name="share-limit"
value="0"
>
<wa-radio-button value="0">None</wa-radio-button>
<wa-radio-button value="5">5</wa-radio-button>
<wa-radio-button value="10">10</wa-radio-button>
<wa-radio-button value="50">50</wa-radio-button>
<wa-radio-button value="100">100</wa-radio-button>
</wa-radio-group>
<wa-divider></wa-divider>
<wa-switch hint="Members are removed after logging out." checked>Temporary Access</wa-switch>
<div class="wa-cluster wa-gap-xs" style="justify-content: flex-end">
<wa-button size="small" appearance="outlined" pill>Cancel</wa-button>
<wa-button size="small" variant="brand" pill>Generate</wa-button>
</div>
</div>
</wa-card>
```
## Role Settings
```html {.example}
<div style="max-width: 78ch; margin: 0 auto;">
<h2>Settings</h2>
<p>Update settings for this server.</p>
<wa-tab-group>
<wa-tab panel="general">
<div class="wa-cluster">
<wa-icon name="user"></wa-icon>
<span>User Permissions</span>
</div>
</wa-tab>
<wa-tab-panel name="general">
<wa-card>
<div class="wa-flank">
<wa-avatar image="https://images.unsplash.com/photo-1741289308283-feba56f857cc?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTl8fGJlYXJkfGVufDB8MnwwfHx8Mg%3D%3D" alt="image avatar"></wa-avatar>
<div class="wa-split">
<div class="wa-stack wa-gap-xs">
<span class="wa-heading-s">Kris Kringle</span>
<span class="wa-caption-m">Admin</span>
</div>
<wa-button appearance="plain">
<wa-icon slot="prefix" name="edit"></wa-icon>
Edit Profile
</wa-button>
</div>
</div>
</wa-card>
<table>
<thead>
<tr>
<th>Name</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="wa-flank">
<wa-avatar image="https://images.unsplash.com/photo-1579824894326-77ec5aaf8703?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NDJ8fHJlaW5kZWVyfGVufDB8MnwwfHx8Mg%3D%3D"></wa-avatar>
<span class="wa-caption-m">Dasher</span>
</div>
</td>
<td>
<wa-select value="moderator">
<wa-option value="moderator">Moderator</wa-option>
<wa-option value="contributor">Contributor</wa-option>
<wa-option value="reader">Reader</wa-option>
</wa-select>
</td>
</tr>
<tr>
<td>
<div class="wa-flank">
<wa-avatar image="https://images.unsplash.com/photo-1691065686144-916ff29d1b4f?q=80&w=2666&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"></wa-avatar>
<span class="wa-caption-m">Dancer</span>
</div>
</td>
<td>
<wa-select value="moderator">
<wa-option value="moderator">Moderator</wa-option>
<wa-option value="contributor">Contributor</wa-option>
<wa-option value="reader">Reader</wa-option>
</wa-select>
</td>
</tr>
<tr>
<td>
<div class="wa-flank">
<wa-avatar image="https://images.unsplash.com/photo-1616742618872-9e8a890d90b2?q=80&w=2712&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"></wa-avatar>
<span class="wa-caption-m">Prancer</span>
</div>
</td>
<td>
<wa-select value="contributor">
<wa-option value="moderator">Moderator</wa-option>
<wa-option value="contributor">Contributor</wa-option>
<wa-option value="reader">Reader</wa-option>
</wa-select>
</td>
</tr>
<tr>
<td>
<div class="wa-flank">
<wa-avatar image="https://images.unsplash.com/photo-1728290403857-1b7909db2baa?q=80&w=2680&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"></wa-avatar>
<span class="wa-caption-m">Vixen</span>
</div>
</td>
<td>
<wa-select value="reader">
<wa-option value="moderator">Moderator</wa-option>
<wa-option value="contributor">Contributor</wa-option>
<wa-option value="reader">Reader</wa-option>
</wa-select>
</td>
</tr>
</tbody>
</table>
</wa-tab-panel>
</wa-tab-group>
</div>
```

View File

@@ -1,168 +0,0 @@
---
title: Pricing
description: 'Help users make informed purchasing decisions with clear, structured pricing.'
isPro: true
---
## Three Tiers
```html{.example}
<div class="wa-grid">
<wa-card>
<div class="wa-stack">
<div class="wa-cluster wa-heading-l">
<wa-icon name="apple-core"></wa-icon>
<h3>Lite</h3>
</div>
<span class="wa-flank wa-align-items-baseline wa-gap-2xs">
<span class="wa-heading-2xl">$60</span>
<span class="wa-caption-l">per year</span>
</span>
<p class="wa-caption-l">An online-only plan for web-based projects.</p>
<wa-button variant="brand" appearance="outlined">Get Lite</wa-button>
</div>
<div slot="footer" class="wa-stack">
<h4 class="wa-heading-s">What You Get</h4>
<div class="wa-stack">
<div class="wa-flank">
<wa-icon name="user" fixed-width></wa-icon>
<span class="wa-caption-m">1 user</span>
</div>
<div class="wa-flank">
<wa-icon name="suitcase" fixed-width></wa-icon>
<span class="wa-caption-m">2 custom kits</span>
</div>
<div class="wa-flank">
<wa-icon name="chart-simple" fixed-width></wa-icon>
<span class="wa-caption-m">Up to 100k pageviews</span>
</div>
</div>
</div>
</wa-card>
<wa-card>
<div class="wa-stack">
<div class="wa-split">
<div class="wa-cluster wa-heading-l">
<wa-icon name="apple-whole"></wa-icon>
<h3>Pro</h3>
</div>
<wa-badge>Most Popular</wa-badge>
</div>
<span class="wa-flank wa-align-items-baseline wa-gap-2xs">
<span class="wa-heading-2xl">$120</span>
<span class="wa-caption-l">per year</span>
</span>
<p class="wa-caption-l">A great all-around plan for online or desktop use.</p>
<wa-button variant="brand">Get Pro</wa-button>
</div>
<div slot="footer" class="wa-stack">
<h4 class="wa-heading-s">What You Get</h4>
<div class="wa-stack">
<div class="wa-flank">
<wa-icon name="user" fixed-width></wa-icon>
<span class="wa-caption-m">5 users</span>
</div>
<div class="wa-flank">
<wa-icon name="suitcase" fixed-width></wa-icon>
<span class="wa-caption-m">20 custom kits</span>
</div>
<div class="wa-flank">
<wa-icon name="chart-simple" fixed-width></wa-icon>
<span class="wa-caption-m">Up to 1M pageviews</span>
</div>
<div class="wa-flank">
<wa-icon name="arrow-down-to-line" fixed-width></wa-icon>
<span class="wa-caption-m">Kit downloads</span>
</div>
<div class="wa-flank">
<wa-icon name="cloud-plus" fixed-width></wa-icon>
<span class="wa-caption-m">Cloud hosting</span>
</div>
</div>
</div>
</wa-card>
<wa-card>
<div class="wa-stack">
<div class="wa-cluster wa-heading-l">
<wa-icon name="crate-apple"></wa-icon>
<h3>Max</h3>
</div>
<span class="wa-flank wa-align-items-baseline wa-gap-2xs">
<span class="wa-heading-2xl">$600</span>
<span class="wa-caption-l">per year</span>
</span>
<p class="wa-caption-l">Our biggest plan with more of everything.</p>
<wa-button variant="brand" appearance="outlined">Get Max</wa-button>
</div>
<div slot="footer" class="wa-stack">
<h4 class="wa-heading-s">What You Get</h4>
<div class="wa-stack">
<div class="wa-flank">
<wa-icon name="user" fixed-width></wa-icon>
<span class="wa-caption-m">50 users</span>
</div>
<div class="wa-flank">
<wa-icon name="suitcase" fixed-width></wa-icon>
<span class="wa-caption-m">Unlimited custom kits</span>
</div>
<div class="wa-flank">
<wa-icon name="chart-simple" fixed-width></wa-icon>
<span class="wa-caption-m">Up to 10M pageviews</span>
</div>
<div class="wa-flank">
<wa-icon name="arrow-down-to-line" fixed-width></wa-icon>
<span class="wa-caption-m">Kit downloads</span>
</div>
<div class="wa-flank">
<wa-icon name="cloud-plus" fixed-width></wa-icon>
<span class="wa-caption-m">Cloud hosting</span>
</div>
</div>
</div>
</wa-card>
</div>
```
## Single Tier
```html {.example}
<wa-card>
<div class="wa-grid">
<div class="wa-stack">
<h3 class="wa-heading-l">Lifetime Membership</h3>
<p>Learn at your own pace with expert-led content, exclusive resources, and a community of like-minded learners.</p>
<wa-divider></wa-divider>
<h4 class="wa-heading-s">Membership Includes:</h4>
<div class="wa-grid">
<div class="wa-flank wa-gap-xs">
<wa-icon name="check"></wa-icon>
<span class="wa-caption-m">Private forum access</span>
</div>
<div class="wa-flank wa-gap-xs">
<wa-icon name="check"></wa-icon>
<span class="wa-caption-m">Priority admission to events</span>
</div>
<div class="wa-flank wa-gap-xs">
<wa-icon name="check"></wa-icon>
<span class="wa-caption-m">Yearly skill assessment</span>
</div>
<div class="wa-flank wa-gap-xs">
<wa-icon name="check"></wa-icon>
<span class="wa-caption-m">Members-only swag</span>
</div>
</div>
</div>
<wa-callout variant="neutral" appearance="filled">
<div class="wa-stack wa-align-items-center" style="justify-content: center">
<h4 class="wa-heading-s">Pay Once, Own it Forever</h4>
<div class="wa-cluster wa-align-items-baseline wa-gap-2xs">
<span class="wa-heading-3xl">$459</span>
<span>USD</span>
</div>
<wa-button variant="brand" style="width: 100%">Get Access</wa-button>
<small class="wa-caption-m">30-day money back guarantee</small>
</div>
</wa-callout>
</div>
</wa-card>
```

View File

@@ -1,8 +1,6 @@
---
title: Blog
description: TODO
unlisted: true
isPro: true
---
TODO Page Description
@@ -368,4 +366,4 @@ TODO Page Description
}
</style>
```
```

View File

@@ -1,10 +1,8 @@
---
title: Business
description: TODO
unlisted: true
isPro: true
---
TODO Page Description
## Examples
## Examples

View File

@@ -0,0 +1,13 @@
---
title: Category Filter
description: TODO
parent: ecommerce
tags: e-commerce
---
TODO Page Description
## With inline actions and expandable sidebar filters
```html{.example}
```

View File

@@ -0,0 +1,12 @@
---
title: Category Preview
description: TODO
parent: ecommerce
tags: e-commerce
---
TODO Page Description
## Three Column (WIP)
```

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