Compare commits

..

98 Commits

Author SHA1 Message Date
Lea Verou
9328feed19 [image-comparer] Remove base part
Had to move the keydown listener to the divider, but that seems like a good change *anyway* since if the content is interactive (such as our own theme previews) we don't really want to be listening to key presses on it, we only really want to be listening to key presses on the divider, which is the actual focusable part.
2025-04-23 12:44:33 -04:00
Lea Verou
2542354d5c [breadcrumb-item] Drop base part, move styling to host 2025-04-23 12:33:51 -04:00
Lea Verou
cfc3f181a3 Fix broken demo, improve passthrough CSS (#879)
Fixes several issues for components using the `display: contents` technique:
- Introduce `--display` CSS property to make it possible to override internal display value of base part.
- Ensure `display: contents` is always applied with `!important` to avoid rendering glitches like the one here
- Ensure non-inherited custom properties are also inherited
- Ensure the `hidden` attribute still works.
2025-04-23 11:52:23 -04:00
Konnor Rogers
7545f04c46 remove all references to /docs/installation (#872) 2025-04-21 17:01:09 -04:00
Lea Verou
38bd6528fe Only apply wa-button-group-vertical class when <wa-radio-group> is a radio button group, fixes #870 2025-04-15 18:07:57 -04:00
Lea Verou
2202ea9642 CSS custom properties to set certain component attributes (#447) 2025-04-15 17:56:38 -04:00
Konnor Rogers
58fbcede51 update patterns to isPro (#869) 2025-04-14 14:59:27 -04:00
Lea Verou
971200cc88 Move domChange() to separate module (#868) 2025-04-14 12:52:00 -04:00
Konnor Rogers
b75d3b615c fix login media query and leaking variables with Turbo (#867)
* fix media queries to use wa-desktop-only

* fix errors in remix.js when changing pages

* prettier
2025-04-14 11:11:08 -04:00
Kelsey Jackson
9d1c47449e Kj/first pattern launch (#861)
* dividing up e-commerce patterns

* started invoice table

* building out the filter

* Initial Commit

* switching machines

* switching machines

* started split image pattern:

* commiting to merge in next

* commiting to switch machines

* upated the product detail patterns

* updated product list patterns

* updated some patterns

* filter work

* added utilities to order history

* added utilities to order-summary

* updated width of container

* switching machines

* switching machines

* fixing conflicts

* editing descriptions

* updated descriptions

* adding some polish

* more filter work

* updated prouct preview

* Revisions to "Product Features"

* "Category Filters" revisions + `wa-placeholder` utility

* cleaned up product list

* tweaked product overview

* tweaked order history

* tweak category preview

* "Checkout Form" revisions

* Re-add `navigation` to "Product Features" carousel

* cleaned up shopping cart

* tweaked order summary

* Add missing file extension

* Fix typo in file name

* Revert checkout form changes in attempt to improve diff

* Reimplement checkout form revisions

* cleaned up checkout form

* Add "What's a Pattern?" and "Using Patterns" to index

* Update category descriptions and headings

* Add docs layout for patterns with stylesheet

* tweaked customer review

* Add checkout form example

* a little more polish

* more tweaks

* switching branches to merge

* created e-commerce index

* unlisted links

* updtated content

* updated content for category preview

* updated order history

* committing to bring branches up to next

* inital commit

* Reorganize app patterns into separate pages

* switching machines

* Add link style utilities

* Refactor product list patterns

* more polish

* Refactor product overview patterns

* switching machines

* Refactor shopping cart patterns

* committing to pull down changes

* updated product preview

* updated image

* updated incentives

* Clean up utility usage and code formatting for category previews

* started updating with style utilities

* Clean up utilities and formatting, replace placeholder text in order history

* updated incentive images

* updated reviews

* added review variant

* more polish

* some heavy duty updates

* removed store navigation

* switching machines
:

* Clean up utility usage and code formatting for order summaries

* Clean up utility usage and code formatting for product previews

* big switchover

* removed old file

* working on sidebar

* updated sidebar

* created info category

* splitting up the old blog page

* a little cleanup and adding detail

* got three done

* updated social share

* Quick formatting adjustment

* initial paywalls

* Use overviews in pattern subcategories (#826)

* Do not error if no pages

* Automatically set parents and tags for patterns

* Update overview.njk

* [WIP] Use overview pages for pattern listings

* Remove explicit parents

---------

Co-authored-by: lindsaym-fa <dev@lindsaym.design>

* some newsletter signups

* added login patterns

* reworked feeds'

* a few more

* added a few more

* add more app patterns

* App pattern tweaks (#827)

* Fix `patterns.css` reference

* Tweaks to action panel patterns

* Tweaks to comments patterns

* Progress on data display patterns

* Progress on empty state patterns

* Use email-related data from recent update in `pattern-main` (altered slightly)

* Tweaks to data display patterns

* Tweaks to empty state patterns

* Tweaks to FAQ patterns

* Tweaks to feed patterns

* Tweaks to grid patterns

* Tweaks to pagination patterns

* Tweaks to pricing patterns

* Tweaks to description list patterns

* Tweaks to leaderboard patterns

* Add and update names and descriptions

* Ensure comments fields have labels

* Tweak recent additions

* switching machines

* catching up with next

* added permissions pattern

* switching machines

* added post articles

* having local browset issues

* a few more app patterns

* took out info patterns

* spiffed up the action panels

* gave the action panels a once over

* added content to data display

* updated a few patterns

* clean up a few e-commerce patterns

* added time componen to reviews

---------

Co-authored-by: lindsaym-fa <dev@lindsaym.design>
Co-authored-by: Lea Verou <lea@verou.me>
Co-authored-by: Lindsay M <126139086+lindsaym-fa@users.noreply.github.com>
2025-04-14 11:10:54 -04:00
Lea Verou
003fd28cb0 Mark code-demo and viewport-demo as pro 2025-04-10 10:57:06 -05:00
Lea Verou
2f300d8930 [Popup] Use viewport as default boundary for flip too (#865) 2025-04-10 10:52:15 -05:00
Lea Verou
f13deb87bb Drop hoist from public API, use Popover API where supported (#351)
* First stab at using the Popover API for PE

* fix popup

* First stab at using the Popover API for PE

* fix popup

* Prettier

* Fix TS error

* Remove workaround

* Default to `strategy = fixed` if Popover API is not supported

* Clear out default UA styles for popovers

* Kill `hoist` with fire 🔥

* Refactor

* Update `@floating-ui/dom`

* Fix flipping and shofting

* Fix autosize

* Use `defaultBoundary` for `flip` too

That way we get the previous behavior for it.

* Remove `strategy`, just use `SUPPORTS_POPOVER` check where relevant

* Remove uses of `strategy`

* Use viewport as the default boundary for shifting and autosizing and add `boundary=scroll` to override

---------

Co-authored-by: konnorrogers <konnor5456@gmail.com>
2025-04-09 16:47:40 -05:00
Konnor Rogers
deb9fd70b3 fix pass through copying (#860)
* fix pass through copying

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

* Document `docs.11tydata.js` better

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

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

* fix radio button pill styles; fixes #759

* remove redundant styles

* fixes #840

* fix focus ring in Safari; fixes #745

* improve details styles; fixes #685

* update examples

* Revert "improve details styles; fixes #685"

This reverts commit 8151872d22.

* revert

* revert

* fix dropdown alignment in button group; closes #374

* fix progress animation in Safari; closes #356

* fix native checkbox indeterminate icon; closes #386

* add comment

* stop running SSR tests locally

* update test

* add FA kit code for codepen 🤞🏻

* remove wa-cloak after components load

* fix whitespace

* update display labels when changed; fixes #702

* fix radio labels (ALPHA-211)

* revert example

* add option as a dep of select

* remove outdated section
2025-03-28 10:57:01 -04:00
Konnor Rogers
21aa85acc0 fix search for webawesome app (#845)
* fix search for webawesome app

* prettier
2025-03-27 16:51:41 -04:00
Lea Verou
404c15b303 Fix race condition, closes #843 2025-03-27 16:14:24 -04:00
Lea Verou
8a26afc334 Fix for theme icons + easier to generate palette icons (#841)
* Make sure components that only appear within page icons are still detected

* Palette icons

* Update theme-icons.css

* Reduce whitespace between swatches

---------

Co-authored-by: lindsaym-fa <dev@lindsaym.design>
2025-03-27 14:25:52 -04:00
Cory LaViska
513a1e35a9 Dialog fixes (#790)
* revert structure and styles to fix WA-A #123

* fix WA-A #201

* update changelog

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

* remove close watcher; fix dialog/drawer backdrop animations
2025-03-27 12:14:35 -04:00
Lea Verou
09f668fc99 Workaround for dark mode 2025-03-26 18:31:08 -04:00
Lea Verou
d451ba98e5 Fix web fonts in theme icons
Instead of raw DSD, use a component that pulls in a child template and then goes over the CSS and extracts font-related rules into the document, just once per rule.
This also fixes theme icons in Vue.
2025-03-26 18:31:08 -04:00
lindsaym-fa
fd287edd56 Change balance of color swatches 2025-03-26 18:31:08 -04:00
Lea Verou
8424b49646 Theme icons, take 1 2025-03-26 18:31:08 -04:00
Lea Verou
fa24c0f70e Update changelog.md 2025-03-26 13:08:44 -04:00
Cory LaViska
1bba87c66d Improve search lists (#837)
* add debounce to search so it feels more natural

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

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

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

* Filter unlisted items from overviews

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

* [Overview] Add id to group headings

* Hide headings from empty groups

Should never happen but you never know

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

* add comment

* Update docs/docs/installation.md

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

* commit PR suggestion

* rename wa-reduce-fouce to wa-cloak

* remove class as requested

* add cloak class

* wait a cycle

* move turbo to same file

* reduce fade

* disable SSR and add Turbo FOUCE helper

* disable SSR

* fix test suite

* workflow dispatch

* update fouce util

* no need to remove cloak class

* simplify fouce util

* add allDefined util

* update changelog

---------

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

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

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

* add a guard for non-server builds

* add a guard for non-server builds

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

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

* Adjust `orange` in Anodized palette

* Adjust `orange` in Bright palette

* Adjust `orange` in Mild palette

* Adjust `orange` in Natural palette

* Adjust `orange` in Vogue palette

* Adjust `orange` in Rudimentary palette

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

* 11ty for webawesome + app

* add flashes

* additional changes

* prettier

* add note about nunjucks

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

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

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

* Add `neutral` color variables

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

* Theme touch-ups

* Remove unused clamped tokens

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

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

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

* Re-add `40-min`, add `70-max`

* Fix mistakes in Mellow theme

* Revert accidental Premium brand color change

* Add changelog
2025-02-18 10:22:32 -05:00
Cory LaViska
c1ecca0169 fix select hint (#760) 2025-02-18 15:09:52 +00:00
276 changed files with 12061 additions and 5298 deletions

View File

@@ -1,11 +1,12 @@
# # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: SSR Tests
on:
push:
branches: [next]
# push:
# branches: [next]
workflow_dispatch:
jobs:
ssr_test:

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" data-fa-kit-code="b10bfbde90" data-cdn-url="{% cdnUrl %}">
<html lang="en" data-fa-kit-code="b10bfbde90" data-cdn-url="{% cdnUrl %}" class="wa-cloak">
<head>
{% include 'head.njk' %}
<meta name="theme-color" content="#f36944">
@@ -10,6 +10,7 @@
<script type="module" src="/assets/scripts/turbo.js"></script>
<script type="module" src="/assets/scripts/search.js"></script>
<script type="module" src="/assets/scripts/outline.js"></script>
{% if hasSidebar %}<script type="module" src="/assets/scripts/sidebar-tweaks.js"></script>{% endif %}
<script defer data-domain="backers.webawesome.com" src="https://plausible.io/js/script.js"></script>
{# Docs styles #}
@@ -19,7 +20,7 @@
</head>
<body class="layout-{{ layout | stripExtension }}{{ ' page-wide' if wide }}">
<!-- use view="desktop" as default to reduce layout jank on desktop site. -->
<wa-page view="desktop" disable-navigation-toggle="">
<wa-page view="desktop" disable-navigation-toggle="" mobile-breakpoint="1140">
<header slot="header" class="wa-split">
{# Logo #}
<div id="docs-branding">
@@ -32,13 +33,13 @@
<span class="wa-desktop-only">{% include "logo.njk" %}</span>
<span class="wa-mobile-only">{% include "logo-simple.njk" %}</span>
</a>
<small id="version-number" class="only-desktop">{{ package.version }}</small>
<wa-badge variant="warning" appearance="filled" class="only-desktop">Alpha</wa-badge>
<small id="version-number" class="wa-desktop-only">{{ package.version }}</small>
<wa-badge variant="warning" appearance="filled" class="wa-desktop-only">Alpha</wa-badge>
</div>
<div id="docs-toolbar" class="wa-cluster wa-gap-xs">
{# Desktop selectors #}
<div class="only-desktop wa-cluster wa-gap-xs">
<div class="wa-desktop-only wa-cluster wa-gap-xs">
{% include "preset-theme-selector.njk" %}
{% include "color-scheme-selector.njk" %}
</div>
@@ -47,8 +48,11 @@
<wa-button id="search-trigger" appearance="outlined" size="small" data-search>
<wa-icon slot="prefix" name="magnifying-glass"></wa-icon>
Search
<kbd slot="suffix" class="only-desktop">/</kbd>
<kbd slot="suffix" class="wa-desktop-only">/</kbd>
</wa-button>
{# Login #}
{% server "loginOrAvatar" %}
</div>
</header>
@@ -75,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

@@ -19,12 +19,24 @@
{% for tint_fg in tints | reverse -%}
{% set color_fg = palettes[paletteId][hue][tint_fg] %}
{% if (tint_fg - tint_bg) | abs == difference %}
<td data-tint-bg="{{ tint_bg }}" data-tint-fg="{{ tint_fg }}">
<div class="color swatch" style="--color: var(--wa-color-{{ hue }}-{{ tint_bg }}); color: var(--wa-color-{{ hue }}-{{ tint_fg }})">
{% set contrast_wcag = '' %}
{% if color_fg and color_bg %}
{% set contrast_wcag = color_bg.contrast(color_fg, 'WCAG21') %}
{% endif %}
{% set contrast_wcag = '' %}
{% if color_fg and color_bg -%}
{% set contrast_wcag = color_bg.contrast(color_fg, 'WCAG21') %}
{%- endif %}
<td v-for="contrast of [contrasts.{{ hue }}['{{ tint_bg }}']['{{ tint_fg }}']]"
data-tint-bg="{{ tint_bg }}" data-tint-fg="{{ tint_fg }}" data-original-contrast="{{ contrast_wcag }}">
<div v-content:number="contrast.value"
class="color swatch" :class="{
'value-up': contrast.value - contrast.original > 0.0001,
'value-down': contrast.original - contrast.value > 0.0001,
'contrast-fail': contrast.value < {{ minContrast }}
}"
style="--color: var(--wa-color-{{ hue }}-{{ tint_bg }}); color: var(--wa-color-{{ hue }}-{{ tint_fg }})"
:style="{
'--color': contrast.bgColor,
color: contrast.fgColor,
}"
>
{% if contrast_wcag %}
{{ contrast_wcag | number({maximumSignificantDigits: 2}) }}
{% else %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

After

Width:  |  Height:  |  Size: 587 B

View File

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

View File

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

After

Width:  |  Height:  |  Size: 631 B

View File

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

View File

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

View File

@@ -0,0 +1,29 @@
{% set themeId = theme.fileSlug or page.fileSlug %}
<wa-scoped class="theme-icon-host theme-overall-icon-host">
<template>
<link rel="stylesheet" href="/dist/styles/utilities.css">
<link rel="stylesheet" href="/dist/styles/native/content.css">
<link rel="stylesheet" href="/dist/styles/themes/{{ themeId }}.css">
<link rel="stylesheet" href="/assets/styles/theme-icons.css">
<div class="theme-icon theme-overall-icon" role="presentation" data-no-anchor data-no-outline>
<div class="row row-1">
<h2>Aa</h2>
<div class="swatches">
<div class="wa-brand"></div>
<div class="wa-success"></div>
<div class="wa-warning"></div>
<div class="wa-danger"></div>
<div class="wa-neutral"></div>
</div>
</div>
<div class="row row-2">
<wa-input value="Input" size="small" inert></wa-input>
<wa-button size="small" variant="brand" inert>Go</wa-button>
</div>
</div>
</template>
</wa-scoped>

View File

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

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,22 +1,83 @@
{% set hasSidebar = true %}
{% set hasOutline = true %}
{% set paletteId = page.fileSlug %}
{% set tints = ["95", "90", "80", "70", "60", "50", "40", "30", "20", "10", "05"] %}
{% extends '../_layouts/block.njk' %}
{% 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>
{% endblock %}
{% block header %}
<div id="palette-app" data-palette-id="{{ paletteId }}">
<div
:class="{
tweaking: tweaking.chroma,
'tweaking-chroma': tweaking.chroma,
'tweaking-hue': tweaking.chroma,
'tweaking-gray-chroma': tweaking.grayChroma,
'tweaked-chroma': tweaked?.chroma,
'tweaked-hue': tweaked?.hue,
'tweaked-any': tweaked
}"
:style="{
'--chroma-scale': chromaScale,
'--gray-chroma': tweaked?.grayChroma ? grayChroma : '',
}">
{% include 'breadcrumbs.njk' %}
<h1 class="title">
<span v-content="title">{{ title }}</span>
<template v-if="saved || tweaked">
<wa-icon-button name="pencil" label="Rename palette" @click="rename"></wa-icon-button>
<wa-icon-button v-if="saved" class="delete" name="trash" label="Delete palette" @click="deleteSaved"></wa-icon-button>
<wa-button @click="save()" :disabled="!unsavedChanges"
:variant="unsavedChanges ? 'success' : 'neutral'" size="small" :appearance="unsavedChanges ? 'accent' : 'outlined'">
<span slot="prefix" class="icon-modifier">
<wa-icon name="sidebar" variant="regular"></wa-icon>
<wa-icon name="circle-plus" class="modifier" style="color: light-dark(var(--wa-color-green-70), var(--wa-color-green-60));"></wa-icon>
</span>
<span v-content="unsavedChanges ? 'Save' : 'Saved'">Save</span>
</wa-button>
</template>
</h1>
<div class="block-info">
<code class="class">.wa-palette-{{ paletteId }}</code>
{% include '../_includes/status.njk' %}
{% if not isPro %}
<wa-badge class="pro" v-if="tweaked">PRO</wa-badge>
{% endif %}
</div>
{% if description %}
<p class="summary">
{{ description | inlineMarkdown | safe }}
</p>
{% endif %}
{% endblock %}
{% block afterContent %}
{% set tints = ["95", "90", "80", "70", "60", "50", "40", "30", "20", "10", "05"] %}
{% set maxChroma = 0 %}
<div id="palette-app">
<div
:class="{ tweaking: tweaking.chroma, 'tweaking-chroma': tweaking.chroma, 'tweaking-hue': tweaking.chroma }"
:style="{ '--chroma-scale': chromaScale }">
<wa-callout size="small" class="tweaked-callout" variant="warning">
<wa-icon name="sliders-simple" slot="icon" variant="regular"></wa-icon>
This palette has been tweaked.
<div class="wa-cluster wa-gap-xs">
<wa-tag v-for="tweakHumanReadable, param in tweaksHumanReadable" removable @wa-remove="reset(param)" v-content="tweakHumanReadable"></wa-tag>
</div>
<wa-button @click="reset()" appearance="outlined" variant="danger">
<span slot="prefix" class="icon-modifier">
<wa-icon name="circle-xmark" variant="regular"></wa-icon>
</span>
Reset
</wa-button>
</wa-callout>
<table class="colors main wa-palette-{{ paletteId }}">
<thead>
@@ -31,25 +92,67 @@
{# Initialize to last hue before gray #}
{%- set hueBefore = hues[hues|length - 2] -%}
{% for hue in hues -%}
{%- set coreColor = palettes[paletteId][hue][palettes[paletteId][hue].maxChromaTint] -%}
{% set coreTint = palettes[paletteId][hue].maxChromaTint %}
{%- set coreColor = palettes[paletteId][hue][coreTint] -%}
{%- set maxChroma = coreColor.c if coreColor.c > maxChroma else maxChroma -%}
<tr data-hue="{{ hue }}" class="color-scale" :class="{tweaking: tweaking.{{ hue }}, tweaked: hueShifts.{{ hue }} }"
{% if hue === 'gray' %}
<tr data-hue="{{ hue }}" class="color-scale"
:class="{tweaking: tweaking.grayChroma, tweaked: tweaked.grayChroma || tweaked.grayColor }">
{% else %}
<tr data-hue="{{ hue }}" class="color-scale"
:class="{tweaking: tweaking.{{ hue }}, tweaked: hueShifts.{{ hue }} }"
:style="{ '--hue-shift': hueShifts.{{ hue }} || '' }">
{% endif %}
<th>
{{ hue | capitalize }}
</th>
<td class="core-column" style="--color: var(--wa-color-{{ hue }})">
{% if hue !== 'gray' %}
{%- 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 -%}
<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">
<div slot="trigger" id="core-{{ hue }}-swatch" data-tint="core" class="color swatch"
style="background-color: var(--wa-color-{{ hue }}); color: var(--wa-color-{{ hue }}-{{ '05' if palettes[paletteId][hue].maxChromaTint > 60 else '95' }});"
>
{{ palettes[paletteId][hue].maxChromaTint }}
<wa-icon name="sliders-simple" class="tweak-icon"></wa-icon>
</div>
<div class="popup">
{% if hue === 'gray' %}
<wa-radio-group class="core-color" orientation="horizontal" v-model="grayColor">
{% for h in hues -%}
{%- if h !== 'gray' -%}
<wa-radio-button id="gray-undertone-{{ h }}" value="{{ h }}" label="{{ h | capitalize }}" style="--color: var(--wa-color-{{ h }})"></wa-radio-button>
<wa-tooltip for="gray-undertone-{{ h }}">
{{ h | capitalize }}
</wa-tooltip>
{%- endif -%}
{%- endfor -%}
<div slot="label">
Gray undertone
</div>
</wa-radio-group>
<div class="decorated-slider gray-chroma-slider" :style="{'--max': maxGrayChroma}">
<wa-slider name="gray-chroma" v-model="grayChroma" ref="grayChromaSlider"
value="0" min="0" :max="maxGrayChroma" step="0.01"
@input="tweaking.grayChroma = true" @change="tweaking.grayChroma = false">
<div slot="label">
Gray colorfulness
<wa-icon-button @click="grayChroma = originalGrayChroma" class="clear-button" name="circle-xmark" library="system" variant="regular" label="Reset"></wa-icon-button>
</div>
</wa-slider>
<div class="label-min">Neutral</div>
<div class="label-max" v-content="moreHue[grayColor]">Warmer/Cooler</div>
</div>
{% else %}
{%- set hueAfter = hues[loop.index0 + 1] -%}
{%- set hueAfter = hues[0] if hueAfter == 'gray' else hueAfter -%}
{%- set minShift = hueRanges[hue].min - coreColor.h | round -%}
{%- set maxShift = hueRanges[hue].max - coreColor.h | round -%}
<div class="decorated-slider hue-shift-slider" style="--min: {{ minShift }}; --max: {{ maxShift }};">
<wa-slider name="{{ hue }}-shift" v-model="hueShifts.{{ hue }}" value="0"
min="{{ minShift }}" max="{{ maxShift }}" step="1"
@@ -63,23 +166,23 @@
<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>`
<code>--wa-color-{{ hue }}</code>
<wa-copy-button value="--wa-color-{{ hue }}" copy-label="--wa-color-{{ hue }}"></wa-copy-button>
</div>
</div>`
</wa-dropdown>
{%- set hueBefore = hue -%}
{% else %}
<div id="core-{{ hue }}-swatch" class="color swatch" data-tint="core" style="background-color: var(--wa-color-{{ hue }}); color: var(--wa-color-{{ hue }}-{{ '05' if palettes[paletteId][hue].maxChromaTint > 60 else '95' }});">
{{ palettes[paletteId][hue].maxChromaTint }}
</div>
{% endif %}
</td>
{% for tint in tints -%}
{%- set color = palettes[paletteId][hue][tint] -%}
<td data-tint="{{ tint }}" style="--color: var(--wa-color-{{ hue }}-{{ tint }})">
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}-{{ tint }})">
<td data-tint="{{ tint }}" style="--color: var(--wa-color-{{ hue }}-{{ tint }})"
:style="{
'--color-tweaked': colors.{{ hue }}[{{ tint }}],
'--color-tweaked-no-gray-chroma': colorsMinusGrayChroma.{{ hue }}[{{ tint }}],
}">
<div class="color swatch" style="--color: var(--wa-color-{{ hue }}-{{ tint }})">
<wa-copy-button value="--wa-color-{{ hue }}-{{ tint }}" copy-label="--wa-color-{{ hue }}-{{ tint }}"></wa-copy-button>
</div>
</td>
@@ -94,7 +197,8 @@
<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" v-model="chromaScale" value="1" step="0.01"
<wa-slider name="chroma-scale" ref="chromaScaleSlider"
v-model="chromaScale" value="1" step="0.01"
min="{{ chromaScaleBounds[0] }}" max="{{ chromaScaleBounds[1] }}"
@input="tweaking.chroma = true"
@change="tweaking.chroma = false">
@@ -107,18 +211,6 @@ style="--min: {{ chromaScaleBounds[0] }}; --max: {{ chromaScaleBounds[1] }};">
<div class="label-max">More vibrant</div>
</div>
</div>
</div>
<script>
globalThis.wa_data = {
paletteId: '{{ paletteId }}',
colors: {{ palettes[paletteId] | dump | safe }},
maxChroma: {{ maxChroma }},
}
</script>
<script type="module" src="{{ page.url }}../tweak.js"></script>
<h2>Used By</h2>
<section class="index-grid">
@@ -201,7 +293,28 @@ If you are using a Web Awesome theme that uses this palette, it will already be
To use a different palette than a theme default, or to use it in a custom theme, you can import this palette directly from the Web Awesome CDN.
{% set stylesheet = 'styles/color/' + page.fileSlug + '.css' %}
{% include 'import-stylesheet-code.md.njk' %}
<wa-tab-group class="import-stylesheet-code">
<wa-tab panel="html">In HTML</wa-tab>
<wa-tab panel="css">In CSS</wa-tab>
<wa-tab-panel name="html">
Add the following code to the `<head>` of your page:
```html { v-content:html="code.html.highlighted" }
<link rel="stylesheet" href="{% cdnUrl stylesheet %}" />
```
</wa-tab-panel>
<wa-tab-panel name="css">
Add the following code at the top of your CSS file:
```css { v-content:html="code.css.highlighted" }
@import url('{% cdnUrl stylesheet %}');
```
</wa-tab-panel>
</wa-tab-group>
{% endmarkdown %}
</div></div> {# end palette app #}
{% endblock %}

View File

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

View File

@@ -25,8 +25,8 @@ wa_data.palettes = {
{% endfor %}
};
</script>
<link href="{{ page.url }}../remix.css" rel="stylesheet">
<script src="{{ page.url }}../remix.js" type="module"></script>
<link href="/docs/themes/remix.css" rel="stylesheet">
<script src="/docs/themes/remix.js" type="module"></script>
{% endblock %}
{% block header %}
@@ -68,7 +68,7 @@ wa_data.palettes = {
<wa-option label="{{ palette.data.title }}" value="{{ palette.fileSlug if not currentPalette }}" {{ (palette.fileSlug if currentPalette) | attr('data-id') }}>
<wa-card with-header>
<div slot="header">
{% include "svgs/" + (palette.data.icon or "thumbnail-placeholder") + ".njk" %}
{% include "svgs/" + (palette.data.icon or "thumbnail-placeholder") + ".njk" ignore missing %}
</div>
<span class="page-name">
{{ palette.data.title }}

View File

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

View File

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

View File

@@ -39,7 +39,7 @@ export function outlinePlugin(options = {}) {
}
// Create a clone of the heading so we can remove links and [data-no-outline] elements from the text content
clone.querySelectorAll('a').forEach(a => a.remove());
clone.querySelectorAll('.wa-visually-hidden, [hidden], [aria-hidden="true"]').forEach(el => el.remove());
clone.querySelectorAll('[data-no-outline]').forEach(el => el.remove());
// Generate the link

View File

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

View File

@@ -0,0 +1,171 @@
/**
* Low-level utility to encapsulate a bit of HTML (mainly to apply certain stylesheets to it without them leaking to the rest of the page)
* Usage: <wa-scoped><template><!-- your HTML here --></template></wa-scoped>
*/
import { discover } from '/dist/webawesome.js';
const imports = new Set();
const fontFaceRules = new Set();
export default class WaScoped extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.observer = new MutationObserver(() => this.render());
this.observer.observe(this, { childList: true, subtree: true, characterData: true });
}
connectedCallback() {
this.render();
this.ownerDocument.documentElement.addEventListener('wa-color-scheme-change', e =>
this.#applyDarkMode(e.detail.dark),
);
}
render() {
this.observer.takeRecords();
this.observer.disconnect();
this.shadowRoot.innerHTML = '';
// To avoid mutating this.childNodes while iterating over it
let nodes = [];
for (let template of this.childNodes) {
// Other solutions we can try if needed: <script type="text/html">, or comment nodes
if (template instanceof HTMLTemplateElement) {
if (template.content.childNodes.length > 0) {
nodes.push(template.content.cloneNode(true));
} else if (template.childNodes.length > 0) {
// Fake template, suck its children out of the light DOM
nodes.push(...template.childNodes);
}
} else {
// Regular child, suck it out of the light DOM
nodes.push(template);
}
}
this.shadowRoot.append(...nodes);
this.#fixStyles();
this.#applyDarkMode();
discover(this.shadowRoot);
this.observer.observe(this, { childList: true, subtree: true, characterData: true });
}
#applyDarkMode(isDark = getComputedStyle(this).colorScheme === 'dark') {
// Hack to make dark mode work
// NOTE If any child nodes actually have .wa-dark, this will override it
for (let node of this.shadowRoot.children) {
node.classList.toggle('wa-dark', isDark);
}
this.classList.toggle('wa-dark', isDark);
}
/**
* @font-face does not work in shadow DOM in Chrome & FF, as of March 2025 https://issues.chromium.org/issues/41085401
* This works around this issue by traversing the shadow DOM CSS looking
* for @font-face rules or CSS imports to known font providers and copies them to the main document
*/
async #fixStyles() {
let styleElements = [...this.shadowRoot.querySelectorAll('link[rel="stylesheet"], style')];
let loadStates = styleElements.map(element => {
try {
if (element.sheet?.cssRules) {
// Already loaded
return Promise.resolve(element.sheet);
}
} catch (e) {
// CORS
return Promise.resolve(null);
}
return new Promise((resolve, reject) => {
element.addEventListener('load', e => resolve(element.sheet));
element.addEventListener('error', e => reject(null));
});
});
await Promise.allSettled(loadStates);
let fontRules = findFontFaceRules(...this.shadowRoot.styleSheets);
if (!fontRules.length) {
return;
}
let doc = this.ownerDocument;
// Why not adoptedStyleSheets? Can't have @import in those yet
let id = `wa-scoped-hoisted-fonts`;
let style = doc.head.querySelector('style#' + id);
if (!style) {
style = Object.assign(doc.createElement('style'), { id, textContent: ' ' });
doc.head.append(style);
}
let sheet = style.sheet;
for (let rule of fontRules) {
let cssText = rule.cssText;
if (rule.type === CSSRule.FONT_FACE_RULE) {
if (fontFaceRules.has(cssText)) {
continue;
}
fontFaceRules.add(cssText);
sheet.insertRule(cssText);
} else if (rule.type === CSSRule.IMPORT_RULE) {
if (imports.has(rule.href)) {
continue;
}
imports.add(rule.href);
sheet.insertRule(cssText, 0);
}
}
}
static observedAttributes = [];
}
customElements.define('wa-scoped', WaScoped);
export const WEB_FONT_HOSTS = [
'fonts.googleapis.com',
'fonts.gstatic.com',
'use.typekit.net',
'fonts.adobe.com',
'kit.fontawesome.com',
'pro.fontawesome.com',
'cdn.materialdesignicons.com',
];
function findFontFaceRules(...stylesheets) {
let ret = [];
for (let sheet of stylesheets) {
let rules;
try {
rules = sheet.cssRules;
} catch (e) {
// CORS
continue;
}
for (let rule of rules) {
if (rule.type === CSSRule.FONT_FACE_RULE) {
ret.push(rule);
} else if (rule.type === CSSRule.IMPORT_RULE) {
if (WEB_FONT_HOSTS.some(host => rule.href.includes(host))) {
ret.push(rule);
} else if (rule.styleSheet) {
ret.push(...findFontFaceRules(rule.styleSheet));
}
}
}
}
return ret;
}

View File

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

View File

@@ -1,3 +1,11 @@
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
function updateResults(input) {
const filter = input.value.toLowerCase().trim();
let filtered = Boolean(filter);
@@ -18,8 +26,10 @@ function updateResults(input) {
}
}
const debouncedUpdateResults = debounce(updateResults, 300);
document.documentElement.addEventListener('input', e => {
if (e.target?.matches('#block-filter wa-input')) {
updateResults(e.target);
debouncedUpdateResults(e.target);
}
});

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

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

View File

@@ -0,0 +1,119 @@
import my from '/assets/scripts/my.js';
const sidebar = {
addChild(a, parentA) {
let parentLi = parentA.closest('li');
let ul = parentLi.querySelector(':scope > ul');
ul ??= parentLi.appendChild(document.createElement('ul'));
let li = document.createElement('li');
li.append(a);
ul.appendChild(li);
// If we are on the same page, update the current link
let url = location.href.replace(/#.+$/, '');
if (url.startsWith(a.href)) {
// Remove existing current
for (let current of document.querySelectorAll('#sidebar a.current')) {
current.classList.remove('current');
}
a.classList.add('current');
}
return a;
},
removeLink(a) {
if (!a || !a.isConnected) {
// Link doesn't exist or is already removed
return;
}
let li = a?.closest('li');
let ul = li?.closest('ul');
let parentA = ul?.closest('li')?.querySelector(':scope > a');
li?.remove();
if (ul?.children.length === 0) {
ul.remove();
}
if (a.classList.contains('current')) {
// If the deleted palette was the current one, the current one is now the parent
parentA.classList.add('current');
}
},
findEntity(entity) {
return document.querySelector(`#sidebar a[href^="${entity.baseUrl}"][data-uid="${entity.uid}"]`);
},
renderEntity(entity) {
let { url, parentUrl } = entity;
// Find parent
let parentA = document.querySelector(`#sidebar a[href="${parentUrl}"]`);
let parentLi = parentA?.closest('li');
if (!parentLi) {
throw new Error(`Cannot find parent url ${parentUrl}`);
}
// Find existing
let a = this.findEntity(entity);
let alreadyExisted = !!a;
a ??= document.createElement('a');
a.textContent = entity.title;
a.href = url;
if (!alreadyExisted) {
a.dataset.uid = entity.uid;
a = sidebar.addChild(a, parentA);
// This is mainly to port Pro badges
let badges = Array.from(parentLi.querySelectorAll('wa-badge'), badge => badge.cloneNode(true));
let append = [...badges];
if (entity.delete) {
let deleteButton = Object.assign(document.createElement('wa-icon-button'), {
name: 'trash',
label: 'Delete',
className: 'delete',
});
deleteButton.addEventListener('click', () => entity.delete());
append.push(deleteButton);
}
if (append.length > 0) {
a.closest('li').append(' ', ...append);
}
}
},
render() {
for (let type in my) {
let controller = my[type];
if (!controller.saved) {
continue;
}
for (let entity of controller.saved) {
let object = controller.getObject(entity);
this.renderEntity(object);
}
}
},
};
globalThis.sidebar = sidebar;
// Update sidebar when my saved stuff changes
my.addEventListener('delete', e => sidebar.removeLink(sidebar.findEntity(e.detail)));
my.addEventListener('save', e => sidebar.renderEntity(e.detail));
sidebar.render();
window.addEventListener('turbo:render', () => sidebar.render());

View File

@@ -1,14 +1,5 @@
// Helper for view transitions
export function domChange(fn, { behavior = 'smooth' } = {}) {
const canUseViewTransitions =
document.startViewTransition && !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (canUseViewTransitions && behavior === 'smooth') {
document.startViewTransition(fn);
} else {
fn(true);
}
}
import { domChange } from './util/dom-change.js';
export { domChange };
export function nextFrame() {
return new Promise(resolve => requestAnimationFrame(resolve));
@@ -100,6 +91,7 @@ const colorScheme = new ThemeAspect({
domChange(() => {
let dark = this.computedValue === 'dark';
document.documentElement.classList.toggle(`wa-dark`, dark);
document.documentElement.dispatchEvent(new CustomEvent('wa-color-scheme-change', { detail: { dark } }));
});
},
});

View File

@@ -1,3 +1,6 @@
import 'https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.10/+esm';
import { preventTurboFouce } from '/dist/webawesome.js';
if (!window.___turboScrollPositions___) {
window.___turboScrollPositions___ = {};
}
@@ -70,3 +73,4 @@ function fixDSD(e) {
window.addEventListener('turbo:before-cache', saveScrollPosition);
window.addEventListener('turbo:before-render', restoreScrollPosition);
window.addEventListener('turbo:render', restoreScrollPosition);
preventTurboFouce();

View File

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

View File

@@ -1,7 +1,7 @@
/**
* Get import code for remixed themes and tweaked palettes.
*/
import { selectors, urls } from './data.js';
import { urls } from './data.js';
export function cssImport(url, options = {}) {
let { language = 'html', cdnUrl = '/dist/', attributes } = options;
@@ -25,82 +25,27 @@ export function cssLiteral(value, options = {}) {
}
}
export function theme(base, params, options) {
// Params in correct order
export const themeParams = ['colors', 'palette', 'brand', 'typography'];
export function getThemeCode(base, params, options) {
let ret = [];
if (base) {
ret.push(urls.theme(base));
}
ret.push(
...Object.entries(params)
.filter(([aspect, id]) => Boolean(id))
.map(([aspect, id]) => urls[aspect](id)),
);
for (let aspect of themeParams) {
let value = params[aspect];
if (value) {
ret.push(urls[aspect](value));
}
}
return ret.map(url => cssImport(url, options)).join('\n');
}
export function palette(paletteId, tweaks, options) {
let imports = [];
if (paletteId) {
imports.push(urls.palette(paletteId));
}
let css = '';
if (tweaks) {
let { hueShifts, chromaScale = 1 } = tweaks;
let declarations = [];
if (chromaScale !== 1) {
declarations.push(`--wa-chroma-scale: ${chromaScale};`);
}
if (hueShifts || chromaScale !== 1) {
let element = document.querySelector(`.wa-palette-${paletteId}`) ?? document.documentElement;
let cs = getComputedStyle(element);
for (let hue in hueShifts) {
let shift = hueShifts[hue];
if ((!shift && chromaScale === 1) || hue === 'orange') {
continue;
}
let shiftCode = shift > 0 ? `+ ${shift}` : `- ${-shift}`;
let c = chromaScale === 1 ? 'c' : `calc(c * var(--wa-chroma-scale))`;
let h = shift ? `calc(h ${shiftCode})` : 'h';
declarations.push(`--wa-color-${hue}-tweak: l ${c} ${h});`);
for (let suffix of ['', '-05', '-10', '-20', '-30', '-40', '-50', '-60', '-70', '-80', '-90', '-95']) {
let baseColor = cs.getPropertyValue(`--wa-color-${hue}${suffix}`);
// Work around https://bugs.webkit.org/show_bug.cgi?id=287637
let colorSpace = ['-95', '-90'].includes(suffix) ? ' lch' : 'oklch';
let value = `${colorSpace}(from ${baseColor.padEnd(7)} var(--wa-color-${hue}-tweak))`;
suffix = (suffix + ':').padEnd(4);
declarations.push(`--wa-color-${hue}${suffix} ${value};`);
}
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;
}
export function cssRule(selector, declarations, { indent = ' ' } = {}) {
selector = Array.isArray(selector) ? selector.flat().join(',\n') : selector;
declarations = Array.isArray(declarations) ? declarations.flat() : declarations;

View File

@@ -29,6 +29,45 @@ export const hueRanges = {
pink: { min: 320, max: 365 }, // 45
};
export const moreHue = {
red: 'Redder',
orange: 'More orange', // https://www.reddit.com/r/grammar/comments/u9n0uo/is_it_oranger_or_more_orange/
yellow: 'Yellower',
green: 'Greener',
cyan: 'More cyan',
blue: 'Bluer',
indigo: 'More indigo',
pink: 'Pinker',
};
/**
* Max gray chroma (% of chroma of undertone) per hue
*/
export const maxGrayChroma = {
red: 0.2,
orange: 0.2,
yellow: 0.25,
green: 0.25,
cyan: 0.3,
blue: 0.35,
indigo: 0.35,
purple: 0.3,
pink: 0.25,
};
export const docsURLs = {
colors: '/docs/themes/',
palette: '/docs/palettes/',
typography: '/docs/themes/',
};
export const icons = {
colors: 'palette',
palette: 'swatchbook',
brand: 'droplet',
typography: 'font-case',
};
export const hues = Object.keys(hueRanges);
export const tints = ['05', '10', '20', '30', '40', '50', '60', '70', '80', '90', '95'];

View File

@@ -13,54 +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;
}
/**
@@ -77,3 +65,40 @@ export default class Permalink extends URLSearchParams {
}
}
}
function equals(value, oldValue) {
if (Array.isArray(value) || Array.isArray(oldValue)) {
value = toArray(value);
oldValue = toArray(oldValue);
if (value.length !== oldValue.length) {
return false;
}
return value.every((v, i) => equals(v, oldValue[i]));
}
// (value ?? oldValue ?? true) returns true if they're both empty (null or undefined)
[value, oldValue] = [value, oldValue].map(v => (!v && v !== false && v !== 0 ? null : v));
return value === oldValue || String(value) === String(oldValue);
}
/**
* Convert a value to an array. `undefined` and `null` values are converted to an empty array.
* @param {*} value - The value to convert.
* @returns {any[]} The converted array.
*/
function toArray(value) {
value ??= [];
if (Array.isArray(value)) {
return value;
}
// Don't convert "foo" into ["f", "o", "o"]
if (typeof value !== 'string' && typeof value[Symbol.iterator] === 'function') {
return Array.from(value);
}
return [value];
}

View File

@@ -0,0 +1,36 @@
export function normalizeAngles(angles) {
// First, normalize
angles = angles.map(h => ((h % 360) + 360) % 360);
// Remove top and bottom 25% and find average
let averageHue =
angles
.toSorted((a, b) => a - b)
.slice(angles.length / 4, -angles.length / 4)
.reduce((a, b) => a + b, 0) / angles.length;
for (let i = 0; i < angles.length; i++) {
let h = angles[i];
let prevHue = angles[i - 1];
let delta = h - prevHue;
if (Math.abs(delta) > 180) {
let equivalent = [h + 360, h - 360];
// Offset hue to minimize difference in the direction that brings it closer to the average
let delta = h - averageHue;
if (Math.abs(equivalent[0] - prevHue) <= Math.abs(equivalent[1] - prevHue)) {
angles[i] = equivalent[0];
} else {
angles[i] = equivalent[1];
}
}
}
return angles;
}
export function subtractAngles(θ1, θ2) {
let [a, b] = normalizeAngles([θ1, θ2]);
return a - b;
}

View File

@@ -0,0 +1,37 @@
let initialPageLoadComplete = document.readyState === 'complete';
if (!initialPageLoadComplete) {
window.addEventListener('load', () => {
initialPageLoadComplete = true;
});
}
/**
* Helper for performing a DOM change using a view transition, wherever supported and reduced motion is not desired.
* @param {function} fn - Function to perform the DOM change. If async, must resolve when the change is complete.
* @param {object} [options] - Options for the transition
* @param {'smooth' | 'instant'} [options.behavior] - Transition behavior. Defaults to 'smooth'. 'instant' will skip the transition.
* @param {boolean} [options.ignoreInitialLoad] - If true, will skip the transition on initial page load. Defaults to true.
*/
export function domChange(fn, { behavior = 'smooth', ignoreInitialLoad = true } = {}) {
const canUseViewTransitions =
document.startViewTransition && !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// Skip transitions on initial page load
if (!initialPageLoadComplete && ignoreInitialLoad) {
fn(false);
return null;
}
if (canUseViewTransitions && behavior === 'smooth') {
const transition = document.startViewTransition(() => {
fn(true);
// Wait a brief delay before finishing the transition to prevent jumpiness
return new Promise(resolve => setTimeout(resolve, 200));
});
return transition;
} else {
fn(false);
return null;
}
}

View File

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

View File

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

View File

@@ -4,6 +4,7 @@
@import 'outline.css';
@import 'search.css';
@import 'cera_typeface.css';
@import 'theme-icons.css';
:root {
--wa-brand-orange: #f36944;
@@ -156,7 +157,7 @@ wa-page > header {
}
/* Pro badges */
wa-badge.pro::part(base) {
wa-badge.pro {
background-color: var(--wa-brand-orange);
border-color: var(--wa-brand-orange);
}
@@ -188,6 +189,29 @@ wa-badge.pro::part(base) {
}
}
}
wa-icon-button.delete {
vertical-align: -0.2em;
margin-inline-start: var(--wa-space-xs);
&:not(li:hover > *, :focus) {
opacity: 0;
}
}
}
wa-icon-button.delete {
&:hover {
color: var(--wa-color-danger-on-quiet);
}
&::part(base):hover {
background: var(--wa-color-danger-fill-quiet);
}
&:not(:hover, :focus) {
opacity: 0.5;
}
}
#sidebar-close-button {
@@ -232,16 +256,32 @@ wa-page > main {
margin-inline: auto;
}
h1.title wa-badge {
vertical-align: middle;
h1.title {
wa-icon-button {
font-size: var(--wa-font-size-l);
color: var(--wa-color-text-quiet);
&::part(base) {
&:not(:hover, :focus) {
opacity: 0.5;
}
}
wa-badge {
vertical-align: middle;
font-size: 1.5rem;
}
}
.block-info {
display: flex;
gap: var(--wa-space-xs);
flex-wrap: wrap;
align-items: center;
margin-block-end: var(--wa-flow-spacing);
code {
line-height: var(--wa-line-height-condensed);
}
}
/* Current link */
@@ -331,10 +371,22 @@ wa-page > main:has(> .index-grid) {
.index-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(22ch, 100%), 1fr));
grid-template-columns: repeat(4, 1fr);
gap: var(--wa-space-2xl);
margin-block-end: var(--wa-space-3xl);
@media screen and (max-width: 1470px) {
grid-template-columns: repeat(3, 1fr);
}
@media screen and (max-width: 960px) {
grid-template-columns: repeat(2, 1fr);
}
@media screen and (max-width: 500px) {
grid-template-columns: repeat(1, 1fr);
}
a {
border-radius: var(--wa-border-radius-l);
text-decoration: none;
@@ -361,7 +413,6 @@ wa-page > main:has(> .index-grid) {
&::part(header) {
background-color: var(--header-background, var(--wa-color-neutral-fill-quiet));
border-bottom: none;
display: flex;
align-items: center;
justify-content: center;
@@ -500,6 +551,27 @@ table.colors {
--icon-color: var(--wa-color-success-fill-quiet);
}
.icon-modifier {
position: relative;
display: inline-flex;
.modifier {
position: absolute;
bottom: -0.1em;
right: -0.3em;
font-size: 60%;
&::part(svg) {
stroke: var(--background-color, var(--wa-color-surface-default));
stroke-width: 100px;
paint-order: stroke;
overflow: visible;
stroke-linecap: round;
stroke-linejoin: round;
}
}
}
/* Layout Examples */
.layout-example-boundary {
border: var(--wa-border-width-s) dashed var(--wa-color-neutral-border-normal);
@@ -536,13 +608,6 @@ table.colors {
margin-block-end: var(--wa-flow-spacing);
}
/** mobile */
@media screen and (max-width: 768px) {
wa-page .only-desktop {
display: none;
}
}
/** desktop */
@media screen and not (max-width: 768px) {
/* Navigation sidebar */

View File

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

View File

@@ -1,3 +1,61 @@
wa-card:has(> .theme-icon-host, > [slot='header'] > .theme-icon-host) {
&::part(header) {
/* We want to add a background color, so any spacing needs to go on .theme-icon */
flex: 1;
padding: 0;
min-block-size: 0;
}
[slot='header'] {
width: 100%;
}
}
.theme-icon-host,
.palette-icon-host {
flex: 1;
border-radius: inherit;
&[slot='header'],
[slot='header']:has(&) {
flex: 1;
border-radius: inherit;
}
}
.palette-icon {
display: grid;
grid-template-columns: repeat(var(--hues, 9), 1fr);
gap: var(--wa-space-3xs);
min-width: 20ch;
min-height: 9ch;
align-content: center;
.swatch {
height: 0.7em;
background: var(--color);
border-radius: var(--wa-border-radius-s);
&[data-suffix=''] {
height: 1.1em;
}
}
}
.theme-icon {
min-width: 18ch;
padding: var(--wa-space-xs) var(--wa-space-m);
border-radius: inherit;
box-sizing: border-box;
h2,
h3,
p {
margin-block: 0;
padding: 0;
}
}
.theme-color-icon {
display: grid;
gap: var(--wa-space-xs);
@@ -25,10 +83,50 @@
display: flex;
flex-direction: column;
gap: var(--wa-space-xs);
}
h3,
p {
margin-block: 0;
padding: 0;
.theme-overall-icon {
display: flex;
flex-flow: column;
gap: var(--wa-space-xs);
justify-content: center;
width: 100%;
min-height: 7.5rem;
box-sizing: border-box;
background: var(--wa-color-surface-lowered);
.row {
display: flex;
gap: var(--wa-space-xs);
align-items: center;
justify-content: space-between;
}
.row-2 {
display: grid;
grid-template-columns: 1fr auto;
contain: inline-size;
width: 100%;
wa-input {
min-width: 1em;
}
}
.swatches {
display: flex;
gap: var(--wa-space-3xs);
> div {
width: 1.25rem;
height: 1.25rem;
border-radius: var(--wa-border-radius-s);
background: var(--wa-color-fill-loud);
color: var(--wa-color-on-loud);
&.wa-brand {
width: 2.5rem;
}
}
}
}

View File

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

View File

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

View File

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

View File

@@ -77,6 +77,31 @@ The details component automatically adapts to right-to-left languages:
</wa-details>
```
### Appearance
Use the `appearance` attribute to change the elements visual appearance.
```html {.example}
<div class="wa-stack">
<wa-details summary="Outlined (default)">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
</wa-details>
<wa-details summary="Filled" appearance="filled">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
</wa-details>
<wa-details summary="Filled + Outlined" appearance="filled outlined">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
</wa-details>
<wa-details summary="Plain" appearance="plain">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
</wa-details>
</div>
```
### Grouping Details
Details are designed to function independently, but you can simulate a group or "accordion" where only one is shown at a time by listening for the `wa-show` event.

View File

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

View File

@@ -17,13 +17,93 @@ Not sure which icon to use? [Find the perfect icon over at Font Awesome!](https:
The default icon library is Font Awesome Free, which comes with two icon families: `classic` and `brands`. Use the `family` attribute to set the icon family.
Many Font Awesome Pro icon families have variants such as `thin`, `light`, `regular`, and `solid`. Font Awesome Pro users can [provide their kit code](/docs/installation/#using-font-awesome-kit-codes) to unlock additional families, including `sharp` and `duotone`. For these icon families, use the `variant` attribute to set the variant.
Many Font Awesome Pro icon families have variants such as `thin`, `light`, `regular`, and `solid`. Font Awesome Pro users can [provide their kit code](/docs/#using-font-awesome-kit-codes) to unlock additional families, including `sharp` and `duotone`. For these icon families, use the `variant` attribute to set the variant.
```html {.example}
<wa-icon family="brands" name="font-awesome"></wa-icon>
<wa-icon family="brands" name="web-awesome"></wa-icon>
```
### Setting defaults via CSS
You can use certain CSS custom properties to set icon defaults, not just on the icon itself, but any ancestor.
This can be useful when you want certain parameters to vary based on context, e.g. icons inside callouts or all icons for a given theme.
:::warning
These CSS properties are intended to set **defaults**, and thus only make a difference when the corresponding attributes are not set.
In future versions of Web Awesome, we may change this behavior to allow CSS properties to override attributes if `!important` is used.
:::
For example, here is how you can use CSS custom properties to set a default icon for each type of callout:
```html {.example}
<wa-callout>
<!-- Look ma, no attributes! -->
<wa-icon slot="icon"></wa-icon>
This is a normal callout.
</wa-callout>
<wa-callout variant="danger">
<wa-icon slot="icon" name="dumpster-fire" variant="solid"></wa-icon>
This is a callout with an explicit icon, which overrides these defaults.
</wa-callout>
<wa-callout variant="warning">
<!-- Look ma, no attributes! -->
<wa-icon slot="icon"></wa-icon>
Here be dragons.
</wa-callout>
<wa-callout variant="danger">
<!-- Look ma, no attributes! -->
<wa-icon slot="icon"></wa-icon>
Here be more dragons.
</wa-callout>
<wa-callout variant="success">
<!-- Look ma, no attributes! -->
<wa-icon slot="icon"></wa-icon>
Success!
</wa-callout>
<style>
wa-callout {
--wa-icon-variant: regular;
--wa-icon-name: info-circle;
&[variant="warning"] {
--wa-icon-name: triangle-exclamation;
}
&[variant="danger"] {
--wa-icon-name: circle-exclamation;
}
&[variant="success"] {
--wa-icon-name: circle-check;
}
}
</style>
```
You can even set icons dynamically, as a response to user interaction or media queries.
For example, here's how we can change the icon on hover:
```html {.example}
<wa-button class="github" href="https://github.com/webawesome/webawesome"><wa-icon slot="prefix" fixed-width></wa-icon> GitHub Repo</wa-button>
<style>
.github {
--wa-icon-name: github;
--wa-icon-family: brands;
&:hover {
--wa-icon-name: arrow-up-right-from-square;
--wa-icon-family: classic;
}
}
</style>
```
### Colors
Icons inherit their color from the current text color. Thus, you can set the `color` property on the `<wa-icon>` element or an ancestor to change the color.
@@ -561,4 +641,4 @@ If you want to change the icons Web Awesome uses internally, you can register an
resolver: name => `/path/to/custom/icons/${name}.svg`
});
</script>
```
```

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
title: Layout
description: Layout components and utility classes help you organize content that can adapt to any device or screen size. See the [installation instructions](#installation) to use Web Awesome's layout tools in your project.
layout: overview
parentOf: layout
categories: ["components", "utilities"]
override:tags: []
---
@@ -9,7 +10,7 @@ override:tags: []
{% markdown %}
## Installation
Layout components are included in Web Awesome's [autoloader](/docs/installation/#quick-start-autoloading-via-cdn). You can also import them individually via [cherry picking](/docs/installation/#cherry-picking).
Layout components are included in Web Awesome's [autoloader](/docs/#quick-start-autoloading-via-cdn). You can also import them individually via [cherry picking](/docs/#cherry-picking).
Layout utilities are bundled with all [style utilities](/docs/utilities). You can import all Web Awesome page styles (including [native styles](/docs/native/)) by including the following stylesheet in your project:
@@ -22,4 +23,4 @@ Or, you can choose to import _only_ the utilities:
```html
<link rel="stylesheet" href="{% cdnUrl 'styles/utilities.css' %}" />
```
{% endmarkdown %}
{% endmarkdown %}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
---
layout: null
permalink: "/docs/palettes/data.json"
eleventyExcludeFromCollections: true
---
{
{% for palette in collections.palettes %}
{% set paletteId = palette.fileSlug -%}
{% set colors = palettes[paletteId] -%}
"{{ paletteId }}": {
"title": "{{ palette.data.title }}",
"colors": {
{% for hue, tints in colors -%}
"{{ hue }}": {
{% for tint, value in tints -%}
{% if tint != "05" -%}
{% set value = value.coords or value -%}
{% set key = "05" if tint == "5" else tint -%}
"{{ key }}": {{ value | dump | safe }}{{ ', ' if not loop.last }}
{%- endif %}
{% endfor -%}
}{{ ', ' if not loop.last }}
{% endfor %} {# end of hue #}
}
}{{ ', ' if not loop.last }} {# end of palette #}
{% endfor %}
}

View File

@@ -1,4 +1,5 @@
---
title: Default
description: This is the palette used in the default theme.
order: 0
---

View File

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

View File

@@ -1,6 +1,7 @@
{
"layout": "palette.njk",
"tags": ["palettes", "palette"],
"wide": true,
"eleventyComputed": {
"snippet": ".wa-palette-{{ page.fileSlug }}",
"icon": "palette",

View File

@@ -25,9 +25,16 @@ wa-dropdown > .color.swatch {
--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 oklch, var(--color-1), var(--color-2));
background: linear-gradient(to right in var(--color-interpolation-space, oklab), var(--color-1), var(--color-2));
}
}
@@ -63,13 +70,20 @@ wa-dropdown > .color.swatch {
.hue-shift-slider {
--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);
--color-tweaked: oklch(from var(--color) l calc(c * var(--chroma-scale)) h);
}
.gray-chroma-slider {
--color: var(--wa-color-gray);
--color-1: oklch(from var(--wa-color-gray) l 0 none);
--color-2: oklch(from var(--color-gray-undertone) l calc(c * var(--max)) h);
margin-top: var(--wa-space-m);
}
.popup {
@@ -91,13 +105,13 @@ wa-dropdown > .color.swatch {
td:not([data-hue='gray'] *) {
--tweak-c: calc(c * var(--chroma-scale, 1));
--tweak-h: calc(h + var(--hue-shift, 0));
--color-tweaked: oklch(from var(--color) l var(--tweak-c) var(--tweak-h));
--color-tweaked-no-chroma-scale: oklch(from var(--color) l c var(--tweak-h));
--color-tweaked-no-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: lch(from var(--color) l var(--tweak-c) var(--tweak-h));
--color-tweaked-no-chroma-scale: lch(from var(--color) l c var(--tweak-h));
--color-tweaked-no-hue-shift: lch(from var(--color) l var(--tweak-c) h);
@@ -111,14 +125,18 @@ wa-dropdown > .color.swatch {
&:is(.tweaking *) {
--color-2-height: 70%;
}
&:is(.tweaking-chroma *) {
--color: var(--color-tweaked-no-chroma-scale);
}
&:is(.tweaking-chroma *) {
--color: var(--color-tweaked-no-chroma-scale);
}
&:is(.tweaking-hue *) {
--color: var(--color-tweaked-no-hue-shift);
}
&:is(.tweaking-hue *) {
--color: var(--color-tweaked-no-hue-shift);
}
&:is(.tweaking-gray-chroma *) {
--color: var(--color-tweaked-no-gray-chroma);
}
}
@@ -138,3 +156,50 @@ wa-dropdown > .color.swatch {
--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,9 +1,13 @@
// TODO move these to local imports
import Color from 'https://colorjs.io/dist/color.js';
import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';
import { cdnUrl, getPaletteCode, hueRanges, hues, Permalink, tints } from '../../assets/scripts/tweak.js';
import { createApp, nextTick } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';
import { cdnUrl, hueRanges, hues, Permalink, tints } from '../../assets/scripts/tweak.js';
import { cssImport, cssLiteral, cssRule } from '../../assets/scripts/tweak/code.js';
import { maxGrayChroma, moreHue, selectors, urls } from '../../assets/scripts/tweak/data.js';
import { subtractAngles } from '../../assets/scripts/tweak/util.js';
import Prism from '/assets/scripts/prism.js';
import content from '/assets/scripts/vue/directives/content.js';
import savedMixin from '/assets/scripts/vue/mixins/saved.js';
await Promise.all(['wa-slider'].map(tag => customElements.whenDefined(tag)));
@@ -17,135 +21,328 @@ await Promise.all(['wa-slider'].map(tag => customElements.whenDefined(tag)));
// return computedColor.endsWith(' 0)');
// })();
let paletteAppSpec = {
data() {
const { paletteId, colors, maxChroma } = wa_data;
let allPalettes = await fetch('/docs/palettes/data.json').then(r => r.json());
globalThis.allPalettes = allPalettes;
// Replace colors with their oklch coords (since they're all opaque and all in the same color space)
for (let hue in colors) {
for (let tint of tints) {
colors[hue][tint] = colors[hue][tint].coords;
for (let palette in allPalettes) {
for (let hue in allPalettes[palette].colors) {
let scale = allPalettes[palette].colors[hue];
for (let tint of tints) {
let color = scale[tint];
if (Array.isArray(color)) {
scale[tint] = new Color('oklch', color);
}
}
}
}
const percentFormatter = value => value.toLocaleString(undefined, { style: 'percent' });
let paletteAppSpec = {
mixins: [savedMixin],
data() {
let appRoot = document.querySelector('#palette-app');
let id = appRoot.dataset.paletteId;
let palette = allPalettes[id];
return {
permalink: new Permalink(),
id,
originalTitle: palette.title,
originalColors: palette.colors,
hueRanges,
hueShifts: Object.fromEntries(hues.map(hue => [hue, 0])),
paletteId,
originalColors: colors,
maxChroma,
chromaScale: 1,
grayChroma: undefined,
grayColor: undefined,
tweaking: {},
type: 'palette',
collection: 'palettes',
};
},
created() {
// 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)),
});
// Non-reactive variables to expose
Object.assign(this, { moreHue });
this.grayChroma = this.originalGrayChroma;
this.grayColor = this.originalGrayColor;
if (location.search) {
// Update from URL
this.permalink.writeTo(this.hueShifts);
// Read URL params and apply them. This facilitates permalinks.
for (let hue in this.hueShifts) {
if (this.permalink.has(hue + '-shift')) {
this.hueShifts[hue] = Number(this.permalink.get(hue + '-shift'));
}
}
if (this.permalink.has('chroma-scale')) {
this.chromaScale = Number(this.permalink.get('chromaScale') || 1);
for (let param of ['chroma-scale', 'gray-color', 'gray-chroma']) {
if (this.permalink.has(param)) {
let value = this.permalink.get(param);
if (!isNaN(value)) {
// Convert numeric values to numbers
value = Number(value);
}
let prop = camelCase(param);
this[prop] = value;
}
}
}
},
mounted() {
if (this.isTweaked) {
// Update contrast colors
updateContrastTables(this.colors);
for (let ref in this.$refs) {
this.$refs[ref].tooltipFormatter = percentFormatter;
}
},
computed: {
/** Default palette title for saving */
defaultTitle() {
return this.originalTitle + ' (tweaked)';
},
tweaks() {
return { hueShifts: this.hueShifts, chromaScale: this.chromaScale };
return {
hueShifts: this.hueShifts,
chromaScale: this.chromaScale,
grayColor: this.grayColor,
grayChroma: this.grayChroma,
};
},
isTweaked() {
return Object.values(this.hueShifts).some(Boolean);
},
paletteHTML() {
return getPaletteCode(this.paletteId, this.tweaks, { language: 'html', cdnUrl });
},
code() {
let ret = {};
for (let language of ['html', 'css']) {
let code = getPaletteCode(this.id, this.colors, this.tweaked, { language, cdnUrl });
ret[language] = {
raw: code,
highlighted: Prism.highlight(code, Prism.languages[language], language),
};
}
paletteCSS() {
return getPaletteCode(this.paletteId, this.tweaks, { language: 'css', cdnUrl });
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 = {};
for (let hue in this.originalColors) {
ret[hue] = {};
if (this.chromaScale !== 1) {
ret.chromaScale = 'More ' + (this.chromaScale > 1 ? 'vibrant' : 'muted');
}
for (let tint of tints) {
ret[hue][tint] = this.originalColors[hue][tint].slice();
for (let hue in this.hueShifts) {
let shift = this.hueShifts[hue];
if (this.hueShifts[hue]) {
ret[hue][tint][2] += 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.chromaScale !== 1) {
ret[hue][tint][1] *= this.chromaScale;
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: {
// Note: These could move to `v-html` directives if we widen the app root
paletteHTML() {
let codeElement = document.querySelector('#usage ~ wa-tab-group.import-stylesheet-code code.language-html');
codeElement.textContent = this.paletteHTML;
let copyButton = codeElement.previousElementSibling;
copyButton.value = this.paletteHTML;
Prism.highlightElement(codeElement);
},
paletteCSS() {
let codeElement = document.querySelector('#usage ~ wa-tab-group.import-stylesheet-code code.language-css');
codeElement.textContent = this.paletteCSS;
let copyButton = codeElement.previousElementSibling;
copyButton.value = this.paletteCSS;
Prism.highlightElement(codeElement);
},
hueShifts: {
deep: true,
handler() {
this.permalink.readFrom(this.hueShifts);
// Update page URL
this.permalink.updateLocation();
// Update contrast colors
updateContrastTables(this.colors);
for (let hue in this.hueShifts) {
this.permalink.set(hue + '-shift', this.hueShifts[hue], 0);
}
},
},
chromaScale() {
this.permalink.set('chroma-scale', this.chromaScale, 1);
// Update page URL
this.permalink.updateLocation();
// Update contrast colors
updateContrastTables(this.colors);
},
grayColor() {
this.permalink.set('gray-color', this.grayColor, this.originalGrayColor);
},
grayChroma() {
this.permalink.set('gray-chroma', this.grayChroma, this.originalGrayChroma);
},
tweaks: {
deep: true,
async handler(value, oldValue) {
await nextTick(); // must run after individual watchers
// Update page URL
this.permalink.updateLocation();
this.unsavedChanges = true;
},
},
},
methods: {
/**
* Remove a specific tweak or all tweaks
* @param {string} [param] - The tweak to remove. If not provided, all tweaks are removed.
*/
reset(param) {
if (!param || param === 'chromaScale') {
this.chromaScale = 1;
}
if (param in this.hueShifts) {
this.hueShifts[param] = 0;
} else if (!param) {
for (let hue in this.hueShifts) {
this.hueShifts[hue] = 0;
}
}
if (!param || param === 'grayColor') {
this.grayColor = this.originalGrayColor;
}
if (!param || param === 'grayChroma') {
this.grayChroma = this.originalGrayChroma;
}
},
},
directives: {
content,
},
compilerOptions: {
@@ -154,41 +351,152 @@ let paletteAppSpec = {
};
function init() {
globalThis.paletteApp = createApp(paletteAppSpec).mount('#palette-app');
}
let paletteAppContainer = document.querySelector('#palette-app');
globalThis.paletteApp?.unmount?.();
function updateContrastTables(colors) {
for (let table of document.querySelectorAll('.contrast-table')) {
let { minContrast } = table.dataset;
for (let tr of table.querySelectorAll('tr[data-hue]')) {
let { hue } = tr.dataset;
for (let td of tr.querySelectorAll('td[data-tint-bg][data-tint-fg]')) {
let swatch = td.querySelector('.color.swatch');
let { tintBg, tintFg, originalContrast } = td.dataset;
let bg = new Color('oklch', colors[hue][tintBg]);
let fg = new Color('oklch', colors[hue][tintFg]);
if (!originalContrast) {
td.dataset.originalContrast = originalContrast = swatch.textContent.trim();
}
let contrast = bg.contrast(fg, 'WCAG21').toLocaleString(undefined, { maximumSignificantDigits: 2 });
swatch.textContent = contrast;
swatch.classList.toggle('value-up', contrast > originalContrast);
swatch.classList.toggle('value-down', contrast < originalContrast);
swatch.classList.toggle('contrast-fail', contrast < minContrast);
swatch.style.setProperty('--color', bg.display());
swatch.style.setProperty('color', fg.display());
}
}
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;
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +0,0 @@
---
title: Order History
description: TODO
parent: ecommerce
tags: e-commerce
---
TODO Page Description
## Invoice panels
```html{.example}
```

View File

@@ -1,13 +0,0 @@
---
title: Product List
description: TODO
parent: ecommerce
tags: e-commerce
---
TODO Page Description
## With split image
```html{.example}
```

View File

@@ -1,129 +0,0 @@
---
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,166 +0,0 @@
---
title: Product Lists
description: TODO
parent: ecommerce
tags: e-commerce
---
TODO Page Description
## With Product Grid
```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>
<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>
<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>
<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>
</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>
```
## 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>
<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,465 +0,0 @@
---
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,10 +0,0 @@
---
title: Shopping Cart
description: TODO
parent: ecommerce
tags: e-commerce
---
TODO Page Description
## Examples

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,84 @@
---
title: Category Filter
description: 'Helps the user find the right products with filters to refine search results by specific attributes.'
icon: checkbox
isPro: true
---
## Sidebar with Checkboxes & Expandable 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>
```

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