Compare commits

...

11 Commits

Author SHA1 Message Date
Lea Verou
bc775fd2b9 Merge branch 'styling-attributes' of https://github.com/shoelace-style/webawesome into styling-attributes 2025-01-13 13:20:31 -05:00
Lea Verou
96fd2941df Experiment 2025-01-13 13:20:25 -05:00
Lea Verou
a07f6280a3 Themes category in docs (#477)
Co-authored-by: Lindsay M <126139086+lindsaym-fa@users.noreply.github.com>
Co-authored-by: lindsaym-fa <dev@lindsaym.design>
2025-01-13 13:14:12 -05:00
Lea Verou
6822b25772 Rewrite color scheme logic (#481)
- Minimize needless swaps
- Centralize logic
- Use an actual `<wa-select>` rather than hacking it with `<wa-dropdown>`
- Move shared includes to shared template
- Rewrite critical theme JS
- Refactor of theme picker code
- Utilize view transitions better and use them for color scheme too
- Do not store default value in localStorage
- Removed unused `*-wide` templates
- Fixed #482
2025-01-10 15:32:28 -05:00
Konnor Rogers
200188b0c3 properly report build failures (#485)
* properly report build failures

* prettier
2025-01-10 13:29:14 -05:00
Lea Verou
bc6fe95f13 npm run build:alpha:serve per @KonnorRogers suggestion 2025-01-10 13:13:01 -05:00
Lea Verou
3a33fa208a Add command for testing the build without having to push to Vercel 2025-01-10 13:13:01 -05:00
Lea Verou
3ec4e6de07 Fix build 2025-01-10 13:13:01 -05:00
Lea Verou
6644288f2b Experiment 2025-01-10 12:29:12 -05:00
Lea Verou
eb07dc1410 Use passthrough copy (#474) 2025-01-09 17:24:18 -05:00
Lindsay M
5bfeb8044e Theme showcase improvements (#480)
* Presentation improvements

* Change `-webkit-text-size-adjust` to supported value (`none` isn't a supported keyword on iOS)

* Try using `text-size-adjust`

* Throwing caution to the wind

* Give up on `text-size-adjust` and add 1-column view
2025-01-09 13:31:31 -05:00
51 changed files with 1026 additions and 881 deletions

View File

@@ -59,6 +59,8 @@ To generate a production build, run the following command.
npm run build
```
You can also run `npm run build:serve` to start an [`http-server`](https://www.npmjs.com/package/http-server) instance on `http://localhost:4000` after the build completes, so you can preview the production build.
### Creating New Components
To scaffold a new component, run the following command, replacing `wa-tag-name` with the desired tag name.

View File

@@ -26,6 +26,9 @@ const globalData = {
layout: 'page.njk',
};
const passThroughExtensions = ['js', 'css', 'png', 'svg', 'jpg', 'mp4'];
const passThrough = [...passThroughExtensions.map(ext => 'docs/**/*.' + ext)];
export default function (eleventyConfig) {
// NOTE - alpha setting removes certain pages
if (isAlpha) {
@@ -155,9 +158,18 @@ export default function (eleventyConfig) {
// eleventyConfig.addPlugin(formatCodePlugin());
// }
eleventyConfig.addPassthroughCopy({
'docs/assets': 'assets',
});
for (let glob of passThrough) {
eleventyConfig.addPassthroughCopy(glob);
}
return {
markdownTemplateEngine: 'njk',
dir: {
input: 'docs',
includes: '_includes',
layouts: '_layouts',
},

View File

@@ -31,13 +31,25 @@ const components = manifest.modules.flatMap(module => {
return prop.kind === 'field' && prop.privacy !== 'private';
});
return {
let ret = {
...declaration,
slug: declaration.tagName.replace(/^wa-/, ''),
methods,
attributes,
properties,
};
// Add named keys to these arrays
for (let thing of ['methods', 'attributes', 'properties', 'cssProperties', 'cssParts', 'cssStates', 'events']) {
if (ret[thing]) {
ret[thing] = ret[thing].reduce((acc, item) => {
acc[item.name] = item;
return acc;
}, ret[thing]);
}
}
return ret;
});
});

File diff suppressed because one or more lines are too long

View File

@@ -1,83 +1,20 @@
<!DOCTYPE html>
<html lang="en" data-fa-kit-code="b10bfbde90" data-cdn-url="{% cdnUrl %}">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{{ description }}">
{% include 'head.njk' %}
<meta name="theme-color" content="#f36944">
{% if noindex %}<meta name="robots" content="noindex">{% endif %}
<title>{{ title }}</title>
<link rel="icon" href="/assets/images/webawesome-logo.svg" />
<link rel="apple-touch-icon" href="/assets/images/app-icon.png">
{# Scripts #}
{# Hydration stuff #}
<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>
<script type="module" src="/assets/scripts/code-examples.js"></script>
<script type="module" src="/assets/scripts/color-scheme.js"></script>
<script type="module" src="/assets/scripts/copy-code.js"></script>
<script type="module" src="/assets/scripts/preset-theme.js"></script>
<script type="module" src="/assets/scripts/scroll.js"></script>
<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>
<script defer data-domain="backers.webawesome.com" src="https://plausible.io/js/script.js"></script>
{# Web Awesome #}
<script type="module" src="/dist/webawesome.ssr-loader.js"></script>
<link rel="stylesheet" id="theme-stylesheet" href="/dist/styles/themes/default.css" />
<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" />
{# Docs styles #}
<link rel="stylesheet" href="/assets/styles/docs.css" />
{# Set the theme to prevent flashing #}
<script>
function getColorScheme() {
return localStorage.getItem('colorScheme') || 'auto';
}
function getPresetTheme () {
return localStorage.getItem('presetTheme') || 'default';
}
function isDark() {
const colorScheme = getColorScheme()
if (colorScheme === 'auto') {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
return colorScheme === 'dark';
}
const oldStylesheet = document.querySelector("#theme-stylesheet")
const newStylesheet = document.createElement("link")
let preset = getPresetTheme()
newStylesheet.href = `/dist/styles/themes/${preset}.css`
newStylesheet.rel = "preload"
newStylesheet.as = "style"
document.head.append(newStylesheet)
function updateStylesheet () {
newStylesheet.rel = "stylesheet"
newStylesheet.id = "theme-stylesheet"
requestAnimationFrame(() => oldStylesheet.remove())
}
newStylesheet.addEventListener("load", updateStylesheet)
document.documentElement.classList.toggle(`wa-dark`, isDark());
</script>
</head>
<body class="layout-{{ layout | stripExtension }}">
<!-- use view="desktop" as default to reduce layout jank on desktop site. -->

View File

@@ -1,15 +1,19 @@
{# Color scheme selector #}
<wa-dropdown class="color-scheme-selector">
<wa-button slot="trigger" appearance="filled" size="small" pill caret title="Press \ to toggle">
<wa-select class="color-scheme-selector" appearance="filled" size="small" value="auto" pill title="Press \ to toggle">
<wa-icon class="only-light" slot="prefix" name="sun" variant="regular"></wa-icon>
<wa-icon class="only-dark" slot="prefix" name="moon" variant="regular"></wa-icon>
<wa-option value="light">
<wa-icon slot="prefix" name="sun" variant="regular"></wa-icon>
Light
</wa-option>
<wa-option value="dark">
<wa-icon slot="prefix" name="moon" variant="regular"></wa-icon>
Dark
</wa-option>
<wa-divider></wa-divider>
<wa-option value="auto">
<wa-icon class="only-light" slot="prefix" name="sun" variant="regular"></wa-icon>
<wa-icon class="only-dark" slot="prefix" name="moon" variant="regular"></wa-icon>
<span class="only-light">Light</span>
<span class="only-dark">Dark</span>
</wa-button>
<wa-menu>
<wa-menu-item type="checkbox" value="light">Light</wa-menu-item>
<wa-menu-item type="checkbox" value="dark">Dark</wa-menu-item>
<wa-divider></wa-divider>
<wa-menu-item type="checkbox" value="auto">System</wa-menu-item>
</wa-menu>
</wa-dropdown>
System
</wa-option>
</wa-select>

53
docs/_includes/head.njk Normal file
View File

@@ -0,0 +1,53 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{{ description }}">
{% if noindex %}<meta name="robots" content="noindex">{% endif %}
<title>{{ title }}</title>
{# Dark mode #}
<script>
let colorScheme = localStorage.colorScheme;
let isDark = localStorage.colorScheme === "dark";
if (!colorScheme || colorScheme === "auto") {
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
}
document.documentElement.classList.toggle('wa-dark', isDark);
</script>
<link rel="icon" href="/assets/images/webawesome-logo.svg" />
<link rel="apple-touch-icon" href="/assets/images/app-icon.png">
{# Scripts #}
{# Hydration stuff #}
<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>
{# Web Awesome #}
<script type="module" src="/dist/webawesome.ssr-loader.js"></script>
<script type="module" src="/assets/scripts/theme-picker.js"></script>
{# Preset Theme #}
<link id="theme-stylesheet" rel="stylesheet" id="theme-stylesheet" href="/dist/styles/themes/{{ forceTheme or 'default' }}.css" render="blocking" fetchpriority="high" />
{% if not forceTheme %}
<script>
if (localStorage.presetTheme) {
let preset = localStorage.presetTheme;
let script = document.currentScript;
let link = script.previousElementSibling;
let newLink = link.cloneNode();
newLink.href = link.href.replace("/default.css", `/${preset}.css`);
newLink.addEventListener('load', () => {
link.remove();
});
link.after(newLink);
}
</script>
<script type="module" src="/assets/scripts/preset-theme-picker.js"></script>
{% endif %}
<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" />

View File

@@ -1,5 +1,5 @@
<div id="page_slots_demo">
<link rel="stylesheet" href="/assets/examples/page-demo/demo.css">
<link rel="stylesheet" href="./demo.css">
{% set slots = components.page.slots %}
<fieldset id="page_slots_fieldset">
@@ -22,5 +22,5 @@
<script type="module">
const cacheBust = new Date().toString()
import(`/assets/examples/page-demo/demo.js?${cacheBust}`)
import(`./demo.js?${cacheBust}`)
</script>

View File

@@ -1,19 +1,9 @@
{# Preset theme selector #}
<wa-dropdown class="preset-theme-selector">
<wa-button slot="trigger" appearance="filled" size="small" pill caret>
<wa-icon slot="prefix" name="paintbrush" variant="regular"></wa-icon>
<span class="preset-theme-selector__text">Default</span>
</wa-button>
<wa-menu>
<wa-menu-item type="checkbox" value="default" data-description="Your trusty companion, like a perfectly broken-in pair of jeans.">Default</wa-menu-item>
<wa-menu-item type="checkbox" value="classic" data-description="Timeless elegance that never goes out of style.">Classic</wa-menu-item>
<wa-menu-item type="checkbox" value="awesome" data-description="Punchy and vibrant, the rockstar of themes.">Awesome</wa-menu-item>
<wa-menu-item type="checkbox" value="active" data-description="Energetic and tactile, always in motion.">Active</wa-menu-item>
<wa-menu-item type="checkbox" value="brutalist" data-description="Sharp, raw, and unapologetically bold.">Brutalist</wa-menu-item>
<wa-menu-item data-alpha="remove" type="checkbox" value="glassy" data-description="TODO">Glassy</wa-menu-item>
<wa-menu-item type="checkbox" value="mellow" data-description="Soft and soothing, like a lazy Sunday morning.">Mellow</wa-menu-item>
<wa-menu-item type="checkbox" value="tailspin" data-description="Like a bird in flight, guiding you from there to here.">Tailspin</wa-menu-item>
<wa-menu-item data-alpha="remove" type="checkbox" value="playful" data-description="TODO">Playful</wa-menu-item>
<wa-menu-item data-alpha="remove" type="checkbox" value="premium" data-description="TODO">Premium</wa-menu-item>
</wa-menu>
</wa-dropdown>
<wa-select appearance="filled" size="small" value="default" pill class="preset-theme-selector">
<wa-icon slot="prefix" name="paintbrush" variant="regular"></wa-icon>
{% for theme in collections.theme | sort %}
<wa-option value="{{ theme.page.fileSlug }}"{{ ' data-alpha="remove"' if theme.noAlpha }}>
{{ theme.data.title }}
</wa-option>
{% endfor %}
</wa-select>

View File

@@ -3,7 +3,6 @@
<ul>
<li><a href="/docs/installation">Installation</a></li>
<li><a href="/docs/usage">Usage</a></li>
<li><a href="/docs/themes">Themes</a></li>
<li><a href="/docs/customizing">Customizing</a></li>
<li><a href="/docs/form-controls">Form Controls</a></li>
<li><a href="/docs/localization">Localization</a></li>
@@ -20,6 +19,7 @@
</ul>
{% for tag, title in {
'themes': 'Themes',
'components': 'Components',
'native': 'Native Styles',
'utilities': 'Style Utilities',

View File

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

View File

@@ -0,0 +1,144 @@
<div class="showcase-examples-wrapper" aria-hidden="true" data-no-outline>
<div class="showcase-examples">
<wa-card with-header with-footer>
<div slot="header" class="wa-split">
<h3 class="wa-heading-m">Your Cart</h3>
<wa-icon-button name="xmark" tabindex="-1"></wa-icon-button>
</div>
<div class="wa-stack wa-gap-xl">
<div class="wa-flank">
<wa-avatar shape="rounded" style="--size: 3em; --background-color: var(--wa-color-green-60); --text-color: var(--wa-color-green-95);">
<wa-icon slot="icon" name="sword-laser" family="duotone" style="font-size: 1.5em;"></wa-icon>
</wa-avatar>
<div class="wa-stack wa-gap-2xs">
<div class="wa-split wa-gap-2xs">
<strong>Initiate Saber</strong>
<strong>$179.99</strong>
</div>
<div class="wa-split wa-gap-2xs wa-caption-m">
<span>Green</span>
<a href="#" tabindex="-1">Remove</a>
</div>
</div>
</div>
<wa-divider></wa-divider>
<div class="wa-flank">
<wa-avatar shape="rounded" style="--size: 3em; --background-color: var(--wa-color-teal-60); --text-color: var(--wa-color-teal-95);">
<wa-icon slot="icon" name="robot-astromech" family="duotone" style="font-size: 1.5em;"></wa-icon>
</wa-avatar>
<div class="wa-stack wa-gap-2xs">
<div class="wa-split wa-gap-2xs">
<strong>Repair Droid</strong>
<strong>$3,049.99</strong>
</div>
<div class="wa-split wa-gap-2xs wa-caption-m">
<span>R-series</span>
<a href="#" tabindex="-1">Remove</a>
</div>
</div>
</div>
</div>
<div slot="footer" class="wa-stack">
<div class="wa-split">
<strong>Subtotal</strong>
<strong>$3,229.98</strong>
</div>
<span class="wa-caption-m">Shipping and taxes calculated at checkout.</span>
<wa-button tabindex="-1" variant="brand">
<wa-icon slot="prefix" name="shopping-bag"></wa-icon>
Checkout
</wa-button>
</div>
</wa-card>
<wa-card>
<wa-avatar shape="rounded" style="--size: 1.9lh; float: left; margin-right: var(--wa-space-m);">
<wa-icon slot="icon" name="hat-wizard" family="duotone" style="font-size: 1.75em;"></wa-icon>
</wa-avatar>
<p class="wa-body-l" style="margin: 0;">&ldquo;All we have to decide is what to do with the time that is given to us. There are other forces at work in this world, Frodo, besides the will of evil.&rdquo;</p>
</wa-card>
<wa-card>
<div class="wa-stack">
<h3 class="wa-heading-m">Sign In</h3>
<wa-input tabindex="-1" label="Email" placeholder="ddjarin@mandalore.gov">
<wa-icon slot="prefix" name="envelope" variant="regular"></wa-icon>
</wa-input>
<wa-input tabindex="-1" label="Password" type="password">
<wa-icon slot="prefix" name="lock" variant="regular"></wa-icon>
</wa-input>
<wa-button tabindex="-1" variant="brand">Sign In</wa-button>
<a href="#" tabindex="-1" class="wa-body-s">I forgot my password</a>
</div>
</wa-card>
<wa-card with-footer>
<div class="wa-stack">
<div class="wa-split">
<h3 class="wa-heading-m">To-Do</h3>
<wa-icon-button tabindex="-1" name="plus" label="Add task"></wa-icon-button>
</div>
<wa-checkbox tabindex="-1" checked>Umbrella for Adelard</wa-checkbox>
<wa-checkbox tabindex="-1" checked>Waste-paper basket for Dora</wa-checkbox>
<wa-checkbox tabindex="-1" checked>Pen and ink for Milo</wa-checkbox>
<wa-checkbox tabindex="-1">Mirror for Angelica</wa-checkbox>
<wa-checkbox tabindex="-1">Silver spoons for Lobelia</wa-checkbox>
</div>
<div slot="footer">
<a href="" tabindex="-1">View all completed</a>
</div>
</wa-card>
<wa-card>
<div class="wa-stack">
<div class="wa-frame wa-border-radius-m" style="align-self: center; max-inline-size: 25ch;">
<img src="https://images.unsplash.com/photo-1667514627762-521b1c815a89?q=20" alt="Album art">
</div>
<div class="wa-flank:end wa-align-items-start">
<div class="wa-stack wa-gap-3xs">
<div class="wa-cluster wa-gap-xs" style="height: 2.25em;">
<strong>The Stone Troll</strong>
<small><wa-badge variant="neutral" appearance="filled">E</wa-badge></small>
</div>
<span class="wa-caption-m">Samwise G</span>
</div>
<wa-icon-button tabindex="-1" name="ellipsis" label="Options"></wa-icon-button>
</div>
<div class="wa-stack wa-gap-2xs">
<wa-progress-bar value="34" style="height: 0.5em"></wa-progress-bar>
<div class="wa-split">
<span class="wa-caption-xs">1:01</span>
<span class="wa-caption-xs">-1:58</span>
</div>
</div>
<div class="wa-grid wa-align-items-center" style="--min-column-size: 1em; justify-items: center;">
<wa-icon-button tabindex="-1" name="backward" label="Skip backward"></wa-icon-button>
<wa-icon-button tabindex="-1" name="pause" style="font-size: var(--wa-font-size-2xl);" label="Pause"></wa-icon-button>
<wa-icon-button tabindex="-1" name="forward" label="Skip forward"></wa-icon-button>
</div>
</div>
</wa-card>
<wa-card>
<div class="wa-stack">
<h3 class="wa-heading-m">Chalmun's Spaceport Cantina</h3>
<div class="wa-cluster wa-gap-xs">
<wa-rating value="4.6" read-only></wa-rating>
<strong>4.6</strong>
<span>(419 reviews)</span>
</div>
<div class="wa-cluster wa-gap-xs">
<div class="wa-cluster wa-gap-3xs">
<wa-icon name="dollar" style="color: var(--wa-color-green-60);"></wa-icon>
<wa-icon name="dollar" style="color: var(--wa-color-green-60);"></wa-icon>
<wa-icon name="dollar" style="color: var(--wa-color-green-60);"></wa-icon>
</div>
<span class="wa-caption-m">&bull;</span>
<wa-tag size="small">Cocktail Bar</wa-tag>
<wa-tag size="small">Gastropub</wa-tag>
<wa-tag size="small">Local Fare</wa-tag>
<wa-tag size="small">Gluten Free</wa-tag>
</div>
<div class="wa-flank wa-gap-xs">
<wa-icon name="location-dot"></wa-icon>
<a href="#" class="wa-caption-m" tabindex="-1">Mos Eisley, Tatooine</a>
</div>
</div>
</wa-card>
</div>
</div>

View File

@@ -1,55 +1,7 @@
<!DOCTYPE html>
<html lang="en" data-fa-kit-code="b10bfbde90" data-cdn-url="{% cdnUrl %}">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{{ description }}">
{% if noindex %}<meta name="robots" content="noindex">{% endif %}
<title>{{ title }}</title>
<link rel="icon" href="/assets/images/webawesome-logo.svg" />
<link rel="apple-touch-icon" href="/assets/images/app-icon.png">
{# Scripts #}
{# Hydration stuff #}
<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>
{# Web Awesome #}
<script type="module" src="/dist/webawesome.ssr-loader.js"></script>
<link rel="stylesheet" id="theme-stylesheet" />
<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" />
{# Set the theme to prevent flashing #}
<script>
function getColorScheme() {
return localStorage.getItem('colorScheme') || 'auto';
}
function getPresetTheme () {
return localStorage.getItem('presetTheme') || 'default';
}
function isDark() {
const colorScheme = getColorScheme()
if (colorScheme === 'auto') {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
return colorScheme === 'dark';
}
const stylesheet = document.getElementById("theme-stylesheet")
let preset = getPresetTheme()
stylesheet.href = `/dist/styles/themes/${preset}.css`
document.documentElement.classList.toggle(`wa-dark`, isDark());
</script>
{% include 'head.njk' %}
</head>
<body class="layout-{{ layout | stripExtension }}">

View File

@@ -1,4 +0,0 @@
{% set hasSidebar = true %}
{% set hasOutline = true %}
{% extends "../_includes/base-wide.njk" %}

85
docs/_layouts/theme.njk Normal file
View File

@@ -0,0 +1,85 @@
{% set hasSidebar = true %}
{% set hasOutline = true %}
{# {% set forceTheme = page.fileSlug %} #}
{% extends '../_includes/base.njk' %}
{% block header %}
<iframe src='{{ page.url }}demo.html'></iframe>
{% endblock %}
{% block afterContent %}
{% markdown %}
## How to use this theme
You can import this theme from the Web Awesome CDN.
<wa-tab-group>
<wa-tab panel="html">In HTML</wa-tab>
<wa-tab panel="css">In CSS</wa-tab>
<wa-tab-panel name="html">
Simply add the following code to the `<head>` of your page:
```html
<link rel="stylesheet" href="{% cdnUrl 'styles/themes/' + page.fileSlug + '.css' %}" />
```
</wa-tab-panel>
<wa-tab-panel name="css">
Simply add the following code at the top of your CSS file:
```css
@import url('{% cdnUrl 'styles/themes/' + page.fileSlug + '.css' %}');
```
</wa-tab-panel>
</wa-tab-group>
## Dark mode
To activate the dark color scheme of the theme on any element and its contents, apply the class `wa-dark` to it.
This means you can use different color schemes throughout the page.
Here, we use the default theme with a dark sidebar:
```html
<html>
<head>
<link rel="stylesheet" href="path/to/web-awesome/dist/styles/themes/default.css" />
</head>
<body>
<nav class="wa-dark">
<!-- dark-themed sidebar -->
</nav>
<!-- light-themed content -->
</body>
</html>
```
You can apply the class to the `<html>` element on your page to activate the dark color scheme for the entire page.
```html
<html class="wa-dark">
<head>
<link rel="stylesheet" href="path/to/web-awesome/dist/styles/themes/{{ page.fileSlug }}.css" />
<!-- other links, scripts, and metadata -->
</head>
<body>
<!-- page content -->
</body>
</html>
```
### Detecting Color Scheme Preference
Web Awesome's themes have both light and dark styles built in.
However, Web Awesome doesn't try to auto-detect the user's light/dark mode preference.
This should be done at the application level.
As a best practice, to provide a dark theme in your app, you should:
- Check for [`prefers-color-scheme`](https://stackoverflow.com/a/57795495/567486) and use its value by default
- Allow the user to override the setting in your app
- Remember the user's preference and restore it on subsequent logins
Web Awesome avoids using the `prefers-color-scheme` media query because not all apps support dark mode, and it would break things for the ones that don't.
{% endmarkdown %}
{% endblock %}

View File

@@ -14,6 +14,18 @@ export function trimPipes(content) {
return typeof content === 'string' ? content.replace(/^(\s|\|)/g, '').replace(/(\s|\|)$/g, '') : content;
}
export function stripQuotes(content) {
return content.replace(/^(['"])(.+)\1$/g, '$2');
}
export function getEnumValues(type) {
return type
.split('|')
.map(value => value.trim())
.filter(Boolean)
.map(stripQuotes);
}
export function keys(obj) {
return Object.keys(obj);
}
@@ -167,6 +179,9 @@ export function sort(arr, by = { 'data.order': 1, 'data.title': '' }) {
* @returns { Object.<string, object[]> } An object with keys for each tag, and an array of items for each tag.
*/
export function groupByTags(collection, tags) {
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)));

View File

@@ -1,63 +0,0 @@
//
// Color scheme selector
//
(() => {
function setColorScheme(newColorScheme) {
colorScheme = newColorScheme;
localStorage.setItem('colorScheme', colorScheme);
// Update the UI
updateSelection();
// Toggle the dark mode class
document.documentElement.classList.toggle(`wa-dark`, window.isDark());
}
function updateSelection() {
const menus = document.querySelectorAll('.color-scheme-selector wa-menu');
menus.forEach(menu => {
if (!menu) return;
[...menu.querySelectorAll('wa-menu-item')].forEach(async item => {
await customElements.whenDefined(item.localName);
await item.updateComplete;
item.checked = item.getAttribute('value') === colorScheme;
});
});
}
let colorScheme = window.getColorScheme();
// Selection is not preserved when changing page, so update when opening dropdown
document.addEventListener('wa-show', event => {
const colorSchemeSelector = event.target.closest('.color-scheme-selector');
if (!colorSchemeSelector) return;
updateSelection();
});
// Listen for selections
document.addEventListener('wa-select', event => {
const selector = event.target.closest('.color-scheme-selector');
const menu = selector?.querySelector('wa-menu');
console.log(menu);
if (!menu) return;
setColorScheme(event.detail.item.value);
});
// Update the color scheme when the preference changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => setColorScheme(colorScheme));
// Toggle with backslash
document.addEventListener('keydown', event => {
if (
event.key === '\\' &&
!event.composedPath().some(el => ['input', 'textarea'].includes(el?.tagName?.toLowerCase()))
) {
event.preventDefault();
setColorScheme(window.isDark() ? 'light' : 'dark');
}
});
// Set the initial color scheme and sync the UI
setColorScheme(colorScheme);
})();

View File

@@ -0,0 +1,64 @@
import { domChange, nextFrame, ThemeAspect } from './theme-picker.js';
const presetTheme = new ThemeAspect({
defaultValue: 'default',
key: 'presetTheme',
picker: 'wa-select.preset-theme-selector',
applyChange() {
const oldStylesheets = [...document.querySelectorAll('#theme-stylesheet')];
const oldStylesheet = oldStylesheets.pop();
if (oldStylesheets.length > 0) {
// Remove all but the last one
for (let stylesheet of oldStylesheets) {
stylesheet.remove();
}
}
const href = `/dist/styles/themes/${this.value}.css`;
if (!oldStylesheet || oldStylesheet.getAttribute('href') !== href) {
const newStylesheet = document.createElement('link');
Object.assign(newStylesheet, { href, id: 'theme-stylesheet', rel: 'preload', as: 'style' });
oldStylesheet.after(newStylesheet);
newStylesheet.addEventListener(
'load',
e => {
domChange(
async instant => {
// Swap stylesheets
newStylesheet.rel = 'stylesheet';
if (instant) {
// If no VT, delay by 1 frame to make it smoother
await nextFrame();
}
oldStylesheet.remove();
},
{ behavior: 'smooth' },
);
},
{ once: true },
);
}
},
});
/**
* Without this, there's a flash of the incorrect preset theme.
*/
function updateSelectionBeforeTurboLoad(e) {
const newElement = e.detail.newBody || e.detail.newFrame || e.detail.newStream;
if (newElement) {
presetTheme.syncUI(newElement);
}
}
['turbo:before-render', 'turbo:before-stream-render', 'turbo:before-frame-render'].forEach(eventName => {
document.addEventListener(eventName, updateSelectionBeforeTurboLoad);
});
window.presetTheme = presetTheme;

View File

@@ -1,114 +0,0 @@
(() => {
function applyThemeChange(stylesheet, newStylesheet) {
newStylesheet.rel = 'stylesheet';
newStylesheet.id = stylesheet.id;
requestAnimationFrame(() => {
stylesheet.remove();
});
}
function setPresetTheme(newPresetTheme) {
presetTheme = newPresetTheme;
localStorage.setItem('presetTheme', presetTheme);
const stylesheet = document.getElementById('theme-stylesheet');
const newStylesheet = Object.assign(document.createElement('link'), {
href: `/dist/styles/themes/${presetTheme}.css`,
rel: 'preload',
as: 'style',
});
newStylesheet.addEventListener(
'load',
() => {
const canUseViewTransitions =
document.startViewTransition && !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (canUseViewTransitions) {
document.startViewTransition(() => applyThemeChange(stylesheet, newStylesheet));
} else {
applyThemeChange(stylesheet, newStylesheet);
}
},
{ once: true },
);
document.head.append(newStylesheet);
updateSelection();
document.documentElement.classList.toggle(`wa-dark`, window.isDark());
}
function updateSelection(container = document) {
const menus = container.querySelectorAll('.preset-theme-selector wa-menu');
if (!menus) return;
[...menus].forEach(menu => {
// Clear all checked states
[...menu.querySelectorAll('wa-menu-item')].forEach(async item => {
await customElements.whenDefined(item.localName);
await item.updateComplete;
item.checked = false; // Reset all items to unchecked first
});
// Then set only the selected item as checked
const selectedItem = menu.querySelector(`wa-menu-item[value="${presetTheme}"]`);
if (selectedItem) {
customElements.whenDefined(selectedItem.localName).then(async () => {
await selectedItem.updateComplete;
selectedItem.checked = true;
container.querySelectorAll('.preset-theme-selector__text').forEach(themeSelector => {
themeSelector.textContent = selectedItem.innerText;
});
});
}
});
}
// Updates any element on the page with [data-theme-name] and [data-theme-description] with the current themes info
function updateThemeNameAndDescription(name, description) {
document.querySelectorAll('[data-theme-name]').forEach(el => (el.textContent = name));
document.querySelectorAll('[data-theme-description]').forEach(el => (el.textContent = description));
}
let presetTheme = window.getPresetTheme();
// Selection is not preserved when changing page, so update when opening dropdown
document.addEventListener('wa-show', event => {
const presetThemeSelector = event.target.closest('.preset-theme-selector');
if (!presetThemeSelector) return;
updateSelection();
});
// Listen for selections
document.addEventListener('wa-select', event => {
const menu = event.target.closest('.preset-theme-selector wa-menu');
if (!menu) return;
setPresetTheme(event.detail.item.value);
updateThemeNameAndDescription(event.detail.item.textContent, event.detail.item.getAttribute('data-description'));
});
// Set the initial preset name/description in the UI by grabbing it from the dropdown (ugly, but quick)
const currentTheme = document.querySelector(`.preset-theme-selector wa-menu-item[value="${presetTheme}"]`);
if (currentTheme) {
updateThemeNameAndDescription(currentTheme.textContent, currentTheme.getAttribute('data-description'));
}
// Update the color scheme when the preference changes
window.matchMedia('(prefers-preset-theme: dark)').addEventListener('change', () => setPresetTheme(presetTheme));
updateSelection();
/**
* Without this, there's a flash of the incorrect preset theme.
*/
function updateSelectionBeforeTurboLoad(e) {
const newElement = e.detail.newBody || e.detail.newFrame || e.detail.newStream;
if (!newElement) {
return;
}
updateSelection(newElement);
}
['turbo:before-render', 'turbo:before-stream-render', 'turbo:before-frame-render'].forEach(eventName => {
document.addEventListener(eventName, updateSelectionBeforeTurboLoad);
});
})();

View File

@@ -0,0 +1,37 @@
/**
* Sync iframe height with its content page (for same-origin iframes)
* NOT CURRENTLY USED ANYWHERE
*/
for (let iframe of document.querySelectorAll('iframe')) {
if (iframe.contentDocument) {
// Already loaded
syncIframeHeight(iframe);
}
iframe.onload = () => {
console.log('iframe loaded');
if (iframe.contentDocument) {
// Same origin
iframe.contentWindow.iframe = iframe;
syncIframeHeight(iframe);
const resizeObserver = new ResizeObserver(entries => {
for (let entry of entries) {
if (entry.target === iframe.contentDocument.body) {
syncIframeHeight(iframe);
}
}
});
resizeObserver.observe(iframe.contentDocument.body);
window.addEventListener('turbo:render', syncIframeHeight(iframe));
}
};
}
function syncIframeHeight(iframe) {
iframe.style.height = '0px';
requestAnimationFrame(() => {
iframe.style.height = iframe.contentDocument.body.scrollHeight + 'px';
});
}

View File

@@ -0,0 +1,115 @@
// Helper for view transitions
export function domChange(fn, { behavior = 'smooth' } = {}) {
const canUseViewTransitions =
document.startViewTransition && !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (canUseViewTransitions && behavior === 'smooth') {
document.startViewTransition(fn);
} else {
fn(true);
}
}
export function nextFrame() {
return new Promise(resolve => requestAnimationFrame(resolve));
}
export class ThemeAspect {
constructor(options) {
Object.assign(this, options);
this.set();
// Update when local storage changes.
// That way changes in one window will propagate to others (including iframes).
window.addEventListener('storage', event => {
if (event.key === this.key) {
this.set();
}
});
// Listen for selections
document.addEventListener('wa-change', event => {
const picker = event.target.closest(this.picker);
if (picker) {
this.set(picker.value);
}
});
}
get() {
return localStorage.getItem(this.key) ?? this.defaultValue;
}
computed = {};
get computedValue() {
if (this.value in this.computed) {
return this.computed[this.value];
}
return this.value;
}
set(value = this.get()) {
if (value === this.value) {
return;
}
this.value = value;
if (this.value === this.defaultValue) {
localStorage.removeItem(this.key);
} else {
localStorage.setItem(this.key, this.value);
}
this.applyChange();
this.syncUI();
}
syncUI(container = document) {
for (let picker of container.querySelectorAll(this.picker)) {
picker.setAttribute('value', this.value);
picker.value = this.value;
}
}
}
const colorScheme = new ThemeAspect({
defaultValue: 'auto',
key: 'colorScheme',
picker: 'wa-select.color-scheme-selector',
computed: {
get auto() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
},
},
applyChange() {
// Toggle the dark mode class
domChange(() => {
let dark = this.computedValue === 'dark';
document.documentElement.classList.toggle(`wa-dark`, dark);
for (let el of document.querySelectorAll('.wa-invert')) {
el.classList.toggle('wa-dark', !dark);
el.classList.toggle('wa-light', dark);
}
});
},
});
// Update the color scheme when the preference changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => colorScheme.set());
// Toggle color scheme with backslash
document.addEventListener('keydown', event => {
if (
event.key === '\\' &&
!event.composedPath().some(el => ['input', 'textarea'].includes(el?.tagName?.toLowerCase()))
) {
event.preventDefault();
colorScheme.set(theming.colorScheme.resolvedValue === 'dark' ? 'light' : 'dark');
}
});

View File

@@ -455,3 +455,20 @@ wa-page > main:has(> .index-grid) {
padding: var(--wa-space-3xl);
}
}
.layout-theme {
wa-page > main {
max-width: 140ch;
.max-line-length {
max-width: 80ch;
}
}
iframe {
width: 100%;
min-height: 16lh;
height: 65vh;
max-height: 21lh;
}
}

View File

@@ -45,7 +45,7 @@ table code {
}
}
</script>
<script type="module" src="/assets/scripts/cheatsheet.js"></script>
<script type="module" src="./cheatsheet.js"></script>
{% for type, all in componentsBy -%}
{% set typeTitle = "CSS custom properties" if type == "cssProperty" else ("CSS parts" if type == "cssPart" else (type | title) + "s") %}

View File

@@ -1,7 +1,7 @@
---
title: Page
description: Pages offer an easy way to scaffold entire page layouts using minimal markup.
tags: [organization, layout]
tags: [pro, organization, layout]
isPro: true
order: 0
# icon: page

View File

@@ -7,7 +7,7 @@ let stylesheets = Array.from(document.querySelectorAll("link[rel=stylesheet][hre
.join('\n');
let includes = `${stylesheets}
<script src="/dist/webawesome.loader.js" type="module"></script>
<link rel="stylesheet" href="/assets/examples/page-demo/page.css">`;
<link rel="stylesheet" href="./demo-page.css">`;
async function render() {
await customElements.whenDefined('wa-checkbox');

View File

@@ -15,7 +15,7 @@ Because these custom properties live at the page level, they're prefixed with `-
To customize a theme, simply override any of these custom properties in your own stylesheet by scoping your styles to `:root`, `:host`, and, if needed, the class for the specific theme you want to override. Here's an example that changes the default brand color (blue) to violet in the light theme using existing [literal colors](/docs/theming/color/#literal-colors).
```css
:root,
:where(:root),
:host,
.wa-theme-default {
/* Changes the brand color to violet across the library */

View File

@@ -0,0 +1,36 @@
---
title: Styling Tests
---
<h2>Appearance and variant tests</h2>
{% for component in componentsBy.attribute.variant %}
{%- if component.attributes.appearance -%}
<h3><a href="../{{ component.url }}"><code>&lt;{{ component.tagName }}&gt;</code></a></h3>
{% set variantValues = component.attributes.variant.type.text | getEnumValues %}
{% set appearanceValues = component.attributes.appearance.type.text | getEnumValues %}
<table>
<thead>
<tr>
<th>Variant</th>
{% for appearance in appearanceValues %}
<th>{{ appearance }}</th>
{%- endfor %}
</tr>
</thead>
{% for variant in variantValues %}
<tr>
<th>{{ variant }}</th>
{% for appearance in appearanceValues %}
<td>
<{{ component.tagName }} variant={{ variant }} appearance={{ appearance }}>{{ variant }} {{ appearance }}</{{ component.tagName }}>
</td>
{%- endfor %}
</tr>
{%- endfor %}
</table>
{% endif %}
{%- endfor %}

View File

@@ -1,415 +0,0 @@
---
title: Themes
description: Everything you need to know about theming Web Awesome.
layout: page-outline
---
<style>
wa-page > main {
max-width: 140ch;
.max-line-length {
max-width: 80ch;
}
}
.theme-showcase {
container: showcase / inline-size;
background-color: var(--wa-color-surface-lowered);
border-radius: var(--wa-border-radius-l);
min-height: 16lh;
height: 65vh;
max-height: 21lh;
padding: var(--wa-space-xl);
overflow: hidden;
margin-block-end: var(--wa-space-xl);
&.wa-flank {
--content-percentage: 55%;
--flank-size: 20ch;
}
}
.showcase-examples-wrapper {
inline-size: 100%;
block-size: 100%;
}
.showcase-examples {
column-gap: var(--wa-space-xl);
& wa-card {
display: inline-block;
width: 100%;
&:has(+ wa-card) {
margin-block-end: var(--wa-space-xl);
}
}
}
@supports not (zoom: 1) {
.showcase-examples {
column-count: 1;
}
@container showcase (width > 700px) {
.showcase-examples {
column-count: 2;
}
}
@container showcase (width > 900px) {
.showcase-examples {
column-count: 3;
}
}
}
@supports (zoom: 1) {
.showcase-examples {
column-count: 2;
zoom: 40%;
}
@container showcase (width > 400px) {
.showcase-examples {
zoom: 55%;
}
}
@container showcase (width > 700px) {
.showcase-examples {
zoom: 70%;
}
}
@container showcase (width > 800px) {
.showcase-examples {
column-count: 3;
zoom: 70%;
}
}
}
</style>
<div class="theme-showcase wa-flank wa-gap-xl">
<div>
<h2 id="theme-showcase-name" data-theme-name></h2>
<p id="theme-showcase-description" data-theme-description></p>
</div>
<div class="showcase-examples-wrapper">
<div class="showcase-examples">
<wa-card with-header with-footer>
<div slot="header" class="wa-split">
<h3 class="wa-heading-m">Your Cart</h3>
<wa-icon-button name="xmark"></wa-icon-button>
</div>
<div class="wa-stack wa-gap-xl">
<div class="wa-flank">
<wa-avatar shape="rounded" style="--size: 3em; --background-color: var(--wa-color-green-60); --text-color: var(--wa-color-green-95);">
<wa-icon slot="icon" name="sword-laser" family="duotone" style="font-size: 1.5em;"></wa-icon>
</wa-avatar>
<div class="wa-stack wa-gap-xs">
<div class="wa-split wa-gap-xs">
<strong>Initiate Saber</strong>
<strong>$179.99</strong>
</div>
<div class="wa-split wa-gap-xs wa-caption-m">
<span>Green</span>
<a href="#">Remove</a>
</div>
</div>
</div>
<wa-divider></wa-divider>
<div class="wa-flank">
<wa-avatar shape="rounded" style="--size: 3em; --background-color: var(--wa-color-teal-60); --text-color: var(--wa-color-teal-95);">
<wa-icon slot="icon" name="robot-astromech" family="duotone" style="font-size: 1.5em;"></wa-icon>
</wa-avatar>
<div class="wa-stack wa-gap-xs">
<div class="wa-split wa-gap-xs">
<strong>Repair Droid</strong>
<strong>$3,049.99</strong>
</div>
<div class="wa-split wa-gap-xs wa-caption-m">
<span>R-series</span>
<a href="#">Remove</a>
</div>
</div>
</div>
</div>
<div slot="footer" class="wa-stack">
<div class="wa-split">
<strong>Subtotal</strong>
<strong>$3,229.98</strong>
</div>
<span class="wa-caption-m">Shipping and taxes calculated at checkout.</span>
<wa-button variant="brand">
<wa-icon slot="prefix" name="shopping-bag"></wa-icon>
Checkout
</wa-button>
</div>
</wa-card>
<wa-card>
<wa-avatar shape="rounded" style="--size: 1.9lh; float: left; margin-right: var(--wa-space-m);">
<wa-icon slot="icon" name="hat-wizard" family="duotone" style="font-size: 1.75em;"></wa-icon>
</wa-avatar>
<p class="wa-body-l" style="margin: 0;">&ldquo;All we have to decide is what to do with the time that is given to us. There are other forces at work in this world, Frodo, besides the will of evil.&rdquo;</p>
</wa-card>
<wa-card>
<div class="wa-stack">
<h3 class="wa-heading-m">Sign In</h3>
<wa-input label="Email" placeholder="ddjarin@mandalore.gov">
<wa-icon slot="prefix" name="envelope" variant="regular"></wa-icon>
</wa-input>
<wa-input label="Password" type="password">
<wa-icon slot="prefix" name="lock" variant="regular"></wa-icon>
</wa-input>
<wa-button variant="brand">Sign In</wa-button>
<a href="#" class="wa-body-s">I forgot my password</a>
</div>
</wa-card>
<wa-card with-footer>
<div class="wa-stack">
<div class="wa-split">
<h3 class="wa-heading-m">To-Do</h3>
<wa-icon-button name="plus" label="Add task"></wa-icon-button>
</div>
<wa-checkbox checked>Umbrella for Adelard</wa-checkbox>
<wa-checkbox checked>Waste-paper basket for Dora</wa-checkbox>
<wa-checkbox checked>Pen and ink for Milo</wa-checkbox>
<wa-checkbox>Mirror for Angelica</wa-checkbox>
<wa-checkbox>Silver spoons for Lobelia</wa-checkbox>
</div>
<div slot="footer">
<a href="">View all completed</a>
</div>
</wa-card>
<wa-card>
<div class="wa-stack">
<div class="wa-frame wa-border-radius-m" style="align-self: center; max-inline-size: 25ch;">
<img src="https://images.unsplash.com/photo-1667514627762-521b1c815a89?q=20" alt="Album art">
</div>
<div class="wa-flank:end wa-align-items-start">
<div class="wa-stack wa-gap-3xs">
<div class="wa-cluster wa-gap-xs" style="height: 2.25em;">
<strong>The Stone Troll</strong>
<small><wa-badge variant="neutral" appearance="filled">E</wa-badge></small>
</div>
<span class="wa-caption-m">Samwise G</span>
</div>
<wa-icon-button name="ellipsis" label="Options"></wa-icon-button>
</div>
<div class="wa-stack wa-gap-2xs">
<wa-progress-bar value="34" style="height: 0.5em"></wa-progress-bar>
<div class="wa-split">
<span class="wa-caption-xs">1:01</span>
<span class="wa-caption-xs">-1:58</span>
</div>
</div>
<div class="wa-grid wa-align-items-center" style="--min-column-size: 1em; justify-items: center;">
<wa-icon-button name="backward" label="Skip backward"></wa-icon-button>
<wa-icon-button name="pause" style="font-size: var(--wa-font-size-2xl);" label="Pause"></wa-icon-button>
<wa-icon-button name="forward" label="Skip forward"></wa-icon-button>
</div>
</div>
</wa-card>
<wa-card>
<div class="wa-stack">
<h3 class="wa-heading-m">Chalmun's Spaceport Cantina</h3>
<div class="wa-cluster wa-gap-xs">
<wa-rating value="4.6" read-only></wa-rating>
<strong>4.6</strong>
<span>(419 reviews)</span>
</div>
<div class="wa-cluster wa-gap-xs">
<div class="wa-cluster wa-gap-3xs">
<wa-icon name="dollar" style="color: var(--wa-color-green-60);"></wa-icon>
<wa-icon name="dollar" style="color: var(--wa-color-green-60);"></wa-icon>
<wa-icon name="dollar" style="color: var(--wa-color-green-60);"></wa-icon>
</div>
<span class="wa-caption-m">&bull;</span>
<wa-tag size="small">Cocktail Bar</wa-tag>
<wa-tag size="small">Gastropub</wa-tag>
<wa-tag size="small">Local Fare</wa-tag>
<wa-tag size="small">Gluten Free</wa-tag>
</div>
<div class="wa-flank wa-gap-xs">
<wa-icon name="location-dot"></wa-icon>
<a href="#" class="wa-caption-m">Mos Eisley, Tatooine</a>
</div>
</div>
</wa-card>
</div>
</div>
</div>
<div class="max-line-length">
Themes are collections of pre-defined CSS custom properties that thread through every Web Awesome component and pattern.
Web Awesome Free includes these pre-made themes:
- **Default** (`default.css`) for a clean look that prioritizes accessibility and performance
- **Classic** (`classic.css`) for the look and feel of Shoelace with more accessible color pairings
- **Awesome** (`awesome.css`) for the familiar, vibrant styles from your friends at Font Awesome
Even more themes are available with Web Awesome <wa-badge>Pro</wa-badge>:
- **Active** (`active.css`)
- **Brutalist** (`brutalist.css`)
- **Mellow** (`mellow.css`)
- **Tailspin** (`tailspin.css`)
To get started right away, include the following in your project, replacing `default.css` at the end with your preferred pre-made theme:
```html
<link rel="stylesheet" href="{% cdnUrl 'styles/themes/default.css' %}" />
```
## What's a Theme?
Themes are a collection of standardized [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/--*) that cover a range of styles from colors to transitions. We use these custom properties throughout Web Awesome components for a cohesive look and feel. Our [Theming pages](/docs/theming/) document these styles so that you can use them freely throughout your project and customize them as needed.
Themes are scoped to unique classes, such as `wa-theme-default` or `wa-theme-classic`. Scoping to unique classes allows you to import multiple themes and use them interchangeably without collisions.
Each theme may also include both light and dark color schemes with the classes `wa-light` and `wa-dark`.
You can use these classes to apply a specific color scheme to an entire page or just a section.
In pre-made themes, we use a light color scheme by default.
Additionally, styles may be scope to the `:root` selector to be activated automatically.
For pre-made themes, *all* custom properties are scoped to `:where(:root)`, the theme class, and `wa-light`.
:::info
We use `:where(:root)` to give these styles 0 [specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity) so that they can be easily overridden. If you plan on using multiple themes in your project, we recommend doing the same for your custom themes.
:::
Finally, we scope themes to `:host` and `:host-context()` to ensure the styles are applied to the shadow roots of custom elements.
For example, the default theme is set up like this:
```css
:where(:root),
:host,
.wa-theme-default,
.wa-light {
/* all CSS custom properties for color, typography, space, etc. */
}
.wa-dark,
:host-context(.wa-dark) {
/* subset of CSS custom properties for a dark color scheme */
}
```
## Using Themes
You can import pre-made themes from the Web Awesome CDN. Simply add the following code to the `<head>` of your page to import the **default** theme:
```html
<link rel="stylesheet" href="{% cdnUrl 'styles/themes/default.css' %}" />
```
Or the **Classic** theme:
```html
<link rel="stylesheet" href="{% cdnUrl 'styles/themes/classic.css' %}" />
```
Or any of our Pro themes, like **Brutalist**:
```html
<link rel="stylesheet" href="{% cdnUrl 'styles/themes/brutalist.css' %}" />
```
To activate the dark color scheme of any theme, apply the class `wa-dark` to the `<html>` element on your page, like this example for the default theme:
```html
<html class="wa-theme-default wa-dark">
<head>
<link rel="stylesheet" href="path/to/web-awesome/dist/styles/themes/default.css" />
<!-- other links, scripts, and metadata -->
</head>
<body>
<!-- page content -->
</body>
</html>
```
Because themes are scoped to specific classes, you can use different color schemes or even different themes throughout the page. Here, we use the default theme with a dark sidebar:
```html
<html>
<head>
<link rel="stylesheet" href="path/to/web-awesome/dist/styles/themes/default.css" />
</head>
<body>
<nav class="wa-dark">
<!-- dark-themed sidebar -->
</nav>
<!-- light-themed content -->
</body>
</html>
```
## Creating Themes
There are two ways to create themes. The easiest way is to customize the default theme. The advanced way is to create a new theme from scratch. Which method you choose depends on your project's requirements and the amount of effort you're willing to invest.
### Customizing a Built-in Theme
Overriding the default theme is the easiest way to customize Web Awesome. You can do this by importing the default theme as-is, then creating a separate stylesheet that overrides [the theming API](/docs/customizing#design-tokens) and adds [component styles](/docs/customizing#css-parts) to your liking. You must import your theme _after_ the default theme.
If you're customizing the default light styles, scope your styles to the following selectors.
```css
:root,
:host,
.wa-theme-default,
.wa-light {
/* your custom styles here */
}
```
If you're customizing the default dark styles, scope your styles to the following selectors.
```css
.wa-dark,
:is(:host-context(.wa-dark)) {
/* your custom styles here */
}
```
By customizing a built-in theme, you'll maintain a smaller stylesheet containing only your changes. Contrast this to [creating a new theme](#creating-a-new-theme), where you need to explicitly define every custom property required by the library. This approach is more "future-proof," as new design tokens that emerge in subsequent versions of Web Awesome will be accounted for by built-in themes.
While this approach is easier to maintain, the drawback is that your theme can't be activated independently — it's tied to the built-in theme you're extending.
### Creating a New Theme
Creating a new theme is more of an undertaking than [customizing an existing one](#customizing-a-built-in-theme). At a minimum, you must implement all of the required custom properties. The easiest way to do this is by "forking" a built-in theme and modifying it from there.
Start by changing the selector to match your theme's name. Assuming your new theme is called "Purple Power", your theme should be scoped like this.
```css
:root,
:host,
.wa-theme-purple-power,
.wa-light {
/* your custom styles here */
}
```
By creating a new theme, you won't be relying on a built-in theme as a foundation. Because of this, you can activate it independently as an alternative to the default theme. This is the recommended approach if you're looking to open source your theme for others to use.
You will, however, need to maintain your theme more carefully, as new versions of Web Awesome change the theming API in ways that your theme won't have accounted for. It's recommended that you clearly specify which version(s) of Web Awesome your theme is designed to work with and keep it up to date as new versions of Web Awesome are released.
## Detecting Color Scheme Preference
Web Awesome's default theme has both light and dark styles built in. However, Web Awesome doesn't try to auto-detect the user's light/dark mode preference. This should be done at the application level. As a best practice, to provide a dark theme in your app, you should:
- Check for [`prefers-color-scheme`](https://stackoverflow.com/a/57795495/567486) and use its value by default
- Allow the user to override the setting in your app
- Remember the user's preference and restore it on subsequent logins
Web Awesome avoids using the `prefers-color-scheme` media query because not all apps support dark mode, and it would break things for the ones that don't.
</div>

6
docs/docs/themes/active.md vendored Normal file
View File

@@ -0,0 +1,6 @@
---
title: Active
description: Energetic and tactile, always in motion.
isPro: true
tags: pro
---

5
docs/docs/themes/awesome.md vendored Normal file
View File

@@ -0,0 +1,5 @@
---
title: Awesome
description: Punchy and vibrant, the rockstar of themes.
order: 0.2
---

6
docs/docs/themes/brutalist.md vendored Normal file
View File

@@ -0,0 +1,6 @@
---
title: Brutalist
description: Sharp, square, and unapologetically bold.
isPro: true
tags: pro
---

5
docs/docs/themes/classic.md vendored Normal file
View File

@@ -0,0 +1,5 @@
---
title: Classic
description: Timeless elegance that never goes out of style.
order: 0.1
---

59
docs/docs/themes/creating.md vendored Normal file
View File

@@ -0,0 +1,59 @@
---
title: Creating Themes
layout: page-outline
order: 999
override:tags: [themes]
---
There are two ways to create themes.
The easiest way is to customize the default theme.
The advanced way is to create a new theme from scratch.
Which method you choose depends on your project's requirements and the amount of effort you're willing to invest.
## Customizing a Built-in Theme
Overriding the default theme is the easiest way to customize Web Awesome.
You can do this by importing the default theme as-is, then creating a separate stylesheet that overrides [the theming API](/docs/customizing#design-tokens) and adds [component styles](/docs/customizing#css-parts) to your liking. You must import your theme _after_ the default theme.
If you're customizing the default light styles, scope your styles to the following selectors.
```css
:where(:root),
:host,
.wa-theme-default,
.wa-light {
/* your custom styles here */
}
```
If you're customizing the default dark styles, scope your styles to the following selectors.
```css
.wa-dark,
:is(:host-context(.wa-dark)) {
/* your custom styles here */
}
```
By customizing a built-in theme, you'll maintain a smaller stylesheet containing only your changes. Contrast this to [creating a new theme](#creating-a-new-theme), where you need to explicitly define every custom property required by the library. This approach is more "future-proof," as new design tokens that emerge in subsequent versions of Web Awesome will be accounted for by built-in themes.
While this approach is easier to maintain, the drawback is that your theme can't be activated independently — it's tied to the built-in theme you're extending.
## Creating a New Theme
Creating a new theme is more of an undertaking than [customizing an existing one](#customizing-a-built-in-theme). At a minimum, you must implement all of the required custom properties. The easiest way to do this is by "forking" a built-in theme and modifying it from there.
Start by changing the selector to match your theme's name. Assuming your new theme is called "Purple Power", your theme should be scoped like this.
```css
:where(:root),
:host,
.wa-theme-purple-power,
.wa-light {
/* your custom styles here */
}
```
By creating a new theme, you won't be relying on a built-in theme as a foundation. Because of this, you can activate it independently as an alternative to the default theme. This is the recommended approach if you're looking to open source your theme for others to use.
You will, however, need to maintain your theme more carefully, as new versions of Web Awesome change the theming API in ways that your theme won't have accounted for. It's recommended that you clearly specify which version(s) of Web Awesome your theme is designed to work with and keep it up to date as new versions of Web Awesome are released.

5
docs/docs/themes/default.md vendored Normal file
View File

@@ -0,0 +1,5 @@
---
title: Default
description: Your trusty companion, like a perfectly broken-in pair of jeans.
order: 0
---

40
docs/docs/themes/demo.njk vendored Normal file
View File

@@ -0,0 +1,40 @@
---
layout: blank
pagination:
data: collections.theme
size: 1
alias: theme
permalink: '/docs/themes/{{ theme.fileSlug }}/demo.html'
eleventyExcludeFromCollections: true
override:tags: []
eleventyComputed:
forceTheme: "{{ theme.fileSlug }}"
---
<script>
document.getElementById('theme-stylesheet').href = '/dist/styles/themes/{{ theme.fileSlug }}.css';
</script>
<script type=module>
document.getElementById('theme-stylesheet').href = '/dist/styles/themes/{{ theme.fileSlug }}.css';
</script>
<link rel="stylesheet" href="/dist/styles/themes/{{ theme.fileSlug }}.css" />
<link rel="stylesheet" href="/docs/themes/showcase.css" />
{% set content %}
<header>
{% include 'breadcrumbs.njk' %}
<h1 class="title">{{ theme.data.title }}</h1>
<p>{% include 'status.njk' %}</p>
<p id="theme-showcase-description">{{ theme.data.description | inlineMarkdown | safe }}</p>
</header>
{% include 'theme-showcase.njk' %}
{% endset %}
<wa-image-comparer style="width: 100%" position="90">
<div slot="after" class="theme-showcase wa-gap-xl">
{{ content | safe }}
</div>
<div slot="before" class="theme-showcase wa-gap-xl wa-invert">
{{ content | safe }}
</div>
</wa-image-comparer>

7
docs/docs/themes/glassy.md vendored Normal file
View File

@@ -0,0 +1,7 @@
---
title: Glassy
description: Smooth, sleek, and reflective.
isPro: true
tags: pro
noAlpha: true
---

55
docs/docs/themes/index.njk vendored Normal file
View File

@@ -0,0 +1,55 @@
---
title: Themes
description: Themes are collections of pre-defined CSS custom properties that thread through every Web Awesome component and pattern.
layout: overview
override:tags: []
forTag: theme
categories:
other: Free
pro: Pro
---
<div class="max-line-length">
{% markdown %}
## What's a Theme?
Themes are a collection of standardized [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/--*) that cover a range of styles from colors to transitions. We use these custom properties throughout Web Awesome components for a cohesive look and feel. Our [Theming pages](/docs/theming/) document these styles so that you can use them freely throughout your project and customize them as needed.
Themes are scoped to unique classes, such as `wa-theme-default` or `wa-theme-classic`.
Scoping to unique classes allows you to import multiple themes and use them interchangeably without collisions.
Please note that if you import multiple themes, the last one will be the default that will apply if no class is used.
Each theme may also include both light and dark color schemes with the classes `wa-light` and `wa-dark`.
You can use these classes to apply a specific color scheme to an entire page or just a section.
In pre-made themes, we use a light color scheme by default.
Additionally, styles may be scoped to the `:root` selector to be activated automatically.
For pre-made themes, *all* custom properties are scoped to `:root`, the theme class, and `wa-light`.
Finally, we scope themes to `:host` and `:host-context()` to ensure the styles are applied to the shadow roots of custom elements.
For example, the default theme is set up like this:
```css
:where(:root),
:host,
.wa-theme-default,
.wa-light {
/* all CSS custom properties for color, typography, space, etc. */
}
.wa-dark,
:host-context(.wa-dark) {
/* subset of CSS custom properties for a dark color scheme */
}
```
## Using Themes
You can import pre-made themes from the Web Awesome CDN.
Refer to each themes page for copyable code snippets.
{% endmarkdown %}
</div>

6
docs/docs/themes/mellow.md vendored Normal file
View File

@@ -0,0 +1,6 @@
---
title: Mellow
description: Soft and soothing, like a lazy Sunday morning.
isPro: true
tags: pro
---

7
docs/docs/themes/playful.md vendored Normal file
View File

@@ -0,0 +1,7 @@
---
title: Playful
description: Fun, colorful, and full of personality.
isPro: true
tags: pro
noAlpha: true
---

7
docs/docs/themes/premium.md vendored Normal file
View File

@@ -0,0 +1,7 @@
---
title: Premium
description: The ultimate in sophistication and style.
isPro: true
tags: pro
noAlpha: true
---

90
docs/docs/themes/showcase.css vendored Normal file
View File

@@ -0,0 +1,90 @@
html {
background: transparent;
}
html,
body,
.theme-showcase {
height: 100vh;
overflow: hidden;
}
.theme-showcase {
background-color: var(--wa-color-surface-lowered);
border-radius: var(--wa-border-radius-l);
padding: var(--wa-space-xl);
box-sizing: border-box;
overflow: hidden;
@media (width < 500px) {
flex-flow: column;
}
header {
min-width: min(25ch, 100vw);
align-self: center;
h1 {
margin-bottom: var(--wa-space-2xs);
}
wa-breadcrumb-item:not(:first-of-type, :last-of-type) {
display: none;
}
p:empty {
display: none;
}
}
}
.showcase-examples-wrapper {
inline-size: 100%;
block-size: 100%;
}
.showcase-examples {
column-gap: var(--wa-space-xl);
& wa-card {
display: inline-block;
width: 100%;
&:has(+ wa-card) {
margin-block-end: var(--wa-space-xl);
}
}
@supports not (zoom: 1) {
column-count: 1;
@media (width > 750px) {
column-count: 2;
}
@media (width > 950px) {
column-count: 3;
}
}
@supports (zoom: 1) {
column-count: 1;
zoom: 40%;
@media (width > 350px) {
column-count: 2;
}
@media (width > 450px) {
zoom: 55%;
}
@media (width > 750px) {
zoom: 70%;
}
@media (width > 950px) {
column-count: 3;
}
}
}

6
docs/docs/themes/tailspin.md vendored Normal file
View File

@@ -0,0 +1,6 @@
---
title: Tailspin
description: Like a bird in flight, guiding you from there to here.
isPro: true
tags: pro
---

4
docs/docs/themes/themes.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"layout": "theme.njk",
"tags": ["themes", "theme"]
}

View File

@@ -46,7 +46,9 @@
"scripts": {
"start": "node scripts/build.js --develop",
"build": "node scripts/build.js",
"build:serve": "npm run build && npx http-server _site -p 4000",
"build:alpha": "node scripts/build.js --alpha",
"build:alpha:serve": "npm run build:alpha && npx http-server _site -p 4000",
"start:alpha": "node scripts/build.js --alpha --develop",
"publish-alpha-cdn": "./publish-alpha-cdn.sh",
"create": "plop --plopfile scripts/plop/plopfile.js",

View File

@@ -74,6 +74,10 @@ function generateManifest() {
execSync('cem analyze --config "custom-elements-manifest.js"');
} catch (error) {
console.error(`\n\n${error.message}`);
if (!isDeveloping) {
process.exit(1);
}
}
spinner.succeed();
@@ -91,6 +95,10 @@ function generateReactWrappers() {
execSync(`node scripts/make-react.js --outdir "${cdnDir}"`, { stdio: 'inherit' });
} catch (error) {
console.error(`\n\n${error.message}`);
if (!isDeveloping) {
process.exit(1);
}
}
spinner.succeed();
@@ -147,6 +155,9 @@ async function generateTypes() {
try {
execSync(`tsc --project ./tsconfig.prod.json --outdir "${cdnDir}"`);
} catch (error) {
if (!isDeveloping) {
process.exit(1);
}
return Promise.reject(error.stdout);
}
@@ -219,6 +230,9 @@ async function generateBundle() {
} catch (error) {
spinner.fail();
console.log(chalk.red(`\n${error}`));
if (!isDeveloping) {
process.exit(1);
}
}
spinner.succeed();
@@ -235,6 +249,10 @@ async function regenerateBundle() {
} catch (error) {
spinner.fail();
console.log(chalk.red(`\n${error}`));
if (!isDeveloping) {
process.exit(1);
}
}
spinner.succeed();
@@ -250,24 +268,31 @@ async function generateDocs() {
if (isAlpha) args.push('--alpha');
if (isDeveloping) args.push('--develop');
// 11ty
const output = (await runScript(join(__dirname, 'docs.js'), args))
// Cleanup the output
.replace('[11ty]', '')
.replace(' seconds', 's')
.replace(/\(.*?\)/, '')
.toLowerCase()
.trim();
let output;
try {
// 11ty
output = (await runScript(join(__dirname, 'docs.js'), args))
// Cleanup the output
.replace('[11ty]', '')
.replace(' seconds', 's')
.replace(/\(.*?\)/, '')
.toLowerCase()
.trim();
// Copy assets
await copy(join(docsDir, 'assets'), join(siteDir, 'assets'), { overwrite: true });
// Copy dist (production only)
if (!isDeveloping) {
await copy(cdnDir, join(siteDir, 'dist'));
}
// Copy dist (production only)
if (!isDeveloping) {
await copy(cdnDir, join(siteDir, 'dist'));
spinner.succeed(`Writing the docs ${chalk.gray(`(${output}`)})`);
} catch (error) {
console.error('\n\n' + chalk.red(error) + '\n');
spinner.fail(chalk.red(`Error while writing the docs.`));
if (!isDeveloping) {
process.exit(1);
}
}
spinner.succeed(`Writing the docs ${chalk.gray(`(${output}`)})`);
}
// Initial build
@@ -368,6 +393,10 @@ if (isDeveloping) {
reload();
} catch (err) {
console.error(chalk.red(err));
if (!isDeveloping) {
process.exit(1);
}
}
});

View File

@@ -5,13 +5,18 @@ import { html, literal } from 'lit/static-html.js';
import { WaBlurEvent } from '../../events/blur.js';
import { WaFocusEvent } from '../../events/focus.js';
import { WaInvalidEvent } from '../../events/invalid.js';
import {
appearanceStyles,
sizeStyles,
variantStyles,
type AppearanceAttribute,
type SizeAttribute,
type VariantAttribute,
} from '../../internal/styling-attributes.js';
import { MirrorValidator } from '../../internal/validators/mirror-validator.js';
import { watch } from '../../internal/watch.js';
import { WebAwesomeFormAssociatedElement } from '../../internal/webawesome-formassociated-element.js';
import nativeStyles from '../../styles/native/button.css';
import appearanceStyles from '../../styles/utilities/appearance.css';
import sizeStyles from '../../styles/utilities/size.css';
import variantStyles from '../../styles/utilities/variants.css';
import { LocalizeController } from '../../utilities/localize.js';
import '../icon/icon.js';
import '../spinner/spinner.js';
@@ -69,13 +74,13 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
@property() title = ''; // make reactive to pass through
/** The button's theme variant. */
@property({ reflect: true }) variant: 'neutral' | 'brand' | 'success' | 'warning' | 'danger' = 'neutral';
@property({ reflect: true }) variant: VariantAttribute = 'neutral';
/** The button's visual appearance. */
@property({ reflect: true }) appearance: 'accent' | 'filled' | 'outlined' | 'plain' = 'accent';
@property({ reflect: true }) appearance: AppearanceAttribute = 'accent';
/** The button's size. */
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
@property({ reflect: true }) size: SizeAttribute = 'medium';
/** Draws the button with a caret. Used to indicate that the button triggers a dropdown menu or similar behavior. */
@property({ type: Boolean, reflect: true }) caret = false;

View File

@@ -1,10 +1,15 @@
import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { WaRemoveEvent } from '../../events/remove.js';
import {
appearanceStyles,
sizeStyles,
variantStyles,
type AppearanceAttribute,
type SizeAttribute,
type VariantAttribute,
} from '../../internal/styling-attributes.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import appearanceStyles from '../../styles/utilities/appearance.css';
import sizeStyles from '../../styles/utilities/size.css';
import variantStyles from '../../styles/utilities/variants.css';
import { LocalizeController } from '../../utilities/localize.js';
import '../icon-button/icon-button.js';
import styles from './tag.css';
@@ -33,14 +38,13 @@ export default class WaTag extends WebAwesomeElement {
private readonly localize = new LocalizeController(this);
/** The tag's theme variant. */
@property({ reflect: true }) variant: 'brand' | 'success' | 'neutral' | 'warning' | 'danger' | 'text' = 'neutral';
@property({ reflect: true }) variant: VariantAttribute = 'neutral';
/** The tag's visual appearance. */
@property({ reflect: true }) appearance: 'accent' | 'outlined accent' | 'filled' | 'outlined' | 'outlined filled' =
'outlined filled';
@property({ reflect: true }) appearance: AppearanceAttribute = 'outlined filled';
/** The tag's size. */
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
@property({ reflect: true }) size: SizeAttribute = 'medium';
/** Draws a pill-style tag with rounded edges. */
@property({ type: Boolean, reflect: true }) pill = false;

View File

@@ -0,0 +1,16 @@
export { default as appearanceStyles } from '../styles/utilities/appearance.css';
export { default as sizeStyles } from '../styles/utilities/size.css';
export { default as variantStyles } from '../styles/utilities/variants.css';
export type AppearanceAttribute =
| 'outlined'
| 'accent'
| 'outlined accent'
| 'accent outlined'
| 'filled'
| 'outlined filled'
| 'filled outlined'
| 'plain';
export type VariantAttribute = 'brand' | 'success' | 'neutral' | 'warning' | 'danger' | 'text';
export type SizeAttribute = 'small' | 'medium' | 'large';