Compare commits

..

154 Commits

Author SHA1 Message Date
Lea Verou
5a0b2da5a1 Merge branch 'next' into custom-palettes 2025-03-21 19:02:11 -04:00
Lea Verou
59dcaaff83 Content hierarchy bugfixes & improvements (#821)
- Sidebar, overview listings, breadcrumbs now based on actual parent-child relationships, rather than increasingly outdated heuristics
- parent properties are now generated automatically from the URL structure, and need only be specified to override that default
- Ability to group by page hierarchy in overview pages, where pages that have >= 2 children become categories

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

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

* add a guard for non-server builds

* add a guard for non-server builds

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

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

* Adjust `orange` in Anodized palette

* Adjust `orange` in Bright palette

* Adjust `orange` in Mild palette

* Adjust `orange` in Natural palette

* Adjust `orange` in Vogue palette

* Adjust `orange` in Rudimentary palette

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

* 11ty for webawesome + app

* add flashes

* additional changes

* prettier

* add note about nunjucks

* prettier
2025-03-18 13:04:24 -04:00
Lea Verou
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
125 changed files with 7762 additions and 5463 deletions

View File

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

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

@@ -50,6 +50,9 @@
Search
<kbd slot="suffix" class="only-desktop">/</kbd>
</wa-button>
{# Login #}
{% server "loginOrAvatar" %}
</div>
</header>
@@ -76,14 +79,19 @@
</aside>
{% endif %}
{# Main #}
<main id="content">
{# Expandable outline #}
{% if hasOutline %}
<nav id="outline-expandable">
<details class="outline-links">
<summary>On this page</summary>
</details>
</nav>
{% endif %}
<div id="flashes">{% server "flashes" %}</div>
{% block header %}
{% include 'breadcrumbs.njk' %}

View File

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

View File

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

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

View File

@@ -47,3 +47,7 @@
<link rel="stylesheet" href="/dist/styles/webawesome.css" />
<link id="color-stylesheet" rel="stylesheet" href="/dist/styles/utilities.css" />
<link rel="stylesheet" href="/dist/styles/forms.css" />
{# Used by Web Awesome App to inject other assets into the head. #}
{% server "head" %}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,47 +1,57 @@
{% 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 v-if="saved" class="title">
{% raw %}{{ saved.title }}{% endraw %}
<wa-icon-button name="pencil" label="Rename palette" @click="rename"></wa-icon-button>
<wa-icon-button class="delete" name="trash" label="Delete palette" @click="deleteSaved"></wa-icon-button>
<h1 class="title">
<span v-content="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"
:variant="unsavedChanges ? 'success' : 'neutral'" size="small" :appearance="unsavedChanges ? 'accent' : 'outlined'">
<span slot="prefix" class="icon-modifier">
<wa-icon name="sidebar" variant="regular"></wa-icon>
<wa-icon name="circle-plus" class="modifier" style="color: light-dark(var(--wa-color-green-70), var(--wa-color-green-60));"></wa-icon>
</span>
<span v-content="unsavedChanges ? 'Save' : 'Saved'">Save</span>
</wa-button>
</template>
</h1>
<h1 v-if="!saved" class="title">{{ title }}</h1>
<div class="block-info">
<code class="class">.wa-palette-{{ paletteId }}</code>
<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 %}
@@ -49,13 +59,28 @@
{{ 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">
@@ -68,15 +93,12 @@
</span>
Reset
</wa-button>
<wa-button v-if="!saved" @click="save" variant="success">
<span slot="prefix" class="icon-modifier">
<wa-icon name="sidebar" variant="regular"></wa-icon>
<wa-icon name="circle-plus" class="modifier" style="color: light-dark(var(--wa-color-green-70), var(--wa-color-green-60));"></wa-icon>
</span>
Save
</wa-button>
</wa-callout>
<h2>Scales</h2>
{% include "palette.njk" %}
<table class="colors main wa-palette-{{ paletteId }}">
<thead>
<tr>
@@ -87,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 }}" hoist>
{{ h | capitalize }}
</wa-tooltip>
{%- endif -%}
{%- endfor -%}
<div slot="label">
Gray undertone
</div>
</wa-radio-group>
<div class="decorated-slider gray-chroma-slider" :style="{'--max': maxGrayChroma}">
<wa-slider name="gray-chroma" v-model="grayChroma" ref="grayChromaSlider"
value="0" min="0" :max="maxGrayChroma" step="0.01"
@input="tweaking.grayChroma = true" @change="tweaking.grayChroma = false">
<div slot="label">
Gray colorfulness
<wa-icon-button @click="grayChroma = originalGrayChroma" class="clear-button" name="circle-xmark" library="system" variant="regular" label="Reset"></wa-icon-button>
</div>
</wa-slider>
<div class="label-min">Neutral</div>
<div class="label-max" v-content="moreHue[grayColor]">Warmer/Cooler</div>
</div>
{% else %}
{%- set hueAfter = hues[loop.index0 + 1] -%}
{%- set hueAfter = hues[0] if hueAfter == 'gray' else hueAfter -%}
{%- set minShift = hueRanges[hue].min - coreColor.h | round -%}
{%- set maxShift = hueRanges[hue].max - coreColor.h | round -%}
<div class="decorated-slider hue-shift-slider" style="--min: {{ minShift }}; --max: {{ maxShift }};">
<wa-slider name="{{ hue }}-shift" v-model="hueShifts.{{ hue }}" value="0"
min="{{ minShift }}" max="{{ maxShift }}" step="1"
@input="tweaking.hue = tweaking.{{hue}} = true"
@change="tweaking.hue = tweaking.{{ hue }} = false">
<div slot="label">
Tweak {{ hue }} hue
<wa-icon-button @click="hueShifts.{{ hue }} = 0" class="clear-button" name="circle-xmark" library="system" variant="regular" label="Reset"></wa-icon-button>
</div>
</wa-slider>
<div class="label-min">More {{hueBefore}}</div>
<div class="label-max">More {{hueAfter}}</div>
</div>
{%- set hueBefore = hue -%}
{% endif %}
<div class="wa-gap-s">
<code>--wa-color-{{ hue }}</code>
<wa-copy-button value="--wa-color-{{ hue }}" copy-label="--wa-color-{{ hue }}"></wa-copy-button>
</div>
</div>`
</wa-dropdown>
</td>
{% for tint in tints -%}
{%- 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">
@@ -218,6 +238,7 @@ style="--min: {{ chromaScaleBounds[0] }}; --max: {{ chromaScaleBounds[1] }};">
{%- endif -%}
{% endfor %}
</section>
{% endif %}
{% markdown %}
## Color Contrast
@@ -310,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="{{ page.url }}../patterns.css" rel="stylesheet">
{% endblock %}

View File

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

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

View File

@@ -13,23 +13,33 @@ sidebar.palettes = {
sidebar.updateCurrent();
},
updateSaved() {
this.saved = localStorage.savedPalettes ? JSON.parse(localStorage.savedPalettes) : [];
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);
},
save(saved = this.saved) {
this.saved = saved ?? [];
if (saved.length > 0) {
localStorage.savedPalettes = JSON.stringify(saved);
/**
* Write palettes to local storage
*/
toLocalStorage() {
if (this.saved.length > 0) {
localStorage.savedPalettes = JSON.stringify(this.saved);
} else {
delete localStorage.savedPalettes;
}
},
};
sidebar.palettes.updateSaved();
addEventListener('storage', event => sidebar.palettes.updateSaved());
sidebar.palettes.fromLocalStorage();
// Palettes were updated in another tab
addEventListener('storage', () => sidebar.palettes.fromLocalStorage());
sidebar.palette = {
getUid() {
@@ -59,7 +69,9 @@ sidebar.palette = {
delete(palette) {
let savedPalettes = sidebar.palettes.saved;
let count = savedPalettes.length;
if (count === 0) {
if (count === 0 || !palette.uid) {
// No stored palettes or this palette has not been saved
return;
}
@@ -68,7 +80,9 @@ sidebar.palette = {
return;
}
savedPalettes = savedPalettes.filter(p => !sidebar.palette.equals(palette, p));
for (let index; index > -1; index = savedPalettes.findIndex(p => p.uid === palette.uid)) {
savedPalettes.splice(index, 1);
}
if (savedPalettes.length === count) {
// Nothing was removed
@@ -96,17 +110,14 @@ sidebar.palette = {
sidebar.updateCurrent();
sidebar.palettes.save(savedPalettes);
sidebar.palettes.toLocalStorage();
if (sidebar.palette.equals(globalThis.paletteApp?.saved, palette)) {
if (globalThis.paletteApp?.saved?.uid === palette.uid) {
// We deleted the currently active palette
paletteApp.postDelete();
}
},
getSaved(palette, savedPalettes = sidebar.palettes.saved) {
return savedPalettes.find(p => sidebar.palette.equals(p, palette));
},
render(palette) {
// Find existing <a>
let { title, id, search, uid } = palette;
@@ -146,23 +157,27 @@ sidebar.palette = {
}
},
save(palette, saved) {
let savedPalettes = sidebar.palettes.saved;
let existing = this.getSaved(saved ?? palette, savedPalettes);
let oldValues;
if (existing) {
// Rename
oldValues = { ...existing };
Object.assign(existing, palette);
} else {
savedPalettes.push(palette);
/**
* 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();
sidebar.palettes.save(savedPalettes);
return palette;
},
};

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

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

View File

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

@@ -440,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);
@@ -641,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

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

View File

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

View File

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

View File

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

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

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,580 +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';
await Promise.all(['wa-slider'].map(tag => customElements.whenDefined(tag)));
// // Detect https://bugs.webkit.org/show_bug.cgi?id=287637
// const SAFARI_OKLCH_BUG = (() => {
// let dummy = document.createElement('div');
// document.body.appendChild(dummy);
// dummy.style.color = 'oklch(from #d5e0e6 l c h)';
// let computedColor = getComputedStyle(dummy).color;
// dummy.remove();
// return computedColor.endsWith(' 0)');
// })();
let allPalettes = await fetch('/docs/palettes/data.json').then(r => r.json());
globalThis.allPalettes = allPalettes;
for (let palette in allPalettes) {
for (let hue in allPalettes[palette].colors) {
let scale = allPalettes[palette].colors[hue];
for (let tint of tints) {
let color = scale[tint];
if (Array.isArray(color)) {
scale[tint] = new Color('oklch', color);
}
}
}
}
const percentFormatter = value => value.toLocaleString(undefined, { style: 'percent' });
let paletteAppSpec = {
data() {
let appRoot = document.querySelector('#palette-app');
let paletteId = appRoot.dataset.paletteId;
let palette = allPalettes[paletteId];
return {
uid: undefined,
paletteId,
paletteTitle: palette.title,
originalColors: palette.colors,
permalink: new Permalink(),
hueRanges,
hueShifts: Object.fromEntries(hues.map(hue => [hue, 0])),
chromaScale: 1,
grayChroma: undefined,
grayColor: undefined,
tweaking: {},
saved: null,
};
},
created() {
// Non-reactive variables to expose
Object.assign(this, { moreHue });
// Read URL params and apply them. This facilitates permalinks.
this.permalink.mapObject(this.hueShifts, {
keyTo: key => key.replace(/-shift$/, ''),
keyFrom: key => key + '-shift',
valueFrom: value => (!value ? '' : Number(value)),
valueTo: value => (!value ? 0 : Number(value)),
});
this.grayChroma = this.originalGrayChroma;
this.grayColor = this.originalGrayColor;
if (location.search) {
// Update from URL
this.permalink.writeTo(this.hueShifts);
for (let param of ['chroma-scale', 'gray-color', 'gray-chroma']) {
if (this.permalink.has(param)) {
let value = this.permalink.get(param);
if (!isNaN(value)) {
// Convert numeric values to numbers
value = Number(value);
}
let prop = camelCase(param);
this[prop] = value;
}
}
if (this.permalink.has('uid')) {
this.uid = Number(this.permalink.get('uid'));
}
this.saved = sidebar.palette.getSaved(this.getPalette());
}
},
mounted() {
for (let ref in this.$refs) {
this.$refs[ref].tooltipFormatter = percentFormatter;
}
},
computed: {
tweaks() {
return {
hueShifts: this.hueShifts,
chromaScale: this.chromaScale,
grayColor: this.grayColor,
grayChroma: this.grayChroma,
};
},
isTweaked() {
return Object.values(this.hueShifts).some(Boolean);
},
code() {
let ret = {};
for (let language of ['html', 'css']) {
let code = getPaletteCode(this.paletteId, this.colors, this.tweaked, { language, cdnUrl });
ret[language] = {
raw: code,
highlighted: Prism.highlight(code, Prism.languages[language], language),
};
}
return ret;
},
colors() {
return applyTweaks.call(this, this.originalColors, this.tweaks, this.tweaked);
},
colorsMinusChromaScale() {
let tweaked = { ...this.tweaked, chromaScale: false };
return applyTweaks.call(this, this.originalColors, this.tweaks, tweaked);
},
colorsMinusHueShifts() {
let tweaked = { ...this.tweaked, hue: false };
return applyTweaks.call(this, this.originalColors, this.tweaks, tweaked);
},
colorsMinusGrayChroma() {
let tweaked = { ...this.tweaked, grayChroma: false };
return applyTweaks.call(this, this.originalColors, this.tweaks, tweaked);
},
tweaked() {
let anyHueTweaked = Object.values(this.hueShifts).some(Boolean);
let hue = anyHueTweaked
? Object.fromEntries(Object.entries(this.hueShifts).map(([hue, shift]) => [hue, shift !== 0]))
: false;
let ret = {
chromaScale: this.chromaScale !== 1,
hue,
grayChroma: this.grayChroma !== this.originalGrayChroma,
grayColor: this.grayColor !== this.originalGrayColor,
};
let anyTweaked = Object.values(ret).some(Boolean);
return anyTweaked ? ret : false;
},
tweaksHumanReadable() {
let ret = {};
if (this.chromaScale !== 1) {
ret.chromaScale = 'More ' + (this.chromaScale > 1 ? 'vibrant' : 'muted');
}
for (let hue in this.hueShifts) {
let shift = this.hueShifts[hue];
if (!shift) {
continue;
}
let relHue = shift < 0 ? arrayPrevious(hues, hue) : arrayNext(hues, hue);
let hueTweak = moreHue[relHue] ?? relHue + 'er';
ret[hue] = capitalize(hueTweak + ' ' + hue + 's');
}
if (this.tweaked.grayChroma || this.tweaked.grayColor) {
if (this.tweaked.grayChroma === 0) {
ret.grayChroma = 'Achromatic grays';
} else {
if (this.tweaked.grayColor) {
ret.grayColor = capitalize(this.grayColor) + ' gray undertone';
}
if (this.tweaked.grayChroma) {
let more = this.tweaked.grayChroma > this.originalGrayChroma;
ret.grayChroma = `More ${more ? 'colorful' : 'neutral'} grays`;
}
}
}
return ret;
},
originalContrasts() {
return getContrasts(this.originalColors);
},
contrasts() {
return getContrasts(this.colors, this.originalContrasts);
},
originalCoreColors() {
let ret = {};
for (let hue in this.originalColors) {
let maxChromaTintRaw = this.originalColors[hue].maxChromaTintRaw;
ret[hue] = this.originalColors[hue][maxChromaTintRaw];
}
return ret;
},
coreColors() {
let ret = {};
for (let hue in this.colors) {
let maxChromaTintRaw = this.colors[hue].maxChromaTintRaw;
ret[hue] = this.colors[hue][maxChromaTintRaw];
}
return ret;
},
originalGrayColor() {
let grayHue = this.originalCoreColors.gray.get('h');
let minDistance = Infinity;
let closestHue = null;
for (let name in this.originalCoreColors) {
if (name === 'gray') {
continue;
}
let hue = this.originalCoreColors[name].get('h');
let distance = Math.abs(subtractAngles(hue, grayHue));
if (distance < minDistance) {
minDistance = distance;
closestHue = name;
}
}
return closestHue ?? 'indigo';
},
originalGrayChroma() {
let coreTint = this.originalColors.gray.maxChromaTint;
let grayChroma = this.originalColors.gray[coreTint].get('c');
if (grayChroma === 0 || grayChroma === null) {
return 0;
}
let grayColorChroma = this.originalColors[this.originalGrayColor][coreTint].get('c');
return grayChroma / grayColorChroma;
},
/**
* We want to preserve the original grayChroma selection so that when the user switches to another undertone
* that supports higher chromas, their selection will be there.
* This property is the gray chroma % that is actually applied.
*/
computedGrayChroma() {
return Math.min(this.grayChroma, this.maxGrayChroma);
},
maxGrayChroma() {
return maxGrayChroma[this.grayColor] ?? 0.3;
},
},
watch: {
hueShifts: {
deep: true,
handler() {
this.permalink.readFrom(this.hueShifts);
},
},
chromaScale() {
this.permalink.set('chroma-scale', this.chromaScale, 1);
},
grayColor() {
this.permalink.set('gray-color', this.grayColor, this.originalGrayColor);
},
grayChroma() {
this.permalink.set('gray-chroma', this.grayChroma, this.originalGrayChroma);
},
tweaks: {
deep: true,
async handler(value, oldValue) {
await nextTick(); // must run after individual watchers
// Update page URL
this.permalink.updateLocation();
if (this.saved) {
this.save({ silent: true });
}
},
},
},
methods: {
getPalette() {
return { id: this.paletteId, uid: this.uid, search: location.search };
},
save({ silent } = {}) {
let title = silent
? (this.saved?.title ?? this.paletteTitle)
: prompt('Palette title:', `${this.paletteTitle} (tweaked)`);
if (!title) {
return;
}
let uid = this.uid;
if (!uid) {
// First time saving
this.uid = uid = sidebar.palette.getUid();
this.permalink.set('uid', uid);
this.permalink.updateLocation();
}
let palette = { ...this.getPalette(), uid, title };
sidebar.palette.save(palette, this.saved);
this.saved = palette;
},
rename() {
if (!this.saved) {
return;
}
let newTitle = prompt('New title:', this.saved.title);
if (!newTitle) {
return;
}
this.saved.title = newTitle;
sidebar.palette.save(this.saved);
},
deleteSaved() {
sidebar.palette.delete(this.saved);
},
postDelete() {
this.saved = null;
this.permalink.delete('uid');
this.uid = undefined;
this.permalink.updateLocation();
},
/**
* Remove a specific tweak or all tweaks
* @param {string} [param] - The tweak to remove. If not provided, all tweaks are removed.
*/
reset(param) {
if (!param || param === 'chromaScale') {
this.chromaScale = 1;
}
if (param in this.hueShifts) {
this.hueShifts[param] = 0;
} else if (!param) {
for (let hue in this.hueShifts) {
this.hueShifts[hue] = 0;
}
}
if (!param || param === 'grayColor') {
this.grayColor = this.originalGrayColor;
}
if (!param || param === 'grayChroma') {
this.grayChroma = this.originalGrayChroma;
}
},
},
directives: {
// Like v-text, but doesn't complain if the element has content,
// making it possible to use in a PE fashion, with the contents being the fallback
content(el, { value, arg }) {
if (!el.dataset.fallback) {
// Store the original content as a fallback the first time
el.dataset.fallback = el.textContent;
}
if (value === '') {
value = el.dataset.fallback;
} else {
if (arg === 'number') {
value = Number(value).toLocaleString(undefined, { maximumSignificantDigits: 2 });
}
}
if (arg === 'html') {
el.innerHTML = value;
} else {
el.textContent = value;
}
},
},
compilerOptions: {
isCustomElement: tag => tag.startsWith('wa-'),
},
};
function init() {
let paletteAppContainer = document.querySelector('#palette-app');
globalThis.paletteApp?.unmount?.();
if (!paletteAppContainer) {
return;
}
globalThis.paletteApp = createApp(paletteAppSpec).mount(paletteAppContainer);
}
init();
addEventListener('turbo:render', init);
export function getPaletteCode(paletteId, colors, tweaked, options) {
let imports = [];
if (paletteId) {
imports.push(urls.palette(paletteId));
}
let css = '';
let declarations = [];
if (tweaked) {
for (let hue in colors) {
if (hue === 'orange') {
continue;
} else if (hue === 'gray') {
if (!tweaked.grayChroma && !tweaked.grayColor) {
continue;
}
} else if (!tweaked.chromaScale && !tweaked.hue?.[hue]) {
continue;
}
for (let tint of tints) {
let color = colors[hue][tint];
let stringified = color.toString({ format: color.inGamut('srgb') ? 'hex' : undefined });
declarations.push(`--wa-color-${hue}-${tint}: ${stringified};`);
}
declarations.push('');
}
if (declarations.length > 0) {
css += cssRule(selectors.palette(paletteId), declarations);
}
}
let ret = imports.map(url => cssImport(url, options)).join('\n');
if (css) {
ret += `\n\n${cssLiteral(css, options)}`;
}
return ret;
}
function arrayNext(array, element) {
let index = array.indexOf(element);
return array[(index + 1) % array.length];
}
function arrayPrevious(array, element) {
let index = array.indexOf(element);
return array[(index - 1 + array.length) % array.length];
}
function applyTweaks(originalColors, tweaks, tweaked) {
let ret = {};
let { hueShifts, chromaScale = 1, grayColor, grayChroma } = tweaks;
if (!tweaked) {
return originalColors;
}
if (tweaked.grayChroma) {
grayChroma = this.computedGrayChroma;
}
for (let hue in originalColors) {
let originalScale = originalColors[hue];
let scale = (ret[hue] = {});
let descriptors = Object.getOwnPropertyDescriptors(originalScale);
Object.defineProperties(scale, {
maxChromaTint: { ...descriptors.maxChromaTint, enumerable: false },
maxChromaTintRaw: { ...descriptors.maxChromaTintRaw, enumerable: false },
});
for (let tint of tints) {
let color = originalScale[tint].clone();
if (tweaked.hue && hueShifts[hue]) {
color.set({ h: h => h + hueShifts[hue] });
}
if (tweaked.chromaScale && chromaScale !== 1) {
color.set({ c: c => c * chromaScale });
}
if (hue === 'gray' && (tweaked.grayChroma || tweaked.grayColor)) {
let colorUndertone = originalColors[grayColor][tint].clone();
color = colorUndertone.set({ c: c => c * grayChroma });
}
scale[tint] = color;
}
}
return ret;
}
function camelCase(str) {
return (str + '').replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
}
function capitalize(str) {
return str[0].toUpperCase() + str.slice(1);
}
function getContrasts(colors, originalContrasts) {
let ret = {};
for (let hue in colors) {
ret[hue] = {};
for (let tintBg of tints) {
ret[hue][tintBg] = {};
let bgColor = colors[hue][tintBg];
if (!bgColor || !bgColor.contrast) {
continue;
}
for (let tintFg of tints) {
let fgColor = colors[hue][tintFg];
let value = bgColor.contrast(fgColor, 'WCAG21');
if (originalContrasts) {
let original = originalContrasts[hue][tintBg][tintFg];
ret[hue][tintBg][tintFg] = { value, original, bgColor, fgColor };
} else {
ret[hue][tintBg][tintFg] = value;
}
}
}
}
return ret;
}

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,43 +0,0 @@
---
title: Action Panel
description: 'These patterns help add user actions to dashboards'
parent: app
tags: app
---
## Examples
### Simple
```html {.example}
<wa-card style="max-width: 480px; margin: 0 auto;">
<div class="wa-align-items-start wa-stack wa-gap-xs">
<h3 class="wa-heading-m">New Dashboard</h3>
<p>Arrange your data into a single view.</p>
<wa-button variant="brand">Build Dashboard</wa-button>
</div>
</wa-card>
```
### with right flank
```html {.example}
<wa-card style="max-width: 960px; margin: 0 auto;">
<div class="wa-flank:end">
<div class="wa-stack wa-gap-xs">
<h3 class="wa-heading-m">Query with the SQL Runner</h3>
<p>Access your database to run ad-hoc queries.</p>
</div>
<wa-button appearance="outlined">New Query</wa-button>
</div>
</wa-card>
```
### with switch
```html {.example}
<wa-card style="max-width: 960px; margin: 0 auto;">
<div class="wa-flank:end">
<div class="wa-stack wa-gap-xs">
<h3 class="wa-heading-m">Auto-renew</h3>
<p>We'll send you a reminder 30 days before we draft your account.</p>
</div>
<wa-switch size="large"></wa-switch>
</div>
</wa-card>
```

View File

@@ -1,75 +0,0 @@
---
title: Comments
description: 'For feedback forms and message boxes'
parent: app
tags: app
---
## Examples
### In card with footer
```html{.example}
<form class="comment-box" style="max-width: 960px; margin: 0 auto;">
<wa-card with-footer>
<wa-textarea resize="horizontal"></wa-textarea>
<div slot="footer" class="wa-cluster" style="justify-content: flex-end;">
<wa-button appearance="outlined">
<wa-icon slot="prefix" name="paperclip" variant="solid"></wa-icon>
Attach a file
</wa-button>
<wa-button variant="success">Comment</wa-button>
</div>
</wa-card>
</form>
```
### with avatar and icon buttons
```html{.example}
<div class="wa-callout wa-neutral wa-outlined" style="max-width: 960px; margin: 0 auto;">
<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..." size="medium"></wa-textarea>
<div class="wa-split">
<div>
<wa-icon-button name="link" variant="solid" label="Bold"></wa-icon-button>
<wa-icon-button name="face-smile" variant="solid" label="Italic"></wa-icon-button>
</div>
</wa-button><wa-button>Comment</wa-button>
</div>
</div>
</div>
</div>
```
### With multiple actions
```html{.example}
<wa-card with-header with-footer style="max-width: 640px; margin: 0 auto;">
<div slot="header">
<h2 class="wa-heading-s">I watched...</h2>
</div>
<div class="wa-stack">
<div class="wa-flank">
<div>
<img src="https://a.ltrbxd.com/resized/film-poster/1/0/2/5/3/3/1/1025331-heretic-2024-0-1000-0-1500-crop.jpg?v=c79c5c8121" width="40"/>
</div>
<span class="wa-heading-l">Heretic</span>
</div>
<wa-divider></wa-divider>
<div class="wa-split">
<span class="wa-heading-s">Date</span><span class="wa-caption-m">Thursday, March 13, 2025</span>
</div>
<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..."></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>
```

View File

@@ -1,161 +0,0 @@
---
title: Data Display
description: TODO
parent: app
tags: app
---
## Examples
### With icon
```html{.example}
<div class="wa-grid" style="max-width: 960px; margin: 0 auto">
<wa-card>
<div class="wa-flank">
<wa-avatar shape="square" label="Square avatar" class="wa-callout wa-neutral">
<wa-icon slot="icon" name="user-plus" variant="solid"></wa-icon>
</wa-avatar>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-xs">Total Subscribers</span>
<div class="wa-cluster">
<span class="wa-heading-l">71,897</span>
<wa-tag size="small" variant="success" appearance="filled outlined" pill><wa-icon fixed-width name="arrow-up"></wa-icon> 122</wa-tag>
</div>
</div>
</div>
</wa-card>
<wa-card>
<div class="wa-flank">
<wa-avatar shape="square" label="Square avatar" class="wa-callout wa-neutral">
<wa-icon slot="icon" name="user-plus" variant="solid"></wa-icon>
</wa-avatar>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-xs">Total Subscribers</span>
<div class="wa-cluster">
<span class="wa-heading-l">71,897</span>
<wa-tag size="small" variant="success" appearance="filled outlined" pill><wa-icon fixed-width name="arrow-up"></wa-icon> 122</wa-tag>
</div>
</div>
</div>
</wa-card>
<wa-card>
<div class="wa-flank">
<wa-avatar shape="square" label="Square avatar" class="wa-callout wa-neutral">
<wa-icon slot="icon" name="user-plus" variant="solid"></wa-icon>
</wa-avatar>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-xs">Total Subscribers</span>
<div class="wa-cluster">
<span class="wa-heading-l">71,897</span>
<wa-tag size="small" variant="success" appearance="filled outlined" pill><wa-icon fixed-width name="arrow-up"></wa-icon> 122</wa-tag>
</div>
</div>
</div>
</wa-card>
</div>
```
### Multi column
```html{.example}
<div style="max-width: 480px; margin: 0 auto">
<wa-card>
<div>
<div class="wa-flank">
<wa-icon family="brands" name="youtube"></wa-icon>
<div class="wa-align-items-center wa-cluster" style="justify-content: space-between;">
<span>YouTube Premium</span>
<span>5 minutes ago</span>
<wa-tag variant="danger" appearance="outlined filled" pill>-$5.00</wa-tag>
</div>
</div>
<wa-divider></wa-divider>
<div class="wa-flank">
<wa-icon family="brands" name="youtube"></wa-icon>
<div class="wa-align-items-center wa-cluster" style="justify-content: space-between;">
<span>YouTube Premium</span>
<span>5 minutes ago</span>
<wa-tag variant="danger" appearance="outlined filled" pill>-$5.00</wa-tag>
</div>
</div>
<wa-divider></wa-divider>
<div class="wa-flank">
<wa-icon family="brands" name="youtube"></wa-icon>
<div class="wa-align-items-center wa-cluster" style="justify-content: space-between;">
<span>YouTube Premium</span>
<span>5 minutes ago</span>
<wa-tag variant="danger" appearance="outlined filled" pill>-$5.00</wa-tag>
</div>
</div>
<wa-divider></wa-divider>
<div class="wa-flank">
<wa-icon family="brands" name="youtube"></wa-icon>
<div class="wa-align-items-center wa-cluster" style="justify-content: space-between;">
<span>YouTube Premium</span>
<span>5 minutes ago</span>
<wa-tag variant="danger" appearance="outlined filled" pill>-$5.00</wa-tag>
</div>
</div>
<wa-divider></wa-divider>
<div class="wa-flank">
<wa-icon family="brands" name="youtube"></wa-icon>
<div class="wa-align-items-center wa-cluster" style="justify-content: space-between;">
<span>YouTube Premium</span>
<span>5 minutes ago</span>
<wa-tag variant="danger" appearance="outlined filled" pill>-$5.00</wa-tag>
</div>
</div>
<wa-divider></wa-divider>
</div>
</wa-card>
</div>
```
### Card with condensed information
```html{.example}
<wa-card style="max-width: 480px; margin: 0 auto;">
<div class="wa-stack">
<section class="wa-split">
<a href="#" class="wa-cluster wa-gap-xs wa-align-items-center">
<span class="wa-caption-m">query</span>
<span class="wa-heading-m">getUser</span>
<wa-icon fixed-width name="arrow-right"></wa-icon>
</a>
<wa-icon-button fixed-width name="ellipsis" label="actions"></wa-icon-button>
</section>
<section class="wa-cluster">
<span class="wa-caption-l">7.15M request • 9% • 734msP95</span>
</section>
<section class="wa-split">
<div class="wa-stack wa-gap-xs">
<span class="wa-caption-l">Cache Hit Rate</span>
<span class="wa-heading-2xl">12.3%</span>
<wa-badge appearance="filled outlined" variant="danger"><wa-icon name="arrow-down"></wa-icon> down from 19.6%</wa-badge>
</div>
<div class="wa-stack wa-gap-xs">
<span class="wa-caption-l">Max CHR</span>
<span class="wa-heading-2xl">72.6%</span>
<wa-badge appearance="filled outlined" variant="success"><wa-icon name="arrow-up"></wa-icon> CHR Impact +5.4%</wa-badge>
</div>
</section>
<wa-divider></wa-divider>
<section class="wa-stack">
<span class="wa-heading-m">90.5 GB (69.8%)</span>
<div class="wa-split">
<span>Cacheable Bandwidth</span>
<span class="wa-cluster wa-gap-2xs">
<wa-icon fixed-width name="dollar-sign"></wa-icon>
<span>$9.50</span>
<wa-icon fixed-width name="circle-question"></wa-icon>
</span>
</div>
<div class="wa-stack">
<wa-progress-bar value="9.8" label="Upload progress"></wa-progress-bar>
<span class="wa-caption-m">Cached 12.8GB (9.8%)</span>
<span class="wa-caption-m">Non-Cacheable 26.3GB (91.2)</span>
<span class="wa-heading-s">Total 129.6GB</span>
</div>
</section>
</div>
</wa-card>
```

View File

@@ -1,244 +0,0 @@
---
title: Description List
description: 'Shows the user information with labels and values in an easy to read format.'
parent: app
tags: app
---
## Examples
### Simple
```html{.example}
<div style="max-width: 960px; margin: 0 auto">
<h3 class="wa-heading-m">Applicant Info</h3>
<p class="wa-caption-m">Personal details.</p>
<wa-divider></wa-divider>
<dl class="wa-stack wa-gap-2xl">
<div class="wa-align-items-start wa-flank" style="--flank-size: 15%;">
<dt class="wa-heading-xs">Full name</dt>
<dd class="wa-caption-m">Bucky Barnes</dd>
</div>
<div class="wa-align-items-start wa-flank" style="--flank-size: 15%;">
<dt class="wa-heading-xs">Application for</dt>
<dd class="wa-caption-m">Machine Learning Engineer</dd>
</div>
<div class="wa-align-items-start wa-flank" style="--flank-size: 15%;">
<dt class="wa-heading-xs">Email address</dt>
<dd class="wa-caption-m">winter_soldier@example.com</dd>
</div>
<div class="wa-align-items-start wa-flank" style="--flank-size: 15%;">
<dt class="wa-heading-xs">Salary expectation</dt>
<dd class="wa-caption-m">
$240,00
</dd>
</div>
<div class="wa-align-items-start wa-flank" style="--flank-size: 15%;">
<dt class="wa-heading-xs">About</dt>
<dd class="wa-caption-m">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 loves, and I'm always striving to atone for those dark days as the Winter Soldier.
</dd>
</div>
<div class="wa-align-items-start wa-flank" style="--flank-size: 15%;">
<dt class="wa-heading-xs">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>
```
### Two Column
```html{.example}
<div style="max-width: 960px; margin: 0 auto">
<h2 class="wa-heading-m">Applicant Information</h2>
<p class="wa-caption-m">Personal details and application.</p>
<wa-divider></wa-divider>
<dl class="wa-grid wa-gap-2xl" style="--min-column-size: 40ch;">
<div class="wa-align-items-start wa-flank" style="--flank-size: 15%;">
<dt class="wa-heading-xs">Full name</dt>
<dd class="wa-caption-m">Bucky Barnes</dd>
</div>
<div class="wa-align-items-start wa-flank" style="--flank-size: 15%;">
<dt class="wa-heading-xs">Application for</dt>
<dd class="wa-caption-m">Machine Learning Engineer</dd>
</div>
<div class="wa-align-items-start wa-flank" style="--flank-size: 15%;">
<dt class="wa-heading-xs">Email address</dt>
<dd class="wa-caption-m">winter_soldier@example.com</dd>
</div>
<div class="wa-align-items-start wa-flank" style="--flank-size: 15%;">
<dt class="wa-heading-xs">Salary expectation</dt>
<dd class="wa-caption-m">
$240,00
</dd>
</div>
<div class="wa-align-items-start wa-flank wa-span-grid" style="--flank-size: 15%;">
<dt class="wa-heading-xs">About</dt>
<dd class="wa-caption-m">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 loves, and I'm always striving to atone for those dark days as the Winter Soldier.
</dd>
</div>
<div class="wa-align-items-start wa-flank wa-span-grid" style="--flank-size: 15%;">
<dt class="wa-heading-xs">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>
```
### Multi Column
```html{.example}
<div style="max-width: 960px; margin: 0 auto">
<h2 class="wa-heading-m">Applicant Information</h2>
<p class="wa-caption-m">Personal details and application.</p>
<wa-divider></wa-divider>
<dl class="wa-stack wa-gap-2xl">
<div class="wa-align-items-start wa-flank" style="--flank-size: 15%;">
<dt class="wa-heading-xs">Full name</dt>
<dd class="wa-caption-m wa-split">
<span>Bucky Barnes</span>
<wa-button appearance="plain" variant="brand" size="small">Update</wa-button>
</dd>
</div>
<div class="wa-align-items-start wa-flank" style="--flank-size: 15%;">
<dt class="wa-heading-xs">Application for</dt>
<dd class="wa-caption-m wa-split">
<span>Machine Learning Engineer</span>
<wa-button appearance="plain" variant="brand" size="small">Update</wa-button>
</dd>
</div>
<div class="wa-align-items-start wa-flank" style="--flank-size: 15%;">
<dt class="wa-heading-xs">Email address</dt>
<dd class="wa-caption-m wa-split">
<span>winter_soldier@example.com</span>
<wa-button appearance="plain" variant="brand" size="small">Update</wa-button>
</dd>
</div>
<div class="wa-align-items-start wa-flank" style="--flank-size: 15%;">
<dt class="wa-heading-xs">Salary expectation</dt>
<dd class="wa-caption-m wa-split">
<span>$240,00</span>
<wa-button appearance="plain" variant="brand" size="small">Update</wa-button>
</dd>
</div>
<div class="wa-align-items-start wa-flank" style="--flank-size: 15%;">
<dt class="wa-heading-xs">About</dt>
<dd class="wa-caption-m wa-split">
<p style="max-width: 70ch;">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 loves, and I'm always striving to atone for those dark days as the Winter Soldier.</p>
<wa-button appearance="plain" variant="brand" size="small">Update</wa-button>
</dd>
</div>
<div class="wa-align-items-start wa-flank" style="--flank-size: 15%;">
<dt class="wa-heading-xs">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>
```
### Narrow with Invoice Details
```html{.example}
<wa-card with-header with-footer class="wa-callout wa-neutral" style="max-width: 480px; margin: 0 auto;">
<div slot="header" class="wa-split">
<dl class="wa-stack wa-gap-2xs">
<dt class="wa-heading-s">Amount</dt>
<dd class="wa-heading-l">$10,560.00</dd>
</dl>
<wa-badge appearance="filled outlined" variant="success">Paid</wa-badge>
</div>
<div>
<dl class="wa-stack" style="margin: 0;">
<div class="wa-flank wa-align-items-stretch">
<dt><wa-icon name="user"></wa-icon></dt>
<dd class="wa-heading-s">Sam Wilson</dd>
</div>
<div class="wa-flank wa-align-items-stretch">
<dt><wa-icon name="calendar-days"></wa-icon></dt>
<dd class="wa-heading-s">June 8, 2015</dd>
</div>
<div class="wa-flank wa-align-items-stretch">
<dt><wa-icon family="brands" name="cc-visa"></wa-icon></dt>
<dd class="wa-heading-s">Paid with Visa</dd>
</div>
</dl>
</div>
<div slot="footer">
<a href="#" class="wa-flank wa-align-items-center wa-gap-2xs">
<span>Download Receipt</span>
<wa-icon name="arrow-right"></wa-icon>
</a>
</div>
</wa-card>
```

View File

@@ -1,113 +0,0 @@
---
title: Empty State
description: TODO
parent: app
tags: app
---
## Examples
### Simple
```html{.example}
<div class="wa-stack wa-align-items-center">
<wa-icon name="folder" style="font-size: 60px;"></wa-icon>
<span class="wa-heading-s">No Projects</span>
<p class="wa-caption-m">Get started by creating a new project.</p>
<wa-button>
<wa-icon slot="prefix" name="plus"></wa-icon>
New Project
</wa-button>
</div>
```
### With border
```html{.example}
<a href="#" class="wa-align-items-center wa-callout wa-neutral wa-outlined wa-stack" style="max-width: 480px; margin: 0 auto; text-decoration: none;">
<wa-icon name="database" style="font-size: 64px;"></wa-icon>
<div class="wa-stack wa-align-items-center wa-gap-2xs">
<p class="wa-heading-m">No DBs</p>
<p>Get started by creating a database.</p>
</div>
</a>
```
### With starting points
```html{.example}
<wa-card with-header with-footer style="max-width: 720px; margin: 0 auto;">
<div slot="header" class="wa-stack wa-gap-xs">
<h2 class="wa-heading-m">Projects</h2>
<p class="wa-caption-m">You havent created a project yet. Get started by selecting a template or start from an empty project.</p>
</div>
<div class="wa-grid" style="--min-column-size: 30ch;">
<a href="#" class="wa-flank" style="text-decoration: none;">
<wa-icon name="bars" class="wa-callout wa-neutral wa-outlined" style="font-size: 16px;"></wa-icon>
<div class="wa-stack wa-gap-2xs">
<span class="wa-align-items-center wa-cluster wa-gap-xs wa-heading-s">
Create a List <wa-icon name="arrow-right"></wa-icon>
</span>
<p class="wa-caption-m">
Another to-do system youll try but eventually give up on.
</p>
</div>
</a>
<a href="#" class="wa-flank" style="text-decoration: none;">
<wa-icon name="image" class="wa-callout wa-neutral wa-outlined" style="font-size: 16px;"></wa-icon>
<div class="wa-stack wa-gap-2xs">
<span class="wa-align-items-center wa-cluster wa-gap-xs wa-heading-s">
Create a Gallery <wa-icon name="arrow-right"></wa-icon>
</span>
<p class="wa-caption-m">
Great for mood boards and inspiration.
</p>
</div>
</a>
<a href="#"class="wa-flank" style="text-decoration: none;">
<wa-icon name="table-cells" class="wa-callout wa-neutral wa-outlined" style="font-size: 16px;"></wa-icon>
<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">
Helps keep up with the numbers.
</p>
</div>
</a>
<a href="#" class="wa-flank" style="text-decoration: none;">
<wa-icon name="calendar" class="wa-callout wa-neutral wa-outlined" style="font-size: 16px;"></wa-icon>
<div class="wa-stack wa-gap-2xs">
<span class="wa-align-items-center wa-cluster wa-gap-xs wa-heading-s">
Create a Calendar <wa-icon name="arrow-right"></wa-icon>
</span>
<p class="wa-caption-m">
Stay on top of your deadlines, or dont — its up to you.
</p>
</div>
</a>
<a href="#" class="wa-flank" style="text-decoration: none;">
<wa-icon name="table-columns" class="wa-callout wa-neutral wa-outlined" style="font-size: 16px;"></wa-icon>
<div class="wa-stack wa-gap-2xs">
<span class="wa-align-items-center wa-cluster wa-gap-xs wa-heading-s">
Create a Board <wa-icon name="arrow-right"></wa-icon>
</span>
<p class="wa-caption-m">
Track tasks in different stages of your project.
</p>
</div>
</a>
<a href="#" class="wa-flank" style="text-decoration: none;">
<wa-icon name="clock" class="wa-callout wa-neutral wa-outlined" style="font-size: 16px;"></wa-icon>
<div class="wa-stack wa-gap-2xs">
<span class="wa-align-items-center wa-cluster wa-gap-xs wa-heading-s">
Create a Timeline <wa-icon name="arrow-right"></wa-icon>
</span>
<p class="wa-caption-m">
Get a birds-eye-view of your procrastination.
</p>
</div>
</a>
</div>
<div slot="footer">
<a href="#">Or start from an empty project →</a>
</div>
</wa-card>
```

View File

@@ -1,92 +0,0 @@
---
title: FAQ
description: TODO
parent: app
tags: app
---
```html{.example}
<div class="wa-grid">
<div>
<h2>Frequently Asked Questions</h2>
<p>Cant find the answer youre looking for? Reach out to our <a href="#">customer support</a> team.</p>
</div>
<dl class="wa-stack wa-gap-m">
<div class="wa-stack wa-gap-xs">
<dt class="wa-heading-m">How do you make holy water?</dt>
<dd class="wa-caption-l">You boil the hell out of it. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quas cupiditate laboriosam fugiat.</dd>
</div>
<div class="wa-stack wa-gap-xs">
<dt class="wa-heading-m">How do you make holy water?</dt>
<dd class="wa-caption-l">You boil the hell out of it. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quas cupiditate laboriosam fugiat.</dd>
</div>
<div class="wa-stack wa-gap-xs">
<dt class="wa-heading-m">How do you make holy water?</dt>
<dd class="wa-caption-l">You boil the hell out of it. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quas cupiditate laboriosam fugiat.</dd>
</div>
<div class="wa-stack wa-gap-xs">
<dt class="wa-heading-m">How do you make holy water?</dt>
<dd class="wa-caption-l">You boil the hell out of it. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quas cupiditate laboriosam fugiat.</dd>
</div>
<div class="wa-stack wa-gap-xs">
<dt class="wa-heading-m">How do you make holy water?</dt>
<dd class="wa-caption-l">You boil the hell out of it. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quas cupiditate laboriosam fugiat.</dd>
</div>
</dl>
</div>
```
```html{.example}
<div style="max-width: 960px; margin: 0 auto;">
<h2>Frequently Asked Questions</h2>
<div class="wa-stack wa-gap-xs">
<wa-details summary="How do you make holy water?">
You boil the hell out of it.
</wa-details>
<wa-details summary="How do you make holy water?">
You boil the hell out of it.
</wa-details>
<wa-details summary="How do you make holy water?">
You boil the hell out of it.
</wa-details>
<wa-details summary="How do you make holy water?">
You boil the hell out of it.
</wa-details>
</div>
</div>
```
```html{.example}
<div>
<h2>Frequently Asked Questions</h2>
<dl class="wa-stack wa-gap-m">
<div class="wa-grid wa-gap-xs">
<dt class="wa-heading-m">How do you make holy water?</dt>
<dd class="wa-caption-l">You boil the hell out of it. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quas cupiditate laboriosam fugiat.</dd>
</div>
<wa-divider></wa-divider>
<div class="wa-grid wa-gap-xs">
<dt class="wa-heading-m">How do you make holy water?</dt>
<dd class="wa-caption-l">You boil the hell out of it. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quas cupiditate laboriosam fugiat.</dd>
</div>
<wa-divider></wa-divider>
<div class="wa-grid wa-gap-xs">
<dt class="wa-heading-m">How do you make holy water?</dt>
<dd class="wa-caption-l">You boil the hell out of it. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quas cupiditate laboriosam fugiat.</dd>
</div>
<wa-divider></wa-divider>
<div class="wa-grid wa-gap-xs">
<dt class="wa-heading-m">How do you make holy water?</dt>
<dd class="wa-caption-l">You boil the hell out of it. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quas cupiditate laboriosam fugiat.</dd>
</div>
<wa-divider></wa-divider>
<div class="wa-grid wa-gap-xs">
<dt class="wa-heading-m">How do you make holy water?</dt>
<dd class="wa-caption-l">You boil the hell out of it. Lorem ipsum dolor sit amet consectetur adipisicing elit. Quas cupiditate laboriosam fugiat.</dd>
</div>
</dl>
</div>
```

View File

@@ -1,86 +0,0 @@
---
title: Feed
description: TODO
parent: app
tags: app
---
```html {.example}
<div class="activity-feed">
<div class="activity-group">
<span class="connector"></span>
<div class="activity">
<wa-icon name="user-circle" class="fa-fw"></wa-icon>
<p>Kicked ass and <strong>chewed bubblegum</strong></p>
<span style="margin-left: auto"><em>Oct. 31st</em></span>
</div>
</div>
<div class="activity-group">
<span class="connector"></span>
<div class="activity">
<wa-icon name="thumbs-up" style="color: blue" class="fa-fw"></wa-icon>
<p>Kicked ass and <strong>chewed bubblegum</strong></p>
<span style="margin-left: auto"><em>Oct. 31st</em></span>
</div>
</div>
<div class="activity-group">
<span class="connector"></span>
<div class="activity">
<wa-icon name="crown" class="fa-fw"></wa-icon>
<p>Kicked ass and <strong>chewed bubblegum</strong></p>
<span style="margin-left: auto"><em>Oct. 31st</em></span>
</div>
</div>
<div class="activity-group">
<span class="connector"></span>
<div class="activity">
<wa-icon name="turtle" style="color: green" class="fa-fw"></wa-icon>
<p>Kicked ass and <strong>chewed bubblegum</strong></p>
<span style="margin-left: auto"><em>Oct. 31st</em></span>
</div>
</div>
</div>
<style>
:root {
--border-color: var(--wa-color-surface-border);
}
.activity-feed {
wa-icon {
margin-right: 1rem;
font-size: 32px;
}
.fa-fw {
text-align: center;
width: 1.25em;
}
.activity {
display: flex;
justify-content: flex-start;
align-items: flex-start;
}
.activity-group:not(:first-child) {
margin-top: .5rem;
}
.activity-group {
position: relative;
}
.connector {
position: absolute;
background-color: var(--border-color);
height: 25%;
width: 0.125rem;
margin-left: -1px;
top: 2.5rem;
left: 1rem;
}
.activity-group:last-of-type .connector {
display: none;
}
}
</style>
```

View File

@@ -1,166 +0,0 @@
---
title: Grid
description: TODO
parent: app
tags: app
---
```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-0">
<span>
<strong>John Carpenter</strong>
<wa-badge pill>Admin</wa-badge>
</span>
<span>Master of Horror</span>
</div>
<wa-avatar image="https://images.unsplash.com/photo-1529778873920-4da4926a72c2?ixlib=rb-1.2.1&auto=format&fit=crop&w=300&q=80"label="Avatar of a gray tabby kitten looking down">
</wa-avatar>
</div>
<div slot="footer" class="wa-grid" style="--min-column-size: 16ch;">
<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-0">
<span>
<strong>John Carpenter</strong>
<wa-badge pill>Admin</wa-badge>
</span>
<span>Master of Horror</span>
</div>
<wa-avatar image="https://images.unsplash.com/photo-1529778873920-4da4926a72c2?ixlib=rb-1.2.1&auto=format&fit=crop&w=300&q=80"label="Avatar of a gray tabby kitten looking down">
</wa-avatar>
</div>
<div slot="footer" class="wa-grid" style="--min-column-size: 16ch;">
<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-0">
<span>
<strong>John Carpenter</strong>
<wa-badge pill>Admin</wa-badge>
</span>
<span>Master of Horror</span>
</div>
<wa-avatar image="https://images.unsplash.com/photo-1529778873920-4da4926a72c2?ixlib=rb-1.2.1&auto=format&fit=crop&w=300&q=80"label="Avatar of a gray tabby kitten looking down">
</wa-avatar>
</div>
<div slot="footer" class="wa-grid" style="--min-column-size: 16ch;">
<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-0">
<span>
<strong>John Carpenter</strong>
<wa-badge pill>Admin</wa-badge>
</span>
<span>Master of Horror</span>
</div>
<wa-avatar image="https://images.unsplash.com/photo-1529778873920-4da4926a72c2?ixlib=rb-1.2.1&auto=format&fit=crop&w=300&q=80"label="Avatar of a gray tabby kitten looking down">
</wa-avatar>
</div>
<div slot="footer" class="wa-grid" style="--min-column-size: 16ch;">
<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-0">
<span>
<strong>John Carpenter</strong>
<wa-badge pill>Admin</wa-badge>
</span>
<span>Master of Horror</span>
</div>
<wa-avatar image="https://images.unsplash.com/photo-1529778873920-4da4926a72c2?ixlib=rb-1.2.1&auto=format&fit=crop&w=300&q=80"label="Avatar of a gray tabby kitten looking down">
</wa-avatar>
</div>
<div slot="footer" class="wa-grid" style="--min-column-size: 16ch;">
<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-0">
<span>
<strong>John Carpenter</strong>
<wa-badge pill>Admin</wa-badge>
</span>
<span>Master of Horror</span>
</div>
<wa-avatar image="https://images.unsplash.com/photo-1529778873920-4da4926a72c2?ixlib=rb-1.2.1&auto=format&fit=crop&w=300&q=80"label="Avatar of a gray tabby kitten looking down">
</wa-avatar>
</div>
<div slot="footer" class="wa-grid" style="--min-column-size: 16ch;">
<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>
```
```html{.example}
<div class="wa-grid">
<wa-card>
<div class="wa-flank">
<div class="wa-callout wa-neutral">GA</div>
<div class="wa-split">
<div class="wa-gap-0 wa-stack">
<span class="wa-heading-xs">Graph API</span>
<span class="wa-caption-m">16 Members</span>
</div>
<wa-icon-button name="ellipsis-vertical" label="actions"></wa-icon-button>
</div>
</div>
</wa-card>
<wa-card>
<div class="wa-flank">
<div class="wa-callout wa-success">GA</div>
<div class="wa-split">
<div class="wa-gap-0 wa-stack">
<span class="wa-heading-xs">Graph API</span>
<span class="wa-caption-m">16 Members</span>
</div>
<wa-icon-button name="ellipsis-vertical" label="actions"></wa-icon-button>
</div>
</div>
</wa-card>
<wa-card>
<div class="wa-flank">
<div class="wa-callout wa-danger">GA</div>
<div class="wa-split">
<div class="wa-gap-0 wa-stack">
<span class="wa-heading-xs">Graph API</span>
<span class="wa-caption-m">16 Members</span>
</div>
<wa-icon-button name="ellipsis-vertical" label="actions"></wa-icon-button>
</div>
</div>
</wa-card>
<wa-card>
<div class="wa-flank">
<div class="wa-callout wa-warning">GA</div>
<div class="wa-split">
<div class="wa-gap-0 wa-stack">
<span class="wa-heading-xs">Graph API</span>
<span class="wa-caption-m">16 Members</span>
</div>
<wa-icon-button name="ellipsis-vertical" label="actions"></wa-icon-button>
</div>
</div>
</wa-card>
</div>
```

View File

@@ -1,7 +0,0 @@
---
title: App
description: TODO
layout: overview
categories: ["app"]
listChildren: true
---

View File

@@ -1,369 +0,0 @@
---
title: Leaderboard
description: TODO
parent: app
tags: app
---
```html{.example}
<div class="wa-stack wa-gap-xs" style="max-width: 960px; margin: 0 auto">
<h2>Collective Activity for Yesterday</h2>
<div class="wa-grid">
<wa-callout>
<wa-icon slot="icon" name="book"></wa-icon>
<div class="wa-stack wa-gap-0">
<div class="wa-heading-xs">Items Studied</div>
<div class="wa-heading-2xl">482,813</div>
</div>
</wa-callout>
<wa-callout variant="warning">
<wa-icon slot="icon" name="medal"></wa-icon>
<div class="wa-stack wa-gap-0">
<div class="wa-heading-xs">Items Mastered</div>
<div class="wa-heading-2xl">67,106</div>
</div>
</wa-callout>
<wa-callout variant="success">
<wa-icon slot="icon" name="plus"></wa-icon>
<div class="wa-stack wa-gap-0">
<div class="wa-heading-xs">Items Created</div>
<div class="wa-heading-2xl">2,080</div>
</div>
</wa-callout>
</div>
<!-- -->
<div class="wa-grid">
<wa-card with-header>
<div slot="header" class="wa-flank">
<wa-icon name="trophy"></wa-icon>
<span class="wa-gap-0 wa-stack">
<h4>Study Leaders</h4>
<p>items mastered last 7 days</p>
</span>
</div>
<div>
<ol>
<li>
<div class="wa-flank">
<div class="wa-frame wa-border-radius-m">
<img src="https://images.unsplash.com/photo-1620428268482-cf1851a36764?q=80&w=40&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" width="40" />
</div>
<div class="wa-stack wa-gap-2xs">
<span>mitsuwo</span>
<span>2,753</span>
</div>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-flank">
<div class="wa-frame wa-border-radius-m">
<img src="https://images.unsplash.com/photo-1639628735078-ed2f038a193e?q=80&w=40&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" width="40" />
</div>
<div class="wa-stack wa-gap-2xs">
<span>mitsuwo</span>
<span>2,753</span>
</div>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-flank">
<div class="wa-frame wa-border-radius-m">
<img src="https://images.unsplash.com/photo-1638803040283-7a5ffd48dad5?q=80&w=40&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" width="40" />
</div>
<div class="wa-stack wa-gap-2xs">
<span>mitsuwo</span>
<span>2,753</span>
</div>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-flank">
<div class="wa-frame wa-border-radius-m">
<img src="https://images.unsplash.com/photo-1620428268482-cf1851a36764?q=80&w=40&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" width="40" />
</div>
<div class="wa-stack wa-gap-2xs">
<span>mitsuwo</span>
<span>2,753</span>
</div>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-flank">
<div class="wa-frame wa-border-radius-m">
<img src="https://images.unsplash.com/photo-1639628735078-ed2f038a193e?q=80&w=40&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" width="40" />
</div>
<div class="wa-stack wa-gap-2xs">
<span>mitsuwo</span>
<span>2,753</span>
</div>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-flank">
<div class="wa-frame wa-border-radius-m">
<img src="https://images.unsplash.com/photo-1638803040283-7a5ffd48dad5?q=80&w=40&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" width="40" />
</div>
<div class="wa-stack wa-gap-2xs">
<span>mitsuwo</span>
<span>2,753</span>
</div>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-flank">
<div class="wa-frame wa-border-radius-m">
<img src="https://images.unsplash.com/photo-1620428268482-cf1851a36764?q=80&w=40&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" width="40" />
</div>
<div class="wa-stack wa-gap-2xs">
<span>mitsuwo</span>
<span>2,753</span>
</div>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-flank">
<div class="wa-frame wa-border-radius-m">
<img src="https://images.unsplash.com/photo-1639628735078-ed2f038a193e?q=80&w=40&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" width="40" />
</div>
<div class="wa-stack wa-gap-2xs">
<span>mitsuwo</span>
<span>2,753</span>
</div>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-flank">
<div class="wa-frame wa-border-radius-m">
<img src="https://images.unsplash.com/photo-1638803040283-7a5ffd48dad5?q=80&w=40&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" width="40" />
</div>
<div class="wa-stack wa-gap-2xs">
<span>mitsuwo</span>
<span>2,753</span>
</div>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-flank">
<div class="wa-frame wa-border-radius-m">
<img src="https://images.unsplash.com/photo-1638803040283-7a5ffd48dad5?q=80&w=40&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" width="40" />
</div>
<div class="wa-stack wa-gap-2xs">
<span>mitsuwo</span>
<span>2,753</span>
</div>
</div>
</li>
</ol>
</div>
</wa-card>
<!-- -->
<wa-card with-header>
<div slot="header" class="wa-flank">
<wa-icon name="trophy"></wa-icon>
<span class="wa-gap-0 wa-stack">
<h4>Creation Leaders</h4>
<p>items created last 7 days</p>
</span>
</div>
<div>
<ol>
<li>
<p>Item 1</p>
<wa-divider></wa-divider>
</li>
<li>
<p>Item 1</p>
<wa-divider></wa-divider>
</li>
<li>
<p>Item 1</p>
<wa-divider></wa-divider>
</li>
</ol>
</div>
</wa-card>
</div>
</div>
```
### Two Column
```html{.example}
<div style="max-width: 960px; margin: 0 auto">
<h2>WTA Rankings</h2>
<div class="wa-grid wa-gap-3xl">
<div class="wa-stack">
<div class="wa-flank">
<div class="wa-border-radius-l wa-frame">
<img src="https://uploads.webawesome.com/serena-2.jpg" />
</div>
<div class="wa-stack wa-gap-s">
<span class="wa-heading-m">Serena Williams</span>
<span class="wa-caption-l">United States</span>
</div>
</div>
<span class="wa-heading-l">SINGLES</span>
<ol class="wa-stack wa-gap-2xs">
<li>
<div class="wa-split">
<span>Williams, Serena</span>
<span>9231</span>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-split">
<span>Williams, Serena</span>
<span>6960</span>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-split">
<span>Li, Na</span>
<span>6785</span>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-split">
<span>Halep, Simona</span>
<span>6070</span>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-split">
<span>Radwanska, Agnieszka</span>
<span>5130</span>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-split">
<span>Sharapova, Maria</span>
<span>4661</span>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-split">
<span>Bouchard, Eugenie</span>
<span>4460</span>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-split">
<span>Kerber, Angelique</span>
<span>4365</span>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-split">
<span>Jankovic, Jelena</span>
<span>3900</span>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-split">
<span>Azarenka, Victoria</span>
<span>3812</span>
</div>
</li>
</ol>
</div>
<!-- -->
<div class="wa-stack">
<div class="wa-cluster">
<div class="wa-border-radius-l wa-frame">
<img src="https://uploads.webawesome.com/roberta.jpg" />
</div>
<div class="wa-border-radius-l wa-frame">
<img src="https://uploads.webawesome.com/sara.jpg" />
</div>
</div>
<span class="wa-heading-l">DOUBLES</span>
<ol class="wa-stack wa-gap-2xs">
<li>
<div class="wa-split">
<span>Errani, Vinci</span>
<span>9231</span>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-split">
<span>Roberts, Paxson</span>
<span>9231</span>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-split">
<span>Smith, Wexler</span>
<span>9231</span>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-split">
<span>Elena, Hisieh</span>
<span>9231</span>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-split">
<span>Pen, Sania</span>
<span>9231</span>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-split">
<span>Makarova, Cara</span>
<span>9231</span>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-split">
<span>Washington, Roosevelt</span>
<span>9231</span>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-split">
<span>King, Little</span>
<span>9231</span>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-split">
<span>Grier, Brown</span>
<span>9231</span>
</div>
<wa-divider></wa-divider>
</li>
<li>
<div class="wa-split">
<span>Burkhart, Silverton</span>
<span>9231</span>
</div>
</li>
</ol>
</div>
</div>
```

View File

@@ -1,44 +0,0 @@
---
title: Pagination
description: TODO
parent: app
tags: app
---
## Simple Pagination
```html{.example}
<div>
<wa-divider></wa-divider>
<div class="wa-split">
<span class="wa-caption-l">Showing 1 to 10 of 50 Results</span>
<span>
<wa-button><wa-icon slot="prefix" name="gear" variant="solid"></wa-icon> Prev</wa-button>
<wa-button><wa-icon slot="suffix" name="gear" variant="solid"></wa-icon>Next </wa-button>
</span>
</div>
</div>
```
## Multi Page
```html {.example}
<wa-card with-footer>
<div class="wa-stack" style="opacity: 25%;">
<div>
<div class="wa-flank">
<wa-avatar label="User avatar"></wa-avatar>
<div>
Some stuff
</div>
</div>
<wa-divider></wa-divider>
</div>
</div>
<div slot="footer" class="wa-split">
<wa-button><wa-icon slot="prefix" name="gear" variant="solid"></wa-icon> Prev</wa-button>
<wa-button><wa-icon slot="suffix" name="gear" variant="solid"></wa-icon>Next </wa-button>
</div>
</wa-card>
```

View File

@@ -1,137 +0,0 @@
---
title: Pricing
description: TODO
parent: app
tags: app
---
```html{.example}
<div class="wa-grid">
<wa-card with-header>
<div slot="header">
<span class="wa-split">
<span class="wa-heading-s">Plan</span>
<wa-badge appearance="filled outlined" variant="success" pill>Most Popular</wa-badge>
</span>
<span class="wa-flank wa-gap-2xs">
<span class="wa-heading-2xl">$10</span>
<span class="wa-caption-l">per user/per month</span>
</span>
<p class="wa-caption-l">Basic Features</p>
<span class="wa-grid">
<wa-button>Get Started</wa-button>
<wa-button appearance="outlined">Talk to sales</wa-button>
</span>
</div>
<div class="wa-stack">
<p class="wa-heading-s">Features</p>
<p class="wa-caption-m">Everything in out free plan plus</p>
<div class="wa-stack">
<div class="wa-flank">
<wa-icon name="check"></wa-icon>
<span class="wa-caption-m">Access to basic features</span>
</div>
<div class="wa-flank">
<wa-icon name="check"></wa-icon>
<span class="wa-caption-m">Basic Reporting and Analytics</span>
</div>
<div class="wa-flank">
<wa-icon name="check"></wa-icon>
<span class="wa-caption-m">10 Individual users</span>
</div>
<div class="wa-flank">
<wa-icon name="check"></wa-icon>
<span class="wa-caption-m">20GB of data for user</span>
</div>
<div class="wa-flank">
<wa-icon name="check"></wa-icon>
<span class="wa-caption-m">Basic Chat and email</span>
</div>
</div>
</div>
</wa-card>
<wa-card with-header>
<div slot="header">
<span class="wa-split">
<span class="wa-heading-s">Plan</span>
<wa-badge appearance="filled outlined" variant="success" pill>Most Popular</wa-badge>
</span>
<span class="wa-flank wa-gap-2xs">
<span class="wa-heading-2xl">$10</span>
<span class="wa-caption-l">per user/per month</span>
</span>
<p class="wa-caption-l">Basic Features</p>
<span class="wa-grid">
<wa-button>Get Started</wa-button>
<wa-button appearance="outlined">Talk to sales</wa-button>
</span>
</div>
<div class="wa-stack wa-gap-2xs">
<p class="wa-heading-s">Features</p>
<p class="wa-caption-m">Everything in out free plan plus</p>
<div class="wa-stack">
<div class="wa-flank">
<wa-icon name="check"></wa-icon>
<span class="wa-caption-m">Access to basic features</span>
</div>
</div>
</div>
</wa-card>
<wa-card with-header>
<div slot="header">
<span class="wa-split">
<span class="wa-heading-s">Plan</span>
<wa-badge appearance="filled outlined" variant="success" pill>Most Popular</wa-badge>
</span>
<span class="wa-flank wa-gap-2xs">
<span class="wa-heading-2xl">$10</span>
<span class="wa-caption-l">per user/per month</span>
</span>
<p class="wa-caption-l">Basic Features</p>
<span class="wa-grid">
<wa-button>Get Started</wa-button>
<wa-button appearance="outlined">Talk to sales</wa-button>
</span>
</div>
<div class="wa-stack wa-gap-2xs">
<p class="wa-heading-s">Features</p>
<p class="wa-caption-m">Everything in out free plan plus</p>
<div class="wa-stack">
<div class="wa-flank">
<wa-icon name="check"></wa-icon>
<span class="wa-caption-m">Access to basic features</span>
</div>
</div>
</div>
</wa-card>
</div>
```
```html{.example}
<div class="wa-callout wa-neutral wa-outlined wa-grid">
<div class="wa-stack">
<h2 class="wa-heading-l">Lifetime membership</h2>
<p>Lorem ipsum dolor sit amet consect etur adipisicing elit. Itaque amet indis perferendis blanditiis repellendus etur quidem assumenda.</p>
<wa-divider></wa-divider>
<h3 class="wa-heading-s">What's included</h3>
<div class="wa-grid">
<span class="wa-flank wa-gap-xs"><wa-icon name="check"></wa-icon><p class="wa-caption-m">Private forum access</p></span>
<span class="wa-flank wa-gap-xs"><wa-icon name="check"></wa-icon><p class="wa-caption-m">Entry to annual conference</p></span>
<span class="wa-flank wa-gap-xs"><wa-icon name="check"></wa-icon><p class="wa-caption-m">Member resources</p></span>
<span class="wa-flank wa-gap-xs"><wa-icon name="check"></wa-icon><p class="wa-caption-m">Official member t-shirt</p></span>
</div>
</div>
<div class="wa-callout wa-neutral wa-stack wa-align-items-center">
<h3 class="wa-heading-s">Pay once, own it forever</h3>
<div>
<span class="wa-heading-3xl">$349</span>
<span>USD</span>
</div>
<wa-button variant="success">Get Access</wa-button>
<p class="wa-caption-s">Invoices and receipts available for easy company reimbursement</p>
</div>
</div>
```
### With templates
### With recommendations grid

View File

@@ -1,7 +1,6 @@
---
title: Blog
description: TODO
unlisted: true
---
TODO Page Description

View File

@@ -1,7 +1,6 @@
---
title: Business
description: TODO
unlisted: true
---
TODO Page Description

View File

@@ -1,85 +1,13 @@
---
title: Category Filter
description: 'Helps the user find the right products with filters to refine search results by specific attributes.'
description: TODO
parent: ecommerce
tags: e-commerce
icon: checkbox
---
## Sidebar with Checkboxes & Expandable Filters
TODO Page Description
## With inline actions and expandable sidebar filters
```html{.example}
<h1>New Arrivals</h1>
<div class="wa-flank wa-align-items-start" style="--flank-size: 200px;">
<form class="wa-stack">
<wa-checkbox checked>All Products</wa-checkbox>
<wa-checkbox>Sale</wa-checkbox>
<wa-checkbox>Travel</wa-checkbox>
<wa-checkbox>Organization</wa-checkbox>
<wa-checkbox>Accessories</wa-checkbox>
<wa-details summary="Color" open>
<div class="wa-stack">
<wa-checkbox>White</wa-checkbox>
<wa-checkbox>Beige</wa-checkbox>
<wa-checkbox>Blue</wa-checkbox>
<wa-checkbox>Brown</wa-checkbox>
<wa-checkbox>Green</wa-checkbox>
</div>
</wa-details>
<wa-details summary="Category">
<div class="wa-stack">
<wa-checkbox>Outdoor</wa-checkbox>
<wa-checkbox>Indoor</wa-checkbox>
<wa-checkbox>All Weather</wa-checkbox>
</div>
</wa-details>
<wa-details summary="Size">
<div class="wa-stack">
<wa-checkbox>Small</wa-checkbox>
<wa-checkbox>Medium</wa-checkbox>
<wa-checkbox>Large</wa-checkbox>
<wa-checkbox>XL</wa-checkbox>
<wa-checkbox>XXL</wa-checkbox>
</div>
</wa-details>
</form>
<div class="wa-placeholder"></div>
</div>
</div>
```
## Sidebar with Dropdowns
```html{.example}
<h1>New Arrivals</h1>
<div class="wa-flank wa-align-items-start">
<div class="wa-stack">
<wa-select label="Product Type" placeholder="Products" value="all-products">
<wa-option value="all-products">All Products</wa-option>
<wa-option value="sale">Sale</wa-option>
<wa-option value="travel">Travel</wa-option>
<wa-option value="organization">Organization</wa-option>
<wa-option value="accessories">Accessories</wa-option>
</wa-select>
<wa-divider></wa-divider>
<wa-select label="Color" placeholder="Color" value="black" multiple>
<wa-option value="black">Black</wa-option>
<wa-option value="white">White</wa-option>
<wa-option value="gray">Gray</wa-option>
</wa-select>
<wa-select label="Category" placeholder="Category" value="outdoor" multiple>
<wa-option value="outdoor">Outdoor</wa-option>
<wa-option value="indoor">Indoor</wa-option>
<wa-option value="all-weather">All Weather</wa-option>
</wa-select>
<wa-select label="Size" placeholder="Size" value="xl xxl" multiple>
<wa-option value="s">Small</wa-option>
<wa-option value="m">Medium</wa-option>
<wa-option value="l">Large</wa-option>
<wa-option value="xl">XL</wa-option>
<wa-option value="xxl">XXL</wa-option>
</wa-select>
</div>
<div class="wa-placeholder"></div>
</div>
```

View File

@@ -1,164 +1,12 @@
---
title: Category Preview
description: 'Help shoppers discover your product offerings with showcases of product categories.'
description: TODO
parent: ecommerce
tags: e-commerce
icon: preview
---
## Split with Image Grid
TODO Page Description
## Three Column (WIP)
```html {.example}
<div class="wa-flank wa-align-items-start" style="--flank-size: 20rem;">
<div class="wa-stack wa-gap-2xl">
<h2 class="wa-heading-xl">Casual Collection</h2>
<p class="wa-body-s">Look good &mdash; without looking like you're trying too hard. Our casual collection includes laid back styles that work in <em>almost</em> any situation.</p>
<wa-button>View the Collection</wa-button>
</div>
<div class="wa-stack">
<div class="wa-frame:landscape wa-border-radius-s">
<img
src="https://images.unsplash.com/photo-1544441893-675973e31985?q=80&w=2340&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="An analog watch, cotton pants, crew neck tee, and pair of tennis shoes (Photograph by Mnz)"
/>
</div>
<div class="wa-grid">
<div class="wa-frame:landscape wa-border-radius-s">
<img
src="https://images.unsplash.com/photo-1548768041-2fceab4c0b85?q=80&w=3540&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="Stack of three folded solid color tees (Photograph by Mnz)"
/>
</div>
<div class="wa-frame:landscape wa-border-radius-s">
<img
src="https://images.unsplash.com/photo-1544441892-794166f1e3be?q=80&w=3540&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="Pair of bright white tennis shoes(Photograph by Mnz)"
/>
</div>
</div>
</div>
</div>
```
## Columns with Tall Images
```html {.example}
<div class="wa-stack">
<h2 class="wa-heading-xl">Shop by Category</h2>
<div class="wa-grid">
<a href="" class="wa-stack wa-link-plain">
<div class="wa-frame:portrait wa-border-radius-s">
<img
src="https://uploads.webawesome.com/organization.jpg"
alt="Inside of a closet filled with clothes on wooden hangers and integrated shelving with shoes"
/>
</div>
<span class="wa-caption-xl">Organization</span>
</a>
<a href="" class="wa-stack wa-link-plain">
<div class="wa-frame:portrait wa-border-radius-s">
<img
src="https://uploads.webawesome.com/bags.jpg"
alt="Young person hugging a small floral patterned book bag between their arms"
/>
</div>
<span class="wa-caption-xl">Bags</span>
</a>
<a href="" class="wa-stack wa-link-plain">
<div class="wa-frame:portrait wa-border-radius-s">
<img
src="https://uploads.webawesome.com/outdoor-2.jpg"
alt="Person in a mountain clearing wearing a waterproof hooded windbreaker in black and orange"
/>
</div>
<span class="wa-caption-xl">Outdoor</span>
</a>
</div>
</div>
```
## Columns with Cards
```html {.example}
<div class="wa-stack">
<div class="wa-split">
<h2 class="wa-heading-xl">Shop by Category</h2>
<a href="" class="wa-cluster">
<span>Browse All Categories</span>
<wa-icon name="arrow-right"></wa-icon>
</a>
</div>
<div class="wa-grid">
<a href="" class="wa-link-plain">
<wa-card style="height: 100%">
<img
slot="image"
src="https://img.fortawesome.com/cfa83f3c/outdoor-3x.jpg"
alt="Two hikers wearing long canvas pants, weatherproof jackets, and backpacks"
/>
<div class="wa-stack wa-gap-xs">
<span class="wa-heading-m">Outdoor</span>
<p class="wa-caption-m">Durable canvas gear for all conditions.</p>
</div>
</wa-card>
</a>
<a href="" class="wa-link-plain">
<wa-card style="height: 100%">
<img
slot="image"
src="https://img.fortawesome.com/cfa83f3c/home.jpg"
alt="Woman sitting on a couch in a bright home, wearing a thick knit sweater"
/>
<div class="wa-stack wa-gap-xs">
<span class="wa-heading-m">Home</span>
<p class="wa-caption-m">Cozy up on the couch and relax in soft cotton.</p>
</div>
</wa-card>
</a>
<a href="" class="wa-link-plain">
<wa-card style="height: 100%">
<img
slot="image"
src="https://img.fortawesome.com/cfa83f3c/fitness.jpg"
alt="Athlete training in fitted active wear tee and shorts"
/>
<div class="wa-stack wa-gap-xs">
<span class="wa-heading-m">Active</span>
<p class="wa-caption-m">Get fit in style with breathable poly blends.</p>
</div>
</wa-card>
</a>
</div>
</div>
```
## Square Image Grid
```html {.example}
<div class="wa-stack wa-gap-2xl">
<div class="wa-stack wa-gap-xs wa-align-items-center">
<h2 class="wa-heading-xl">New Arrivals</h2>
<p class="wa-caption-l">Explore brand new furniture to accentuate your home aesthetic &mdash; just for you.</p>
</div>
<div class="wa-grid">
<div class="wa-stack">
<div class="wa-frame wa-border-radius-m">
<img
src="https://uploads.webawesome.com/indoor-furniture.jpg"
alt="Sunny room with a mid-century modern couch, accent chair, and elegant lamp"
/>
</div>
<wa-button appearance="outlined">View Indoor Furniture</wa-button>
</div>
<div class="wa-stack">
<div class="wa-frame wa-border-radius-m">
<img
src="https://uploads.webawesome.com/outdoor-furniture.jpg"
alt="Covered patio with rustic wooden cabinets, writing desk, and stool"
/>
</div>
<wa-button appearance="outlined">View Outdoor Furniture</wa-button>
</div>
</div>
</div>
```

View File

@@ -1,246 +0,0 @@
---
title: Checkout Form
description: 'Let shoppers checkout with ease with streamlined forms to capture shipping and payment info.'
parent: ecommerce
tags: e-commerce
---
## Full Form with Order Summary Card
```html {.example}
<div class="wa-grid wa-gap-3xl">
<div class="wa-stack">
<h4>Contact</h4>
<wa-input type="email" label="Email Address"></wa-input>
<wa-divider></wa-divider>
<h4>Shipping</h4>
<wa-select label="Country" value="us">
<wa-option value="ca">Canada</wa-option>
<wa-option value="mx">Mexico</wa-option>
<wa-option value="us">United States</wa-option>
</wa-select>
<div class="wa-grid">
<wa-input label="First Name"></wa-input>
<wa-input label="Last Name"></wa-input>
</div>
<wa-input label="Company"></wa-input>
<wa-input label="Address"></wa-input>
<div class="wa-grid" style="--min-column-size: 10ch;">
<wa-input label="City"></wa-input>
<wa-input label="State"></wa-input>
<wa-input label="Postal Code"></wa-input>
</div>
<wa-input label="Phone"></wa-input>
<wa-divider></wa-divider>
<wa-radio-group label="Shipping Method" name="shipping-method" value="standard" orientation="horizontal">
<wa-radio value="standard" hint="7-10 business days">Standard</wa-radio>
<wa-radio value="express" hint="2-5 business days">Express</wa-radio>
</wa-radio-group>
<wa-divider></wa-divider>
<h4>Payment</h4>
<wa-radio-group label="Payment Method" name="payment-method" value="credit" orientation="horizontal">
<wa-radio value="credit">Credit Card</wa-radio>
<wa-radio value="paypal">Paypal</wa-radio>
</wa-radio-group>
<wa-input label="Card Number"></wa-input>
<wa-input label="Name on Card"></wa-input>
<div class="wa-grid">
<wa-input label="Expiration Date" placeholder="MM/YY"></wa-input>
<wa-input label="CVC"></wa-input>
</div>
</div>
<div class="wa-stack">
<h4>Order Summary</h4>
<wa-card>
<div class="wa-stack">
<div class="wa-flank wa-align-items-start" style="--flank-size: 7rem">
<div class="wa-frame wa-border-radius-s">
<img src="https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w1OTAyOTl8MHwxfGFsbHx8fHx8fHx8fDE3MTg2NDM1MzB8&ixlib=rb-4.0.3&q=80&w=1080" alt="">
</div>
<div class="wa-stack wa-gap-xs">
<div class="wa-split">
<span class="wa-heading-s">Dolce Runners</span>
<wa-icon-button name="trash" label="Remove from cart"></wa-icon-button>
</div>
<span class="wa-caption-m">Cream/Seafoam</span>
<span class="wa-caption-m">12.5</span>
<div class="wa-split">
<span>$135.00</span>
<wa-select value="1" size="small" style="max-width: 8ch">
<wa-option value="1">1</wa-option>
<wa-option value="2">2</wa-option>
<wa-option value="3">3</wa-option>
</wa-select>
</div>
</div>
</div>
<wa-divider></wa-divider>
<div class="wa-flank wa-align-items-start" style="--flank-size: 7rem">
<div class="wa-frame wa-border-radius-s">
<img src="https://images.unsplash.com/photo-1514989940723-e8e51635b782?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w1OTAyOTl8MHwxfGFsbHx8fHx8fHx8fDE3MTg2NDM1Njh8&ixlib=rb-4.0.3&q=80&w=1080" alt="">
</div>
<div class="wa-stack wa-gap-xs">
<div class="wa-split">
<span class="wa-heading-s">Dunk High</span>
<wa-icon-button name="trash" label="Remove from cart"></wa-icon-button>
</div>
<span class="wa-caption-m">Sand/Amber/Black</span>
<span class="wa-caption-m">12.5</span>
<div class="wa-split">
<span>$180.00</span>
<wa-select value="1" size="small" style="max-width: 8ch">
<wa-option value="1">1</wa-option>
<wa-option value="2">2</wa-option>
<wa-option value="3">3</wa-option>
</wa-select>
</div>
</div>
</div>
<wa-divider></wa-divider>
<div class="wa-flank wa-align-items-start" style="--flank-size: 7rem">
<div class="wa-frame wa-border-radius-s">
<img src="https://images.unsplash.com/photo-1606107557195-0e29a4b5b4aa?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w1OTAyOTl8MHwxfGFsbHx8fHx8fHx8fDE3MTg2NDM2MTF8&ixlib=rb-4.0.3&q=80&w=1080" alt="">
</div>
<div class="wa-stack wa-gap-xs">
<div class="wa-split">
<span class="wa-heading-s">NB Runner</span>
<wa-icon-button name="trash" label="Remove from cart"></wa-icon-button>
</div>
<span class="wa-caption-m">Forrest Green</span>
<span class="wa-caption-m">12.5</span>
<div class="wa-split">
<span>$48.99</span>
<wa-select value="1" size="small" style="max-width: 8ch">
<wa-option value="1">1</wa-option>
<wa-option value="2">2</wa-option>
<wa-option value="3">3</wa-option>
</wa-select>
</div>
</div>
</div>
<wa-divider></wa-divider>
<div class="wa-stack">
<div class="wa-split wa-caption-m">
<span>Subtotal</span>
<span>$363.99</span>
</div>
<div class="wa-split wa-caption-m">
<span>Shipping</span>
<span>FREE</span>
</div>
<div class="wa-split wa-heading-m">
<span>Total</span>
<span>$363.99</span>
</div>
</div>
<wa-divider></wa-divider>
<wa-button variant="brand">Confirm Order</wa-button>
</div>
</wa-card>
</div>
</div>
```
## Short Form with Order Summary
```html {.example}
<div class="wa-grid wa-gap-3xl">
<div class="wa-stack wa-gap-xl">
<h2>Payment</h2>
<wa-input type="email" label="Email" placeholder="ex. tanderson@metacortex.com">
<wa-icon slot="prefix" name="envelope"></wa-icon>
</wa-input>
<wa-input label="Card Number" placeholder="1234 1234 1234 1234">
<wa-icon slot="prefix" name="credit-card"></wa-icon>
</wa-input>
<div class="wa-grid" style="--min-column-size: 12ch">
<wa-input label="Expiration" placeholder="MM/YY">
<wa-icon slot="prefix" name="calendar"></wa-icon>
</wa-input>
<wa-input label="CVC" placeholder="CVC">
<wa-icon slot="prefix" name="lock"></wa-icon>
</wa-input>
</div>
<wa-input label="Cardholder Name" placeholder="Thomas Anderson">
<wa-icon slot="prefix" name="user"></wa-icon>
</wa-input>
<div class="wa-grid" style="--min-column-size: 12ch">
<wa-select label="Country" value="us">
<wa-icon slot="prefix" name="globe"></wa-icon>
<wa-option value="ca">Canada</wa-option>
<wa-option value="us">United States</wa-option>
<wa-option value="mx">Mexico</wa-option>
</wa-select>
<wa-input label="ZIP" placeholder="12345">
<wa-icon slot="prefix" name="location-dot"></wa-icon>
</wa-input>
</div>
<wa-switch checked>Sign me up for more offers from this store</wa-switch>
<wa-button variant="brand">Pay Now</wa-button>
</div>
<div class="wa-stack wa-gap-xl">
<h2>Order Summary</h2>
<div class="wa-split">
<div class="wa-cluster">
<div class="wa-frame wa-border-radius-m" style="max-width: 4rem">
<img src="https://images.unsplash.com/photo-1618677366787-9727aacca7ea?q=80&w=3255&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" alt="Glasses with black wire frames and dark tinted, circular lenses (Photograph by Colin Lloyd)">
</div>
<strong>Morpheus</strong>
</div>
<div class="wa-cluster">
<wa-input type="number" value="1" style="max-width: 5rem"></wa-input>
<span>$120.00</span>
</div>
</div>
<div class="wa-split">
<div class="wa-cluster">
<div class="wa-frame wa-border-radius-m" style="max-width: 4rem">
<img src="https://images.unsplash.com/photo-1511499767150-a48a237f0083?q=80&w=3558&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" alt="Glasses with rose gold wire frames and green tinted, circular lenses (Photograph by Charles Deluvio)">
</div>
<div class="wa-stack wa-gap-3xs">
<strong>Seraph</strong>
<em class="wa-caption-m">Tinted</em>
</div>
</div>
<div class="wa-cluster">
<wa-input type="number" value="1" style="max-width: 5rem"></wa-input>
<span>$180.00</span>
</div>
</div>
<div class="wa-split">
<div class="wa-cluster">
<div class="wa-frame wa-border-radius-m" style="max-width: 4rem">
<img src="https://images.unsplash.com/photo-1547104442-a40f335740cb?q=80&w=3348&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" alt="Glasses with tortoise shell half frames and large, rounded lenses (Photograph by Sincerely Media)">
</div>
<div class="wa-stack wa-gap-3xs">
<strong>Keymaker</strong>
<em class="wa-caption-m">Glossy</em>
</div>
</div>
<div class="wa-cluster">
<wa-input type="number" value="1" style="max-width: 5rem"></wa-input>
<span>$50.00</span>
</div>
</div>
<div class="wa-flank:end">
<wa-input placeholder="Discount code or gift card"></wa-input>
<wa-button appearance="filled">Apply</wa-button>
</div>
<div class="wa-stack wa-gap-s">
<div class="wa-split">
<span>Subtotal</span>
<strong>$530.00</strong>
</div>
<div class="wa-split">
<span>Shipping</span>
<span>$8.00</span>
</div>
<div class="wa-split">
<strong>Total</strong>
<strong>$538.00</strong>
</div>
</div>
</div>
</div>
```

View File

@@ -1,125 +0,0 @@
---
title: Incentives
description: 'Encourage shoppers to buy your products with value propositions, discounts, and promotions.'
parent: ecommerce
tags: e-commerce
---
## 3 Column
```html{.example}
<div class="wa-gap-3xl wa-stack" style="max-width: 960px; margin: 0 auto;">
<div class="wa-align-items-center wa-grid">
<div>
<span class="wa-heading-xl">Get the Best Instruction from our Educators.</span>
<p class="wa-caption-l">At the beginning at least, but then we realized we could make a lot more money if we kinda stopped caring about that. Our new strategy is to write a bunch of things that look really good in the headlines, then clarify in the small print but hope people don't actually read it.</p>
</div>
<div class="wa-frame wa-border-radius-l">
<img src="https://uploads.webawesome.com/online-learning.jpg" />
</div>
</div>
<div class="wa-grid">
<div class="wa-stack wa-gap-xs">
<wa-icon name="stopwatch" style="font-size: 32px;"></wa-icon>
<span class="wa-heading-s">Learn at your Speed</span>
<p class="wa-caption-m">It's not actually free we just price it into the products. Someone's paying for it, and it's not us.</p>
</div>
<div class="wa-stack wa-gap-xs">
<wa-icon name="chart-line" style="font-size: 32px;"></wa-icon>
<span class="wa-heading-s">Track Progress</span>
<p class="wa-caption-m">If it breaks in the first 10 years we'll replace it. After that you're on your own though.</p>
</div>
<div class="wa-stack wa-gap-xs">
<wa-icon name="people-group" style="font-size: 32px;"></wa-icon>
<span class="wa-heading-s">Active Community</span>
<p class="wa-caption-m">If you don't like it, trade it to one of your friends for something of theirs. Don't send it here though.</p>
</div>
</div>
</div>
```
## 2 Column with Cards
```html{.example}
<div class="wa-grid" style="--min-column-size: 41ch;">
<wa-card>
<div class="wa-flank">
<div>
<wa-icon name="hands" style="font-size: 2.5rem;"></wa-icon>
</div>
<div class="wa-gap-s wa-stack">
<span class="wa-heading-m">Hands-on training</span>
<p class="wa-caption-l">Upskill effectively with AI-powered coding exercises, practice tests, and quizzes.</p>
</div>
</div>
</wa-card>
<wa-card>
<div class="wa-flank">
<div>
<wa-icon name="medal" style="font-size: 2.5rem;"></wa-icon>
</div>
<div class="wa-gap-s wa-stack">
<span class="wa-heading-m">Certification prep</span>
<p class="wa-caption-l">Prep for industry-recognized certifications by solving real-world challenges and earn badges along the way</p>
</div>
</div>
</wa-card>
<wa-card>
<div class="wa-flank">
<div>
<wa-icon name="chart-line" style="font-size: 2.5rem;"></wa-icon>
</div>
<div class="wa-gap-s wa-stack">
<div class="wa-split wa-gap-2xs">
<span class="wa-heading-m">Insights and analytics</span>
<wa-badge appearance="filled outlined" variant="warning">Pro Plan</wa-badge>
</div>
<p class="wa-caption-l">Fast-track goals with advanced insights plus a dedicated customer success team to help drive effective learning.</p>
</div>
</div>
</wa-card>
<wa-card>
<div class="wa-flank">
<div>
<wa-icon name="puzzle-piece" style="font-size: 2.5rem;"></wa-icon>
</div>
<div class="wa-gap-s wa-stack">
<div class="wa-split wa-gap-2xs">
<span class="wa-heading-m">Customizable content</span>
<wa-badge appearance="filled outlined" variant="warning">Pro Plan</wa-badge>
</div>
<p class="wa-caption-l">Create tailored learning paths for team and organization goals and even host your own content and resources.</p>
</div>
</div>
</wa-card>
</div>
```
## 4 Column
```html{.example}
<div>
<div class="wa-grid">
<div class="wa-stack wa-gap-xs">
<wa-icon name="magnifying-glass" style="font-size: 32px;"></wa-icon>
<span class="wa-heading-s">SEO Consulting</span>
<p class="wa-caption-m">It's not actually free we just price it into the products. Someone's paying for it, and it's not us.</p>
</div>
<div class="wa-stack wa-gap-xs">
<wa-icon name="chalkboard-user" style="font-size: 32px;"></wa-icon>
<span class="wa-heading-s">In Person Training</span>
<p class="wa-caption-m">If it breaks in the first 10 years we'll replace it. After that you're on your own though.</p>
</div>
<div class="wa-stack wa-gap-xs">
<wa-icon name="people-arrows" style="font-size: 32px;"></wa-icon>
<span class="wa-heading-s">1 on 1 Sessions</span>
<p class="wa-caption-m">If you don't like it, trade it to one of your friends for something of theirs. Don't send it here though.</p>
</div>
<div class="wa-stack wa-gap-xs">
<wa-icon name="code" style="font-size: 32px;"></wa-icon>
<span class="wa-heading-s">Web Development</span>
<p class="wa-caption-m">If you don't like it, trade it to one of your friends for something of theirs. Don't send it here though.</p>
</div>
</div>
</div>
```

View File

@@ -1,190 +1,14 @@
---
title: Order History
description: 'Empower your customers to view past purchases and track upcoming orders with comprehensive order histories.'
description: TODO
parent: ecommerce
tags: e-commerce
---
## List
TODO Page Description
## Invoice panels
```html{.example}
```html {.example}
<div class="wa-stack wa-gap-2xl">
<h2>Order History</h2>
<p class="wa-caption-m">Check the status of recent orders, manage returns, and download invoices.</p>
<dl class="wa-split">
<span class="wa-stack wa-gap-0">
<dt>Order number</dt>
<dd>WU88191111</dd>
</span>
<span class="wa-stack wa-gap-0">
<dt>Date placed</dt>
<dd>January 22, 2021</dd>
</span>
<span class="wa-stack wa-gap-0">
<dt>Total amount</dt>
<dd>$590.00</dd>
</span>
<span class="wa-cluster">
<wa-button variant="neutral" appearance="outlined">View Order</wa-button>
<wa-button variant="neutral" appearance="outlined">View Invoice</wa-button>
</span>
</dl>
<wa-divider></wa-divider>
<div class="wa-flank" style="--flank-size: 12rem">
<div class="wa-frame wa-border-radius-s" style="aspect-ratio: 3 / 2">
<img
src="https://img.fortawesome.com/cfa83f3c/light-fixtures.jpg"
alt=""
/>
</div>
<div class="wa-stack">
<div class="wa-split">
<span><strong>Dome Light Fixtures</strong></span>
<span><strong>$215.00</strong></span>
</div>
<p class="wa-caption-m">Illuminate your space with elegance and style with stunning Dome Light Fixtures. The shape of these lights complements both modern and traditional interiors.</p>
<div class="wa-split">
<wa-badge appearance="filled" variant="success">Delivered</wa-badge>
<div class="wa-cluster">
<wa-button size="small" appearance="plain" variant="neutral">View Product</wa-button>
<wa-button size="small" appearance="accent" variant="brand">Buy Again</wa-button>
</div>
</div>
</div>
</div>
<wa-divider></wa-divider>
<div class="wa-flank" style="--flank-size: 12rem">
<div class="wa-frame wa-border-radius-s" style="aspect-ratio: 3 / 2">
<img
src="https://img.fortawesome.com/cfa83f3c/modern-chair.jpg"
alt=""
/>
</div>
<div class="wa-stack">
<div class="wa-split">
<span><strong>Reading Chair</strong></span>
<span><strong>$115.00</strong></span>
</div>
<p class="wa-caption-m">Add a pop of color and a touch of elegance to any room with our Reading Chair featuring vibrant yellow fabric upholstery.</p>
<div class="wa-split">
<wa-badge appearance="filled" variant="brand">Out for delivery</wa-badge>
<div class="wa-cluster">
<wa-button size="small" appearance="plain" variant="neutral">View Product</wa-button>
<wa-button size="small" appearance="accent" variant="brand">Buy Again</wa-button>
</div>
</div>
</div>
</div>
<wa-divider></wa-divider>
<div class="wa-flank" style="--flank-size: 12rem">
<div class="wa-frame wa-border-radius-s" style="aspect-ratio: 3 / 2">
<img
src="https://img.fortawesome.com/cfa83f3c/sofa.jpg"
alt=""
/>
</div>
<div class="wa-stack">
<div class="wa-split">
<span><strong>Custom Sofa</strong></span>
<span><strong>$260.00</strong></span>
</div>
<p class="wa-caption-m">Experience luxury and comfort like never before with our Custom Sofa, designed to elevate any living space. This sofa features exquisite velvet upholstery for an air of sophistication.</p>
<div class="wa-split">
<wa-badge appearance="filled" variant="neutral">Preparing to ship</wa-badge>
<div class="wa-cluster">
<wa-button size="small" appearance="plain" variant="neutral">View Product</wa-button>
<wa-button size="small" appearance="accent" variant="brand">Buy Again</wa-button>
</div>
</div>
</div>
</div>
</div>
```
## Invoice Table
```html {.example}
<div class="wa-stack wa-gap-2xl">
<wa-callout appearance="filled" variant="neutral">
<div class="wa-flank:end wa-align-items-center">
<dl class="wa-grid">
<div class="wa-stack wa-gap-0">
<dt>Date Placed</dt>
<dd>
<wa-format-date date="2021-01-22" month="long" day="numeric" year="numeric"></wa-format-date>
</dd>
</div>
<div class="wa-stack wa-gap-0">
<dt>Order Number</dt>
<dd>WU88191111</dd>
</div>
<div class="wa-stack wa-gap-0">
<dt>Total Amount</dt>
<dd>$590.00</dd>
</div>
</dl>
<wa-button>View Invoice</wa-button>
</div>
</wa-callout>
<table>
<thead>
<tr>
<th>Product</th>
<th>Price</th>
<th>Status</th>
<th>Info</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="wa-cluster wa-align-items-start">
<div class="wa-frame:landscape wa-border-radius-s" style="max-width: 8rem">
<img
src="https://img.fortawesome.com/cfa83f3c/light-fixtures.jpg"
alt=""
/>
</div>
<span>Dome Light Fixtures</span>
</div>
</td>
<td>$215.00</td>
<td>Delivered Jan 25, 2021</td>
<td><a href="">View</a></td>
</tr>
<tr>
<td>
<div class="wa-cluster wa-align-items-start">
<div class="wa-frame:landscape wa-border-radius-s" style="max-width: 8rem">
<img
src="https://img.fortawesome.com/cfa83f3c/modern-chair.jpg"
alt=""
/>
</div>
<span>Reading Chair</span>
</div>
</td>
<td>$115.00</td>
<td>Delivered Jan 25, 2021</td>
<td><a href="">View</a></td>
</tr>
<tr>
<td>
<div class="wa-cluster wa-align-items-start">
<div class="wa-frame:landscape wa-border-radius-s" style="max-width: 8rem">
<img
src="https://img.fortawesome.com/cfa83f3c/sofa.jpg"
alt=""
/>
</div>
<span>Custom Sofa</span>
</div>
</td>
<td>$260.00</td>
<td>Delivered Jan 25, 2021</td>
<td><a href="">View</a></td>
</tr>
</tbody>
</table>
</div>
```

View File

@@ -1,296 +1,13 @@
---
title: Order Summary
description: 'Give shoppers confidence in their purchases with summaries of everything included in their order.'
title: Product List
description: TODO
parent: ecommerce
tags: e-commerce
---
## Simple
```html {.example}
<div class="wa-stack wa-gap-xl">
<wa-callout variant="success">
<em>Payment Successful</em>
<wa-icon slot="icon" name="circle-check"></wa-icon>
</wa-callout>
<wa-card>
<div class="wa-stack wa-gap-xl">
<h2>Thank you for ordering from us!</h2>
<p class="wa-caption-l">We're processing your order now. A confirmation email will be sent to you momentarily!</p>
<dl class="wa-cluster">
<dt>Order #</dt>
<dd>49548790-24545</dd>
</dl>
<wa-divider></wa-divider>
<div class="wa-flank:end wa-align-items-start wa-gap-xl" style="--flank-size: 14em">
<div class="wa-stack wa-gap-xl">
<ul class="wa-stack wa-gap-xl">
<li class="wa-flank wa-align-items-start">
<div class="wa-frame wa-border-radius-s">
<img
src="https://uploads.webawesome.com/vase-1.jpg"
alt=""
/>
</div>
<div class="wa-split">
<span class="wa-heading-s">Spotted Flower Pot</span>
<span>$75.00</span>
</div>
</li>
<wa-divider></wa-divider>
<li class="wa-flank wa-align-items-start">
<div class="wa-frame wa-border-radius-s">
<img
src="https://uploads.webawesome.com/decorative-vase.jpg"
alt=""
/>
</div>
<div class="wa-split">
<span class="wa-heading-s">Decorative Vase</span>
<span>$51.00</span>
</div>
</li>
</ul>
<wa-divider></wa-divider>
<dl class="wa-stack wap-gap-2xs wa-caption-l">
<div class="wa-split">
<dt>Subtotal</dt>
<dd>$126.00</dd>
</div>
<div class="wa-split">
<dt>Shipping</dt>
<dd>$8.00</dd>
</div>
<div class="wa-split">
<dt>Taxes</dt>
<dd>$6.40</dd>
</div>
<div class="wa-split">
<dt>Total</dt>
<dd>$140.40</dd>
</div>
</dl>
</div>
<wa-callout variant="neutral" appearance="filled">
<dl class="wa-stack" style="margin: 0">
<dt>Shipping Address</dt>
<dd>
<address class="wa-stack wa-gap-2xs">
<span>Donna Noble</span>
<span>56 Front Street</span>
<span>Las Cruces, NM 56929</span>
</address>
</dd>
<dt>Payment Information</dt>
<dd class="wa-flank wa-gap-s">
<wa-icon label="Visa" class="wa-body-xl" family="brands" name="cc-visa" style="color: #224DBA;"></wa-icon>
<span>Ending with 9065</span>
</dd>
</dl>
</wa-callout>
</div>
</div>
</wa-card>
<wa-button size="large" variant="brand" appearance="plain">
<wa-icon slot="suffix" name="arrow-right" variant="solid"></wa-icon>
Continue Shopping
</wa-button>
</div>
```
## With Details
```html {.example}
<div class="wa-stack">
<h2>Order Details</h2>
<div class="wa-split">
<div class="wa-cluster">
<span>Order placed <wa-format-date date="2025-02-26T09:00:00-04:00" month="long" day="numeric" year="numeric"></wa-format-date></span>
<wa-divider vertical style="height: 2em"></wa-divider>
<span>Order # 45646456-4656-4542</span>
</div>
<wa-button size="small" appearance="outlined" pill>View Invoice</wa-button>
</div>
<wa-card>
<div class="wa-split wa-align-items-start">
<div class="wa-stack">
<h3 class="wa-heading-s">Shipping Address</h3>
<address class="wa-stack wa-gap-xs wa-caption-m">
<span>Johnny Blaze</span>
<span>200 Park Avenue</span>
<span>Manhattan, NY 45789-3412</span>
<span>United States</span>
</address>
<wa-button size="small" appearance="outlined" pill>Change</wa-button>
</div>
<div class="wa-stack">
<h3 class="wa-heading-s">Payment Method</h3>
<div class="wa-flank wa-gap-s">
<wa-icon class="wa-body-xl" family="brands" name="cc-visa" style="color: #224DBA;"></wa-icon>
<span class="wa-caption-m">Visa ending in 9542</span>
</div>
</div>
<div class="wa-stack">
<h3 class="wa-heading-s">Order Summary</h3>
<dl class="wa-stack wa-gap-xs wa-caption-m">
<div class="wa-split">
<dt>Item(s) Subtotal</dt>
<dd>$39.00</dd>
</div>
<div class="wa-split">
<dt>Shipping & Handling</dt>
<dd>$0.00</dd>
</div>
<div class="wa-split">
<dt>Pre-tax Total</dt>
<dd>$39.00</dd>
</div>
<div class="wa-split">
<dt>Tax</dt>
<dd>$39.00</dd>
</div>
<wa-divider></wa-divider>
<div class="wa-split wa-body-m">
<dt>Grand Total</dt>
<dd>$39.00</dd>
</div>
</dl>
</div>
</div>
</wa-card>
<wa-card>
<div class="wa-flank:end wa-align-items-start" style="--flank-size: 12rem">
<div class="wa-stack">
<h3 class="wa-heading-s">Arriving Saturday</h3>
<div class="wa-flank wa-align-items-start">
<div class="wa-frame wa-border-radius-s">
<img
src="https://uploads.webawesome.com/sparkling-water.jpg"
alt=""
/>
</div>
<div class="wa-stack">
<a href="" class="wa-caption-m">Mineragua Sparkling Water 12 Count</a>
<span class="wa-caption-s">Sold by: <a href="">Mineragua</a></span>
<div class="wa-cluster">
<span class="wa-heading-s">$39.00</span>
<wa-button appearance="outlined" size="small" pill>
<wa-icon slot="prefix" name="rotate" variant="solid"></wa-icon>
Buy Again
</wa-button>
</div>
</div>
</div>
</div>
<div class="wa-stack wa-gap-xs">
<wa-button size="small" variant="brand" pill>Track Package</wa-button>
<wa-button size="small" appearance="outlined" variant="neutral" pill>Cancel Item(s)</wa-button>
<wa-button size="small" appearance="outlined" variant="neutral" pill>Ask Question</wa-button>
<wa-button size="small" appearance="outlined" variant="neutral" pill>Write Review</wa-button>
</div>
</div>
</wa-card>
</div>
```
## With Status & Description
```html {.example}
<div class="wa-stack wa-gap-xl">
<div class="wa-split">
<div class="wa-cluster">
<h2>Order #7093</h2>
<a href="">View Invoice</a>
</div>
<p class="wa-caption-m">Order placed <wa-format-date date="2025-06-12T09:00:00-04:00" month="long" day="numeric" year="numeric"></wa-format-date></p>
</div>
<wa-card>
<div class="wa-flank wa-align-items-start">
<div class="wa-frame wa-border-radius-s">
<img
src="https://uploads.webawesome.com/vase-1.jpg"
alt=""
/>
</div>
<div class="wa-stack wa-align-items-start wa-gap-s">
<div class="wa-split wa-gap-s">
<h3 class="wa-heading-m">Spotted Flower Pot</h3>
<span>$75.00</span>
</div>
<p class="wa-caption-m">Wood fired, salt glaze</p>
<wa-tag variant="success" appearance="filled" size="small">Delivered</wa-tag>
</div>
</div>
</wa-card>
<wa-card>
<div class="wa-flank wa-align-items-start">
<div class="wa-frame wa-border-radius-s">
<img
src="https://uploads.webawesome.com/decorative-vase.jpg"
alt=""
/>
</div>
<div class="wa-stack wa-align-items-start wa-gap-s">
<div class="wa-split wa-gap-s">
<h3 class="wa-heading-m">Decorative Vase</h3>
<span>$51.00</span>
</div>
<p class="wa-caption-m">High quality Japanese Kutani-yaki ceramic-ware</p>
<wa-tag variant="neutral" appearance="filled" size="small">Shipping Soon</wa-tag>
</div>
</div>
</wa-card>
<wa-card>
<div class="wa-flank wa-align-items-start">
<div class="wa-frame wa-border-radius-s">
<img
src="https://uploads.webawesome.com/cuong-duyen-ceramic.jpg"
alt=""
/>
</div>
<div class="wa-stack wa-align-items-start wa-gap-s">
<div class="wa-split wa-gap-s">
<h3 class="wa-heading-m">Cuong Duyen Ceramic</h3>
<span>$48.00</span>
</div>
<p class="wa-caption-m">Koishiwara-yaki style with crystalline glaze</p>
<wa-tag variant="brand" appearance="filled" size="small">Out for Delivery</wa-tag>
</div>
</div>
</wa-card>
<wa-divider></wa-divider>
<wa-callout variant="neutral" appearance="filled">
<div class="wa-grid">
<div class="wa-stack">
<h3 class="wa-heading-s">Shipping Address</h3>
<address class="wa-stack wa-gap-xs wa-caption-m">
<span>Donna Noble</span>
<span>56 Front Street</span>
<span>Las Cruces, NM 56929</span>
</address>
</div>
<div class="wa-stack">
<h3 class="wa-heading-s">Order Summary</h3>
<dl class="wa-stack wa-gap-xs wa-caption-m">
<div class="wa-split">
<dt>Item(s) Subtotal</dt>
<dd>$174.00</dd>
</div>
<div class="wa-split">
<dt>Shipping & Handling</dt>
<dd>$0.00</dd>
</div>
<div class="wa-split">
<dt>Tax</dt>
<dd>$17.40</dd>
</div>
<wa-divider></wa-divider>
<div class="wa-split wa-body-m">
<dt>Total</dt>
<dd>$191.40</dd>
</div>
</dl>
</div>
</div>
</wa-callout>
</div>
TODO Page Description
## With split image
```html{.example}
```

View File

@@ -0,0 +1,129 @@
---
title: Product Detail
description: TODO
parent: ecommerce
tags: e-commerce
---
TODO Page Description
## With color and size selector
```html{.example}
<div class="with-inline-price">
<wa-card with-header>
<div class="card-header" slot="header">
<span class="card-title">Graphic Tank</span>
<wa-icon-button name="close" label="close-modal"></wa-icon-button>
</div>
<div class="card-body">
<img style="border-radius: var(--border-radius)" src="/assets/images/patterns/gervyn-louis-IS03ajI00Fc-unsplash.jpg" />
<form class="detail">
<span class="price">$32</span>
<span class="rating"><wa-rating></wa-rating><a style="margin-left: .5rem; " href="*">36 Reviews</a></span>
<wa-radio-group style="margin-bottom: 1rem;" label="Select an option" name="a" value="1">
<wa-radio-button value="Black">Black</wa-radio-button>
<wa-radio-button value="White">White</wa-radio-button>
<wa-radio-button value="Gray">Gray</wa-radio-button>
</wa-radio-group>
<wa-select label="Sizes" placeholder="select size">
<wa-option value="option-1">Option 1</wa-option>
<wa-option value="option-2">Option 2</wa-option>
<wa-option value="option-3">Option 3</wa-option>
</wa-select>
<wa-button size="medium" style="width: 100%; margin-top: auto;">Medium</wa-button>
</form>
</div>
</wa-card>
</div>
<style>
.with-inline-price {
wa-card {
width: 100%;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
.card-title {
font-size: large;
font-weight: 600;
}
}
.card-body {
display: grid;
grid-template-columns: 35% 1fr;
gap: 1rem;
.detail {
display: flex;
flex-direction: column;
.price {
font-size: xx-large;
font-weight: 600;
}
.rating {
margin-bottom: 1rem;
}
}
}
}
}
</style>
```
## with large selector
```html{.example}
<wa-card class="large-selector">
<div class="card-body">
<div style="grid-column: 1/6">
<img style="border-radius: var(--border-radius); height: 100%; object-fit: cover;" src="/assets/images/patterns/gervyn-louis-IS03ajI00Fc-unsplash.jpg" />
</div>
<div style="grid-column: 6/-1" class="info">
<h2>Basic Tank</h2>
<wa-icon-button name="close" label="close-modal"></wa-icon-button>
<section>
<p style="font-size: x-large;font-weight: 600;">$32</p>
<div style="display: flex; align-items: flex-start">
<p>3.9</p>
<wa-rating></wa-rating>
<a href="*" style="margin-left: auto;">See all 512 Reviews</a>
</div>
</section>
<section>
<form>
<wa-radio-group label="Color" hint="Choose the most appropriate option." name="a" value="black" style="margin-bottom: 1rem;">
<wa-radio value="black">Black</wa-radio>
<wa-radio value="gray">Gray</wa-radio>
</wa-radio-group>
<wa-radio-group label="Size" hint="Select an option that makes you proud." name="a" value="medium" style="margin-bottom: 1rem;">
<wa-radio-button value="small">S</wa-radio-button>
<wa-radio-button value="medium">M</wa-radio-button>
<wa-radio-button value="large">L</wa-radio-button>
<wa-radio-button value="extra-large">XL</wa-radio-button>
</wa-radio-group>
<wa-button size="medium" style="width: 100%; margin-top: auto;margin-bottom: 1rem;">Medium</wa-button>
<a href="*" style="display: inline-block;width: 100%;text-align: center;">View full details</a>
</form>
</section>
</div>
</div>
</wa-card>
<style>
.large-selector .card-body {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 1rem;
.info {
position: relative;
wa-icon-button {
position: absolute;
top: 0;
right: 0;
}
}
}
</style>
```

View File

@@ -1,184 +1,166 @@
---
title: Product Lists
description: 'Let shoppers browse and compare products with detailed lists of the products in your store.'
description: TODO
parent: ecommerce
tags: e-commerce
---
## Simple Grid with Ratings
TODO Page Description
## With Product Grid
```html {.example}
<div class="wa-grid wa-gap-2xl">
<a class="wa-stack wa-align-items-center wa-gap-xs wa-link-plain" href="">
<div class="wa-frame wa-border-radius-m">
<img
src="https://images.unsplash.com/photo-1633933329864-5d4c4423ad54?q=80&w=3387&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="Bunch of fresh basil leaves with purple veins (Photograph by Svitlana)"
/>
```html{.example}
<div class="with-product-grid">
<div class="grid-item">
<img class="grid-item-image" src="/assets/images/patterns/mad-rabbit-tattoo-7N4FMowSGek-unsplash.jpg" />
<div class="grid-item-name">Shirt</div>
<wa-rating label="Rating" readonly value="3"></wa-rating>
<a class="grid-item-reviews" href="#">38 Reviews</a>
<div class="grid-item-price">$170</div>
</div>
<strong>Basil</strong>
<wa-rating label="Rating" size="small" readonly value="5"></wa-rating>
<span class="wa-caption-m">41 Reviews</span>
<strong>$8.59</strong>
</a>
<a class="wa-stack wa-align-items-center wa-gap-xs wa-link-plain" href="">
<div class="wa-frame wa-border-radius-m">
<img
src="https://images.unsplash.com/photo-1662892194342-f95c33cc16e3?q=80&w=3000&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="Bunch of cut chamomile blooms (Photograph by Rootnot Creations)"
/>
<div class="grid-item">
<img class="grid-item-image" src="/assets/images/patterns/mad-rabbit-tattoo-7N4FMowSGek-unsplash.jpg" />
<div class="grid-item-name">Shirt</div>
<wa-rating label="Rating" readonly value="3"></wa-rating>
<a class="grid-item-reviews" href="#">38 Reviews</a>
<div class="grid-item-price">$170</div>
</div>
<strong>Chamomile</strong>
<wa-rating label="Rating" size="small" readonly value="3"></wa-rating>
<span class="wa-caption-m">17 Reviews</span>
<strong>$10.29</strong>
</a>
<a class="wa-stack wa-align-items-center wa-gap-xs wa-link-plain" href="">
<div class="wa-frame wa-border-radius-m">
<img
src="https://images.unsplash.com/photo-1636396279461-f875646332d9?q=80&w=3360&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="Canvas bundle of cut lavender blooms (Photograph by volant)"
/>
<div class="grid-item">
<img class="grid-item-image" src="/assets/images/patterns/mad-rabbit-tattoo-7N4FMowSGek-unsplash.jpg" />
<div class="grid-item-name">Shirt</div>
<wa-rating label="Rating" readonly value="3"></wa-rating>
<a class="grid-item-reviews" href="#">38 Reviews</a>
<div class="grid-item-price">$170</div>
</div>
<strong>Lavender</strong>
<wa-rating label="Rating" size="small" readonly value="4"></wa-rating>
<span class="wa-caption-m">29 Reviews</span>
<strong>$9.99</strong>
</a>
<a class="wa-stack wa-align-items-center wa-gap-xs wa-link-plain" href="">
<div class="wa-frame wa-border-radius-m">
<img
src="https://images.unsplash.com/photo-1501085934018-450c8e615dbc?q=80&w=3387&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="Blooming marjoram plant (Photograph by Monika Grabkowska)"
/>
<div class="grid-item">
<img class="grid-item-image" src="/assets/images/patterns/mad-rabbit-tattoo-7N4FMowSGek-unsplash.jpg" />
<div class="grid-item-name">Shirt</div>
<wa-rating label="Rating" readonly value="3"></wa-rating>
<a class="grid-item-reviews" href="#">38 Reviews</a>
<div class="grid-item-price">$170</div>
</div>
<strong>Marjoram</strong>
<wa-rating label="Rating" size="small" readonly value="4"></wa-rating>
<span class="wa-caption-m">11 Reviews</span>
<strong>$8.59</strong>
</a>
<a class="wa-stack wa-align-items-center wa-gap-xs wa-link-plain" href="">
<div class="wa-frame wa-border-radius-m">
<img
src="https://images.unsplash.com/photo-1688633767797-455f59c98272?q=80&w=3540&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="Group of mature oregano plants (Photograph Nikolett Emmert)"
/>
</div>
<strong>Oregano</strong>
<wa-rating label="Rating" size="small" readonly value="5"></wa-rating>
<span class="wa-caption-m">38 Reviews</span>
<strong>$8.59</strong>
</a>
<a class="wa-stack wa-align-items-center wa-gap-xs wa-link-plain" href="">
<div class="wa-frame wa-border-radius-m">
<img
src="https://images.unsplash.com/photo-1603109731710-dba41b1096a7?q=80&w=2259&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="Cluster of peppermint plants (Photograph by Josefin)"
/>
</div>
<strong>Peppermint</strong>
<wa-rating label="Rating" size="small" readonly value="5"></wa-rating>
<span class="wa-caption-m">26 Reviews</span>
<strong>$9.99</strong>
</a>
<a class="wa-stack wa-align-items-center wa-gap-xs wa-link-plain" href="">
<div class="wa-frame wa-border-radius-m">
<img
src="https://images.unsplash.com/photo-1726994803809-0e065bd4b25b?q=80&w=3387&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="Mature rosemary stems (Photograph by 360floralflaves)"
/>
</div>
<strong>Rosemary</strong>
<wa-rating label="Rating" size="small" readonly value="4"></wa-rating>
<span class="wa-caption-m">34 Reviews</span>
<strong>$8.59</strong>
</a>
<a class="wa-stack wa-align-items-center wa-gap-xs wa-link-plain" href="">
<div class="wa-frame wa-border-radius-m">
<img
src="https://images.unsplash.com/photo-1659834742696-44573974981b?q=80&w=3542&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="Group of sage plants (Photograph by Susie Burleson)"
/>
</div>
<strong>Sage</strong>
<wa-rating label="Rating" size="small" readonly value="5"></wa-rating>
<span class="wa-caption-m">24 Reviews</span>
<strong>$9.29</strong>
</a>
</div>
</div>
<style>
.with-product-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
border: var(--wa-panel-border-width) var(--wa-border-style) var(--wa-color-neutral-border-quiet);
.grid-item {
padding: 1.5rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.grid-item:nth-of-type(odd) {
border-right: var(--wa-panel-border-width) var(--wa-border-style) var(--wa-color-neutral-border-quiet);
}
.grid-item:not(:nth-last-child(-n + 2)) {
border-bottom: var(--wa-panel-border-width) var(--wa-border-style) var(--wa-color-neutral-border-quiet);
}
.grid-item-image {
width: 100%;
object-fit: cover;
}
.grid-item-name {
margin-top: 1rem;
font-weight: var(--wa-font-weight-bold);
}
.grid-item wa-rating {
--symbol-size: var(--wa-font-size-m);
margin-top: .5rem;
}
.grid-item-reviews {
--wa-link-decoration-default: none;
--wa-color-text-link: var(--wa-color-gray-50);
font-size: var(--wa-font-size-m);
}
.grid-item-price {
font-size: var(--wa-font-size-2xl);
font-weight: var(--wa-font-weight-bold);
}
}
</style>
```
## Even Card Grid with Details
```html {.example}
<div class="wa-grid" style="--min-column-size: 50ch">
<div class="wa-grid">
<a href="" class="wa-link-plain">
<wa-card>
<img slot="image"
src="https://images.unsplash.com/photo-1622445272461-c6580cab8755?q=80&w=3387&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="Man in a relaxed fit, white, crew neck t-shirt (Photography by Mediamodifier)"
/>
<div class="wa-stack">
<div class="wa-flank:end wa-align-items-start wa-heading-m">
<span>Plain Classic Tee</span>
<span>$24</span>
</div>
<p class="wa-caption-m">Keep it casual or dress it up. Soft, 100% cotton with a crew neckline, perfect for any occasion.</p>
<em class="wa-caption-m">8 colors</em>
</div>
</wa-card>
</a>
<a href="" class="wa-link-plain">
<wa-card>
<img slot="image"
src="https://images.unsplash.com/photo-1554568218-0f1715e72254?q=80&w=3387&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="Woman in a light heather t-shirt printed with sharp black ink (Photograph by Christian Bolt)"
/>
<div class="wa-stack">
<div class="wa-flank:end wa-align-items-start wa-heading-m">
<span>One-color Graphic Tee</span>
<span>$32</span>
</div>
<p class="wa-caption-m">Your own spin on our classic tee. Hand screen printed for the ultimate accuracy and quality.</p>
<em class="wa-caption-m">6 colors</em>
</div>
</wa-card>
</a>
## Card with full details
```html{.example}
<div class="card-with-details">
<wa-card with-footer>
<img class="grid-item-image" src="/assets/images/patterns/mad-rabbit-tattoo-7N4FMowSGek-unsplash.jpg" />
<div slot="footer" class="card-footer details">
<span class="detail-name">Basic Tee 8-pack</span>
<p class="detail-description">Get the full lineup of our Basic Tees. Have a fresh shirt all week, and an extra for laundry day.</p>
<span class="detail-color">8 colors</span>
<span class="detail-price">$256</span>
</div>
</wa-card>
<wa-card with-footer>
<img class="grid-item-image" src="/assets/images/patterns/mad-rabbit-tattoo-7N4FMowSGek-unsplash.jpg" />
<div slot="footer" class="card-footer details">
<span class="detail-name">Basic Tee 8-pack</span>
<p class="detail-description">Get the full lineup of our Basic Tees. Have a fresh shirt all week, and an extra for laundry day.</p>
<span class="detail-color">8 colors</span>
<span class="detail-price">$256</span>
</div>
</wa-card>
<wa-card with-footer>
<img class="grid-item-image" src="/assets/images/patterns/mad-rabbit-tattoo-7N4FMowSGek-unsplash.jpg" />
<div slot="footer" class="card-footer details">
<span class="detail-name">Basic Tee 8-pack</span>
<p class="detail-description">Get the full lineup of our Basic Tees. Have a fresh shirt all week, and an extra for laundry day.</p>
<span class="detail-color">8 colors</span>
<span class="detail-price">$256</span>
</div>
</wa-card>
<wa-card with-footer>
<img class="grid-item-image" src="/assets/images/patterns/mad-rabbit-tattoo-7N4FMowSGek-unsplash.jpg" />
<div slot="footer" class="card-footer details">
<span class="detail-name">Basic Tee 8-pack</span>
<p class="detail-description">Get the full lineup of our Basic Tees. Have a fresh shirt all week, and an extra for laundry day.</p>
<span class="detail-color">8 colors</span>
<span class="detail-price">$256</span>
</div>
</wa-card>
</div>
<div class="wa-grid">
<a href="" class="wa-link-plain">
<wa-card>
<img slot="image"
src="https://images.unsplash.com/photo-1567098260939-5d9cee055592?q=80&w=2832&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="Man in a black t-shirt printed with many-colored gradients (Photograph by Marcel)"
/>
<div class="wa-stack">
<div class="wa-flank:end wa-align-items-start wa-heading-m">
<span>Multi-color Graphic Tee</span>
<span>$36</span>
</div>
<p class="wa-caption-m">Make a statement. Screen printed with vibrant, quality inks to last wash after wash.</p>
<em class="wa-caption-m">4 colors</em>
</div>
</wa-card>
</a>
<a href="" class="wa-link-plain">
<wa-card>
<img slot="image"
src="https://images.unsplash.com/photo-1709185727063-c3caae752a64?q=80&w=3387&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="Woman in a black t-shirt with a bright white logo printed on the pocket (Photograph by SASI)"
/>
<div class="wa-stack">
<div class="wa-flank:end wa-align-items-start wa-heading-m">
<span>Pocket Graphic Tee</span>
<span>$29</span>
</div>
<p class="wa-caption-m">Go classic with a bit of your own flair. Screen printed, eye-catching detail on the pocket.</p>
<em class="wa-caption-m">6 colors</em>
</div>
</wa-card>
</a>
</div>
</div>
```
<style>
.card-with-details {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.card-with-details wa-card::part(body) {
padding: 0;
}
.card-with-details .card-footer {
display: flex;
flex-direction: column;
}
.details {
.detail-description {
color: var(--wa-color-gray-50);
}
.detail-name {
font-size: var(--wa-font-size-l);
font-weight: var(--wa-font-weight-action);
}
.detail-color {
color: var(--wa-color-gray-50);
font-style: italic;
}
.detail-price {
font-size: var(--wa-font-size-xl);
font-weight: var(--wa-font-weight-action);
}
}
</style>
```
## With color swatches (WIP)
```html{.example}
```

View File

@@ -1,431 +0,0 @@
---
title: Product Overview
description: 'Showcase your products with overviews including images, ratings, features, options, and more.'
parent: ecommerce
tags: e-commerce
---
## Split with Image
```html {.example}
<div class="wa-grid wa-gap-2xl">
<div class="wa-stack wa-gap-2xl">
<h2>San Ignacio Pache</h2>
<p>A smooth, balanced Arabica varietal, grown and roasted on the Guerrero family's farm. Rich caramel and malt flavors blend with bright citrus for a complex brew suitable for drip, pour over, espresso, or however you take your coffee.</p>
<div class="wa-stack">
<wa-select label="Bag Size" value="12oz">
<wa-option value="12oz">12 oz &ndash; $19.95</wa-option>
<wa-option value="3lb">3 lb &ndash; $72.00</wa-option>
<wa-option value="5lb">5 lb &ndash; $99.75</wa-option>
</wa-select>
<wa-select label="Bean Type" value="whole">
<wa-option value="whole">Whole</wa-option>
<wa-option value="drip">Drip Grind</wa-option>
<wa-option value="espresso">Espresso Grind</wa-option>
</wa-select>
</div>
<div class="wa-stack">
<div class="wa-flank">
<wa-input type="number" aria-label="Quantity" value="1" min="1" style="max-width: 8ch"></wa-input>
<wa-button variant="brand">
<wa-icon slot="prefix" name="basket-shopping"></wa-icon>
Add to Basket
</wa-button>
</div>
<div class="wa-flank wa-caption-m">
<wa-icon name="truck"></wa-icon>
<span>Free shipping on orders over $60</span>
</div>
</div>
<dl class="wa-grid">
<div class="wa-flank">
<wa-avatar>
<wa-icon slot="icon" name="coffee-bean"></wa-icon>
</wa-avatar>
<div class="wa-stack wa-gap-0">
<dt>Roast</dt>
<dd>Medium</dd>
</div>
</div>
<div class="wa-flank">
<wa-avatar>
<wa-icon slot="icon" name="earth-americas"></wa-icon>
</wa-avatar>
<div class="wa-stack wa-gap-0">
<dt>Origin</dt>
<dd>San Ignacio, Peru</dd>
</div>
</div>
<div class="wa-flank">
<wa-avatar>
<wa-icon slot="icon" name="sun-haze"></wa-icon>
</wa-avatar>
<div class="wa-stack wa-gap-0">
<dt>Process</dt>
<dd>Washed</dd>
</div>
</div>
<div class="wa-flank">
<wa-avatar>
<wa-icon slot="icon" name="mug-hot"></wa-icon>
</wa-avatar>
<div class="wa-stack wa-gap-0">
<dt>Tasting Notes</dt>
<dd>Caramel, malt, orange</dd>
</div>
</div>
</dl>
</div>
<div class="wa-frame wa-border-radius-m">
<img
src="https://images.unsplash.com/photo-1600396538702-d234dbb79139?q=80&w=3833&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="Whole roasted coffee beans (Photograph by Jocelyn Morales)"
/>
</div>
</div>
```
## With Image Grid
```html {.example}
<div class="wa-stack wa-gap-2xl">
<wa-breadcrumb>
<wa-breadcrumb-item>Clothing</wa-breadcrumb-item>
<wa-breadcrumb-item>Women's</wa-breadcrumb-item>
<wa-breadcrumb-item>Shirts &amp; Tops</wa-breadcrumb-item>
</wa-breadcrumb>
<div class="wa-grid wa-gap-xs" style="--min-column-size: 10ch">
<div class="wa-frame" style="height: 100%; width: 100%">
<img class="wa-border-radius-s"
src="https://images.unsplash.com/photo-1614792568992-ded1c487c1dd?q=80&w=2487&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="(Photograph by Patrick Perkins)"
/>
</div>
<div class="wa-grid wa-gap-xs">
<div class="wa-frame" style="aspect-ratio: 3 / 2">
<img class="wa-border-radius-s"
src="https://images.unsplash.com/photo-1614725078749-29c421fd0e51?q=80&w=2487&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="(Photograph by Patrick Perkins)"
/>
</div>
<div class="wa-frame" style="aspect-ratio: 3 / 2">
<img class="wa-border-radius-s"
src="https://images.unsplash.com/photo-1614725808713-e6bbe418fc5d?q=80&w=2487&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="(Photograph by Patrick Perkins)"
/>
</div>
</div>
<div class="wa-frame" style="height: 100%; width: 100%">
<img class="wa-border-radius-s"
src="https://images.unsplash.com/photo-1614725078379-9d1330a08c95?q=80&w=2487&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="(Photograph by Patrick Perkins)"
/>
</div>
</div>
<div class="wa-grid wa-gap-xl" style="--min-column-size: 30ch">
<h2 class="wa-heading-l">Cropped Fitted Tank Top</h2>
<span class="wa-body-xl">$59</span>
</div>
<div class="wa-grid wa-gap-xl" style="--min-column-size: 30ch">
<div class="wa-stack wa-gap-xl">
<div class="wa-cluster">
<wa-rating label="Rating" readonly value="3.5"></wa-rating>
<a href="">117 Reviews</a>
</div>
<p>Made with a breathable, stretchy fabric blend for unparalleled comfort and flattering style. Pairs perfectly with your favorite high-waisted jeans for lazy summer weekends or lively nights out.</p>
<div class="wa-stack wa-gap-xs">
<h3 class="wa-heading-xs">Good to Know</h3>
<p class="wa-body-xs">95% cotton, 5% elastane. Our tops are pre-shrunk to ensure a consistent fit with no surprises. Machine wash cold. Tumble dry low.</p>
</div>
</div>
<div class="wa-stack">
<wa-radio-group label="Color" name="color" value="black" orientation="horizontal">
<wa-radio-button id="radio-black" value="black">
<wa-icon name="square" label="Black" style="color: black;"></wa-icon>
<wa-tooltip for="radio-black">Black</wa-tooltip>
</wa-radio-button>
<wa-radio-button id="radio-gray" value="gray">
<wa-icon name="square" label="Gray" style="color: gray;"></wa-icon>
<wa-tooltip for="radio-gray">Gray</wa-tooltip>
</wa-radio-button>
<wa-radio-button id="radio-indigo" value="indigo">
<wa-icon name="square" label="Indigo" style="color: indigo;"></wa-icon>
<wa-tooltip for="radio-indigo">Indigo</wa-tooltip>
</wa-radio-button>
<wa-radio-button id="radio-olive" value="olive">
<wa-icon name="square" label="Olive" style="color: olive;"></wa-icon>
<wa-tooltip for="radio-olive">Olive</wa-tooltip>
</wa-radio-button>
</wa-radio-group>
<wa-radio-group label="Size" name="size" value="s" orientation="horizontal">
<wa-radio-button value="xs">XS</wa-radio-button>
<wa-radio-button value="s">S</wa-radio-button>
<wa-radio-button value="m">M</wa-radio-button>
<wa-radio-button value="l">L</wa-radio-button>
<wa-radio-button value="xl">XL</wa-radio-button>
</wa-radio-group>
<wa-button variant="brand">
<wa-icon slot="prefix" name="cart-plus" variant="solid"></wa-icon>
Add to Cart
</wa-button>
</div>
</div>
</div>
```
## With Tiered Images
```html {.example}
<div class="wa-stack wa-gap-2xl">
<wa-breadcrumb>
<wa-breadcrumb-item>Clothing</wa-breadcrumb-item>
<wa-breadcrumb-item>Men's</wa-breadcrumb-item>
<wa-breadcrumb-item>Shirts &amp; Tops</wa-breadcrumb-item>
</wa-breadcrumb>
<div class="wa-grid wa-gap-2xl" style="--min-column-size: 35ch">
<div class="wa-stack wa-gap-xs">
<img class="wa-border-radius-s"
src="https://images.unsplash.com/photo-1630643583573-c68623718072?q=80&w=3387&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="(Photograph by Gervyn Louis)"
/>
<div class="wa-grid wa-gap-xs" style="--min-column-size: 0ch">
<img class="wa-border-radius-s"
src="https://images.unsplash.com/photo-1571666274590-f8cc87006500?q=80&w=3387&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="(Photograph by Gervyn Louis)"
/>
<img class="wa-border-radius-s"
src="https://images.unsplash.com/photo-1630643591760-a6ed60ef499f?q=80&w=3387&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="(Photograph by Gervyn Louis)"
/>
</div>
</div>
<div class="wa-stack">
<div class="wa-split">
<h2 class="wa-heading-l">Graphic Cutoff Tee</h2>
<span class="wa-body-xl">$65</span>
</div>
<div class="wa-split">
<div class="wa-cluster">
<wa-rating label="Rating" readonly value="4.2"></wa-rating>
<span>4.2</span>
</div>
<a href="#">144 Reviews</a>
</div>
<wa-radio-group label="Color" name="color" value="black" orientation="horizontal">
<wa-radio-button value="black">
<wa-icon slot="prefix" name="shirt" style="color: black;"></wa-icon>
Vintage Black
</wa-radio-button>
<wa-radio-button value="gray">
<wa-icon slot="prefix" name="shirt" style="color: gray;"></wa-icon>
Faded Gray
</wa-radio-button>
</wa-radio-group>
<wa-radio-group label="Size" name="size" value="s" orientation="horizontal">
<wa-radio-button value="xs">XS</wa-radio-button>
<wa-radio-button value="s">S</wa-radio-button>
<wa-radio-button value="m">M</wa-radio-button>
<wa-radio-button value="l">L</wa-radio-button>
<wa-radio-button value="xl">XL</wa-radio-button>
</wa-radio-group>
<wa-button variant="brand">
<wa-icon slot="prefix" name="bag-shopping" variant="solid"></wa-icon>
Add to Bag
</wa-button>
<wa-divider></wa-divider>
<h3 class="wa-heading-s">Description</h3>
<p>Stay cool, <em>slay</em> cool. Train hard and recover in style with this ultra-breathable cutoff tee. Made from 100% organic, quick-drying cotton to keep the air flowing whether you're lifting, sprinting, or crushing HIIT sessions.</p>
<wa-divider></wa-divider>
<h3 class="wa-heading-s">Highlights</h3>
<div class="wa-grid">
<wa-card class="wa-span-grid">
<div class="wa-stack">
<wa-icon name="hand-holding-heart"></wa-icon>
<h4 class="wa-heading-s">People and Planet First</h4>
<p class="wa-caption-m">Ethical production, fair wages, and sustainable materials empower every part of our supply chain.</p>
</div>
</wa-card>
<wa-card>
<div class="wa-stack">
<wa-icon name="earth-americas"></wa-icon>
<h4 class="wa-heading-s">International Shipping</h4>
<p class="wa-caption-m">Wherever you are, your order will meet you there.</p>
</div>
</wa-card>
<wa-card>
<div class="wa-stack">
<wa-icon name="arrow-right-arrow-left"></wa-icon>
<h4 class="wa-heading-s">90-day Returns</h4>
<p class="wa-caption-m">Not happy? Return your item and get a full refund.</p>
</div>
</wa-card>
</div>
</div>
</div>
</div>
```
## With Carousel and Collapsible Details
```html {.example}
<div class="wa-stack wa-gap-2xl">
<wa-carousel pagination navigation loop style="--aspect-ratio: 3 / 2;">
<wa-carousel-item>
<img
src="https://images.unsplash.com/photo-1601379327928-bedfaf9da2d0?q=80&w=3456&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="Four folded and stacked knit sweaters in three colors (Photograph by Tijana Drndarski)"
/>
</wa-carousel-item>
<wa-carousel-item>
<img
src="https://images.unsplash.com/photo-1519804270019-39e929a7afb5?q=80&w=3774&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="Knit sweater in sand color full view, showing waffle knit pattern, relaxed fit, and crew neckline (Photograph by Jonathan Zerger)"
/>
</wa-carousel-item>
<wa-carousel-item>
<img
src="https://images.unsplash.com/photo-1519805614447-6f49142e6697?q=80&w=3633&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="Knit sweater in sand color shoulder detail, showing relaxed fit on broader shoulders (Photograph by Jonathan Zerger)"
/>
</wa-carousel-item>
<wa-carousel-item>
<img
src="https://images.unsplash.com/photo-1522230130022-498e355165c5?q=80&w=3774&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="Knit sweater in sand color sleeve detail, showing loose fit around the arms (Photograph by Jonathan Zerger)"
/>
</wa-carousel-item>
</wa-carousel>
<div class="wa-grid wa-gap-2xl" style="--min-column-size: 30ch;">
<div class="wa-stack">
<div class="wa-split">
<h3>Pullover Sweater</h3>
<span class="wa-body-xl">$140</span>
</div>
<wa-rating label="Rating" precision="0.5" value="4.5" readonly></wa-rating>
<p>Wrap yourself in warmth and effortless style with this wool knit Pullover Sweater. Designed for unparalleled comfort. The relaxed fit and classic crew neckline make it a versatile staple for layering or wearing solo.</p>
<wa-radio-group label="Color" name="color" value="sand" orientation="horizontal">
<wa-radio-button value="sand">
<wa-icon slot="prefix" name="circle" style="color: burlywood;"></wa-icon>
Sand
</wa-radio-button>
<wa-radio-button value="shale">
<wa-icon slot="prefix" name="circle" style="color: silver;"></wa-icon>
Shale
</wa-radio-button>
<wa-radio-button value="slate">
<wa-icon slot="prefix" name="circle" style="color: dimgray;"></wa-icon>
Slate
</wa-radio-button>
</wa-radio-group>
<wa-radio-group label="Size" name="size" value="s" orientation="horizontal">
<wa-radio-button value="xs">XS</wa-radio-button>
<wa-radio-button value="s">S</wa-radio-button>
<wa-radio-button value="m">M</wa-radio-button>
<wa-radio-button value="l">L</wa-radio-button>
<wa-radio-button value="xl">XL</wa-radio-button>
</wa-radio-group>
<wa-button variant="brand">Add to Cart</wa-button>
</div>
<div class="wa-stack">
<wa-details summary="Size and Fit" open>
<ul class="wa-body-s">
<li>True to size with a relaxed fit</li>
<li>Fits all shoulder shapes, broad to narrow</li>
<li>No pinching in the arms or irritating seams</li>
<li>Ribbed cuffs and hem</li>
</ul>
</wa-details>
<wa-details summary="Materials and Care">
<ul class="wa-body-s">
<li>Durable Merino and Yak wool blend</li>
<li>Machine wash cold on delicate cycle</li>
<li>Lay flat to dry</li>
<li>Made with <wa-icon name="heart" label="love"></wa-icon> in Bentonville, USA</li>
</ul>
</wa-details>
<wa-details summary="Shipping">
<ul class="wa-body-s">
<li>Flat $9 shipping free for orders under $200.</li>
<li>Free shipping on orders over $200, anywhere in the world.</li>
</ul>
</wa-details>
</div>
</div>
</div>
```
## With Tabs
```html {.example}
<div class="wa-flank:end wa-align-items-start wa-gap-2xl" style="--flank-size: 30ch">
<div class="wa-stack">
<img class="wa-border-radius-l"
src="https://img.fortawesome.com/cfa83f3c/icon-grid-wallpaper.png"
alt="Sample of 48 line-style icons"
/>
<wa-tab-group>
<wa-tab panel="license">License</wa-tab>
<wa-tab panel="faq">FAQ</wa-tab>
<wa-tab-panel name="license">
<p class="wa-body-s">Your purchase includes a perpetual Font Awesome Pro License to use Classic Light icons on unlimited projects. <a href="">Read the full license terms.</a></p>
</wa-tab-panel>
<wa-tab-panel name="faq">
<dl class="wa-stack wa-body-s">
<dt>Do I need to renew my subscription to receive fixes?</dt>
<dd>We split up Font Awesome releases into regular updates and bug-fix updates. With a Font Awesome Pro plan that has a perpetual license, you'll always be entitled to bug-fix updates for your last version, even after your subscription has expired.</dd>
<dt>Can I use Font Awesome Pro in themes, plug-ins, or open source projects?</dt>
<dd>For themes and open source projects, right now it's best to just use Font Awesome Free. We are working a better solution, so feel free to get in touch if you have thoughts.</dd>
<dt>Do you offer enterprise licenses for Font Awesome Pro?</dt>
<dd>We don't currently offer Enterprise-level licenses, but we may do so in the future. Get in touch if interested.</dd>
</dl>
</wa-tab-panel>
</wa-tab-group>
</div>
<div class="wa-stack wa-gap-l">
<wa-badge appearance="filled">Sale</wa-badge>
<h2>Icon Pack: Classic Light</h2>
<p class="wa-body-l">Easy, readable icons with a lighter touch.</p>
<div class="wa-cluster wa-gap-xs wa-body-l">
<s>$60</s>
<strong>$49</strong>
</div>
<wa-button variant="brand" size="large">
<wa-icon slot="prefix" name="arrow-down-to-line" variant="solid"></wa-icon>
Get Icons
</wa-button>
<wa-divider></wa-divider>
<h3 class="wa-heading-m">What's in the Pack</h3>
<ul class="wa-stack wa-gap-xs">
<li class="wa-flank">
<wa-icon name="badge-check"></wa-icon>
<span>3,323 icons</span>
</li>
<li class="wa-flank">
<wa-icon name="badge-check"></wa-icon>
<span>Pre-bundled Font Awesome kit</span>
</li>
<li class="wa-flank">
<wa-icon name="badge-check"></wa-icon>
<span>Ligature-based desktop font files</span>
</li>
<li class="wa-flank">
<wa-icon name="badge-check"></wa-icon>
<span>Individual SVGs + SVG sprites</span>
</li>
<li class="wa-flank">
<wa-icon name="badge-check"></wa-icon>
<span>Web fonts + SVG framework</span>
</li>
<li class="wa-flank">
<wa-icon name="badge-check"></wa-icon>
<span>SCSS/LESS CSS preprocessor files</span>
</li>
<li class="wa-flank">
<wa-icon name="badge-check"></wa-icon>
<span>Perpetual Pro license</span>
</li>
</ul>
</div>
</div>
```

View File

@@ -1,162 +0,0 @@
---
title: Product Preview
description: 'Give shoppers a quick look at your products as they browse with modal previews.'
parent: ecommerce
tags: e-commerce
icon: preview
---
## With Product Options
```html {.example}
<wa-card with-header>
<div class="wa-split" slot="header">
<h3 class="wa-heading-l">Stan Smith® Camo Tongue Tee</h3>
<wa-icon-button name="close" label="Close Preview"></wa-icon-button>
</div>
<div class="wa-grid wa-gap-xl">
<div class="wa-frame wa-border-radius-l" style="aspect-ratio: auto">
<img
src="https://images.unsplash.com/photo-1660997351262-6c31d8a35b6c?q=80&w=2000&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="Stan Smith graphic crew-neck tee in honeydew color"
/>
</div>
<div class="wa-split:column wa-align-items-stretch wa-gap-xl">
<div class="wa-stack wa-gap-xl">
<div class="wa-cluster">
<span class="wa-heading-2xl">$32</span>
<wa-divider vertical style="height: 2em"></wa-divider>
<wa-rating label="Rating" value="3.75" readonly></wa-rating>
<a href="" class="wa-caption-m">36 Reviews</a>
</div>
<p>An ode to the “Sneaker that go with everything” …even this tee.</p>
<wa-divider></wa-divider>
<div class="wa-stack wa-gap-s">
<h4 class="wa-heading-s">Categories</h4>
<div class="wa-cluster wa-gap-2xs">
<a href=""><wa-tag appearance="outlined" size="small" pill>Men's</wa-tag></a>
<a href=""><wa-tag appearance="outlined" size="small" pill>Sneakers</wa-tag></a>
<a href=""><wa-tag appearance="outlined" size="small" pill>Tees</wa-tag></a>
<a href=""><wa-tag appearance="outlined" size="small" pill>Lifestyle</wa-tag></a>
<a href=""><wa-tag appearance="outlined" size="small" pill>Fashion</wa-tag></a>
<a href=""><wa-tag appearance="outlined" size="small" pill>Casual</wa-tag></a>
<a href=""><wa-tag appearance="outlined" size="small" pill>Stan Smith</wa-tag></a>
<a href=""><wa-tag appearance="outlined" size="small" pill>Tennis</wa-tag></a>
<a href=""><wa-tag appearance="outlined" size="small" pill>Sports</wa-tag></a>
</div>
</div>
<wa-divider></wa-divider>
<div class="wa-stack wa-gap-s">
<wa-select label="Color" value="honeydew">
<wa-option value="hotpink">
<wa-icon slot="prefix" name="circle" style="color: hotpink;"></wa-icon>
Hot Pink
</wa-option>
<wa-option value="honeydew">
<wa-icon slot="prefix" name="circle" style="color: honeydew;"></wa-icon>
Honeydew
</wa-option>
<wa-option value="coral">
<wa-icon slot="prefix" name="circle" style="color: lightcoral;"></wa-icon>
Coral
</wa-option>
<wa-option value="wheat">
<wa-icon slot="prefix" name="circle" style="color: wheat;"></wa-icon>
Wheat
</wa-option>
<wa-option value="lilac">
<wa-icon slot="prefix" name="circle" style="color: #C8A2C8;"></wa-icon>
Lilac
</wa-option>
<wa-option value="burnt-orange">
<wa-icon slot="prefix" name="circle" style="color: #FF5733"></wa-icon>
Burnt Orange
</wa-option>
</wa-select>
<wa-select label="Size" value="large">
<wa-option value="small">Small</wa-option>
<wa-option value="medium">Medium</wa-option>
<wa-option value="large">Large</wa-option>
<wa-option value="xl">XL</wa-option>
<wa-option value="xxl">XXL</wa-option>
</wa-select>
</div>
</div>
<wa-button variant="brand">
Add to Cart
<wa-icon slot="suffix" name="cart-shopping" variant="solid"></wa-icon>
</wa-button>
</div>
</div>
</wa-card>
```
## With Description & Details
```html{.example}
<wa-card>
<div class="wa-split" slot="header">
<h3 class="wa-heading-l">Champion® Crossbody Bag</h3>
<wa-icon-button name="close" label="Close Preview"></wa-icon-button>
</div>
<div class="wa-grid wa-gap-xl">
<div class="wa-frame wa-border-radius-l" style="aspect-ratio: auto">
<img
src="https://images.unsplash.com/photo-1643467358005-899641cab7b5?q=80&w=2487&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="Black weatherproof crossbody bag with two large zipper pockets"
/>
</div>
<div class="wa-split:column wa-align-items-stretch wa-gap-xl">
<div class="wa-stack wa-gap-xl">
<div class="wa-split wa-align-items-start">
<span class="wa-heading-2xl">$40</span>
<wa-icon-button id="favorite" label="Favorite" name="heart" variant="regular"></wa-icon-button>
<wa-tooltip for="favorite">Add to Favorites</wa-tooltip>
</div>
<div class="wa-split">
<div class="wa-cluster wa-gap-xs">
<span>3.9</span>
<wa-rating value="3.9" readonly></wa-rating>
</div>
<a href="">See 512 Reviews</a>
</div>
<wa-divider></wa-divider>
<wa-callout size="small">
<wa-icon slot="icon" name="circle-info" variant="regular"></wa-icon>
You purchased this item on <wa-format-date date="2023-02-20T09:00:00-04:00" month="long" day="numeric" year="numeric"></wa-format-date>
</wa-callout>
<div class="wa-gap-xs wa-stack">
<h4 class="wa-heading-m">About</h4>
<p class="wa-body-s">The Champion® Crossbody Bag is crafted for the trendsetter. Its sleek silhouette, paired with a tonal branded adjustable sling strap, ensures you look effortlessly cool no matter where you go.</p>
</div>
<wa-divider></wa-divider>
<div class="wa-gap-xs wa-stack">
<h4 class="wa-heading-m">Details</h4>
<dl class="wa-grid" style="--min-column-size: 15ch">
<div class="wa-gap-2xs wa-stack">
<dt class="wa-body-s">Care Instructions</dt>
<dd class="wa-caption-m">Hand Wash Only</dd>
</div>
<div class="wa-gap-2xs wa-stack">
<dt class="wa-body-s">Origin</dt>
<dd class="wa-caption-m">Imported</dd>
</div>
<div class="wa-gap-2xs wa-stack">
<dt class="wa-body-s">Country of Origin</dt>
<dd class="wa-caption-m">China</dd>
</div>
</dl>
</div>
</div>
<div class="wa-flank:end wa-align-items-end">
<wa-button variant="brand" size="medium">
<wa-icon slot="suffix" name="cart-shopping" variant="solid"></wa-icon>Add to Cart
</wa-button>
<wa-button appearance="outlined" size="medium">
<wa-icon slot="suffix" name="arrow-right" variant="solid"></wa-icon>View Full Details
</wa-button>
</div>
</div>
</div>
</wa-card>
```

View File

@@ -0,0 +1,465 @@
---
title: Product Reviews
description: TODO
parent: ecommerce
tags: e-commerce
---
TODO Page Description
## With images grid
```html{.example}
<div class="with-image-grid">
<wa-breadcrumb>
<wa-breadcrumb-item>Clothing</wa-breadcrumb-item>
<wa-breadcrumb-item>Men's</wa-breadcrumb-item>
<wa-breadcrumb-item>Shirts &amp; Tops</wa-breadcrumb-item>
</wa-breadcrumb>
<div class="image-grid">
<img src="/assets/images/patterns/gervyn-louis-IS03ajI00Fc-unsplash.jpg" />
<img src="/assets/images/patterns/gervyn-louis-KXvd7y7AU6Q-unsplash.jpg" />
<img src="/assets/images/patterns/gervyn-louis-semwwyXFQho-unsplash.jpg" />
<img src="/assets/images/patterns/mad-rabbit-tattoo-7N4FMowSGek-unsplash.jpg" />
</div>
<div>
<h2>Tank top</h2>
<p>The Basic Tee 6-Pack allows you to fully express your vibrant personality with three grayscale options. Feeling adventurous? Put on a heather gray tee. Want to be a trendsetter? Try our exclusive colorway: "Black". Need to add an extra pop of color to your outfit? Our white tee has you covered.</p>
<h3>Highlights</h3>
<ul>
<li>Hand cut and sewn locally</li>
</ul>
<h3>Highlights</h3>
<p>The 6-Pack includes two black, two white, and two heather gray Basic Tees. Sign up for our subscription service and be the first to get new, exciting colors, like our upcoming "Charcoal Gray" limited release.</p>
<span>$192</span>
<div>
<wa-rating label="Rating" precision="0.5" value="2.5"></wa-rating>
<a href="#">117 Reviews</a>
</div>
<wa-radio-group label="Select an option" hint="Select an option that makes you proud." name="a" value="1">
<wa-radio-button value="1">Option 1</wa-radio-button>
<wa-radio-button value="2">Option 2</wa-radio-button>
<wa-radio-button value="3">Option 3</wa-radio-button>
</wa-radio-group>
<wa-radio-group label="Select an option" hint="Select an option that makes you proud." name="a" value="1">
<wa-radio-button value="1">Option 1</wa-radio-button>
<wa-radio-button value="2">Option 2</wa-radio-button>
<wa-radio-button value="3">Option 3</wa-radio-button>
</wa-radio-group>
<wa-button>Add to Cart</wa-button>
</div>
</div>
<style>
.with-image-grid {
wa-breadcrumb::part(base) {
margin-bottom: 1rem;
}
.image-grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 1rem;
}
.image-grid img:nth-of-type(1) {
grid-column: 1/-1;
}
.image-grid img:nth-of-type(2) {
grid-column: 1/7;
}
.image-grid img:nth-of-type(3) {
grid-column: 7/-1;
}
.image-grid img:nth-of-type(4) {
grid-column: 1/-1;
}
}
</style>
```
## With Tiered Images
```html{.example}
<div class="with-tiered-images">
<wa-breadcrumb>
<wa-breadcrumb-item>Clothing</wa-breadcrumb-item>
<wa-breadcrumb-item>Men's</wa-breadcrumb-item>
<wa-breadcrumb-item>Shirts &amp; Tops</wa-breadcrumb-item>
</wa-breadcrumb>
<div>
<div class="heading">
<h2>Basic Tee</h2>
<span style="font-size: var(--wa-font-size-2xl)">$35</span>
</div>
<div class="rating">
<span>3.9</span>
<wa-rating label="Rating" precision="0.5" value="3.9"></wa-rating>
<a href="#">117 Reviews</a>
</div>
<div class="tiered-images">
<img src="/assets/images/patterns/gervyn-louis-IS03ajI00Fc-unsplash.jpg" />
<img src="/assets/images/patterns/gervyn-louis-KXvd7y7AU6Q-unsplash.jpg" />
<img src="/assets/images/patterns/gervyn-louis-semwwyXFQho-unsplash.jpg" />
</div>
</div>
<wa-radio-group label="Select an option" hint="Select an option that makes you proud." name="a" value="1">
<wa-radio-button value="1">Option 1</wa-radio-button>
<wa-radio-button value="2">Option 2</wa-radio-button>
<wa-radio-button value="3">Option 3</wa-radio-button>
</wa-radio-group>
<wa-radio-group label="Select an option" hint="Select an option that makes you proud." name="a" value="1">
<wa-radio-button value="1">Option 1</wa-radio-button>
<wa-radio-button value="2">Option 2</wa-radio-button>
<wa-radio-button value="3">Option 3</wa-radio-button>
</wa-radio-group>
<wa-button>Add to Cart</wa-button>
<h3>Description</h3>
<p>The Basic tee is an honest new take on a classic. The tee uses super soft, pre-shrunk cotton for true comfort and a dependable fit. They are hand cut and sewn locally, with a special dye technique that gives each tee it's own look.</p>
<p>Looking to stock your closet? The Basic tee also comes in a 3-pack or 5-pack at a bundle discount.</p>
<hr />
<h3>Highlights</h3>
<ul>
<li>Hand cut and sewn locally</li>
</ul>
<div>
<wa-card>
<wa-icon family="solid" name="earth-americas"></wa-icon>
<h3>International delivery</h3>
<p>Get your order in 2 years</p>
</wa-card>
<wa-card>
<wa-icon family="solid" name="earth-americas"></wa-icon>
<h3>International delivery</h3>
<p>Get your order in 2 years</p>
</wa-card>
</div>
</div>
<style>
.with-tiered-images {
wa-breadcrumb::part(base) {
margin-bottom: 1rem;
}
.heading {
display: flex;
justify-content: space-between;
align-items: center;
}
.rating {
display: flex;
span {
display: inline-block;
margin-right: 1rem;
}
wa-rating {
margin-right: 1rem;
}
}
.tiered-images {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 1rem;
}
.tiered-images img:nth-of-type(1) {
grid-column: 1/-1;
}
.tiered-images img:nth-of-type(2) {
grid-column: 1/7;
}
.tiered-images img:nth-of-type(3) {
grid-column: 7/-1;
}
</style>
```
## with images and expandable details
```html {.example}
<wa-carousel class="carousel-thumbnails" navigation loop>
<wa-carousel-item>
<img
alt="The sun shines on the mountains and trees (by Adam Kool on Unsplash)"
src="/assets/examples/carousel/pullover-1.jpg"
/>
</wa-carousel-item>
<wa-carousel-item>
<img
alt="A waterfall in the middle of a forest (by Thomas Kelly on Unsplash)"
src="/assets/examples/carousel/pullover-2.jpg"
/>
</wa-carousel-item>
<wa-carousel-item>
<img
alt="The sun is setting over a lavender field (by Leonard Cotte on Unsplash)"
src="/assets/examples/carousel/pullover-3.jpg"
/>
</wa-carousel-item>
<wa-carousel-item>
<img
alt="A field of grass with the sun setting in the background (by Sapan Patel on Unsplash)"
src="/assets/examples/carousel/pullover-4.jpg"
/>
</wa-carousel-item>
<wa-carousel-item>
<img
alt="A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash)"
src="/assets/examples/carousel/pullover-5.jpg"
/>
</wa-carousel-item>
</wa-carousel>
<div class="thumbnails">
<div class="thumbnails__scroller">
<img alt="Thumbnail by 1" class="thumbnails__image active" src="/assets/examples/carousel/pullover-1.jpg" />
<img alt="Thumbnail by 2" class="thumbnails__image" src="/assets/examples/carousel/pullover-2.jpg" />
<img alt="Thumbnail by 3" class="thumbnails__image" src="/assets/examples/carousel/pullover-3.jpg" />
<img alt="Thumbnail by 4" class="thumbnails__image" src="/assets/examples/carousel/pullover-4.jpg" />
<img alt="Thumbnail by 5" class="thumbnails__image" src="/assets/examples/carousel/pullover-5.jpg" />
</div>
</div>
<div>
<h3 style="--wa-space-xl: 0;">Pullover Sweater</h3>
<span class="price-big">$140</span>
<wa-rating class="sweater-rating" label="Rating" precision="0.5" value="2.5"></wa-rating>
<p>The Zip Tote Basket is the perfect midpoint between shopping tote and comfy backpack. With convertible straps, you can hand carry, should sling, or backpack this convenient and spacious bag. The zip top and durable canvas construction keeps your goods protected for all-day use.</p>
<wa-radio-group label="Select Color" hint="Select an option that makes you proud." name="a" value="1">
<wa-radio-button value="1"></wa-radio-button>
<wa-radio-button value="2"></wa-radio-button>
<wa-radio-button value="3"></wa-radio-button>
</wa-radio-group>
<div>
<wa-button>Add to cart</wa-button>
<wa-icon-button name="gear" label="Settings"></wa-icon-button>
</div>
<div class="details-group-example">
<wa-details summary="First" open>
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="Second">
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="Third">
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>
</div>
<style>
.carousel-thumbnails {
--slide-aspect-ratio: 3 / 2;
}
wa-radio-button #shadow-root div .button--medium {
padding: var(--wa-space-xs) var(--wa-space-xs);
}
.color-circle {
--background: #000;
background: var(--background);
width: 50px;
height: 100%;
}
.sweater-rating {
margin-bottom: 1rem;
}
.price-big {
display: block;
font-size: 32px;
}
.thumbnails {
display: flex;
justify-content: center;
margin-bottom: 1rem;
}
.thumbnails__scroller {
display: flex;
gap: var(--wa-space-s);
overflow-x: auto;
scrollbar-width: none;
scroll-behavior: smooth;
scroll-padding: var(--wa-space-s);
}
.thumbnails__scroller::-webkit-scrollbar {
display: none;
}
.thumbnails__image {
width: 64px;
height: 64px;
object-fit: cover;
opacity: 0.3;
will-change: opacity;
transition: 250ms opacity;
cursor: pointer;
}
.thumbnails__image.active {
opacity: 1;
}
.details-group-example wa-details:not(:last-of-type) {
margin-bottom: var(--wa-space-2xs);
}
</style>
<script>
{
const carousel = document.querySelector('.carousel-thumbnails');
const scroller = document.querySelector('.thumbnails__scroller');
const thumbnails = document.querySelectorAll('.thumbnails__image');
scroller.addEventListener('click', e => {
const target = e.target;
if (target.matches('.thumbnails__image')) {
const index = [...thumbnails].indexOf(target);
carousel.goToSlide(index);
}
});
carousel.addEventListener('wa-slide-change', e => {
const slideIndex = e.detail.index;
[...thumbnails].forEach((thumb, i) => {
thumb.classList.toggle('active', i === slideIndex);
if (i === slideIndex) {
thumb.scrollIntoView({
block: 'nearest'
});
}
});
});
}
const container = document.querySelector('.details-group-example');
// Close all other details when one is shown
container.addEventListener('wa-show', event => {
if (event.target.localName === 'wa-details') {
[...container.querySelectorAll('wa-details')].map(details => (details.open = event.target === details));
}
});
</script>
```
## Split with image
```html {.example}
<div class="split-with-image">
<div class="div-1">
<wa-breadcrumb>
<wa-breadcrumb-item>Clothing</wa-breadcrumb-item>
<wa-breadcrumb-item>Men's</wa-breadcrumb-item>
<wa-breadcrumb-item>Shirts &amp; Tops</wa-breadcrumb-item>
</wa-breadcrumb>
<h2>Everyday Ruck Snack</h2>
<span>
<span>$220</span> |
<wa-rating label="Rating" precision="0.5" value="2.5"></wa-rating>
<span>1624 reviews</span>
</span>
<p>Don't compromise on snack-carrying capacity with this lightweight and spacious bag. The drawstring top keeps all your favorite chips, crisps, fries, biscuits, crackers, and cookies secure.</p>
<span><wa-icon family="solid" name="check"></wa-icon> In stock and ready to ship</span>
</div>
<div class="div-2">
<img src="/assets/images/patterns/gervyn-louis-IS03ajI00Fc-unsplash.jpg" />
</div>
<div class="div-3">
<wa-radio-group label="Select an option" hint="Select an option that makes you proud." name="a" value="1">
<wa-radio-button value="1">Option 1</wa-radio-button>
<wa-radio-button value="2">Option 2</wa-radio-button>
<wa-radio-button value="3">Option 3</wa-radio-button>
</wa-radio-group>
</div>
</div>
<style>
.split-with-image {
display: grid;
/* grid-template-columns: repeat(2, 1fr); */
/* height: 1000px; */
/* gap: 1rem; */
.div-1 {
}
.div-2 {
/* background-color: black;
grid-column-start: 2;
grid-row: span 2 / span 2; */
}
.div-3 {
}
}
</style>
```
## With tabs
```html{.example}
<div>
<wa-rating class="sweater-rating" label="Rating" precision="0.5" value="2.5"></wa-rating>
<h2>Application UI Icon Pack</h2>
<img alt="Sample of 30 icons with friendly and fun details in outline, filled, and brand color styles." src="https://tailwindui.com/img/ecommerce-images/product-page-05-product-01.jpg" class="aqk aql">
<p>The Application UI Icon Pack comes with over 200 icons in 3 styles: outline, filled, and branded. This playful icon pack is tailored for complex application user interfaces with a friendly and legible look.</p>
<wa-button variant="brand">Brand</wa-button>
<wa-button variant="success">Success</wa-button>
<hr />
<h3>Highlights</h3>
<ul>
<li>200+ SVG icons in 3 unique styles</li>
<li>Compatible with Figma, Sketch, and Adobe XD</li>
<li>Drawn on 24 x 24 pixel grid</li>
</ul>
<hr />
<h3>License</h3>
<p>For personal and professional use. You cannot resell or redistribute these icons in their original or modified state. <a href="#">Read full license</a></p>
<hr />
<h3>Share</h3>
<wa-icon family="brands" name="facebook"></wa-icon>
<wa-icon family="brands" name="instagram"></wa-icon>
<wa-icon family="brands" name="x-twitter"></wa-icon>
<wa-tab-group>
<wa-tab panel="general">General</wa-tab>
<wa-tab panel="custom">Custom</wa-tab>
<wa-tab panel="advanced">Advanced</wa-tab>
<wa-tab panel="disabled" disabled>Disabled</wa-tab>
<wa-tab-panel name="general">
<div></div>
<div>
<h3>Hector Gibbons</h3>
<p>July 12, 2021</p>
<wa-rating label="Rating" precision="0.5" value="2.5"></wa-rating>
<p>Blown away by how polished this icon pack is. Everything looks so consistent and each SVG is optimized out of the box so I can use it directly with confidence. It would take me several hours to create a single icon this good, so it's a steal at this price.</p>
</div>
</wa-tab-panel>
<wa-tab-panel name="custom">This is the custom tab panel.</wa-tab-panel>
<wa-tab-panel name="advanced">This is the advanced tab panel.</wa-tab-panel>
<wa-tab-panel name="disabled">This is a disabled tab panel.</wa-tab-panel>
</wa-tab-group>
</div>
```

View File

@@ -1,213 +0,0 @@
---
title: Product Reviews
description: 'Help shoppers make informed decisions with ratings, reviews, and testimonials from your customers.'
parent: ecommerce
tags: e-commerce
---
## Multi column
```html{.example}
<div style="max-width: 960px; margin: 0 auto;">
<span class="wa-heading-m">Recent Reviews</span>
<wa-divider></wa-divider>
<div class="wa-flank wa-gap-s" style="--flank-size: 20%">
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-s">Viktor Vaughn</span>
<span class="wa-caption-m"><em>September 23rd, 2023</em></span>
</div>
<div class="wa-flank">
<wa-rating label="Rating" readonly value="5"></wa-rating>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-s">Rating Title</span>
<p class="wa-caption-m">Best treadmill I've ever owned! It has a sleek design, and the features are top-notch. I use it daily for my cardio workouts, and the motor is powerful enough to keep up with my running. Its easy to adjust the speed and incline, and the display is clear and simple to read. Worth every penny!</p>
</div>
</div>
</div>
<wa-divider></wa-divider>
<div class="wa-flank wa-gap-s" style="--flank-size: 20%">
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-s">Ben Grimm</span>
<span class="wa-caption-m"><em>May 5th, 2023</em></span>
</div>
<div class="wa-flank">
<wa-rating label="Rating" readonly value="4"></wa-rating>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-s">Rating Title</span>
<p class="wa-caption-m">Decent treadmill for the price, but I feel like the belt could be a little wider for comfort. The cushioning is good, but sometimes I experience a slight wobble when running at high speeds. For casual walking, it's fine, but Im not sure its built for intense runners.</p>
</div>
</div>
</div>
<wa-divider></wa-divider>
<div class="wa-flank wa-gap-s" style="--flank-size: 20%">
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-s">Johnny Storm</span>
<span class="wa-caption-m"><em>March 3rd, 2023</em></span>
</div>
<div class="wa-flank">
<wa-rating label="Rating" readonly value="4"></wa-rating>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-s">Rating Title</span>
<p class="wa-caption-m">This treadmill has been a great addition to my home gym. It's sturdy, easy to use, and I like that it tracks my steps and heart rate. The only downside is that it's a bit bulky, so I had to rearrange my space to make room for it. Overall, I'm happy with the performance and would recommend it.</p>
</div>
</div>
</div>
<wa-divider></wa-divider>
<div class="wa-flank wa-gap-s" style="--flank-size: 20%">
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-s">Sue Storm</span>
<span class="wa-caption-m"><em>February 26th, 2023</em></span>
</div>
<div class="wa-flank">
<wa-rating label="Rating" readonly value="4"></wa-rating>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-s">Rating Title</span>
<p class="wa-caption-m">I absolutely love my new treadmill! Its perfect for my daily workouts. The setup was quick, and its so quiet that I can use it while watching TV without any interruptions. The different incline levels really help mix up my routine, and the built-in programs keep things interesting. Highly recommend for anyone looking to stay fit at home!</p>
</div>
</div>
</div>
</div>
```
## With Ratings Distribution
```html {.example}
<div style="max-width: 960px; margin: 0 auto;">
<div class="wa-align-items-start wa-flank wa-gap-2xl">
<div class="wa-gap-s wa-stack">
<span class="wa-heading-m">Customer Reviews</span>
<div class="wa-stack wa-gap-xs"><wa-rating label="Rating" precision="0.5" value="4.6" size="small"></wa-rating> <span class="wa-caption-m">Based on 1624 reviews</span></div>
<div class="wa-stack">
<span class="wa-cluster wa-gap-2xs">
<span>5</span>
<wa-icon name="star" style="font-size: 12px;"></wa-icon>
<wa-progress-bar value="63" label="Upload progress" style="height: 6px; width: 50%"></wa-progress-bar>
<span>63%</span>
</span>
<span class="wa-cluster wa-gap-2xs">
<span>4</span>
<wa-icon name="star" style="font-size: 12px;"></wa-icon>
<wa-progress-bar value="17" label="Upload progress" style="height: 6px; width: 50%"></wa-progress-bar>
<span>17%</span>
</span>
<span class="wa-cluster wa-gap-2xs">
<span>3</span>
<wa-icon name="star" style="font-size: 12px;"></wa-icon>
<wa-progress-bar value="15" label="Upload progress" style="height: 6px; width: 50%"></wa-progress-bar>
<span>15%</span>
</span>
<span class="wa-cluster wa-gap-2xs">
<span>2</span>
<wa-icon name="star" style="font-size: 12px;"></wa-icon>
<wa-progress-bar value="3" label="Upload progress" style="height: 6px; width: 50%"></wa-progress-bar>
<span>3%</span>
</span>
<span class="wa-cluster wa-gap-2xs">
<span>1</span>
<wa-icon name="star" style="font-size: 12px;"></wa-icon>
<wa-progress-bar value="2" label="Upload progress" style="height: 6px; width: 50%"></wa-progress-bar>
<span>2%</span>
</span>
</div>
</div>
<div>
<div>
<div class="wa-flank">
<wa-avatar image="https://images.unsplash.com/photo-1494790108377-be9c29b29330?q=80&w=2574&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"></wa-avatar>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-s">Emily Selman</span>
<wa-rating label="Rating" precision="0.5" value="2.5"></wa-rating>
</div>
</div>
<p class="wa-caption-l"><em>This is the bag of my dreams. I took it on my last vacation and was able to fit an absurd amount of snacks for the many long and hungry flights.</em></p>
</div>
<wa-divider></wa-divider>
<div>
<div class="wa-flank">
<wa-avatar image="https://images.unsplash.com/photo-1599566150163-29194dcaad36?q=80&w=2574&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"></wa-avatar>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-s">Emily Selman</span>
<wa-rating label="Rating" precision="0.5" value="2.5"></wa-rating>
</div>
</div>
<p class="wa-caption-l"><em>This is the bag of my dreams. I took it on my last vacation and was able to fit an absurd amount of snacks for the many long and hungry flights.</em></p>
</div>
<wa-divider></wa-divider>
<div>
<div class="wa-flank">
<wa-avatar image="https://images.unsplash.com/photo-1580489944761-15a19d654956?q=80&w=2561&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"></wa-avatar>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-s">Emily Selman</span>
<wa-rating label="Rating" precision="0.5" value="2.5"></wa-rating>
</div>
</div>
<p class="wa-caption-l"><em>This is the bag of my dreams. I took it on my last vacation and was able to fit an absurd amount of snacks for the many long and hungry flights.</em></p>
</div>
<wa-divider></wa-divider>
<div>
<div class="wa-flank">
<wa-avatar image="https://images.unsplash.com/photo-1566492031773-4f4e44671857?q=80&w=2574&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"></wa-avatar>
<div class="wa-stack wa-gap-2xs">
<span class="wa-heading-s">Emily Selman</span>
<wa-rating label="Rating" precision="0.5" value="2.5"></wa-rating>
</div>
</div>
<p class="wa-caption-l"><em>This is the bag of my dreams. I took it on my last vacation and was able to fit an absurd amount of snacks for the many long and hungry flights.</em></p>
</div>
</div>
</div>
</div>
```
## Two Column
```html{.example}
<div class="wa-stack" style="max-width: 960px; margin: 0 auto;">
<div class="wa-flank wa-align-items-center">
<div class="wa-stack wa-align-items-center wa-gap-xs">
<wa-avatar label="User avatar" image="https://images.unsplash.com/photo-1607746882042-944635dfe10e?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"></wa-avatar>
<p>Ripley</p>
<div>
<wa-icon-button name="thumbs-up" label="I don't like this review" style="color: var(--wa-color-success-fill-loud);"></wa-icon-button>
<wa-icon-button name="thumbs-down" label="I like this review" style="color: var(--wa-color-danger-fill-loud);"></wa-icon-button>
</div>
</div>
<div>
<wa-rating label="Rating" precision="0.5" value="5" readonly></wa-rating>
<p>I recently purchased the Modern Sofa Couch, and I couldn't be happier with my decision! The process from ordering to delivery was smooth and hassle-free</p>
</div>
</div>
<wa-divider></wa-divider>
<div class="wa-flank wa-align-items-center">
<div class="wa-stack wa-align-items-center wa-gap-xs">
<wa-avatar label="User avatar" image="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?q=80&w=2574&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"></wa-avatar>
<p>Kane</p>
<div>
<wa-icon-button name="thumbs-up" label="I don't like this review" style="color: var(--wa-color-success-fill-loud);"></wa-icon-button>
<wa-icon-button name="thumbs-down" label="I like this review" style="color: var(--wa-color-danger-fill-loud);"></wa-icon-button>
</div>
</div>
<div>
<wa-rating label="Rating" precision="0.5" value="3.4" readonly></wa-rating>
<p>The cushions are soft yet supportive, and the sectional layout gives plenty of space to stretch out. Its perfect for movie nights or just lounging with a good book.</p>
</div>
</div>
<wa-divider></wa-divider>
<div class="wa-flank wa-align-items-center">
<div class="wa-stack wa-align-items-center wa-gap-xs">
<wa-avatar label="User avatar" image="https://images.unsplash.com/photo-1728577740843-5f29c7586afe?q=80&w=2680&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"></wa-avatar>
<p>Parker</p>
<div>
<wa-icon-button name="thumbs-up" label="I don't like this review" style="color: var(--wa-color-success-fill-loud);"></wa-icon-button>
<wa-icon-button name="thumbs-down" label="I like this review" style="color: var(--wa-color-danger-fill-loud);"></wa-icon-button>
</div>
</div>
<div>
<wa-rating label="Rating" precision="0.5" value="3.8" readonly></wa-rating>
<p>The leather is high quality, but its a little firmer than I thought. That said, after sitting on it for a while, it does soften up and feels more comfortable. Its perfect if youre looking for a more structured seating experience.</p>
</div>
</div>
<wa-divider></wa-divider>
</div>
```

View File

@@ -1,279 +1,10 @@
---
title: Shopping Cart
description: 'Give shoppers an overview of selected items with shopping carts that let them edit items and proceed to checkout.'
description: TODO
parent: ecommerce
tags: e-commerce
---
## Two Columns with Summary Card
TODO Page Description
```html {.example}
<div class="wa-stack wa-gap-2xl">
<h2>Shopping Cart</h2>
<div class="wa-grid wa-align-items-start wa-gap-2xl">
<div class="wa-stack wa-gap-xl">
<article class="wa-flank wa-gap-xl" style="--flank-size: 8rem">
<div class="wa-frame wa-border-radius-m">
<img
src="https://images.unsplash.com/photo-1523381294911-8d3cead13475?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w1OTAyOTl8MHwxfGFsbHx8fHx8fHx8fDE3MTg2NDIzNDd8&ixlib=rb-4.0.3&q=80&w=1080"
alt=""
/>
</div>
<div class="wa-flank:end wa-align-items-baseline">
<div class="wa-stack wa-gap-xs">
<h3 class="wa-heading-s">Classic Tee</h3>
<span class="wa-caption-m">Sage Green</span>
<span class="wa-caption-m">Large</span>
<span>$20.00</span>
</div>
<wa-icon-button name="xmark" label="Remove" id="remove-1"></wa-icon-button>
<wa-tooltip for="remove-1">Remove</wa-tooltip>
</div>
</article>
<article class="wa-flank wa-gap-xl" style="--flank-size: 8rem">
<div class="wa-frame wa-border-radius-m">
<img
src="https://images.unsplash.com/photo-1564859227552-81fde4a1df0b?q=80&w=2671&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt=""
/>
</div>
<div class="wa-flank:end wa-align-items-baseline">
<div class="wa-stack wa-gap-xs">
<h3 class="wa-heading-s">RVCA Graphic</h3>
<span class="wa-caption-m">White</span>
<span class="wa-caption-m">Large</span>
<span>$25.00</span>
</div>
<wa-icon-button name="xmark" label="Remove" id="remove-2"></wa-icon-button>
<wa-tooltip for="remove-2">Remove</wa-tooltip>
</div>
</article>
<article class="wa-flank wa-gap-xl" style="--flank-size: 8rem">
<div class="wa-frame wa-border-radius-m">
<img
src="https://images.unsplash.com/photo-1503341733017-1901578f9f1e?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt=""
/>
</div>
<div class="wa-flank:end wa-align-items-baseline">
<div class="wa-stack wa-gap-xs">
<h3 class="wa-heading-s">Stay Wild Graphic</h3>
<span class="wa-caption-m">Black</span>
<span class="wa-caption-m">Large</span>
<span>$18.00</span>
</div>
<wa-icon-button name="xmark" label="Remove" id="remove-3"></wa-icon-button>
<wa-tooltip for="remove-3">Remove</wa-tooltip>
</div>
</article>
</div>
<wa-card>
<div slot="header">
<h3 class="wa-heading-m">Order Summary</h3>
</div>
<div class="wa-stack">
<div class="wa-split">
<span class="wa-caption-l">Subtotal</span>
<strong>$63.00</strong>
</div>
<wa-divider></wa-divider>
<div class="wa-split">
<span class="wa-caption-l">Shipping</span>
<strong>$5.00</strong>
</div>
<wa-divider></wa-divider>
<div class="wa-split">
<span class="wa-caption-l">Tax</span>
<strong>$5.50</strong>
</div>
<wa-divider></wa-divider>
<div class="wa-split wa-body-l">
<span>Total</span>
<strong>$73.50</strong>
</div>
<wa-button variant="brand">Checkout</wa-button>
</div>
</wa-card>
</div>
</div>
</div>
```
## Single Column
```html {.example}
<div class="wa-stack wa-gap-2xl" style="max-width: 60ch; margin: auto">
<h2>Your Cart</h2>
<wa-divider></wa-divider>
<article class="wa-flank" style="--flank-size: 12rem">
<div class="wa-frame wa-border-radius-m" style="aspect-ratio: 3 / 2">
<img
src="https://images.unsplash.com/photo-1594787317357-dcda50fd1d78?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w1OTAyOTl8MHwxfGFsbHx8fHx8fHx8fDE3MTg2NDI4MDd8&ixlib=rb-4.0.3&q=80&w=1080"
alt=""
/>
</div>
<div class="wa-split:column wa-align-items-stretch wa-gap-xs">
<div class="wa-stack wa-gap-xs">
<span class="wa-split wa-gap-xs">
<h3 class="wa-heading-m">Convertible</h3>
<span>$32.00</span>
</span>
<wa-tag size="small" variant="neutral" appearance="filled" pill style="width: fit-content">Cherry Red</wa-tag>
</div>
<div class="wa-split">
<wa-badge appearance="filled" variant="success">In Stock</wa-badge>
<wa-button appearance="plain" size="small" variant="danger">
<wa-icon slot="suffix" name="trash"></wa-icon>
Remove
</wa-button>
</div>
</div>
</article>
<wa-divider></wa-divider>
<article class="wa-flank" style="--flank-size: 12rem">
<div class="wa-frame wa-border-radius-m" style="aspect-ratio: 3 / 2">
<img
src="https://images.unsplash.com/photo-1597670250484-0e9aff7f8804?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w1OTAyOTl8MHwxfGFsbHx8fHx8fHx8fDE3MTg2NDI4NTB8&ixlib=rb-4.0.3&q=80&w=1080"
alt=""
/>
</div>
<div class="wa-split:column wa-align-items-stretch wa-gap-xs">
<div class="wa-stack wa-gap-xs">
<span class="wa-split wa-gap-xs">
<h3 class="wa-heading-m">Racers (3 Pack)</h3>
<span>$80.00</span>
</span>
<wa-tag size="small" variant="neutral" appearance="filled" pill style="width: fit-content">Assorted Colors</wa-tag>
</div>
<div class="wa-split">
<wa-badge appearance="filled" variant="success">In Stock</wa-badge>
<wa-button appearance="plain" size="small" variant="danger">
<wa-icon slot="suffix" name="trash"></wa-icon>
Remove
</wa-button>
</div>
</div>
</article>
<wa-divider></wa-divider>
<article class="wa-flank" style="--flank-size: 12rem">
<div class="wa-frame wa-border-radius-m" style="aspect-ratio: 3 / 2">
<img
src="https://images.unsplash.com/photo-1594787826350-19386fdb2363?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w1OTAyOTl8MHwxfGFsbHx8fHx8fHx8fDE3MTg2NDI4ODV8&ixlib=rb-4.0.3&q=80&w=1080"
alt=""
/>
</div>
<div class="wa-split:column wa-align-items-stretch wa-gap-xs">
<div class="wa-stack wa-gap-xs">
<span class="wa-split wa-gap-xs">
<h3 class="wa-heading-m">Volkswagen T2</h3>
<span>$60.00</span>
</span>
<wa-tag size="small" variant="neutral" appearance="filled" pill style="width: fit-content">Red/White</wa-tag>
</div>
<div class="wa-split">
<wa-badge appearance="filled" variant="warning">Low Stock</wa-badge>
<wa-button appearance="plain" size="small" variant="danger">
<wa-icon slot="suffix" name="trash"></wa-icon>
Remove
</wa-button>
</div>
</div>
</article>
<wa-divider></wa-divider>
<div class="wa-stack">
<div class="wa-split">
<h3 class="wa-heading-m">Subtotal</h3>
<span class="wa-body-l">$172.00</span>
</div>
<span class="wa-caption-m">Shipping and taxes calculated at checkout</span>
<wa-button size="large" variant="brand">Checkout</wa-button>
<div class="cluster">
<span class="wa-caption-m">Not quite ready?</span>
<wa-button appearance="plain" size="small" variant="brand">
Continue Shopping
<wa-icon name="arrow-right"></wa-icon>
</wa-button>
</div>
</div>
</div>
```
## Drawer
```html {.example viewport}
<wa-drawer label="Shopping Cart" with-header with-footer open>
<div class="wa-stack">
<article class="wa-flank" style="--flank-size: 6rem">
<div class="wa-frame wa-border-radius-m">
<img
src="https://images.unsplash.com/photo-1704677982224-89cd6d039fa6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w1OTAyOTl8MHwxfGFsbHx8fHx8fHx8fDE3MTg2NDEwOTJ8&ixlib=rb-4.0.3&q=80&w=1080"
alt=""
/>
</div>
<div class="wa-stack wa-gap-2xs">
<div class="wa-split wa-gap-2xs">
<strong>AJ1 Low</strong>
<strong>$170.00</strong>
</div>
<span class="wa-caption-m">Multi-color</span>
<div class="wa-split wa-gap-2xs">
<span class="wa-body-s">Qty: 1</span>
<wa-button appearance="plain" size="small">Remove</wa-button>
</div>
</div>
</article>
<wa-divider></wa-divider>
<article class="wa-flank" style="--flank-size: 6rem">
<div class="wa-frame wa-border-radius-m">
<img
src="https://images.unsplash.com/photo-1672908615254-71a0b373eaba?q=80&w=3560&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="(Photograph by Hamed darzi)"
/>
</div>
<div class="wa-stack wa-gap-2xs">
<div class="wa-split wa-gap-2xs">
<strong>The Trails</strong>
<strong>$35.00</strong>
</div>
<span class="wa-caption-m">Twilight Blue</span>
<div class="wa-split wa-gap-2xs">
<span class="wa-body-s">Qty: 1</span>
<wa-button appearance="plain" size="small">Remove</wa-button>
</div>
</div>
</article>
<wa-divider></wa-divider>
<article class="wa-flank" style="--flank-size: 6rem">
<div class="wa-frame wa-border-radius-m">
<img
src="https://images.unsplash.com/photo-1693443687750-611ad77f3aba?q=80&w=3540&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="(Photograph by tian dayong)"
/>
</div>
<div class="wa-stack wa-gap-2xs">
<div class="wa-split wa-gap-2xs">
<strong>Outcast 2-pack</strong>
<strong>$27.00</strong>
</div>
<span class="wa-caption-m">Black / White</span>
<div class="wa-split wa-gap-2xs">
<span class="wa-body-s">Qty: 1</span>
<wa-button appearance="plain" size="small">Remove</wa-button>
</div>
</div>
</article>
</div>
<div slot="footer" class="wa-stack" style="width: 100%">
<div class="wa-split">
<strong>Subtotal</strong>
<strong>$232.00</strong>
</div>
<span class="wa-caption-m">Shipping and taxes calculated at checkout.</span>
<wa-button variant="brand">Checkout</wa-button>
<wa-button appearance="plain" size="small" variant="brand">
Continue Shopping
<wa-icon name="arrow-right"></wa-icon>
</wa-button>
</div>
</wa-drawer>
```
## Examples

View File

@@ -1,85 +0,0 @@
---
title: Store Navigation
description: 'Help shoppers explore categories and find products with all of the links they need to navigate your store.'
parent: ecommerce
unlisted: true
---
## Popup Menu
```html{.example}
<wa-dropdown>
<wa-button slot="trigger" caret>Shop</wa-button>
<wa-menu class="mm-grid">
<div>
<wa-menu-label>Shop by Department</wa-menu-label>
<wa-menu-item value="apple">Mens</wa-menu-item>
<wa-menu-item value="banana">Womens</wa-menu-item>
<wa-menu-item value="orange">Kids</wa-menu-item>
<wa-menu-item value="orange">
Infants
<wa-menu slot="submenu">
<wa-menu-item value="uppercase">Newborns</wa-menu-item>
<wa-menu-item value="lowercase">6 Months</wa-menu-item>
<wa-menu-item value="capitalize">12 Months</wa-menu-item>
</wa-menu>
</wa-menu-item>
<wa-menu-item value="orange">Big & Tall</wa-menu-item>
</div>
<div>
<wa-menu-label>Shop by Category</wa-menu-label>
<wa-menu-item value="apple">Shirts</wa-menu-item>
<wa-menu-item value="banana">Pants</wa-menu-item>
<wa-menu-item value="orange">Shoes</wa-menu-item>
</div>
<div>
<wa-menu-label>Just Arrived</wa-menu-label>
<wa-menu-item>
<a href="#">
<img style="width: 100%; max-width: 200px;" src="https://images.unsplash.com/photo-1523381294911-8d3cead13475?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w1OTAyOTl8MHwxfGFsbHx8fHx8fHx8fDE3MTg2NDIzNDd8&ixlib=rb-4.0.3&q=80&w=1080" />
</a>
</wa-menu-item>
</div>
<wa-menu-item style="grid-column: 1/-1;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<p style="margin:0;">footer with something cool in it</p>
<wa-button variant="brand" size="small">Signup now</wa-button>
</div>
</wa-menu-item>
</wa-menu>
</wa-dropdown>
<style>
.mm-grid {
display: grid;
grid-template-columns: repeat(3, auto);
gap: 1rem;
.card-overview small {
color: var(--wa-color-text-quiet);
}
.card-overview [slot='footer'] {
display: flex;
justify-content: space-between;
align-items: center;
}
}
</style>
```

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +0,0 @@
---
title: E-commerce
description: ''
layout: page
---
{% set ecommercePages = collections['e-commerce'] %}
<section class="index-grid">
{%- for page in ecommercePages -%}
<a href="{{ page.url }}"{{ page.data.keywords | attr('data-keywords') }}>
<wa-card with-header>
<div slot="header">
{% include "svgs/" + (page.data.icon or "thumbnail-placeholder") + ".njk" %}
</div>
<span class="page-name">{{ page.data.title }}</span>
{% if pageSubtitle -%}
<div class="wa-caption-s">{{ pageSubtitle }}</div>
{%- endif %}
</wa-card>
</a>
{%- endfor -%}
</section>

View File

@@ -1,7 +1,6 @@
---
title: Business
description: TODO
unlisted: true
---
TODO Page Description

View File

@@ -1,7 +1,6 @@
---
title: Entertainment
description: TODO
unlisted: true
---
TODO Page Description

View File

@@ -1,29 +1,6 @@
---
title: Patterns
description: Patterns are reusable UI solutions to common design problems, ready to copy and paste into any project.
description: Patterns are reusable solutions to common design problems.
layout: overview
categories: ["app", "e-commerce"]
listChildren: true
override:tags: []
---
<div class="max-line-length">
{% markdown %}
## What's a Pattern?
A pattern is a code snippet composed of components, style utilities, and native HTML that you can copy and paste into any project that uses Web Awesome.
It's a chunk of a user interface, rather than a single component, that allows you to implement UI solutions without designing something from scratch.
Patterns are designed according to proven usability practices so they're responsive, accessible, and cohesive out-of-the-box. Importantly, patterns don't handle business logic or functionality like form submissions, data processing, encryption, etc. It's up to you to implement the logic you need for your project.
Patterns are written as standard HTML, so you can use them just as you would any ol' HTML and customize them to fit your specific needs.
## Using Patterns
To use a pattern in your project, refer to each pattern's docs for a copyable code snippet. Paste the snippet wherever you'd like the pattern to appear in your project.
Because patterns use a combination of Web Awesome features, they work best when you have [native styles](/docs/native), [style utilities](/docs/utilities), and a [theme](/docs/themes) installed in addition to Web Awesome [components](/docs/components). Refer to the [Installation page](/docs/installation) to set up all of these features in your project.
{% endmarkdown %}
</div>

View File

@@ -1,7 +1,6 @@
---
title: Membership
description: TODO
unlisted: true
---
TODO Page Description

View File

@@ -1,7 +1,6 @@
---
title: News
description: TODO
unlisted: true
---
TODO Page Description

View File

@@ -1,7 +1,6 @@
---
title: Non-profit
description: TODO
unlisted: true
---
TODO Page Description

View File

@@ -1,7 +0,0 @@
.anchor-heading:has(+ wa-code-demo, + template + wa-code-demo) {
font-size: var(--wa-font-size-l);
}
wa-code-demo:has(+ .anchor-heading) {
margin-block-end: var(--wa-space-3xl);
}

View File

@@ -1,5 +1,5 @@
{
"layout": "patterns.njk",
"layout": "block.njk",
"tags": ["patterns"],
"wide": true,
"noAlpha": true

View File

@@ -1,7 +1,6 @@
---
title: Portfolio
description: TODO
unlisted: true
---
TODO Page Description

View File

@@ -1,7 +1,6 @@
---
title: Product Landing
description: TODO
unlisted: true
---
TODO Page Description

View File

@@ -17,6 +17,7 @@ During the alpha period, things might break! We take breaking changes very serio
- Fixed `wa-pill` class for text fields
- Fixed `pill` style for `<wa-input>` elements
- Fixed a bug in `<wa-color-picker>` that prevented light dismiss from working when clicking immediately above the color picker dropdown
- Fixed a bug in `<wa-select multiple>` that sometimes resulted in empty `<div>` elements being output
## 3.0.0-alpha.11

View File

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

View File

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

View File

@@ -94,31 +94,6 @@
}
}
}
.selected-swatch,
wa-select[name='brand'] 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);
}
wa-select[name='brand'] wa-option {
white-space: nowrap;
&::before {
width: 1em;
margin-inline: var(--wa-space-xs);
}
&::part(checked-icon) {
order: 2;
}
}
}
#test_select wa-option:state(selected) {

View File

@@ -54,8 +54,12 @@ function init() {
urlParams: new Permalink(),
};
data.urlParams.mapObject(data.params);
data.urlParams.writeTo(data.params);
// Apply params from permalink
for (let key in data.params) {
if (data.urlParams.has(key)) {
data.params[key] = data.urlParams.get(key);
}
}
if (computed.isRemixed) {
// Start with the remixing UI open if the theme has been remixed
@@ -121,14 +125,22 @@ function render(changedAspect) {
let brand = data.params.brand || data.defaultParams.brand;
selects.brand.style.setProperty('--color', `var(--wa-color-${brand})`);
selects.brand.className = `wa-palette-${computed.palette}`;
// Add current palette class and remove any other palette classes
let paletteClass = `wa-palette-${computed.palette}`;
selects.brand.className = selects.brand.className.replace(/\bwa-palette-[a-z]+\b/g, paletteClass);
selects.brand.classList.add(paletteClass);
for (let aspect in data.params) {
let value = data.params[aspect];
selects[aspect].value = value;
}
data.urlParams.readFrom(data.params);
for (let key in data.params) {
if (data.params[key]) {
data.urlParams.set(key, data.params[key]);
}
}
// Update demo URL
domChange(() => {

View File

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

View File

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

View File

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

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