Compare commits

..

110 Commits

Author SHA1 Message Date
Kelsey Jackson
c3de5a8915 removed old file 2025-03-14 16:02:39 -05:00
Kelsey Jackson
c18aa23d76 merging pattern-maiin 2025-03-14 16:00:32 -05:00
Kelsey Jackson
54e14a20c0 Merge pull request #703 from shoelace-style/kj/e-commerce-patterns
Kj/e commerce patterns
2025-03-14 15:57:58 -05:00
Kelsey Jackson
97b8e96a6e Merge branch 'pattern-main' into kj/e-commerce-patterns 2025-03-14 15:56:08 -05:00
Kelsey Jackson
55a362b741 Merge branch 'pattern-main' into kj/app-pattern 2025-03-14 15:55:45 -05:00
Kelsey Jackson
d9cafdafb7 Merge branch 'next' into pattern-main 2025-03-14 15:55:15 -05:00
Kelsey Jackson
81bf6865ec big switchover 2025-03-14 06:10:40 -05:00
lindsaym-fa
873eb47d18 Clean up utility usage and code formatting for product previews 2025-03-11 17:20:07 -04:00
lindsaym-fa
a564d3061f Clean up utility usage and code formatting for order summaries 2025-03-11 14:22:04 -04:00
Kelsey Jackson
ef865396f2 switching machines
:
2025-03-11 09:26:58 -05:00
Kelsey Jackson
903085257d removed store navigation 2025-03-07 11:12:39 -06:00
Kelsey Jackson
27237441a1 some heavy duty updates 2025-03-07 10:59:24 -06:00
Kelsey Jackson
b0291653f8 more polish 2025-03-06 22:43:13 -06:00
Kelsey Jackson
3962c50844 added review variant 2025-03-06 12:59:44 -06:00
Kelsey Jackson
314801c368 updated reviews 2025-03-05 16:40:36 -06:00
Kelsey Jackson
dc30ed8503 updated incentive images 2025-03-05 16:37:49 -06:00
lindsaym-fa
e12be798b8 Clean up utilities and formatting, replace placeholder text in order history 2025-03-04 14:30:00 -05:00
Kelsey Jackson
7f29f1b4ea started updating with style utilities 2025-03-04 13:09:41 -06:00
lindsaym-fa
3563d6a2dd Clean up utility usage and code formatting for category previews 2025-03-04 12:14:48 -05:00
Kelsey Jackson
007d93077f updated incentives 2025-03-04 11:04:31 -06:00
Kelsey Jackson
80b78aeb16 Merge branch 'pattern-main' into kj/e-commerce-patterns 2025-03-04 10:14:31 -06:00
Kelsey Jackson
8bc8ea85a5 Merge branch 'next' into pattern-main 2025-03-04 10:14:14 -06:00
Kelsey Jackson
61a6cd9fb9 updated image 2025-03-03 14:46:54 -06:00
Kelsey Jackson
7fb3fd5982 updated product preview 2025-03-03 14:22:45 -06:00
Kelsey Jackson
dc580b3351 committing to pull down changes 2025-03-03 10:38:10 -06:00
lindsaym-fa
74719d4c06 Refactor shopping cart patterns 2025-02-28 15:46:06 -05:00
Kelsey Jackson
47a7cdedd0 switching machines 2025-02-28 09:37:05 -06:00
lindsaym-fa
d906940726 Refactor product overview patterns 2025-02-27 17:04:10 -05:00
Kelsey Jackson
a782470c6a more polish 2025-02-27 11:47:56 -06:00
lindsaym-fa
de175ed4a1 Refactor product list patterns 2025-02-26 11:39:41 -05:00
lindsaym-fa
3612f8fdfa Add link style utilities 2025-02-26 11:39:11 -05:00
Kelsey Jackson
bc76df4b31 switching machines 2025-02-26 07:31:36 -06:00
lindsaym-fa
9ad7f4a6be Reorganize app patterns into separate pages 2025-02-25 12:02:29 -05:00
Kelsey Jackson
a7457630aa inital commit 2025-02-24 16:45:27 -06:00
Kelsey Jackson
e19928123f Merge branch 'pattern-main' into kj/e-commerce-patterns 2025-02-24 11:05:08 -06:00
Kelsey Jackson
e644862f58 Merge branch 'next' into pattern-main 2025-02-24 11:04:45 -06:00
Kelsey Jackson
76529c628e committing to bring branches up to next 2025-02-24 11:04:07 -06:00
Kelsey Jackson
62ded7b15a updated order history 2025-02-21 15:27:43 -06:00
Kelsey Jackson
65af9980f1 updated content for category preview 2025-02-21 10:08:33 -06:00
Kelsey Jackson
db592194fd updtated content 2025-02-21 09:45:19 -06:00
Kelsey Jackson
0f9ea14033 unlisted links 2025-02-19 11:12:05 -06:00
Kelsey Jackson
057b76a10a created e-commerce index 2025-02-19 10:09:42 -06:00
Kelsey Jackson
b466ba9d0f Merge pull request #748 from shoelace-style/lm/ecommerce-patterns-revisions
Suggested Revisions to E-commerce Patterns
2025-02-18 13:34:45 -06:00
Kelsey Jackson
9c979931da fixed conflicts 2025-02-18 13:33:46 -06:00
Kelsey Jackson
898311590a switching branches to merge 2025-02-18 13:31:53 -06:00
Kelsey Jackson
5f4510f355 more tweaks 2025-02-18 09:56:49 -06:00
Kelsey Jackson
b8eeb3db23 a little more polish 2025-02-14 07:42:36 -06:00
lindsaym-fa
8574270340 Add checkout form example 2025-02-13 23:06:50 -05:00
lindsaym-fa
aa042a0a6e Merge branch 'kj/e-commerce-patterns' into lm/ecommerce-patterns-revisions 2025-02-13 21:53:47 -05:00
Kelsey Jackson
fe81a41a6b Merge branch 'kj/e-commerce-patterns' of github.com:shoelace-style/webawesome into kj/e-commerce-patterns 2025-02-13 15:09:27 -06:00
Kelsey Jackson
f3628ad2d8 tweaked customer review 2025-02-13 15:08:30 -06:00
lindsaym-fa
169337077d Add docs layout for patterns with stylesheet 2025-02-13 15:49:09 -05:00
lindsaym-fa
406d9a9708 Update category descriptions and headings 2025-02-13 15:47:21 -05:00
lindsaym-fa
faaf75c0a2 Add "What's a Pattern?" and "Using Patterns" to index 2025-02-13 13:22:46 -05:00
Kelsey Jackson
13b67db869 Merge branch 'kj/e-commerce-patterns' of github.com:shoelace-style/webawesome into kj/e-commerce-patterns 2025-02-13 10:35:40 -06:00
Kelsey Jackson
b7fdda4b03 cleaned up checkout form 2025-02-13 10:35:31 -06:00
lindsaym-fa
d60e675702 Reimplement checkout form revisions 2025-02-13 10:23:46 -05:00
lindsaym-fa
414a318f0b Revert checkout form changes in attempt to improve diff 2025-02-13 10:00:45 -05:00
lindsaym-fa
ff8ed89645 Merge branch 'kj/e-commerce-patterns' into lm/ecommerce-patterns-revisions 2025-02-13 09:55:45 -05:00
lindsaym-fa
e027ab1291 Fix typo in file name 2025-02-13 09:52:56 -05:00
lindsaym-fa
d8f97b15b4 Add missing file extension 2025-02-13 09:52:00 -05:00
Kelsey Jackson
a8ed6f2c19 tweaked order summary 2025-02-12 16:14:21 -06:00
Kelsey Jackson
c7f5dc69ad cleaned up shopping cart 2025-02-12 15:07:03 -06:00
lindsaym-fa
42381722c5 Re-add navigation to "Product Features" carousel 2025-02-12 15:10:42 -05:00
lindsaym-fa
b2f9ec573f Merge branch 'kj/e-commerce-patterns' into lm/ecommerce-patterns-revisions 2025-02-12 15:05:33 -05:00
lindsaym-fa
b85e59b8ca "Checkout Form" revisions 2025-02-12 15:01:29 -05:00
Kelsey Jackson
8bd69694ce tweak category preview 2025-02-12 13:28:00 -06:00
Kelsey Jackson
7672923479 tweaked order history 2025-02-12 12:05:43 -06:00
Kelsey Jackson
c94006c6aa tweaked product overview 2025-02-12 11:21:17 -06:00
Kelsey Jackson
91ce2f2271 cleaned up product list 2025-02-12 10:08:51 -06:00
lindsaym-fa
9c609e44de "Category Filters" revisions + wa-placeholder utility 2025-02-11 17:06:40 -05:00
lindsaym-fa
28a8e4250a Revisions to "Product Features" 2025-02-11 17:05:38 -05:00
Kelsey Jackson
656a38cd3d updated prouct preview 2025-02-11 11:58:52 -06:00
Kelsey Jackson
9b90e56b45 more filter work 2025-02-11 10:57:29 -06:00
Kelsey Jackson
0c84a2ff37 Merge branch 'pattern-main' into kj/e-commerce-patterns 2025-02-11 09:11:36 -06:00
Kelsey Jackson
511cca7931 Merge branch 'next' into pattern-main 2025-02-11 09:10:58 -06:00
Kelsey Jackson
2069d223f0 adding some polish 2025-02-11 09:10:18 -06:00
Kelsey Jackson
37d2455965 updated descriptions 2025-02-10 14:37:00 -06:00
Kelsey Jackson
85b05fc655 editing descriptions 2025-02-07 15:50:15 -06:00
Kelsey Jackson
52328d9805 fixing conflicts 2025-02-07 14:24:40 -06:00
Kelsey Jackson
2c89c5f510 switching machines 2025-02-07 11:42:00 -06:00
Kelsey Jackson
5ac9e8cff1 Merge branch 'pattern-main' into kj/e-commerce-patterns 2025-02-05 12:16:50 -06:00
Kelsey Jackson
e238e35b3b Merge branch 'next' into pattern-main 2025-02-05 12:16:22 -06:00
Kelsey Jackson
c6d8d37871 switching machines 2025-01-31 09:43:40 -06:00
Kelsey Jackson
4dddb183fd updated width of container 2025-01-29 13:39:51 -06:00
Kelsey Jackson
d49ed53e9a added utilities to order-summary 2025-01-29 13:33:48 -06:00
Kelsey Jackson
99e2ffc17b added utilities to order history 2025-01-29 13:06:14 -06:00
Kelsey Jackson
97c79c29b9 Merge branch 'kj/e-commerce-patterns' of github.com:shoelace-style/webawesome into kj/e-commerce-patterns 2025-01-29 12:32:10 -06:00
Kelsey Jackson
7da9f53e27 filter work 2025-01-28 15:04:03 -06:00
Kelsey Jackson
a9cc02193f updated some patterns 2025-01-27 13:30:18 -06:00
Kelsey Jackson
9ad1c8f060 updated product list patterns 2025-01-27 13:17:09 -06:00
Kelsey Jackson
e0eaea2024 upated the product detail patterns 2025-01-27 11:20:40 -06:00
Kelsey Jackson
a8b7b6a93f Merge branch 'pattern-main' into kj/e-commerce-patterns 2025-01-23 08:56:50 -06:00
Kelsey Jackson
581e47043a Merge branch 'pattern-main' into kj/e-commerce-patterns 2025-01-22 18:50:38 -06:00
Kelsey Jackson
9ffe0cfe3c Merge branch 'next' into pattern-main 2025-01-22 18:50:11 -06:00
Kelsey Jackson
45df4a924f commiting to switch machines 2025-01-22 14:08:14 -06:00
Kelsey Jackson
43eed9d15e Merge branch 'next' into kj/e-commerce-patterns 2025-01-22 12:30:32 -06:00
Kelsey Jackson
a0ff411463 commiting to merge in next 2025-01-22 12:27:47 -06:00
Kelsey Jackson
04cd027c7e started split image pattern: 2025-01-17 14:44:14 -06:00
Kelsey Jackson
a68a157ebc switching machines 2025-01-15 16:41:13 -06:00
Kelsey Jackson
b2a24a3b52 Merge branch 'pattern-main' into kj/e-commerce-patterns 2025-01-09 11:31:46 -06:00
Kelsey Jackson
cbb456ffda switching machines 2025-01-09 11:29:32 -06:00
Kelsey Jackson
e0038c3125 Initial Commit 2025-01-09 10:59:33 -06:00
Kelsey Jackson
3e5d3120b1 Merge branch 'next' into kj/e-commerce-patterns 2025-01-08 16:19:29 -06:00
Kelsey Jackson
020f4cbaed building out the filter 2025-01-07 11:26:29 -06:00
Kelsey Jackson
9106bb88cf fixed merge conflicts 2025-01-06 10:34:51 -06:00
Kelsey Jackson
a822ab98c8 Merge branch 'next' into kj/e-commerce-patterns 2025-01-02 11:57:42 -06:00
Kelsey Jackson
026777c085 started invoice table 2024-12-18 09:53:15 -06:00
Kelsey Jackson
6d87b9b24a Merge branch 'next' into kj/e-commerce-patterns 2024-12-17 12:23:27 -06:00
Kelsey Jackson
d5affa01e9 dividing up e-commerce patterns 2024-12-11 15:06:01 -06:00
369 changed files with 5521 additions and 14125 deletions

View File

@@ -31,7 +31,7 @@ jobs:
- name: Lint
run: npm run prettier
- name: Build
run: npm run build
run: npm run build:alpha
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run CSR tests

View File

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

4
.gitignore vendored
View File

@@ -1,8 +1,12 @@
_site
.cache
.DS_Store
package.json
package-lock.json
dist/
dist-cdn/
docs/public/pagefind
node_modules
src/react
.astro
cdn/

View File

@@ -1,7 +1,5 @@
*.hbs
*.md
!docs/docs/patterns/**/*.md
docs/docs/patterns/blog-news/post-list.md
.cache
.github
cspell.json

View File

@@ -148,7 +148,6 @@
"scrollbars",
"scrollend",
"scroller",
"Scrollers",
"Segoe",
"semibold",
"shadowrootmode",

View File

@@ -1,15 +1,14 @@
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';
import { currentLink } from './_utils/current-link.js';
import { highlightCodePlugin } from './_utils/highlight-code.js';
import { markdown } from './_utils/markdown.js';
import { removeDataAlphaElements } from './_utils/remove-data-alpha-elements.js';
// import { formatCodePlugin } from './_utils/format-code.js';
// import litPlugin from '@lit-labs/eleventy-plugin-lit';
import litPlugin from '@lit-labs/eleventy-plugin-lit';
import { readFile } from 'fs/promises';
import nunjucks from 'nunjucks';
// import componentList from './_data/componentList.js';
import componentList from './_data/componentList.js';
import * as filters from './_utils/filters.js';
import { outlinePlugin } from './_utils/outline.js';
import { replaceTextPlugin } from './_utils/replace-text.js';
@@ -17,36 +16,29 @@ import { searchPlugin } from './_utils/search.js';
import process from 'process';
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 packageData = JSON.parse(await readFile('./package.json', 'utf-8'));
const isAlpha = process.argv.includes('--alpha');
const isDev = process.argv.includes('--develop');
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/**');
eleventyConfig.ignores.add('**/layout/**');
eleventyConfig.ignores.add('**/patterns/**');
eleventyConfig.ignores.add('**/style-utilities/**');
eleventyConfig.ignores.add('**/components/code-demo.md');
eleventyConfig.ignores.add('**/components/viewport-demo.md');
}
// Add template data
for (let name in globalData) {
@@ -63,38 +55,7 @@ export default function (eleventyConfig) {
// Shortcodes - {% shortCode arg1, arg2 %}
eleventyConfig.addShortcode('cdnUrl', location => {
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: '',
},
});
return `https://early.webawesome.com/webawesome@${packageData.version}/dist/` + location.replace(/^\//, '');
});
// Paired shortcodes - {% shortCode %}content{% endShortCode %}
@@ -102,6 +63,9 @@ export default function (eleventyConfig) {
// Helpers
// Remove elements that have [data-alpha="remove"]
eleventyConfig.addPlugin(removeDataAlphaElements({ isAlpha }));
// Use our own markdown instance
eleventyConfig.setLibrary('md', markdown);
@@ -153,14 +117,28 @@ export default function (eleventyConfig) {
]),
);
eleventyConfig.addPreprocessor('unpublished', '*', (data, content) => {
if (data.unpublished && process.env.ELEVENTY_RUN_MODE === 'build') {
// Exclude "unpublished" pages from final builds.
return false;
}
// 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`;
});
return content;
});
eleventyConfig.addPlugin(litPlugin, {
mode: 'worker',
componentModules,
});
}
// Build the search index
eleventyConfig.addPlugin(
@@ -188,31 +166,6 @@ 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 @@
export { hueRanges as default } from '../assets/data/index.js';
export { hueRanges as default } from '../assets/scripts/tweak/data.js';

View File

@@ -1,61 +0,0 @@
import fs from 'fs';
import path from 'path';
// import { inlined } from '../../dist/components/icon/library.wa.js';
const __dirname = path.resolve();
const THEME_DIR = path.join(__dirname, 'dist/styles/themes/');
const themeFiles = fs.readdirSync(THEME_DIR).filter(file => file.endsWith('.css') && !file.endsWith('base.css'));
const declarationRegex = /^\s*--wa-(?<property>[a-z-]+)?:\s*(?<value>.+?)\s*(\/\*.+?\*\/)?\s*;$/gm;
const importRegex = /^\s*@import\s+url\(['"](?<path>.+?)['"]\);$/gm;
const themes = {};
for (const file of themeFiles) {
const id = file.replace('.css', '');
const { imports, declarations } = readCSSFile(file);
let theme = { palette: 'default', declarations, imports };
for (const url of imports) {
if (url.endsWith('/color.css')) {
// Color settings
const color = readCSSFile(url);
for (const colorUrl of color.imports) {
if (colorUrl.startsWith('../../color/')) {
// Color palette
theme.palette = getFileSlug(colorUrl);
} else if (colorUrl.startsWith('../../brand/')) {
// Brand color
theme.brand = getFileSlug(colorUrl);
}
}
} else if (url.endsWith('/dimension.css')) {
theme.dimension = true;
}
}
let icon = {};
icon.family = theme.declarations['icon-family'] ?? theme.default?.iconFamily ?? 'classic';
icon.variant = theme.declarations['icon-variant'] ?? theme.default?.iconVariant ?? 'solid';
theme.icons = icon;
theme.rounding = Number(theme.declarations['border-radius-scale'] ?? theme.default?.rounding ?? 1);
theme.spacing = Number(theme.declarations['space-scale'] ?? theme.default?.spacing ?? 1);
theme.borderWidth = Number(theme.declarations['border-width-scale'] ?? theme.default?.borderWidth ?? 1);
themes[id] = theme;
}
export default themes;
function readCSSFile(url) {
const contents = fs.readFileSync(path.join(THEME_DIR, url), 'utf8');
const imports = [...contents.matchAll(importRegex)].map(match => match.groups.path);
const declarations = Object.fromEntries(
[...contents.matchAll(declarationRegex)].map(match => [match.groups.property, match.groups.value]),
);
return { imports, declarations };
}
function getFileSlug(url) {
return url.split('/').pop().replace('.css', '');
}

View File

@@ -1,9 +1,10 @@
<!DOCTYPE html>
<html lang="en" data-fa-kit-code="b10bfbde90" data-cdn-url="{% cdnUrl %}" class="wa-cloak">
<html lang="en" data-fa-kit-code="b10bfbde90" data-cdn-url="{% cdnUrl %}">
<head>
{% include 'head.njk' %}
<meta name="theme-color" content="#f36944">
<script type="module" src="/assets/scripts/code-examples.js"></script>
<script type="module" src="/assets/scripts/scroll.js"></script>
<script type="module" src="/assets/scripts/turbo.js"></script>
@@ -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="" mobile-breakpoint="1140">
<wa-page view="desktop" disable-navigation-toggle="">
<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="wa-desktop-only">{{ package.version }}</small>
<wa-badge variant="warning" appearance="filled" class="wa-desktop-only">Alpha</wa-badge>
<small id="version-number" class="only-desktop">{{ package.version }}</small>
<wa-badge variant="warning" appearance="filled" class="only-desktop">Alpha</wa-badge>
</div>
<div id="docs-toolbar" class="wa-cluster wa-gap-xs">
{# Desktop selectors #}
<div class="wa-desktop-only wa-cluster wa-gap-xs">
<div class="only-desktop wa-cluster wa-gap-xs">
{% include "preset-theme-selector.njk" %}
{% include "color-scheme-selector.njk" %}
</div>
@@ -47,11 +48,8 @@
<wa-button id="search-trigger" appearance="outlined" size="small" data-search>
<wa-icon slot="prefix" name="magnifying-glass"></wa-icon>
Search
<kbd slot="suffix" class="wa-desktop-only">/</kbd>
<kbd slot="suffix" class="only-desktop">/</kbd>
</wa-button>
{# Login #}
{% server "loginOrAvatar" %}
</div>
</header>
@@ -78,19 +76,14 @@
</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,11 +1,8 @@
{% set ancestors = page.url | ancestors %}
{% if ancestors.length > 0 %}
{% set breadcrumbs = page.url | breadcrumbs %}
{% if breadcrumbs.length > 0 %}
<wa-breadcrumb id="docs-breadcrumbs">
{% for ancestor in ancestors %}
{% if ancestor.page.url != "/" %}
<wa-breadcrumb-item href="{{ ancestor.page.url }}">{{ ancestor.data.title }}</wa-breadcrumb-item>
{% endif %}
{% for crumb in breadcrumbs %}
<wa-breadcrumb-item href="{{ crumb.url }}">{{ crumb.title }}</wa-breadcrumb-item>
{% endfor %}
<wa-breadcrumb-item>{# Current page #}</wa-breadcrumb-item>
</wa-breadcrumb>

View File

@@ -1,18 +1,12 @@
{# Cards for pages listed by category #}
<section id="grid" class="index-grid">
{% set groupedPages = allPages | groupPages(categories, page) %}
{% for category, pages in groupedPages -%}
{% if groupedPages.meta.groupCount > 1 and pages.length > 0 %}
<h2 class="index-category" id="{{ category | slugify }}">
{% if 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 -%}
{% 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 -%}
{%- endfor -%}
</section>

View File

@@ -1,7 +1,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{{ description }}">
{% if noindex or unlisted %}<meta name="robots" content="noindex">{% endif %}
{% if noindex %}<meta name="robots" content="noindex">{% endif %}
<title>{{ title }}</title>
@@ -23,17 +23,14 @@
<script src="/assets/scripts/hydration-errors.js"></script>
<link rel="stylesheet" href="/assets/styles/hydration-errors.css">
<link rel="preconnect" href="https://cdn.jsdelivr.net">
{# Internal components #}
<script type="module" src="/assets/components/scoped.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.10/+esm"></script>
{# Web Awesome #}
<script type="module" src="/dist/webawesome.loader.js"></script>
<script type="module" src="/dist/webawesome.ssr-loader.js"></script>
<script type="module" src="/assets/scripts/theme-picker.js"></script>
{# Preset Theme #}
{% if noTheme %}
{% elif forceTheme %}
{% if forceTheme %}
<link id="theme-stylesheet" rel="stylesheet" id="theme-stylesheet" href="/dist/styles/themes/{{ forceTheme }}.css" render="blocking" fetchpriority="high" />
{% else %}
<noscript><link id="theme-stylesheet" rel="stylesheet" id="theme-stylesheet" href="/dist/styles/themes/default.css" render="blocking" fetchpriority="high" /></noscript>
@@ -50,6 +47,3 @@
<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

@@ -1,13 +1,13 @@
{%- if not page.data.unlisted -%}
{% if page.url %}<a href="{{ page.url }}"{{ page.data.keywords | attr('data-keywords') }}>{% endif %}
<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" ignore missing %}
{% include "svgs/" + (page.data.icon or "thumbnail-placeholder") + ".njk" %}
</div>
<span class="page-name">{{ page.data.title }}</span>
{% if pageSubtitle -%}
<div class="wa-caption-s">{{ pageSubtitle }}</div>
{%- endif %}
</wa-card>
{% if page.url %}</a>{% endif %}
</a>
{% endif %}

View File

@@ -2,7 +2,7 @@
<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 }}">
<wa-option value="{{ theme.page.fileSlug }}"{{ ' data-alpha="remove"' if theme.noAlpha }}>
{{ theme.data.title }}
</wa-option>
{% endfor %}

View File

@@ -1,4 +1,4 @@
<wa-dialog id="site-search" without-header light-dismiss>
<wa-dialog id="site-search" no-header light-dismiss>
<div id="site-search-container">
{# Header #}
<header>

View File

@@ -1,12 +1,9 @@
{# 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 groupItem %}
{% if groupUrl | getCollectionItemFromUrl %}
<a href="{{ groupUrl }}" title="Overview">{{ title or (tag | capitalize) }}
<wa-icon name="grid-2"></wa-icon>
</a>
@@ -15,8 +12,10 @@
{% endif %}
</h2>
<ul>
{% for page in children %}
{% for page in collections[tag] | sort %}
{% if not page.data.parent -%}
{% include 'sidebar-link.njk' %}
{%- endif %}
{% endfor %}
</ul>
</wa-details>

View File

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

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 587 B

View File

@@ -1,3 +0,0 @@
<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M63 6.32811V61.3125C63 66.375 56.6719 68.9062 53.2969 65.25L49.9219 61.7344C43.7344 55.2656 35.7188 51.1875 27 50.0625V64.125C27 68.4844 23.3438 72 19.125 72H16.875C12.5156 72 9 68.4844 9 64.125V49.5C3.9375 49.5 0 45.5625 0 40.5V27C0 22.0781 3.9375 18 9 18H20.1094L24.1875 17.8594C34.0312 17.2969 43.1719 13.0781 49.9219 5.90624L53.2969 2.39061C56.6719 -1.26564 63 1.12499 63 6.32811ZM22.5 49.6406L20.1094 49.5H13.5V64.125C13.5 66.0937 14.9062 67.5 16.875 67.5H19.125C20.9531 67.5 22.5 66.0937 22.5 64.125V49.6406ZM56.5312 5.48436L53.1562 8.99999C47.25 15.1875 39.6562 19.5469 31.5 21.375V46.2656C39.6562 48.0937 47.25 52.3125 53.1562 58.6406L56.5312 62.1562C57.2344 62.8594 58.5 62.2969 58.5 61.3125V6.32811C58.5 5.20311 57.2344 4.78124 56.5312 5.48436ZM27 22.0781C26.1562 22.2187 25.3125 22.3594 24.4688 22.3594L20.25 22.5H9C6.46875 22.5 4.5 24.6094 4.5 27V40.5C4.5 43.0312 6.46875 45 9 45H20.25L24.4688 45.2812C25.3125 45.4219 26.1562 45.4219 27 45.5625V22.0781ZM69.75 27C70.875 27 72 28.125 72 29.25V38.25C72 39.5156 70.875 40.5 69.75 40.5C68.4844 40.5 67.5 39.5156 67.5 38.25V29.25C67.5 28.125 68.4844 27 69.75 27Z" fill="var(--wa-color-neutral-on-quiet"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,10 +0,0 @@
<svg width="96" height="57" viewBox="0 0 96 57" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 1H84C90.0751 1 95 5.92487 95 12V45C95 51.0751 90.0751 56 84 56H12C5.92487 56 1 51.0751 1 45V12C1 5.92487 5.92487 1 12 1Z" fill="var(--wa-color-surface-default)" stroke="var(--wa-color-surface-border)" stroke-width="2"/>
<rect x="7" y="8" width="41" height="5" fill="var(--wa-color-neutral-on-quiet)"/>
<rect x="7" y="19" width="23" height="14" rx="3" fill="var(--wa-color-neutral-fill-normal)"/>
<rect x="35" y="19" width="23" height="14" rx="3" fill="var(--wa-color-neutral-fill-normal)"/>
<rect x="63" y="19" width="23" height="14" rx="3" fill="var(--wa-color-neutral-fill-normal)"/>
<rect x="7" y="36" width="23" height="14" rx="3" fill="var(--wa-color-neutral-fill-normal)"/>
<rect x="35" y="36" width="23" height="14" rx="3" fill="var(--wa-color-neutral-fill-normal)"/>
<rect x="63" y="36" width="23" height="14" rx="3" fill="var(--wa-color-neutral-fill-normal)"/>
</svg>

Before

Width:  |  Height:  |  Size: 986 B

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

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

View File

@@ -1,31 +0,0 @@
<svg width="96" height="64" viewBox="0 0 96 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 1H84C90.0751 1 95 5.92487 95 12V52C95 58.0751 90.0751 63 84 63H12C5.92487 63 1 58.0751 1 52V12C1 5.92487 5.92487 1 12 1Z" fill="var(--wa-color-surface-default)"/>
<path d="M12 1H84C90.0751 1 95 5.92487 95 12V52C95 58.0751 90.0751 63 84 63H12C5.92487 63 1 58.0751 1 52V12C1 5.92487 5.92487 1 12 1Z" stroke="var(--wa-color-surface-border)" stroke-width="2"/>
<rect x="12" y="12" width="24" height="4" rx="2" fill="var(--wa-color-neutral-fill-quiet)"/>
<rect x="12" y="21" width="24" height="4" rx="2" fill="var(--wa-color-neutral-fill-quiet)"/>
<rect x="12" y="30" width="24" height="4" rx="2" fill="var(--wa-color-neutral-fill-quiet)"/>
<rect x="44" y="12" width="24" height="4" rx="2" fill="var(--wa-color-neutral-fill-quiet)"/>
<rect x="44" y="21" width="24" height="4" rx="2" fill="var(--wa-color-neutral-fill-quiet)"/>
<rect x="44" y="30" width="24" height="4" rx="2" fill="var(--wa-color-neutral-fill-quiet)"/>
<rect x="76" y="12" width="20" height="4" rx="2" fill="url(#paint0_linear_302_2)"/>
<rect x="76" y="21" width="20" height="4" rx="2" fill="url(#paint1_linear_302_2)"/>
<rect x="76" y="30" width="20" height="4" rx="2" fill="url(#paint2_linear_302_2)"/>
<rect x="4" y="44" width="88" height="16" rx="8" fill="var(--wa-color-neutral-fill-normal)"/>
<path d="M85.8438 51.6562C86.0469 51.8438 86.0469 52.1719 85.8438 52.3594L82.8438 55.3594C82.6562 55.5625 82.3281 55.5625 82.1406 55.3594C81.9375 55.1719 81.9375 54.8438 82.1406 54.6562L84.7812 52L82.1406 49.3594C81.9375 49.1719 81.9375 48.8438 82.1406 48.6562C82.3281 48.4531 82.6562 48.4531 82.8438 48.6562L85.8438 51.6562Z" fill="var(--wa-color-neutral-border-loud)"/>
<path d="M10.1406 51.6562L13.1406 48.6562C13.3281 48.4531 13.6562 48.4531 13.8438 48.6562C14.0469 48.8438 14.0469 49.1719 13.8438 49.3594L11.2031 52L13.8438 54.6562C14.0469 54.8438 14.0469 55.1719 13.8438 55.3594C13.6562 55.5625 13.3281 55.5625 13.1406 55.3594L10.1406 52.3594C9.9375 52.1719 9.9375 51.8438 10.1406 51.6562Z" fill="var(--wa-color-neutral-border-loud)"/>
<path d="M20 52C20 49.7909 21.7909 48 24 48H48C50.2091 48 52 49.7909 52 52V52C52 54.2091 50.2091 56 48 56H24C21.7909 56 20 54.2091 20 52V52Z" fill="var(--wa-color-neutral-border-loud)"/>
<defs>
<linearGradient id="paint0_linear_302_2" x1="76" y1="14" x2="96" y2="14" gradientUnits="userSpaceOnUse">
<stop offset="0.533654" stop-color="var(--wa-color-neutral-fill-quiet)"/>
<stop offset="1" stop-color="var(--wa-color-neutral-fill-quiet)" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint1_linear_302_2" x1="76" y1="23" x2="96" y2="23" gradientUnits="userSpaceOnUse">
<stop offset="0.533654" stop-color="var(--wa-color-neutral-fill-quiet)"/>
<stop offset="1" stop-color="var(--wa-color-neutral-fill-quiet)" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint2_linear_302_2" x1="76" y1="32" x2="96" y2="32" gradientUnits="userSpaceOnUse">
<stop offset="0.533654" stop-color="var(--wa-color-neutral-fill-quiet)"/>
<stop offset="1" stop-color="var(--wa-color-neutral-fill-quiet)" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,14 @@
<div class="showcase-examples-wrapper" aria-hidden="true" data-no-outline>
<div class="showcase-examples">
<wa-card>
<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="--background-color: var(--wa-color-green-60); --text-color: var(--wa-color-green-95);">
<wa-icon slot="icon" name="sword-laser"></wa-icon>
<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">
@@ -23,8 +23,8 @@
</div>
<wa-divider></wa-divider>
<div class="wa-flank">
<wa-avatar shape="rounded" style="--background-color: var(--wa-color-cyan-60); --text-color: var(--wa-color-cyan-95);">
<wa-icon slot="icon" name="robot-astromech"></wa-icon>
<wa-avatar shape="rounded" style="--size: 3em; --background-color: var(--wa-color-cyan-60); --text-color: var(--wa-color-cyan-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">
@@ -52,7 +52,7 @@
</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" style="font-size: 1.75em;"></wa-icon>
<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>
@@ -69,7 +69,7 @@
<a href="#" tabindex="-1" class="wa-body-s">I forgot my password</a>
</div>
</wa-card>
<wa-card>
<wa-card with-footer>
<div class="wa-stack">
<div class="wa-split">
<h3 class="wa-heading-m">To-Do</h3>
@@ -118,7 +118,7 @@
<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" readonly tabindex="-1"></wa-rating>
<wa-rating value="4.6" read-only></wa-rating>
<strong>4.6</strong>
<span>(419 reviews)</span>
</div>
@@ -140,196 +140,5 @@
</div>
</div>
</wa-card>
<wa-card>
<div class="wa-stack">
<div class="wa-flank:end">
<h3 id="odds-label" class="wa-heading-m">Tell Me the Odds</h3>
<wa-switch size="large" aria-labelledby="odds-label" tabindex="-1"></wa-switch>
</div>
<p class="wa-body-s">Allow protocol droids to inform you of probabilities, such as the success rate of navigating an asteroid field. We recommend setting this to "Never."</p>
</div>
</wa-card>
<wa-card>
<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-center">
<dt><wa-icon name="user" label="Name" fixed-width></wa-icon></dt>
<dd>Tom Bombadil</dd>
</div>
<div class="wa-flank wa-align-items-center">
<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-center">
<dt><wa-icon name="coin-vertical" fixed-width></wa-icon></dt>
<dd>Paid with copper pennies</dd>
</div>
</dl>
</div>
<div slot="footer">
<a href="" class="wa-cluster wa-gap-2xs" tabindex="-1">
<span>Download Receipt</span>
<wa-icon name="arrow-right"></wa-icon>
</a>
</div>
</wa-card>
<wa-card>
<div class="wa-stack">
<div class="wa-split">
<div class="wa-cluster wa-heading-l">
<wa-icon name="book-sparkles"></wa-icon>
<h3>Fellowship</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">Carry great power (and great responsibility).</p>
<wa-button variant="brand" tabindex="-1">Get this Plan</wa-button>
</div>
<div slot="footer" class="wa-stack wap-gap-s">
<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">9 users</span>
</div>
<div class="wa-flank">
<wa-icon name="ring" fixed-width></wa-icon>
<span class="wa-caption-m">1 ring</span>
</div>
<div class="wa-flank">
<wa-icon name="chess-rook" fixed-width></wa-icon>
<span class="wa-caption-m">API access to Isengard</span>
</div>
<div class="wa-flank">
<wa-icon name="feather" fixed-width></wa-icon>
<span class="wa-caption-m">Priority eagle support</span>
</div>
</div>
</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">Migs Mayfeld</h3 class="wa-heading-s">
<wa-badge pill>Admin</wa-badge>
</div>
<span class="wa-caption-m">Bounty Hunter</span>
</div>
<wa-avatar image="https://images.unsplash.com/photo-1633268335280-a41fbde58707?q=80&w=3348&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" label="Avatar of a man wearing a sci-fi helmet (Photograph by Nandu Vasudevan)"></wa-avatar>
</div>
<div slot="footer" class="wa-grid wa-gap-xs" style="--min-column-size: 10ch;">
<wa-button appearance="outlined" tabindex="-1">
<wa-icon slot="prefix" name="at"></wa-icon>
Email
</wa-button>
<wa-button appearance="outlined" tabindex="-1">
<wa-icon slot="prefix" name="phone"></wa-icon>
Phone
</wa-button>
</div>
</wa-card>
<wa-card>
<div class="wa-flank:end">
<a href="" class="wa-flank wa-link-plain" tabindex="-1">
<wa-avatar shape="rounded" style="--background-color: var(--wa-color-yellow-90); --text-color: var(--wa-color-yellow-50)">
<wa-icon slot="icon" name="egg-fried"></wa-icon>
</wa-avatar>
<div class="wa-gap-2xs wa-stack">
<span class="wa-heading-s">Second Breakfast</span>
<span class="wa-caption-m">19 Items</span>
</div>
</a>
<wa-dropdown>
<wa-icon-button id="more-actions-2" slot="trigger" name="ellipsis-vertical" label="View menu" tabindex="-1"></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">View menu</wa-tooltip>
</div>
</wa-card>
<wa-card with-header with-footer>
<div slot="header" class="wa-stack wa-gap-xs">
<h2 class="wa-heading-m">Decks</h2>
</div>
<div class="wa-stack wa-gap-xl">
<p class="wa-caption-m">You havent created any decks yet. Get started by selecting an aspect that matches your play style.</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" tabindex="-1">
<wa-avatar shape="rounded" style="--background-color: var(--wa-color-blue-90);color: var(--wa-color-blue-50);">
<wa-icon slot="icon" name="shield"></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">
Vigilance <wa-icon name="arrow-right"></wa-icon>
</span>
<p class="wa-caption-m">
Protect, defend, and restore as you ready heavy-hitters.
</p>
</div>
</a>
<a href="" class="wa-flank wa-align-items-start wa-link-plain" tabindex="-1">
<wa-avatar shape="rounded" style="--background-color: var(--wa-color-green-90);color: var(--wa-color-green-50);">
<wa-icon slot="icon" name="chevrons-up"></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">
Command <wa-icon name="arrow-right"></wa-icon>
</span>
<p class="wa-caption-m">
Build imposing armies and stockpile resources.
</p>
</div>
</a>
<a href=""class="wa-flank wa-align-items-start wa-link-plain" tabindex="-1">
<wa-avatar shape="rounded" style="--background-color: var(--wa-color-red-90);color: var(--wa-color-red-50);">
<wa-icon slot="icon" name="explosion"></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">
Aggression <wa-icon name="arrow-right"></wa-icon>
</span>
<p class="wa-caption-m">
Relentlessly deal damage and apply pressure to your opponent.
</p>
</div>
</a>
<a href="" class="wa-flank wa-align-items-start wa-link-plain" tabindex="-1">
<wa-avatar shape="rounded" style="--background-color: var(--wa-color-yellow-90);color: var(--wa-color-yellow-50);">
<wa-icon slot="icon" name="moon-stars"></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">
Cunning <wa-icon name="arrow-right"></wa-icon>
</span>
<p class="wa-caption-m">
Disrupt and frustrate your opponent with dastardly tricks.
</p>
</div>
</a>
</div>
</div>
<div slot="footer">
<a href="" class="wa-cluster wa-gap-xs" tabindex="-1">
<span>Or start a deck from scratch</span>
<wa-icon name="arrow-right"></wa-icon>
</a>
</div>
</wa-card>
</div>
</div>

View File

@@ -2,7 +2,6 @@
<html lang="en" data-fa-kit-code="b10bfbde90" data-cdn-url="{% cdnUrl %}">
<head>
{% include 'head.njk' %}
{% block head %}{% endblock %}
</head>
<body class="layout-{{ layout | stripExtension }}">

View File

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

View File

@@ -1,5 +1,6 @@
---
layout: page-outline
tags: ["overview"]
---
{% set forTag = forTag or (page.url | split('/') | last) %}
{% if description %}
@@ -12,10 +13,8 @@ layout: page-outline
</wa-input>
</div>
{% set allPages = allPages or collections[forTag] %}
{% if allPages and allPages.length > 0 %}
{% set allPages = collections[forTag] %}
{% 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,9 +1,4 @@
{% if hasSidebar == undefined %}
{% set hasSidebar = true %}
{% endif %}
{% if hasOutline == undefined %}
{% set hasOutline = false %}
{% endif %}
{% set hasSidebar = true %}
{% set hasOutline = false %}
{% extends "../_includes/base.njk" %}

View File

@@ -30,21 +30,12 @@
{% include 'breadcrumbs.njk' %}
<h1 class="title">
<span v-content="title">{{ title }}</span>
<template v-if="saved || tweaked">
<wa-icon-button name="pencil" label="Rename palette" @click="rename"></wa-icon-button>
<wa-icon-button v-if="saved" class="delete" name="trash" label="Delete palette" @click="deleteSaved"></wa-icon-button>
<wa-button @click="save()" :disabled="!unsavedChanges"
:variant="unsavedChanges ? 'success' : 'neutral'" size="small" :appearance="unsavedChanges ? 'accent' : 'outlined'">
<span slot="prefix" class="icon-modifier">
<wa-icon name="sidebar" variant="regular"></wa-icon>
<wa-icon name="circle-plus" class="modifier" style="color: light-dark(var(--wa-color-green-70), var(--wa-color-green-60));"></wa-icon>
</span>
<span v-content="unsavedChanges ? 'Save' : 'Saved'">Save</span>
</wa-button>
</template>
<h1 v-if="saved" class="title">
{% raw %}{{ saved.title }}{% endraw %}
<wa-icon-button name="pencil" label="Rename palette" @click="rename"></wa-icon-button>
<wa-icon-button class="delete" name="trash" label="Delete palette" @click="deleteSaved"></wa-icon-button>
</h1>
<h1 v-if="!saved" class="title">{{ title }}</h1>
<div class="block-info">
<code class="class">.wa-palette-{{ paletteId }}</code>
@@ -68,7 +59,7 @@
<wa-icon name="sliders-simple" slot="icon" variant="regular"></wa-icon>
This palette has been tweaked.
<div class="wa-cluster wa-gap-xs">
<wa-tag v-for="tweakHumanReadable, param in tweaksHumanReadable" removable @wa-remove="reset(param)" v-content="tweakHumanReadable"></wa-tag>
<wa-tag v-for="tweakHumanReadable, param in tweaksHumanReadable" removable @wa-remove="reset(param)">{% raw %}{{ tweakHumanReadable }}{% endraw %}</wa-tag>
</div>
<wa-button @click="reset()" appearance="outlined" variant="danger">
@@ -77,6 +68,13 @@
</span>
Reset
</wa-button>
<wa-button v-if="!saved" @click="save" variant="success">
<span slot="prefix" class="icon-modifier">
<wa-icon name="sidebar" variant="regular"></wa-icon>
<wa-icon name="circle-plus" class="modifier" style="color: light-dark(var(--wa-color-green-70), var(--wa-color-green-60));"></wa-icon>
</span>
Save
</wa-button>
</wa-callout>
<table class="colors main wa-palette-{{ paletteId }}">
@@ -122,8 +120,19 @@
</div>
<div class="popup">
{% if hue === 'gray' %}
<swatch-select label="Gray undertone" shape="circle" :values="hues" v-model="grayColor"></swatch-select>
<wa-radio-group class="core-color" orientation="horizontal" v-model="grayColor">
{% for h in hues -%}
{%- if h !== 'gray' -%}
<wa-radio-button id="gray-undertone-{{ h }}" value="{{ h }}" label="{{ h | capitalize }}" style="--color: var(--wa-color-{{ h }})"></wa-radio-button>
<wa-tooltip for="gray-undertone-{{ h }}" hoist>
{{ h | capitalize }}
</wa-tooltip>
{%- endif -%}
{%- endfor -%}
<div slot="label">
Gray undertone
</div>
</wa-radio-group>
<div class="decorated-slider gray-chroma-slider" :style="{'--max': maxGrayChroma}">
<wa-slider name="gray-chroma" v-model="grayChroma" ref="grayChromaSlider"
value="0" min="0" :max="maxGrayChroma" step="0.01"

View File

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

View File

@@ -5,22 +5,35 @@
{% extends '../_includes/base.njk' %}
{% block head %}
<script>
globalThis.wa_data ??= {};
wa_data.baseTheme = "{{ page.fileSlug }}";
wa_data.themes = {
{% for theme in collections.theme -%}
"{{ theme.fileSlug }}": {
"title": "{{ theme.data.title }}",
"palette": "{{ theme.data.palette }}",
"brand": "{{ theme.data.brand }}"
},
{% endfor %}
};
wa_data.palettes = {
{% for palette in collections.palette -%}
"{{ palette.fileSlug }}": {
"title": "{{ palette.data.title }}",
},
{% endfor %}
};
</script>
<link href="{{ page.url }}../remix.css" rel="stylesheet">
<script type="module" src="{{ page.url }}../edit/index.js"></script>
<script src="{{ page.url }}../remix.js" type="module"></script>
{% endblock %}
{% block header %}
<script>
if (location.pathname.endsWith('/custom/') && !location.search) {
location.href = "../edit/";
}
</script>
<div id="theme-app" data-theme-id="{{ page.fileSlug }}">
<iframe ref="preview" :src="'{{ page.url }}demo.html' + urlParams" src='{{ page.url }}demo.html' id="demo"></iframe>
<iframe src='{{ page.url }}demo.html' id="demo"></iframe>
{% if page.fileSlug !== 'custom' %}
<wa-details id="mix_and_match" class="wa-gap-m" :open="saved || unsavedChanges">
<wa-details id="mix_and_match" class="wa-gap-m" >
<h4 slot="summary" data-no-anchor data-no-outline id="remix">
<wa-icon name="arrows-rotate"></wa-icon>
Remix this theme
@@ -28,64 +41,92 @@ if (location.pathname.endsWith('/custom/') && !location.search) {
<wa-tooltip for="what-is-remixing">Customize this theme by changing its colors and/or remixing it with design elements from other themes!</wa-tooltip>
</h4>
<wa-select name="palette" label="Color palette" clearable v-model="theme.palette">
<wa-icon name="swatchbook" slot="prefix" variant="regular"></wa-icon>
<wa-option v-for="(palette, paletteId) in palettes" :label="palette.title" :value="paletteId === baseTheme.palette ? '' : paletteId">
<palette-card :palette="paletteId" size="small">
<template #extra>
<wa-badge v-if="paletteId === baseTheme.palette" variant="neutral" appearance="outlined">Theme default</wa-badge>
</template>
</palette-card>
</wa-option>
</wa-select>
<color-select :model-value="computed.brand" @update:model-value="value => theme.brand = value" label="Brand color"
:values="hues"></color-select>
<wa-select name="colors" class="theme-colors-select" label="Color contrast from…" value="" clearable v-model="theme.colors">
<wa-select name="colors" label="Colors from…" value="" clearable>
<wa-icon name="palette" slot="prefix" variant="regular"></wa-icon>
<template v-for="(themeMeta, themeId) in themes">
<wa-option v-if="themeId !== 'custom'" :label="themeMeta.title" :value="themeId === computed.colors ? '' : themeId">
<theme-card :theme="themeId" type="colors" :rest="{base: computed.base, palette: computed.palette, brand: computed.brand}" size="small">
<template #extra>
<wa-badge v-if="themeId === theme.base" variant="neutral" appearance="outlined">This theme</wa-badge>
</template>
</theme-card>
{% for theme in collections.theme | sort %}
{% set currentTheme = theme.fileSlug == page.fileSlug %}
<wa-option label="{{ theme.data.title }}" value="{{ theme.fileSlug if not currentTheme }}" {{ (theme.fileSlug if currentTheme) | attr('data-id') }}>
<wa-card with-header>
<div slot="header">
{% include "svgs/theme-color.njk" %}
</div>
<span class="page-name">
{{ theme.data.title }}
{% if theme.data.isPro %}<wa-badge class="pro">PRO</wa-badge>{% endif %}
{% if currentTheme %}<wa-badge variant="neutral" appearance="outlined">This theme</wa-badge>{% endif %}
</span>
</wa-card>
</wa-option>
</template>
{% endfor %}
</wa-select>
<wa-select name="typography" label="Typography from…" clearable v-model="theme.typography">
<wa-icon name="font-case" slot="prefix"></wa-icon>
<wa-select name="palette" label="Palette" clearable>
<wa-icon name="swatchbook" slot="prefix" variant="regular"></wa-icon>
{% set defaultPalette = palette %}
{% for palette in collections.palette | sort %}
{% set currentPalette = palette.fileSlug == defaultPalette %}
<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" %}
</div>
<span class="page-name">
{{ palette.data.title }}
{% if palette.data.isPro %}<wa-badge class="pro">PRO</wa-badge>{% endif %}
{% if currentPalette %}<wa-badge variant="neutral" appearance="outlined">Theme default</wa-badge>{% endif %}
</span>
</wa-card>
</wa-option>
{% endfor %}
{% set palette = defaultPalette %}
</wa-select>
<wa-option v-for="(themeMeta, themeId) in themes" :label="themeMeta.title" :value="themeId === theme.base ? '' : themeId">
<fonts-card :theme="themeId" size="small">
<template #extra>
<wa-badge v-if="themeId === theme.base" variant="neutral" appearance="outlined">This theme</wa-badge>
</template>
</fonts-card>
</wa-option>
<wa-select name="brand" label="Brand color" value="" clearable>
<div class="selected-swatch" slot="prefix"></div>
{% for hue in hues %}
{% set currentBrand = hue == brand %}
<wa-option label="{{ hue | capitalize }}" value="{{ hue if not currentBrand }}" {{ (hue if currentBrand) | attr('data-id') }} style="--color: var(--wa-color-{{ hue }})">
{{ hue | capitalize }}
{% if currentBrand %}<wa-badge variant="neutral" appearance="outlined">Theme default</wa-badge>{% endif %}
</wa-option>
{% endfor %}
</wa-select>
<wa-select name="typography" label="Typography from…" clearable>
<wa-icon name="font-case" slot="prefix"></wa-icon>
{% for theme in collections.theme | sort %}
{% set currentTheme = theme.fileSlug == page.fileSlug %}
<wa-option label="{{ theme.data.title }}" value="{{ theme.fileSlug if not currentTheme }}" {{ (theme.fileSlug if currentTheme) | attr('data-id') }}>
<wa-card with-header>
<div slot="header">
{% include "svgs/theme-typography.njk" %}
</div>
<span class="page-name">
{{ theme.data.title }}
{% if theme.data.isPro %}<wa-badge class="pro">PRO</wa-badge>{% endif %}
{% if currentTheme %}<wa-badge variant="neutral" appearance="outlined">This theme</wa-badge>{% endif %}
</span>
</wa-card>
</wa-option>
{% endfor %}
</wa-select>
</wa-details>
{% endif %}
<h2>Color</h2>
{% set paletteURL = '/docs/palettes/' + palette + '/' %}
<div class="index-grid">
{% if page.fileSlug === 'custom' %}
<palette-card :palette="computed.palette" subtitle="Color palette"></palette-card>
{% else %}
{% set themePage = page %}
{% set paletteURL = '/docs/palettes/' + palette + '/' %}
{% set page = paletteURL | getCollectionItemFromUrl %}
{% set pageSubtitle = "Default color palette" %}
{% include 'page-card.njk' %}
{% set page = themePage %}
{% endif %}
<wa-card class="wa-palette-{{ palette }}" style="--header-background: var(--wa-color-{{ brand }})"
:class="`wa-palette-${computed.palette}`" :style="{'--header-background': palettes[computed.palette]?.colors[computed.brand]?.key}">
{% set themePage = page %}
{% set page = paletteURL | getCollectionItemFromUrl %}
{% set pageSubtitle = "Default color palette" %}
{% include 'page-card.njk' %}
{% set page = themePage %}
<wa-card style="--header-background: var(--wa-color-{{ brand }})" class="wa-palette-{{ palette }}">
<div slot="header"></div>
<div class="page-name" v-content="capitalize(computed.brand)">{{ brand | capitalize }}</div>
<div class="wa-caption-s">{{ 'Brand color' if page.fileSlug === 'custom' else 'Default brand color' }}</div>
<div class="page-name">{{ brand | capitalize }}</div>
<div class="wa-caption-s">Default brand color</div>
</wa-card>
</div>
{% endblock %}
@@ -98,25 +139,7 @@ if (location.pathname.endsWith('/custom/') && !location.search) {
You can import this theme from the Web Awesome CDN.
{% set stylesheet = 'styles/themes/' + page.fileSlug + '.css' %}
<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>
{% include 'import-stylesheet-code.md.njk' %}
## Dark mode
@@ -180,6 +203,5 @@ systemDark.addEventListener('change', applyDark);
applyDark();
```
</div> {# end theme app #}
{% endmarkdown %}
{% endblock %}

View File

@@ -102,7 +102,12 @@ const templates = {
export function codeExamplesPlugin(eleventyConfig, options = {}) {
const defaultOptions = {
container: 'body',
defaultOpen: () => false,
defaultOpen: (code, { outputPathIndex }) => {
return (
outputPathIndex === 1 && // is first
code.textContent.length < 500
); // is short
},
};
options = { ...defaultOptions, ...options };

View File

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

View File

@@ -29,9 +29,6 @@ function getCollection(name) {
}
export function getCollectionItemFromUrl(url, collection) {
if (!url) {
return null;
}
collection ??= getCollection.call(this, 'all') || [];
return collection.find(item => item.url === url);
}
@@ -45,33 +42,35 @@ export function split(text, separator) {
return (text + '').split(separator).filter(Boolean);
}
export function ancestors(url, { withCurrent = false, withRoot = false } = {}) {
let ret = [];
let currentUrl = url;
let currentItem = getCollectionItemFromUrl.call(this, url);
export function breadcrumbs(url, { withCurrent = false } = {}) {
const parts = split(url, '/');
const ret = [];
if (!currentItem) {
// Might have eleventyExcludeFromCollections, jump to parent
let parentUrl = this.ctx.parentUrl;
if (parentUrl) {
url = parentUrl;
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];
}
}
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;
}
@@ -181,185 +180,69 @@ export function sort(arr, by = { 'data.order': 1, 'data.title': '' }) {
/**
* Group an 11ty collection (or any array of objects with a `data.tags` property) by certain tags.
* @param {object[]} collection
* @param { Object<string, string> | string[]} [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.
* @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.
*/
export function groupPages(collection, options = {}, page) {
export function groupByTags(collection, tags) {
if (!collection) {
console.error(`Empty collection passed to groupPages() to group by ${JSON.stringify(options)}`);
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);
}
if (Array.isArray(options)) {
options = { tags: options };
}
let { tags, groups, titles = {}, other = 'Other' } = options;
if (groups === undefined && Array.isArray(tags)) {
groups = tags;
}
let grouping;
if (tags) {
grouping = {
isGroup: item => undefined,
getCandidateGroups: item => item.data.tags,
getGroupMeta: group => ({}),
};
} else {
grouping = {
isGroup: item => (item.data.children.length >= 2 ? item.page.url : undefined),
getCandidateGroups: item => {
let parentUrl = item.data.parentUrl;
if (page?.url === parentUrl) {
return [];
}
return [parentUrl];
},
getGroupMeta: group => {
let item = byUrl[group] || getCollectionItemFromUrl.call(this, group);
return {
title: item?.data.title,
url: group,
item,
};
},
sortGroups: groups => sort(groups.map(url => byUrl[url]).filter(Boolean)).map(item => item.page.url),
};
}
let byUrl = {};
let byParentUrl = {};
let ret = Object.fromEntries(tags.map(tag => [tag, []]));
ret.other = [];
for (let item of collection) {
let url = item.page.url;
let parentUrl = item.data.parentUrl;
let categorized = false;
byUrl[url] = item;
if (parentUrl) {
byParentUrl[parentUrl] ??= [];
byParentUrl[parentUrl].push(item);
}
}
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);
}
for (let tag of tags) {
if (item.data.tags.includes(tag)) {
ret[tag].push(item);
categorized = true;
}
}
}
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;
if (!categorized) {
ret.other.push(item);
}
}
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];
// Remove empty categories
for (let category in ret) {
if (ret[category].length === 0) {
delete ret[category];
}
}
return ret;
}
function capitalize(str) {
str += '';
return str.charAt(0).toUpperCase() + str.slice(1);
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);
}
const IDENTITY = x => x;
@@ -399,12 +282,3 @@ export function attr(value, name) {
return safe(ret);
}
/**
* Format an object as JSON, with formatting & indentation (unlike the default `dump` filter)
* @param {*} value
* @returns {string}
*/
export function json(value) {
return JSON.stringify(value, null, 2);
}

View File

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

View File

@@ -0,0 +1,23 @@
import { parse } from 'node-html-parser';
/**
* Eleventy plugin to add remove elements with <div data-alpha="remove"> from the alpha build.
*/
export function removeDataAlphaElements(options = {}) {
options = {
isAlpha: false,
...options,
};
return function (eleventyConfig) {
eleventyConfig.addTransform('remove-data-alpha-elements', content => {
const doc = parse(content, { blockTextElements: { code: true } });
if (options.isAlpha) {
doc.querySelectorAll('[data-alpha="remove"]').forEach(el => el.remove());
}
return doc.toString();
});
};
}

View File

@@ -2,7 +2,6 @@
import { mkdir, writeFile } from 'fs/promises';
import lunr from 'lunr';
import { parse } from 'node-html-parser';
import * as path from 'path';
import { dirname, join } from 'path';
function collapseWhitespace(string) {
@@ -24,23 +23,9 @@ export function searchPlugin(options = {}) {
};
return function (eleventyConfig) {
const pagesToIndex = new Map();
eleventyConfig.addPreprocessor('exclude-unlisted-from-search', '*', function (data, content) {
if (data.unlisted) {
// no-op
} else {
pagesToIndex.set(data.page.inputPath, {});
}
return content;
});
const pagesToIndex = [];
eleventyConfig.addTransform('search', function (content) {
if (!pagesToIndex.has(this.page.inputPath)) {
return content;
}
const doc = parse(content, {
blockTextElements: {
script: false,
@@ -56,7 +41,7 @@ export function searchPlugin(options = {}) {
doc.querySelectorAll(selector).forEach(el => el.remove());
});
pagesToIndex.set(this.page.inputPath, {
pagesToIndex.push({
title: collapseWhitespace(options.getTitle(doc)),
description: collapseWhitespace(options.getDescription(doc)),
headings: options.getHeadings(doc).map(collapseWhitespace),
@@ -67,9 +52,8 @@ export function searchPlugin(options = {}) {
return content;
});
eleventyConfig.on('eleventy.after', ({ directories }) => {
const { output } = directories;
const outputFilename = path.resolve(join(output, 'search.json'));
eleventyConfig.on('eleventy.after', ({ dir }) => {
const outputFilename = join(dir.output, 'search.json');
const map = [];
const searchIndex = lunr(async function () {
let index = 0;
@@ -79,7 +63,7 @@ export function searchPlugin(options = {}) {
this.field('h', { boost: 10 });
this.field('c');
for (const [_inputPath, page] of pagesToIndex) {
for (const page of pagesToIndex) {
this.add({ id: index, t: page.title, h: page.headings, c: page.content });
map[index] = { title: page.title, description: page.description, url: page.url };
index++;

View File

@@ -1,172 +0,0 @@
/**
* Low-level utility to encapsulate a bit of HTML (mainly to apply certain stylesheets to it without them leaking to the rest of the page)
* Usage: <wa-scoped><template><!-- your HTML here --></template></wa-scoped>
*/
import { discover } from '/dist/webawesome.js';
const imports = new Set();
const fontFaceRules = new Set();
export default class WaScoped extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.observer = new MutationObserver(records => this.render(records));
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(records) {
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) {
if (!(template instanceof HTMLTemplateElement)) {
if (template.nodeType === Node.ELEMENT_NODE) {
console.warn('<wa-scoped> can only contain <template> elements');
}
continue;
}
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);
}
}
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

@@ -1,65 +0,0 @@
import { deepEntries } from '../scripts/util/deep.js';
import { themeConfig } from './theming.js';
import themes from '/assets/data/themes.js';
/**
* Map of font pairings (body + heading) to the first theme that uses them.
*/
export const pairings = {};
// NOTE Do not use Symbols, we want these to be enumerable when used as keys
export const sameAs = { body: '$body' };
export const fontNames = {
'system-ui': 'OS Default',
'ui-serif': 'OS Default Serif',
'ui-sans-serif': 'OS Default Sans Serif',
'ui-monospace': 'OS Default Code Font',
'ui-monospace': 'OS Default Code Font',
};
export function defaultTitle(fonts) {
let { body, heading = sameAs.body } = fonts;
let names = [body];
if (heading !== sameAs.body) {
names.unshift(heading);
}
return names.map(name => fontNames[name] ?? name).join(' • ');
}
for (let id in themes) {
let theme = themes[id];
let { fonts } = theme;
if (fonts) {
let { body, heading = sameAs.body } = fonts;
pairings[body] ??= {};
pairings[body][heading] ??= {
id, // First theme that uses this pairing
ids: new Set([id]), // All themes that use this pairing
url: themeConfig.typography.url(id), // Stylesheet URL
fonts,
get title() {
return defaultTitle(this.fonts);
},
};
pairings[body][heading].ids.add(id);
}
}
export const pairingsEntries = deepEntries(pairings, {
descend(value, key, parent, path) {
if (value?.fonts) {
return false; // Don't recurse into pairing objects
}
},
filter(value, key, parent, path) {
// Only keep 2 levels (body → heading → pairing)
return path.length === 1;
},
});
export const pairingsList = pairingsEntries.map(arg => arg.at(-1));

View File

@@ -1,7 +0,0 @@
export const iconLibraries = {
default: {
title: 'Font Awesome',
family: ['classic', 'sharp', 'duotone', 'sharp-duotone'],
style: ['solid', 'regular', 'light', 'thin'],
},
};

View File

@@ -1,6 +0,0 @@
export * from './colors.js';
// export * from './fonts.js';
export * from './icons.js';
export * from './theming.js';
export const cdnUrl = globalThis.document ? document.documentElement.dataset.cdnUrl : '/dist/';

View File

@@ -1,57 +0,0 @@
---
layout: null
permalink: '/assets/data/palettes.js'
eleventyExcludeFromCollections: true
---
import Color from 'https://colorjs.io/dist/color.js';
const palettes = {
{%- for palette in collections.palette | sort %}
{%- if not palette.data.unlisted %}
{% set paletteId = palette.fileSlug -%}
{%- set colors = palettes[paletteId] -%}
'{{ paletteId }}': {
id: '{{ paletteId }}',
title: '{{ palette.data.title }}',
colors: {
{% for hue, tints in colors -%}
'{{ hue }}': {
{% for tint, value in tints -%}
{%- if tint != '05' -%}
'{{ '05' if tint == '5' else tint }}': '{{ value | safe }}',
{%- endif %}
{% endfor %}
get key() {
return this[this.maxChromaTint];
}
},
{% endfor -%} // end colors
}
}, // end palette
{%- endif -%}
{% endfor %}
};
// Create Color instances for each color
for (let palette in palettes) {
for (let hue in palettes[palette].colors) {
let scale = palettes[palette].colors[hue];
for (let tint in scale) {
let color = scale[tint];
try {
if (Array.isArray(color)) {
scale[tint] = new Color('oklch', color);
}
else if (typeof color === 'string' && isNaN(color)) {
scale[tint] = new Color(color);
}
} catch (e) {
console.error(e);
}
}
}
}
export default palettes;

View File

@@ -1,27 +0,0 @@
---
layout: null
permalink: '/assets/data/themes.js'
eleventyExcludeFromCollections: true
---
export default {
{%- for theme in collections.theme | sort %}
{%- if not theme.data.unlisted and theme.fileSlug !== 'edit' and theme.fileSlug !== 'custom' %}
{% set themeId = theme.fileSlug -%}
{%- set themeMeta = themes[themeId] -%}
'{{ themeId }}': {
id: '{{ themeId }}',
title: '{{ theme.data.title }}',
palette: '{{ themeMeta.palette }}',
brand: '{{ themeMeta.brand }}',
isPro: {{ theme.data.isPro or 'pro' in theme.data.tags }},
fonts: {{ (theme.data.fonts | json or 'null') | safe }},
icons: {{ (themeMeta.icons | json or 'null') | safe }},
rounding: {{ themeMeta.rounding }},
spacing: {{ themeMeta.spacing }},
borderWidth: {{ themeMeta.borderWidth }},
dimension: {{ (theme.data.dimension or themeMeta.dimension or false) | json | safe }},
},
{%- endif %}
{% endfor %}
};

View File

@@ -1,118 +0,0 @@
import { deepEach, isPlainObject } from '../scripts/util/deep.js';
/**
* Data related to themes, theme remixing
* Must work in both browser and Node.js
*/
export const cdnUrl = globalThis.document ? document.documentElement.dataset.cdnUrl : '/dist/';
// This should eventually replace all uses of `urls` and `themeParams`
export const themeConfig = {
base: { url: id => `styles/themes/${id}.css`, default: 'default' },
colors: {
url: id => `styles/themes/${id}/color.css`,
docs: '/docs/themes/',
icon: 'palette',
default() {
return this.base;
},
},
palette: {
url: id => `styles/color/${id}.css`,
docs: '/docs/palette/',
icon: 'swatchbook',
default(baseTheme) {
return baseTheme?.palette;
},
},
brand: {
url: id => `styles/brand/${id}.css`,
icon: 'droplet',
default(baseTheme) {
return baseTheme?.brand;
},
},
typography: {
url: id => `styles/themes/${id}/typography.css`,
docs: '/docs/themes/',
icon: 'font-case',
default() {
return this.base;
},
},
icon: {
library: {
cssProperty: '--wa-icon-library',
default: 'default',
},
family: {
cssProperty: '--wa-icon-family',
default(baseTheme) {
return baseTheme?.icon?.family ?? 'classic';
},
},
style: {
cssProperty: '--wa-icon-variant',
default(baseTheme) {
return baseTheme?.icon?.style ?? 'solid';
},
},
},
rounding: {
cssProperty: '--wa-border-radius-scale',
default(baseTheme) {
return baseTheme?.rounding ?? 1;
},
},
spacing: {
cssProperty: '--wa-space-scale',
default(baseTheme) {
return baseTheme?.spacing ?? 1;
},
},
borderWidth: {
cssProperty: '--wa-border-width-scale',
default(baseTheme) {
return baseTheme?.borderWidth ?? 1;
},
},
dimensionality: {
url: id => `styles/themes/${id}/dimension.css`,
docs: '/docs/themes/',
icon: 'cube',
default() {
return this.base;
},
},
};
export function getPath(key) {
if (key.startsWith('icon-')) {
// TODO detect what the nested prefixes are from theme config metadata
return ['icon', ...key.slice(5)];
}
}
// Shallow remixing params in correct order
// base must be first. brand needs to come after palette, which needs to come after colors.
export const themeParams = Object.keys(themeConfig).filter(aspect => themeConfig[aspect].url);
export const urls = themeParams.reduce((acc, aspect) => {
acc[aspect] = themeConfig[aspect].url;
return acc;
}, {});
export const themeDefaults = { ...themeConfig };
deepEach(themeDefaults, (value, key, parent, path) => {
if (isPlainObject(value)) {
// Replace w/ default value or shallow clone
return value.default ?? { ...value };
}
});
export const selectors = {
palette: id =>
[':where(:root)', ':host', ":where([class^='wa-theme-'], [class*=' wa-theme-'])", `.wa-palette-${id}`].join(',\n'),
theme: id => [':where(:root)', ':host', `.wa-theme-${id}`].join(',\n'),
};

View File

@@ -0,0 +1,59 @@
document.addEventListener('click', event => {
const toggle = event.target?.closest('.code-example-toggle');
const pen = event.target?.closest('.code-example-pen');
// Toggle source
if (toggle) {
const codeExample = toggle.closest('.code-example');
const isOpen = !codeExample.classList.contains('open');
toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
codeExample.classList.toggle('open', isOpen);
}
// Edit in CodePen
if (pen) {
const codeExample = pen.closest('.code-example');
const code = codeExample.querySelector('code');
const cdnUrl = document.documentElement.dataset.cdnUrl;
const html =
`<script type="module" src="${cdnUrl}webawesome.loader.js"></script>\n` +
`<link rel="stylesheet" href="${cdnUrl}styles/themes/default.css">\n` +
`<link rel="stylesheet" href="${cdnUrl}styles/webawesome.css">\n` +
`<link rel="stylesheet" href="${cdnUrl}styles/utilities.css">\n\n` +
`${code.textContent}`;
const css = 'html > body {\n padding: 2rem !important;\n}';
const js = '';
const form = document.createElement('form');
form.action = 'https://codepen.io/pen/define';
form.method = 'POST';
form.target = '_blank';
const data = {
title: '',
description: '',
tags: ['webawesome'],
editors: '1000',
head: '<meta name="viewport" content="width=device-width">',
html_classes: '',
css_external: '',
js_external: '',
js_module: true,
js_pre_processor: 'none',
html,
css,
js,
};
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'data';
input.value = JSON.stringify(data);
form.append(input);
document.documentElement.append(form);
form.submit();
form.remove();
}
});

View File

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

View File

@@ -51,7 +51,7 @@
<button class="diff-dialog-toggle">
Show Hydration Mismatch
</button>
<wa-dialog class="diff-dialog" light-dismiss>
<wa-dialog class="diff-dialog" with-header light-dismiss>
<div class="diff-grid">
<div>
<div>Server</div>

View File

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

View File

@@ -1,178 +0,0 @@
import { deepEach, deepGet, deepSet } from './util/deep.js';
import { camelCase, kebabCase } from './util/string.js';
export default class Permalink extends URLSearchParams {
/** Params changed since last URL I/O */
changed = false;
constructor(params) {
super(location.search);
this.params = params;
}
toJSON() {
return Object.fromEntries(this.entries());
}
/**
* Set multiple values from an object. Nested values will be joined with a hyphen.
* @param {object} values - The object containing the values to set.
* @param {object} defaults - The object containing the default values.
*
*/
setAll(values, defaults) {
deepEach(values, (value, key, parent, path) => {
let fullPath = [...path, key];
let param = fullPath.map(kebabCase).join('-');
if (typeof value === 'object') {
// We'll handle this when we descend into it
return;
}
let defaultValue = deepGet(defaults, fullPath);
if (equals(value, defaultValue)) {
// Remove the param from the URL
this.delete(param);
return;
}
this.set(param, value);
});
}
/**
* Convert the URL params to a (potentially nested) object.
* @param {object} options - Options object.
* @param {(key: string, value: string) => string[]} options.getPath - Function to get the path of a param.
* @returns {object} The nested object.
*/
toObject(options = {}) {
// Default getPath() assumes hyphens always mean nesting
let { ignoreKeys = [], getPath = param => param.split('-') } = options;
// Get all values as a nested object
let obj = {};
for (let [key, value] of this.entries()) {
let path = getPath(key, value);
if (path === null || ignoreKeys.includes(key)) {
// Skip this param
continue;
}
// Default to key if `getPath()` returns undefined
path ??= key;
path = Array.isArray(path) ? path : [path];
// Camel case any remaining hyphens
path = path.map(camelCase);
deepSet(obj, path, value);
}
return obj;
}
delete(key, value) {
let hadValue = this.has(key);
super.delete(key, value);
if (hadValue) {
this.changed = true;
}
}
set(key, value, defaultValue) {
if (equals(value, defaultValue) || equals(value, '')) {
value = null;
}
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();
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);
}
this.sort();
this.changed ||= changed;
}
/**
* Update page URL if it has changed since last time
*/
updateLocation() {
if (this.changed) {
// If theres already a search, replace it.
// We dont want to clog the users history while they iterate
let search = this.toString();
let historyAction = location.search && search ? 'replaceState' : 'pushState';
history[historyAction](null, '', `?${search}`);
this.changed = false;
}
}
}
function equals(value, oldValue) {
if (Array.isArray(value) || Array.isArray(oldValue)) {
value = toArray(value);
oldValue = toArray(oldValue);
if (value.length !== oldValue.length) {
return false;
}
return value.every((v, i) => equals(v, oldValue[i]));
}
// (value ?? oldValue ?? true) returns true if they're both empty (null or undefined)
[value, oldValue] = [value, oldValue].map(v => (!v && v !== false && v !== 0 ? null : v));
return value === oldValue || String(value) === String(oldValue);
}
/**
* Convert a value to an array. `undefined` and `null` values are converted to an empty array.
* @param {*} value - The value to convert.
* @returns {any[]} The converted array.
*/
function toArray(value) {
value ??= [];
if (Array.isArray(value)) {
return value;
}
// Don't convert "foo" into ["f", "o", "o"]
if (typeof value !== 'string' && typeof value[Symbol.iterator] === 'function') {
return Array.from(value);
}
return [value];
}

View File

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

View File

@@ -1,5 +1,14 @@
import { domChange } from './util/dom-change.js';
export { domChange };
// Helper for view transitions
export function domChange(fn, { behavior = 'smooth' } = {}) {
const canUseViewTransitions =
document.startViewTransition && !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (canUseViewTransitions && behavior === 'smooth') {
document.startViewTransition(fn);
} else {
fn(true);
}
}
export function nextFrame() {
return new Promise(resolve => requestAnimationFrame(resolve));
@@ -91,7 +100,6 @@ const colorScheme = new ThemeAspect({
domChange(() => {
let dark = this.computedValue === 'dark';
document.documentElement.classList.toggle(`wa-dark`, dark);
document.documentElement.dispatchEvent(new CustomEvent('wa-color-scheme-change', { detail: { dark } }));
});
},
});
@@ -106,6 +114,6 @@ document.addEventListener('keydown', event => {
!event.composedPath().some(el => ['input', 'textarea'].includes(el?.tagName?.toLowerCase()))
) {
event.preventDefault();
colorScheme.set(colorScheme.get() === 'dark' ? 'light' : 'dark');
colorScheme.set(theming.colorScheme.resolvedValue === 'dark' ? 'light' : 'dark');
}
});

View File

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

View File

@@ -1,6 +1,6 @@
/**
* Get import code for remixed themes and tweaked palettes.
*/
export { cdnUrl, hueRanges, hues, selectors, tints, urls } from '../data/index.js';
export { default as Permalink } from './permalink.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,8 +1,7 @@
/**
* Get import code for remixed themes and tweaked palettes.
*/
import { selectors, themeConfig } from '../../data/theming.js';
import { deepEach, deepGet } from '/assets/scripts/util/deep.js';
import { urls } from './data.js';
export function cssImport(url, options = {}) {
let { language = 'html', cdnUrl = '/dist/', attributes } = options;
@@ -22,65 +21,29 @@ export function cssLiteral(value, options = {}) {
if (language === 'css') {
return value;
} else {
return `<style${options.attributes ?? ''}>\n${value}\n</style>`;
return `<style>\n${value}\n</style>`;
}
}
/**
* Get code for a theme, including tweaks
* @param {*} theme
* @param {*} options
* @returns
*/
export function getThemeCode(theme, options = {}) {
let urls = [];
let declarations = [];
let id = options.id ?? theme.base ?? 'default';
// Params in correct order
export const themeParams = ['colors', 'palette', 'brand', 'typography'];
deepEach(themeConfig, (config, aspect, obj, path) => {
if (!config?.default) {
// We're not in a config object
return;
}
export function getThemeCode(base, params, options) {
let ret = [];
let value = deepGet(theme, [...path, aspect]);
if (base) {
ret.push(urls.theme(base));
}
if (!value && value !== 0) {
return;
}
for (let aspect of themeParams) {
let value = params[aspect];
if (config.url) {
// This is implemented by pulling in different CSS files
urls.push(config.url(value));
} else {
if (config.cssProperty) {
declarations.push(`${config.cssProperty}: ${value};`);
}
}
});
let ret = urls.map(url => cssImport(url, options)).join('\n');
if (declarations.length > 0) {
let cssCode = cssRule(selectors.theme(id), declarations, options);
if (theme.icon?.kit) {
let faKitAttribute = ` data-fa-kit-code="${theme.icon.kit}"`;
options.attributes ??= '';
options.attributes += faKitAttribute;
cssCode =
`/* Note: To use Font Awesome Pro icons,\n set ${faKitAttribute} on the <link> (or any other) element */\n\n` +
cssCode;
}
cssCode = cssLiteral(cssCode, options);
if (ret) {
ret += '\n\n' + cssCode;
if (value) {
ret.push(urls[aspect](value));
}
}
return ret;
return ret.map(url => cssImport(url, options)).join('\n');
}
export function cssRule(selector, declarations, { indent = ' ' } = {}) {

View File

@@ -1,9 +1,21 @@
/**
* Data related to palettes and colors.
* Data related to theme remixing and palette tweaking
* Must work in both browser and Node.js
*/
export const cdnUrl = globalThis.document ? document.documentElement.dataset.cdnUrl : '/dist/';
export const tints = ['05', '10', '20', '30', '40', '50', '60', '70', '80', '90', '95'];
export const urls = {
theme: id => `styles/themes/${id}.css`,
colors: id => `styles/themes/${id}/color.css`,
palette: id => `styles/color/${id}.css`,
brand: id => `styles/brand/${id}.css`,
typography: id => `styles/themes/${id}/typography.css`,
};
export const selectors = {
palette: id =>
[':where(:root)', ':host', ":where([class^='wa-theme-'], [class*=' wa-theme-'])", `.wa-palette-${id}`].join(',\n'),
};
export const hueRanges = {
red: { min: 5, max: 35 }, // 30
@@ -17,9 +29,6 @@ export const hueRanges = {
pink: { min: 320, max: 365 }, // 45
};
export const hues = Object.keys(hueRanges);
export const allHues = [...hues, 'gray'];
export const moreHue = {
red: 'Redder',
orange: 'More orange', // https://www.reddit.com/r/grammar/comments/u9n0uo/is_it_oranger_or_more_orange/
@@ -45,3 +54,20 @@ export const maxGrayChroma = {
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

@@ -0,0 +1,81 @@
const IDENTITY = x => x;
export default class Permalink extends URLSearchParams {
/** Params changed since last URL I/O */
changed = false;
constructor(params) {
super(location.search);
this.params = params;
}
toJSON() {
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 (!value || value == defaultValue) {
super.delete(key);
if (oldValue) {
this.changed = true;
}
} else {
super.set(key, value);
if (String(value) !== String(oldValue)) {
this.changed = true;
}
}
this.sort();
}
/**
* Update page URL if it has changed since last time
*/
updateLocation() {
if (this.changed) {
// If theres already a search, replace it.
// We dont want to clog the users history while they iterate
let search = this.toString();
let historyAction = location.search && search ? 'replaceState' : 'pushState';
history[historyAction](null, '', `?${search}`);
this.changed = false;
}
}
}

View File

@@ -1,17 +0,0 @@
/**
* Picks a random element from an array.
* @param {any[]} arr
*/
export function sample(arr) {
if (!Array.isArray(arr)) {
return arr;
}
if (arr.length < 2) {
return arr[0];
}
let index = Math.floor(Math.random() * arr.length);
return arr[index];
}

View File

@@ -1,180 +0,0 @@
/**
* @typedef { string | number | Symbol } Property
* @typedef { (value: any, key: Property, parent: object, path: Property[]) => any } EachCallback
*/
export function isPlainObject(obj) {
return isObject(obj, 'Object');
}
export function isObject(obj, type) {
if (!obj || typeof obj !== 'object') {
return false;
}
let proto = Object.getPrototypeOf(obj);
return proto.constructor?.name === type;
}
export function deepMerge(target, source, options = {}) {
let {
emptyValues = [undefined],
containers = ['Object', 'EventTarget'],
isContainer = value => containers.some(type => isObject(value, type)),
} = options;
if (isContainer(target) && isContainer(source)) {
for (let key in source) {
if (key in target && isContainer(target[key]) && isContainer(source[key])) {
target[key] = deepMerge(target[key], source[key], options);
} else if (!emptyValues.includes(source[key])) {
target[key] = source[key];
}
}
return target;
}
return target ?? source;
}
/**
* Iterate over a deep array, recursively for plain objects
* @param { any } obj The object to iterate over. Can be an array or a plain object, or even a primitive value.
* @param { EachCallback } callback. value is === parent[key]
* @param { object } [parentObj] The parent object of the current value Mainly used internally to facilitate recursion.
* @param { Property } [key] The key of the current value. Mainly used internally to facilitate recursion.
* @param { Property[] } [path] Any existing path (not including the key). Mainly used internally to facilitate recursion.
*/
export function deepEach(obj, callback, parentObj, key, path = []) {
if (key !== undefined) {
let ret = callback(obj, key, parentObj, path);
if (ret !== undefined) {
if (ret === false) {
// Do not descend further
return;
}
// Overwrite value
parentObj[key] = ret;
obj = ret;
}
}
let newPath = key !== undefined ? [...path, key] : path;
if (Array.isArray(obj)) {
for (let i = 0; i < obj.length; i++) {
deepEach(obj[i], callback, obj, i, newPath);
}
} else if (isPlainObject(obj)) {
for (let key in obj) {
deepEach(obj[key], callback, obj, key, newPath);
}
}
}
/**
* Get a value from a deeply nested object
* @param {*} obj
* @param {PropertyPath} path
* @returns
*/
export function deepGet(obj, path) {
if (path.length === 0) {
return obj;
}
let ret = obj;
for (let key of path) {
if (ret === undefined) {
return undefined;
}
ret = ret[key];
}
return ret;
}
/**
* Set a value in a deep object, creating object literals as needed
* @param { * } obj
* @param { Property[] } path
* @param { any } value
*/
export function deepSet(obj, path, value) {
if (path.length === 0) {
return;
}
let key = path.pop();
let ret = path.reduce((acc, property) => {
if (acc[property] === undefined) {
acc[property] = {};
}
return acc[property];
}, obj);
ret[key] = value;
}
export function deepClone(obj) {
if (!obj) {
return obj;
}
let ret = obj;
if (Array.isArray(obj)) {
ret = obj.map(item => deepClone(item));
} else if (isPlainObject(obj)) {
ret = { ...obj };
for (let key in obj) {
ret[key] = deepClone(obj[key]);
}
}
return ret;
}
/**
* Like Object.entries, but for deeply nested objects.
* For shallow objects the output is the same as Object.entries.
* @param {*} obj
* @param { object } options
* @param { EachCallback } each - If this returns false, the entry is not added to the result and the recursion is stopped.
* @param { EachCallback } filter - If this returns false, the entry is not added to the result.
* @param { EachCallback } descend - If this returns false, recursion is stopped.
* @returns {any[][]}
*/
export function deepEntries(obj, options = {}) {
let { each, filter, descend } = options;
let entries = [];
deepEach(obj, (value, key, parent, path) => {
let ret = each?.(value, key, parent, path);
if (ret !== false) {
let included = filter?.(value, key, parent, path) ?? true;
if (included) {
entries.push([...path, key, value]);
}
let descendRet = descend?.(value, key, parent, path);
if (descendRet === false) {
return false; // Stop recursion
}
}
return ret;
});
return entries;
}

View File

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

View File

@@ -1,42 +0,0 @@
/**
* Make the first letter of a string uppercase
* @param {*} str
* @returns
*/
export function capitalize(str) {
str += '';
return str[0].toUpperCase() + str.slice(1);
}
/**
* Convert a readable string to a slug.
* @param {*} str - Input string. If argument is not a string, it will be stringified.
* @returns {string} - The slugified string
*/
export function slugify(str) {
return (str + '')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '') // Convert accented letters to ASCII
.replace(/[^\w\s-]/g, '') // Remove remaining non-ASCII characters
.trim()
.replace(/\s+/g, '-') // Convert whitespace to hyphens
.toLowerCase();
}
/**
* Convert a string to camel case.
* @param {string} str - The string to convert.
* @returns {string} The camel case string.
*/
export function camelCase(str) {
return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
}
/**
* Convert a string to kebab case.
* @param {string} str - The string to convert.
* @returns {string} The kebab case string.
*/
export function kebabCase(str) {
return str.replace(/([A-Z])/g, '-$1').toLowerCase();
}

View File

@@ -0,0 +1,87 @@
.code-example {
border: var(--wa-border-style) var(--wa-panel-border-width) var(--wa-color-neutral-border-quiet);
border-radius: var(--wa-border-radius-l);
color: var(--wa-color-text-normal);
margin-block-end: var(--wa-flow-spacing);
}
.code-example-preview {
padding: 2rem;
border-bottom: var(--wa-border-style) var(--wa-panel-border-width) var(--wa-color-neutral-border-quiet);
> :first-child {
margin-block-start: 0;
}
> :last-child {
margin-block-end: 0;
}
}
.code-example-source {
border-bottom: var(--wa-border-style) var(--wa-panel-border-width) var(--wa-color-neutral-border-quiet);
}
.code-example:not(.open) .code-example-source {
display: none;
}
.code-example.open .code-example-toggle wa-icon {
rotate: 180deg;
}
.code-example-source pre {
position: relative;
border-radius: 0;
margin: 0;
white-space: normal;
}
.code-example-source:not(:has(+ .code-example-buttons)) {
border-bottom: none;
pre {
border-bottom-right-radius: var(--wa-border-radius-l);
border-bottom-left-radius: var(--wa-border-radius-l);
}
}
.code-example-buttons {
display: flex;
align-items: stretch;
button {
all: unset;
flex: 1 0 auto;
font-size: 0.875rem;
color: var(--wa-color-text-quiet);
border-left: var(--wa-border-style) var(--wa-panel-border-width) var(--wa-color-neutral-border-quiet);
text-align: center;
padding: 0.5rem;
cursor: pointer;
&:first-of-type {
border-left: none;
border-bottom-left-radius: var(--wa-border-radius-l);
}
&:last-of-type {
border-bottom-right-radius: var(--wa-border-radius-l);
}
&:focus-visible {
outline: var(--wa-focus-ring);
}
}
.code-example-pen {
flex: 0 0 100px;
white-space: nowrap;
}
wa-icon {
width: 1em;
height: 1em;
vertical-align: -2px;
}
}

View File

@@ -1,9 +1,9 @@
@import 'code-examples.css';
@import 'code-highlighter.css';
@import 'copy-code.css';
@import 'outline.css';
@import 'search.css';
@import 'cera_typeface.css';
@import 'theme-icons.css';
:root {
--wa-brand-orange: #f36944;
@@ -256,6 +256,15 @@ wa-page > main {
}
h1.title {
wa-icon-button {
font-size: var(--wa-font-size-l);
color: var(--wa-color-text-quiet);
&:not(:hover, :focus) {
opacity: 0.5;
}
}
wa-badge {
vertical-align: middle;
font-size: 1.5rem;
@@ -361,29 +370,29 @@ wa-page > main:has(> .index-grid) {
.index-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(auto-fit, minmax(min(22ch, 100%), 1fr));
gap: var(--wa-space-2xl);
margin-block-end: var(--wa-space-3xl);
@media screen and (max-width: 1470px) {
grid-template-columns: repeat(3, 1fr);
}
@media screen and (max-width: 960px) {
grid-template-columns: repeat(2, 1fr);
}
@media screen and (max-width: 500px) {
grid-template-columns: repeat(1, 1fr);
}
a {
border-radius: var(--wa-border-radius-l);
text-decoration: none;
}
wa-card {
box-shadow: none;
--spacing: var(--wa-space-m);
inline-size: 100%;
&:hover {
--border-color: var(--wa-color-brand-border-loud);
border-color: var(--border-color);
box-shadow: 0 0 0 var(--wa-border-width-s) var(--border-color);
.page-name {
color: var(--wa-color-brand-on-quiet);
}
}
[slot='header'] {
display: flex;
@@ -391,17 +400,18 @@ 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;
min-block-size: calc(6rem + var(--spacing));
}
}
}
wa-card .page-name {
font-size: var(--wa-font-size-s);
font-weight: var(--wa-font-weight-action);
.page-name {
font-size: var(--wa-font-size-s);
font-weight: var(--wa-font-weight-action);
}
}
.index-category {
@@ -410,146 +420,6 @@ wa-card .page-name {
margin-block-start: var(--wa-space-2xl);
}
/* Interactive cards */
wa-card[role='button'][tabindex='0'],
button,
a[href],
wa-option,
wa-radio,
wa-checkbox {
/* Disabled state */
&:is(:disabled, [disabled], [aria-disabled='true']) {
&:is(wa-card, :has(> wa-card)) {
opacity: 60%;
cursor: not-allowed;
}
}
&:where(:not(:disabled, [disabled], [aria-disabled='true'])) {
&:has(> wa-card) {
/* Parents only (not interactive <wa-card>) */
margin: calc(var(--wa-border-width-m) + 1px);
padding: 0;
/* Hover state */
&:hover,
&:state(hover),
&:state(current) {
/* Do not change the parent background as a hover effect (we style the card instead) */
background: transparent !important;
}
&::part(control),
&:is(wa-option)::part(checked-icon) {
--background-color-checked: var(--wa-color-brand-fill-loud);
--checked-icon-scale: 0.5;
--offset: var(--wa-space-2xs);
position: absolute;
inset: calc(var(--offset) + var(--wa-border-width-m));
inset-block-end: auto;
inset-inline-start: auto;
z-index: 1;
margin: 0;
background: var(--wa-color-brand-fill-loud);
color: var(--wa-color-brand-on-loud);
}
&::part(checked-icon) {
color: var(--wa-color-brand-on-loud);
}
&:is(wa-option)::part(checked-icon) {
inset-block-start: calc(var(--wa-space-smaller) - 0.5em);
inset-inline-end: calc(var(--wa-space-smaller) - 0.5em);
width: 1em;
height: 1em;
line-height: 1em;
padding: 0.4em;
border-radius: var(--wa-border-radius-circle);
text-align: center;
font-size: var(--wa-font-size-xs);
}
}
/* Hover state */
&:hover,
&:state(hover),
&:state(current) {
&:is(wa-card),
> wa-card {
--border-color: var(--wa-color-brand-border-loud);
border-color: var(--border-color);
box-shadow: 0 0 0 var(--wa-border-width-s) var(--border-color);
}
}
&:is(wa-card, :has(> wa-card)) {
/* Interactive card parent */
position: relative;
cursor: pointer;
/* Unselected state */
&:where(:not(:state(checked), :state(selected), [aria-checked='true'], [aria-selected='true'])) {
&::part(checked-icon),
&::part(control) {
display: none;
}
}
}
&:is(wa-card),
> wa-card {
/* The card itself */
box-shadow: none;
}
}
}
/* Selected cards */
:state(selected),
:state(checked),
[aria-checked='true'],
[aria-selected='true'] {
&:is(wa-card, :has(> wa-card)) {
background: transparent;
}
&:is(wa-card),
> wa-card {
--border-color: var(--wa-color-brand-border-loud);
box-shadow: 0 0 0 var(--wa-border-width-m) var(--border-color);
&::part(body) {
background: var(--wa-color-brand-fill-quiet);
}
}
}
wa-select:has(> wa-option > wa-card) {
&::part(listbox) {
--column-width: 1fr;
--columns: 1;
--gap: var(--wa-space-smaller);
display: grid;
grid-template-columns: repeat(auto-fill, minmax(var(--column-width), 1fr));
width: calc(var(--columns) * var(--column-width) + (var(--columns) - 1) * var(--gap) + 2 * var(--wa-space));
max-width: var(--auto-size-available-width, 90vw);
gap: var(--gap);
padding: var(--wa-space-smaller) var(--wa-space);
}
> wa-option > wa-card {
--spacing: var(--wa-space-s);
}
}
wa-radio:has(> wa-card) {
grid-template-columns: 1fr;
width: auto;
}
/* Swatches */
.swatch {
position: relative;
@@ -726,6 +596,13 @@ table.colors {
margin-block-end: var(--wa-flow-spacing);
}
/** mobile */
@media screen and (max-width: 768px) {
wa-page .only-desktop {
display: none;
}
}
/** desktop */
@media screen and not (max-width: 768px) {
/* Navigation sidebar */

View File

@@ -7,9 +7,8 @@
margin: 0 auto;
overflow: hidden;
&::part(dialog) {
margin-block-start: 10vh;
margin-block-end: 0;
&::part(base) {
margin-block: 10rem;
}
&::part(body) {
@@ -24,26 +23,26 @@
@media screen and (max-width: 900px) {
max-width: calc(100% - 2rem);
&::part(dialog) {
&::part(base) {
margin-block: 1rem;
}
#site-search-container {
max-height: none;
}
}
}
#site-search-container {
display: flex;
flex-direction: column;
max-height: calc(100vh - 18rem);
max-height: calc(100vh - 20rem);
@media screen and (max-width: 900px) {
max-height: calc(100dvh - 2rem);
}
}
/* Header */
#site-search-container header {
header {
flex: 0 0 auto;
align-items: center;
align-items: middle;
/* Fixes an iOS Safari 16.4 bug that draws the parent element's border radius incorrectly when showing/hiding results */
border-radius: var(--wa-border-radius-l);
}

View File

@@ -1,86 +1,9 @@
wa-card:has(
> .theme-icon-host,
> [slot='header'] > .theme-icon-host,
> .fonts-icon-host,
> [slot='header'] > .fonts-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,
.fonts-icon-host,
.palette-icon-host {
flex: 1;
border-radius: inherit;
&[slot='header'],
[slot='header']:has(&) {
flex: 1;
border-radius: inherit;
}
}
.theme-icon:not(.theme-color-icon),
.palette-icon,
.icons-icon {
min-height: 5.5rem;
}
.palette-icon {
display: grid;
grid-template-columns: repeat(var(--hues, 9), 1fr);
gap: var(--wa-space-3xs);
min-width: 20ch;
align-content: center;
.swatch {
height: 0.7em;
background: var(--color);
border-radius: var(--wa-border-radius-s);
&[data-suffix=''] {
height: 1.1em;
}
}
}
.theme-icon,
.fonts-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: flex;
display: grid;
gap: var(--wa-space-xs);
min-width: 15ch;
background: var(--wa-color-surface-lowered);
& + & {
border-start-start-radius: 0;
border-start-end-radius: 0;
}
grid-template-columns: repeat(4, auto);
div {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
@@ -91,127 +14,21 @@ wa-card:has(
padding: var(--wa-space-2xs) var(--wa-space-xs);
color: var(--text-color);
font-weight: var(--wa-font-weight-semibold);
&.plain {
font-weight: var(--wa-font-weight-bold);
}
}
}
.theme-icon.theme-overall-icon,
.fonts-icon {
.theme-typography-icon {
display: flex;
flex-flow: column;
gap: var(--wa-space-2xs);
justify-content: center;
width: 100%;
min-height: 6.75rem;
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;
}
}
}
}
.theme-icon.theme-dimensionality-icon {
display: flex;
flex-flow: column;
gap: var(--wa-space-2xs);
justify-content: center;
width: 100%;
min-height: 6.75rem;
box-sizing: border-box;
wa-card {
display: block;
&::part(body) {
display: flex;
gap: var(--wa-space-xs);
}
wa-input {
flex: 4;
min-width: 1em;
}
wa-button {
flex: 1;
}
}
}
.fonts-icon {
font-family: var(--wa-font-family-body);
padding-block: var(--wa-space-s);
overflow: hidden;
position: relative;
& h2,
& p {
white-space: nowrap;
}
&::after {
content: '';
position: absolute;
right: 0;
width: 50%;
height: 100%;
background-image: linear-gradient(to left, var(--wa-color-surface-lowered), 20%, transparent);
}
}
.icons-icon {
display: grid;
grid-template-columns: repeat(var(--columns, 5), auto);
flex-direction: column;
gap: var(--wa-space-xs);
place-items: center;
place-content: center;
& wa-icon {
font-size: 1.25em;
h3,
p {
margin-block: 0;
padding: 0;
}
}
.page-card {
wa-badge {
margin-inline: var(--wa-space-3xs);
}
}
:is(.theme-card, .icons-card)::part(header) {
background: var(--wa-color-surface-lowered);
}
.icons-card::part(header) {
color: var(--wa-color-neutral-on-quiet);
}

View File

@@ -1,145 +0,0 @@
/* App UI, for themer, palette tweaking etc */
:root {
--fa-sliders-simple: '\f1de';
}
.title {
wa-icon-button {
font-size: var(--wa-font-size-l);
color: var(--wa-color-text-quiet);
&:not(:hover, :focus) {
opacity: 0.5;
}
}
}
.popup {
background: var(--wa-color-surface-default);
border: 1px solid var(--wa-color-surface-border);
padding: var(--wa-space-xl);
border-radius: var(--wa-border-radius-m);
max-height: 90dvh;
overflow: auto;
code {
white-space: nowrap;
}
}
.color-select {
&.default::part(display-input) {
opacity: 0.6;
font-style: italic;
}
> small {
margin-inline-start: var(--wa-space-xs);
padding-block: 0 var(--wa-space-xs);
}
&::part(combobox)::before,
wa-option::before {
content: '';
display: inline-block;
width: 1.2em;
aspect-ratio: 1;
margin-inline-end: var(--wa-space-xs);
flex: none;
border-radius: var(--wa-border-radius-m);
background: var(--color);
border: 1px solid var(--wa-color-surface-default);
}
wa-option {
white-space: nowrap;
&::before {
width: 1em;
margin-inline: var(--wa-space-xs);
}
&::part(checked-icon) {
order: 2;
}
}
.default-badge {
opacity: 0.6;
margin-inline-start: var(--wa-space-xs);
}
}
.swatch-select {
padding: 2px;
wa-radio-button {
--swatch-border-color: color-mix(in oklab, canvastext, transparent 80%);
&::part(base) {
/* a <button> */
width: 2em;
height: 2em;
padding: 0;
border-radius: var(--border-radius, var(--wa-border-radius-m));
background: var(--color);
background-clip: border-box;
border-color: var(--swatch-border-color);
}
}
&.swatch-shape-circle {
--border-radius: var(--wa-border-radius-circle);
}
wa-radio-button:is([checked], :state(checked)) {
--swatch-border-color: var(--wa-color-surface-default);
&::part(base) {
box-shadow:
inset 0 0 0 var(--indicator-width) var(--wa-color-surface-default),
0 0 0 calc(var(--indicator-width) + 1px) var(--indicator-color);
}
}
&::part(form-control-input) {
flex-wrap: wrap;
gap: var(--wa-space-xs);
}
}
/* Repeated to increase specificity */
.editable-text.editable-text {
display: inline-flex;
align-items: center;
gap: var(--wa-space-xs);
--edit-hint-color: oklab(from var(--wa-color-warning-fill-quiet) l a b / 50%);
> .text {
&:hover,
&:focus {
background-color: var(--edit-hint-color);
box-shadow: 0 0 0 var(--wa-space-2xs) var(--edit-hint-color);
color: inherit;
border-radius: calc(var(--wa-border-radius-m) - var(--wa-space-2xs));
}
}
> input {
font: inherit;
margin-block: calc(-1 * var(--wa-space-smaller));
field-sizing: content;
}
wa-icon-button {
font-size: 90%;
}
}
.info-tip-default-trigger {
color: var(--wa-color-text-quiet);
&:not(:hover, :focus) {
opacity: 65%;
}
}

View File

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

View File

@@ -1,86 +0,0 @@
import inputMixin from '../mixins/input.js';
const template = `
<span class="editable-text">
<template v-if="isEditing">
<input ref="input" class="wa-size-s" :aria-label="label" :value="value" @input="handleInput" @keydown.enter="done" @keydown.esc="cancel" @blur="handleBlur" />
<wa-icon-button v-if="blur !== 'done'" name="check" label="Done editing" @click="done"></wa-icon-button>
</template>
<template v-else>
<span class="text" ref="wrapper" @focus="edit" @click="edit" tabindex="0">{{ value }}</span>
<wa-icon-button name="pencil" :label="'Edit ' + label" @click="edit"></wa-icon-button>
</template>
</span>
`;
export default {
mixins: [inputMixin],
props: {
label: {
type: String,
default: 'Rename',
},
blur: {
type: String,
validator(value) {
return ['', 'done', 'cancel'].includes(value);
},
},
},
emits: ['update:modelValue', 'submit'],
data() {
return {
previousValue: undefined,
isEditing: false,
};
},
computed: {},
methods: {
edit(event) {
if (this.isEditing) {
return;
}
event.stopPropagation();
this.isEditing = true;
this.previousValue = this.value;
this.$nextTick(() => {
this.$refs.input.focus();
this.$refs.input.select();
});
},
done(event) {
if (!this.isEditing) {
return;
}
event.stopPropagation();
this.isEditing = false;
if (!this.previousValue || this.previousValue !== this.value) {
this.$emit('submit', this.value);
}
},
cancel(event) {
if (!this.isEditing) {
return;
}
event.stopPropagation();
this.isEditing = false;
this.value = this.previousValue;
},
handleBlur(event) {
this.done(event);
},
},
template,
compilerOptions: {
isCustomElement: tag => tag.startsWith('wa-'),
},
};

View File

@@ -1,132 +0,0 @@
import themes from '../../data/themes.js';
import PageCard from './page-card.js';
import { defaultTitle, pairings, sameAs } from '/assets/data/fonts.js';
import { themeConfig } from '/assets/data/theming.js';
import { cssImport, getThemeCode } from '/assets/scripts/tweak/code.js';
const template = `
<page-card class="fonts-card" :info="computedPairing">
<template #icon>
<wa-scoped slot="header" class="fonts-icon-host" inert :key="html">
<template v-html="html"></template>
<template>
<link rel="stylesheet" href="/dist/styles/native/content.css">
<link rel="stylesheet" href="/assets/styles/theme-icons.css">
<div class="fonts-icon" role="presentation">
<h2>When my six o'clock alarm buzzes, I require a pot of good java.</h2>
<p>By quarter past seven, I've jotted hazy musings in a flax-bound notebook, sipping lukewarm espresso.</p>
</div>
</template>
</wa-scoped>
</template>
<slot></slot>
<template #extra>
<slot name="extra" />
</template>
</page-card>
`;
export default {
props: {
theme: String,
src: String,
fonts: Object,
pairing: Object,
},
data() {
return {};
},
computed: {
content() {
let pairingTitle = this.computedPairing.title;
// let themeTitle = this.themeId ? `As seen in ${this.themeMeta.title}` : '';
if (this.title) {
return { title: this.title, subtitle: this.subtitle ?? pairingTitle };
} else {
return { title: pairingTitle, subtitle: this.subtitle };
}
},
url() {
let ret = this.src ?? this.pairing?.url;
if (!ret && this.theme) {
return themeConfig.typography.url(this.theme);
}
return ret;
},
themeId() {
return this.theme ?? this.pairing?.id;
},
themeMeta() {
return themes[this.themeId] ?? {};
},
computedFonts() {
let ret = this.fonts ?? this.pairing?.fonts ?? this.themeMeta?.fonts;
let defaults = themes.default.fonts;
return Object.assign({}, defaults, { ...ret });
},
computedPairing() {
let ret;
if (this.pairing) {
ret = { ...this.pairing };
} else {
// Get from theme
let fonts = this.computedFonts;
let { body, heading = sameAs.body } = fonts;
let pairing = pairings[body]?.[heading];
ret = Object.assign({ fonts }, pairing);
}
ret.url = this.url;
ret.title ??= defaultTitle(fonts);
return ret;
},
computed() {
let ret = { fonts: this.computedFonts };
for (let key in ret.fonts) {
if (ret.fonts[key] === sameAs.body) {
ret.fonts[key] = ret.fonts.body;
}
}
ret.pairing = this.computedPairing;
ret.theme = this.themeId;
ret.url = this.url;
return ret;
},
html() {
let { id, url } = this.computedPairing;
if (id) {
let theme = { typography: id };
return getThemeCode(theme, { id, language: 'html' });
} else {
return cssImport(url, { language: 'html' });
}
},
},
template,
components: {
PageCard,
},
compilerOptions: {
isCustomElement: tag => tag.startsWith('wa-'),
},
};

View File

@@ -1,175 +0,0 @@
import { sample } from '../../scripts/util/array.js';
import { capitalize } from '../../scripts/util/string.js';
import PageCard from './page-card.js';
import { iconLibraries } from '/assets/data/icons.js';
const iconNames = [
'user',
'paper-plane',
'face-laugh',
'pen-to-square',
'trash',
'cart-shopping',
'link',
'sun',
'bookmark',
'sparkles',
'thumbs-up',
'gear',
];
const brands = new Set(['web-awesome', 'font-awesome']);
const ICON_GRID = { columns: 6, rows: 2 };
const TOTAL_ICONS = ICON_GRID.columns * ICON_GRID.rows;
const template = `
<page-card class="icons-card" :class="'icons-' + type + '-card'" :pro="$slots.default ? false : iconsMeta.isPro" :info="iconsMeta">
<template #icon>
<div slot="header" class="icons-icon" :class="'icons-' + type + '-icon'" :style="{ '--columns': ICON_GRID.columns }">
<template v-for="icon of icons">
<wa-icon v-bind="icon"></wa-icon>
</template>
</div>
</template>
<slot></slot>
</page-card>
`;
const defaultDefaults = {
library: 'default',
family: 'classic',
style: 'solid',
};
export default {
props: {
library: String,
family: String,
style: String,
defaults: Object,
type: {
type: String,
validate(value) {
return ['library', 'family', 'style'].includes(value);
},
},
vary: {
type: [Array, String],
validate(value) {
if (Array.isArray(value)) {
return value.every(v => ['family', 'style'].includes(v));
}
return ['family', 'style'].includes(value);
},
default() {
return [];
},
},
},
data() {
return {};
},
created() {
Object.assign(this, { iconNames, brands, ICON_GRID });
},
computed: {
computedLibrary() {
return this.library ?? 'default';
},
libraryMeta() {
return iconLibraries[this.computedLibrary] ?? {};
},
defaultTitle() {
let titles = {};
for (let key in this.computed) {
let value = this.computed[key];
if (key === 'library') {
titles[key] = iconLibraries[value].title;
}
titles[key] ??= capitalize(value);
}
if (this.type) {
return titles[this.type];
} else {
return titles.library + ' ' + titles.family + ' • ' + titles.style;
}
},
icons() {
let { family, style } = this.computed;
let library = this.libraryMeta;
let vary = Array.isArray(this.vary) ? this.vary : [this.vary];
let ret = [];
if (vary.length > 0) {
for (let param of vary) {
let allValues = library[param];
let random = (allValues.random ??= []);
while (random.length < TOTAL_ICONS) {
random.push(sample(allValues));
}
}
}
while (ret.length < TOTAL_ICONS) {
ret.push(
...iconNames.map((name, i) => {
let index = ret.length + i;
return {
library: this.computedLibrary,
name,
family: !this.family && vary.includes('family') ? library.family.random[index] : family,
variant: !this.style && vary.includes('style') ? library.style.random[index] : style,
};
}),
);
}
return ret.slice(0, TOTAL_ICONS);
},
computedDefaults() {
return Object.assign({}, defaultDefaults, this.defaults);
},
computed() {
let { library, family, style } = this;
let ret = { library, family, style };
for (let key in this.computedDefaults) {
if (!ret[key]) {
ret[key] = this.computedDefaults[key];
}
}
return ret;
},
iconsMeta() {
return { title: this.defaultTitle };
},
},
methods: {
capitalize,
},
template,
components: {
PageCard,
},
compilerOptions: {
isCustomElement: tag => tag.startsWith('wa-'),
},
};

View File

@@ -1,13 +0,0 @@
export { default as ColorSelect } from './color-select.js';
export { default as EditableText } from './editable-text.js';
export { default as FontsCard } from './fonts-card.js';
export { default as IconsCard } from './icons-card.js';
export { default as InfoTip } from './info-tip.js';
export { default as PageCard } from './page-card.js';
export { default as PaletteCard } from './palette-card.js';
export { default as SwatchSelect } from './swatch-select.js';
export { default as ThemeCard } from './theme-card.js';
export { default as UiPanelContainer } from './ui-panel-container.js';
export { default as UiPanel } from './ui-panel.js';
export { default as UiScrollable } from './ui-scrollable.js';
export { default as UiSlider } from './ui-slider.js';

View File

@@ -1,39 +0,0 @@
const template = `
<slot>
<wa-icon :slot class="info-tip-default-trigger" :id="id" name="circle-question" variant="regular" tabindex="0"></wa-icon>
</slot>
<wa-tooltip :slot :for="id" ref="tooltip"><slot name="content">{{ text }}</slot></wa-tooltip>
`;
let maxUid = 0;
export default {
props: {
slot: String,
text: String,
},
data() {
let uid = ++maxUid;
return { uid, id: 'info-tip-' + uid };
},
mounted() {
let tooltip = this.$refs.tooltip;
if (tooltip) {
// Find trigger
let trigger = tooltip.previousElementSibling;
if (trigger) {
if (trigger.id) {
// Already has id
this.id = trigger.id;
} else {
trigger.id = this.id;
}
}
}
},
computed: {},
template,
compilerOptions: {
isCustomElement: tag => tag.startsWith('wa-'),
},
};

View File

@@ -1,83 +0,0 @@
/**
* Generic component for displaying a (possibly interactive) card that represents a page
* For more specific use cases check out theme-card, icons-card, etc.
*/
export const ICON_PLACEHOLDER = `
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 7C1 3.68629 3.68629 1 7 1H43C46.3137 1 49 3.68629 49 7V43C49 46.3137 46.3137 49 43 49H7C3.68629 49 1 46.3137 1 43V7Z" stroke="var(--wa-color-surface-border)" stroke-width="2" stroke-linecap="round" stroke-dasharray="6 6"/>
<path d="M14.1566 18.7199L21.5367 16.7424C22.6036 16.4565 23.7003 17.0896 23.9862 18.1566L26.8463 28.8306C27.1322 29.8975 26.499 30.9942 25.4321 31.2801L18.052 33.2576C16.985 33.5435 15.8884 32.9103 15.6025 31.8434L12.7424 21.1694C12.4565 20.1024 13.0897 19.0057 14.1566 18.7199Z" stroke="var(--wa-color-neutral-border-normal)" stroke-width="2"/>
<path d="M33.8449 16.3273H26.2045C23.9953 16.3273 22.2045 18.1181 22.2045 20.3273V31.3778C22.2045 33.587 23.9953 35.3778 26.2045 35.3778H33.8449C36.0541 35.3778 37.8449 33.587 37.8449 31.3778V20.3273C37.8449 18.1181 36.0541 16.3273 33.8449 16.3273Z" fill="var(--wa-color-neutral-border-normal)" stroke="var(--wa-color-neutral-fill-quiet)" stroke-width="2"/>
</svg>`;
const template = `
<wa-card with-header class="page-card" :aria-disabled="disabled ? 'true' : null" :inert="disabled"
@click="handleClick" @keyup.enter="handleClick" @keyup.space="handleClick"
:role="action ? 'button' : null" :tabindex="action? 0 : null">
<slot name="icon" slot="header">
<div slot="header" v-html="icon || ICON_PLACEHOLDER"></div>
</slot>
<div class="page-name">
<div>
<slot>
{{ content.title }}
<wa-badge class="pro" v-if="pro">PRO</wa-badge>
<div v-if="content.subtitle" class="wa-caption-m">{{ content.subtitle }}</div>
</slot>
</div>
<slot name="extra"></slot>
<wa-icon v-if="action" name="angle-right" class="angle-right" variant="regular"></wa-icon>
</div>
</wa-card>
`;
export default {
props: {
title: String,
subtitle: String,
info: Object,
icon: String,
pro: Boolean,
disabled: Boolean,
action: Function,
},
data() {
return {};
},
created() {
Object.assign(this, { ICON_PLACEHOLDER });
},
computed: {
content() {
let defaultTitle = this.info?.title ?? {};
if (this.title) {
return { title: this.title, subtitle: this.subtitle ?? defaultTitle };
} else {
return { title: defaultTitle, subtitle: this.subtitle };
}
},
},
methods: {
handleClick(event) {
if (this.disabled) {
event.stopImmediatePropagation();
return;
}
if (this.action) {
this.action(event);
}
},
},
template,
components: {},
compilerOptions: {
isCustomElement: tag => tag.startsWith('wa-'),
},
};

View File

@@ -1,63 +0,0 @@
import palettes from '../../data/palettes.js';
import PageCard from './page-card.js';
import { hues } from '/assets/data/index.js';
// TODO import from data.js once available
const allHues = [...hues, 'gray'];
const template = `
<page-card class="palette-card" :pro="$slots.default ? false : paletteMeta.isPro" :info="paletteMeta">
<template #icon>
<wa-scoped slot="header" class="palette-icon-host">
<template>
<link rel="stylesheet" :href="'/dist/styles/color/' + palette + '.css'">
<link rel="stylesheet" href="/assets/styles/theme-icons.css">
<div class="palette-icon" style="--hues: {{ hues|length }}; --suffixes: {{ suffixes|length }}">
<template v-for="(hue, hueIndex) of hues">
<div class="swatch" v-for="(suffix, suffixIndex) of suffixes"
:data-hue="hue" :data-suffix="suffix"
:style="{
'--color': 'var(--wa-color-' + hue + suffix + ')',
gridColumn: hueIndex + 1,
gridRow: suffixIndex + 1
}">&nbsp;</div>
</template>
</div>
</template>
</wa-scoped>
</template>
<slot></slot>
<template #extra>
<slot name="extra" />
</template>
</page-card>
`;
export default {
props: {
palette: String,
},
data() {
return {};
},
created() {
Object.assign(this, { hues: allHues, suffixes: ['-80', '', '-20'] });
},
computed: {
paletteMeta() {
return palettes[this.palette] ?? {};
},
},
template,
components: {
PageCard,
},
compilerOptions: {
isCustomElement: tag => tag.startsWith('wa-'),
},
};

View File

@@ -1,167 +0,0 @@
.sidebar.panel-container {
position: relative;
display: flex;
flex-flow: column;
gap: 0;
padding: 0;
width: 32ch;
overflow: hidden;
scrollbar-width: thin;
}
.panel {
/* Remove the uniform spacing used in wa-details */
--spacing: 0;
/* Specify value to manually set spacing where needed */
--panel-spacing: var(--wa-space-2xl);
--panel-background: var(--wa-color-surface-default);
display: flex;
flex-flow: column;
max-height: 100%;
margin-bottom: 0;
position: relative;
background: var(--panel-background);
border: none;
transition:
translate var(--wa-transition-slow) allow-discrete,
opacity var(--wa-transition-slow) 50ms allow-discrete;
/* Ensure horizontal scrollbar isn't visible when translate takes effect */
overflow-x: hidden !important;
@starting-style {
display: block;
}
.panel-header {
flex-direction: row-reverse;
justify-content: start;
gap: var(--wa-space-xs);
cursor: pointer;
background: var(--panel-background);
color: var(--wa-color-text-normal);
padding-block-end: var(--panel-spacing);
padding-inline: var(--panel-spacing);
transition: inherit;
transition-property: all;
margin-block: 0;
font-size: inherit;
[data-step='0'] &,
.previous & {
padding-block-start: var(--panel-spacing);
}
.back-icon {
vertical-align: -0.15em;
margin-inline-end: var(--wa-space-xs);
font-size: var(--wa-font-size-m);
transition: transform var(--wa-transition-normal) var(--wa-transition-easing);
}
&:hover .back-icon {
transform: translateX(-0.25em);
}
label {
pointer-events: none;
font: inherit;
color: inherit;
}
}
.panel-content {
flex: 1;
min-height: 0;
display: flex;
flex-flow: column;
gap: var(--panel-spacing);
padding-block-end: var(--panel-spacing);
padding-inline: var(--panel-spacing);
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin-block-end: 0;
}
&:not(.open) {
padding: 0;
&:not(.previous, .next) {
/* Hide all but the immediately preceding or following steps */
display: none;
}
&.next {
height: 0;
overflow: hidden;
}
&.next {
opacity: 0;
}
&.next {
translate: 100% 0%;
}
.panel-header {
font-size: var(--wa-font-size-s);
margin: 0;
}
.panel-content {
opacity: 0;
pointer-events: none;
content-visibility: hidden;
padding: 0;
}
}
&.open {
flex: 1;
opacity: 1;
.panel-header {
font-size: var(--wa-font-size-l);
.back-icon {
display: none;
}
}
}
.panel-content {
flex: 1;
min-height: 0;
display: flex;
flex-flow: column;
transition: inherit;
@starting-style {
display: flex;
content-visibility: visible;
}
}
&:not(.open) {
&.previous {
.panel-content {
opacity: 0;
translate: -100% 0%;
}
}
&.next {
.panel-content {
opacity: 0;
translate: inherit;
}
}
}
}

View File

@@ -1,89 +0,0 @@
/**
* Scrollable element in a vertical flex container
* Showing shadows as an indicator of scrollability (PE wherever scroll-timeline is supported for now, can be polyfilled with JS later)
*/
.scrollable {
--scroll-shadow-height: 0.5em;
flex-shrink: 1;
min-height: 0;
overflow: auto;
position: relative;
scrollbar-width: inherit;
&:is(.panel-content > div) {
display: flex;
flex-flow: column;
gap: inherit;
}
.scroll-shadow {
position: sticky;
z-index: 1;
inset-inline: 0;
display: block;
&::before {
content: '';
position: absolute;
inset-inline: 0;
height: var(--scroll-shadow-height);
pointer-events: none;
mix-blend-mode: multiply;
background: radial-gradient(farthest-side, var(--wa-color-shadow) 10%, transparent) center / 120% 200%;
transition: var(--wa-transition-slow);
/* transition-property: opacity, transform, display, height, min-height; */
transition-behavior: allow-discrete;
}
}
&:not(.can-scroll-top) .scroll-shadow-top,
&:not(.can-scroll-bottom) .scroll-shadow-bottom {
opacity: 0;
&::before {
height: 0;
}
}
&:not(.can-scroll-top) .scroll-shadow-top {
&::before {
transform: translateY(-100%);
}
}
.scroll-shadow-top {
top: 0;
&::before {
background-position: bottom;
}
}
&:not(.can-scroll-bottom) .scroll-shadow-bottom {
&::before {
transform: translateY(100%);
}
}
.scroll-shadow-bottom {
top: 100%;
&::before {
bottom: 0;
background-position: top;
}
}
}
.scrollable:where(.panel-content) {
.scroll-shadow-top {
/* TODO convert this magic number to a token that explains what it is */
margin-bottom: -18px;
}
.scroll-shadow-bottom {
transform: translateY(var(--padding-bottom, var(--panel-spacing)));
}
}

View File

@@ -1,54 +0,0 @@
import { capitalize } from '../../scripts/util/string.js';
import inputMixin from '../mixins/input.js';
import InfoTip from './info-tip.js';
const template = `
<wa-radio-group :label class="swatch-select" :class="'swatch-shape-' + shape" orientation="horizontal" :value @input="handleInput">
<info-tip v-for="value in values">
<wa-radio-button :value :label="getLabel(value)" :style="{'--color': getColor(value)}"></wa-radio-button>
<template #content>
{{ getLabel(value) }}
</template>
</info-tip>
</wa-radio-group>
`;
export default {
mixins: [inputMixin],
props: {
modelValue: String,
name: String,
label: String,
shape: {
type: String,
default: 'rounded',
validator: value => ['circle', 'rounded'].includes(value),
},
getLabel: {
type: Function,
default: capitalize,
},
getColor: {
type: Function,
default: value => `var(--wa-color-${value})`,
},
values: {
type: Array,
default: [],
},
},
computed: {},
methods: {
capitalize,
},
template,
components: {
InfoTip,
},
compilerOptions: {
isCustomElement: tag => tag.startsWith('wa-'),
},
};

View File

@@ -1,119 +0,0 @@
import themes from '../../data/themes.js';
import { capitalize } from '../../scripts/util/string.js';
import PageCard from './page-card.js';
import { getThemeCode } from '/assets/scripts/tweak/code.js';
const iconTemplates = {
colors: `
<div class="theme-icon theme-color-icon" role="presentation">
<div style="background: var(--wa-color-brand-fill-loud); border-color: var(--wa-color-brand-border-loud); color: var(--wa-color-brand-on-loud);">A</div>
<div style="background: var(--wa-color-brand-fill-normal); border-color: var(--wa-color-brand-border-normal); color: var(--wa-color-brand-on-normal);">A</div>
<div style="background: var(--wa-color-brand-fill-quiet); border-color: var(--wa-color-brand-border-quiet); color: var(--wa-color-brand-on-quiet);">A</div>
</div>
<div class="wa-invert theme-icon theme-color-icon" role="presentation">
<div style="background: var(--wa-color-brand-fill-loud); border-color: var(--wa-color-brand-border-loud); color: var(--wa-color-brand-on-loud);">A</div>
<div style="background: var(--wa-color-brand-fill-normal); border-color: var(--wa-color-brand-border-normal); color: var(--wa-color-brand-on-normal);">A</div>
<div style="background: var(--wa-color-brand-fill-quiet); border-color: var(--wa-color-brand-border-quiet); color: var(--wa-color-brand-on-quiet);">A</div>
</div>`,
dimensionality: `
<wa-card size="small">
<wa-input value="Input" size="small"></wa-input>
<wa-button size="small" variant="brand">Go</wa-button>
</wa-card>
`,
overall: `
<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>
</div>
<div class="row row-2">
<wa-input value="Input" size="small"></wa-input>
<wa-button size="small" variant="brand">Go</wa-button>
</div>`,
};
const template = `
<page-card class="theme-card" :class="type + '-card'" :info="themeMeta" :data-theme="theme">
<template #icon>
<wa-scoped slot="header" class="theme-icon-host" inert :key="themeCode">
<template v-html="themeCode"></template>
<template>
<link rel="stylesheet" href="/dist/styles/utilities.css">
<link rel="stylesheet" href="/dist/styles/native/content.css">
<link rel="stylesheet" href="/assets/styles/theme-icons.css">
<template v-if="type === 'colors'">
${iconTemplates.colors}
</template>
<div v-else-if="type in iconTemplates && type !== 'overall'" class="theme-icon" :class="'theme-' + type + '-icon'" v-html="iconTemplates[type]" role="presentation">
</div>
<div v-else class="theme-icon theme-overall-icon" :class="'wa-theme-' + theme" role="presentation">
${iconTemplates.overall}
</div>
</template>
</wa-scoped>
</template>
<slot></slot>
<template #extra>
<slot name="extra" />
</template>
</page-card>
`;
export default {
props: {
theme: String,
type: {
type: String,
validator(value) {
return !value || value in iconTemplates;
},
},
rest: Object,
},
data() {
return {};
},
created() {
this.iconTemplates = iconTemplates;
},
computed: {
themeMeta() {
let ret = themes[this.theme] ? { ...themes[this.theme] } : {};
// if (this.type === 'dimensionality' && typeof ret.dimension === 'string') {
// ret.title = capitalize(ret.dimension);
// }
return ret;
},
themeCode() {
let theme = { ...(this.rest || {}), [this.type || 'base']: this.theme };
theme.base ||= 'default';
// if (theme.dimensionality) {
// if (!themes[theme.dimensionality]?.dimension || theme.dimensionality === theme.base) {
// theme.dimensionality = '';
// }
// }
return getThemeCode(theme, { id: this.theme, language: 'html', cdnUrl: '/dist/' });
},
},
template,
components: {
PageCard,
},
compilerOptions: {
isCustomElement: tag => tag.startsWith('wa-'),
},
};

View File

@@ -1,120 +0,0 @@
const template = `
<section class="panel-container" ref="container" :style="{'--panel-step': step}" @open="handleOpen">
<slot ref="panels"></slot>
</section>
`;
export default {
props: {
/** Currently selected id */
modelValue: String,
},
emits: ['update:modelValue'],
data() {
return {
value: '',
previousValue: '',
step: 0,
trail: [],
};
},
mounted() {
let { container } = this.$refs;
let activePanel = container.querySelector(':scope > .open');
if (activePanel) {
let { step, value } = activePanel.dataset;
this.step = Number(step);
this.value = value;
this.$emit('update:modelValue', this.value);
}
},
computed: {
panels() {
if (!this.$refs.container) {
return new Map();
}
let { container } = this.$refs;
return new Map(
[...container.querySelectorAll(':scope > .panel')].map(panel => [
panel.dataset.value,
Number(panel.dataset.step),
]),
);
},
},
methods: {
handleOpen(e) {
let { value, step } = e.detail;
this.value = value;
this.step = step;
},
updatePanels() {
let { container } = this.$refs;
if (!container) {
return;
}
let { step, value } = this;
if (this.panels.get(value) !== step) {
// Hasn't stabilized yet
return;
}
let previousValue = this.trail.findLast(panel => this.panels.get(panel) === step - 1);
for (let panel of container.querySelectorAll(':scope > .panel')) {
let panelStep = Number(panel.dataset.step);
let panelValue = panel.dataset.value;
let isPrevious = previousValue ? panelValue === previousValue : panelStep === step - 1;
let isOpen = panelValue === value;
let isNext = panelStep === step + 1;
panel.classList.toggle('previous', isPrevious);
panel.classList.toggle('open', isOpen);
panel.classList.toggle('next', isNext);
}
},
},
watch: {
value() {
if (this.value !== this.modelValue) {
this.$emit('update:modelValue', this.value);
}
},
modelValue: {
immediate: true,
async handler(value, previousValue) {
if (this.value !== this.modelValue) {
this.value = this.modelValue;
}
if (previousValue) {
this.trail.push(previousValue);
}
this.updatePanels();
},
},
step() {
this.updatePanels();
},
},
template,
components: {},
compilerOptions: {
isCustomElement: tag => tag.startsWith('wa-'),
},
};

View File

@@ -1,73 +0,0 @@
import UiScrollable from './ui-scrollable.js';
const template = `
<ui-scrollable :disabled="!open" role="group" :name="name || 'panel'" :data-value="value" :data-step="step" class="panel" :class="{open}">
<h2 :inert="open" class="panel-header" @click="openPanel" ref="panelHeader">
<wa-icon name="chevron-left" class="back-icon" />
<slot name="title">{{ title }}</slot>
</h2>
<div class="panel-content">
<slot></slot>
</div>
</ui-scrollable>
`;
export default {
props: {
title: String,
name: String,
step: Number,
/** Id of this panel */
value: String,
/** Currently selected id */
modelValue: String,
},
emits: ['update:modelValue', 'open'],
data() {
return {};
},
mounted() {
if (this.open) {
this.$refs.panelHeader.dispatchEvent(
new CustomEvent('open', { detail: { value: this.value, step: this.step }, bubbles: true }),
);
}
},
computed: {
open() {
return this.value === this.modelValue;
},
},
methods: {
openPanel() {
let wasOpen = this.open;
this.$emit('update:modelValue', wasOpen ? '' : this.value);
},
},
watch: {
open: {
immediate: true,
handler(open) {
if (open && this.$refs.panelHeader) {
this.$refs.panelHeader.dispatchEvent(
new CustomEvent('open', { detail: { value: this.value, step: this.step }, bubbles: true }),
);
}
},
},
},
template,
components: {
UiScrollable,
},
compilerOptions: {
isCustomElement: tag => tag.startsWith('wa-'),
},
};

View File

@@ -1,77 +0,0 @@
const template = `
<div class="scrollable" :class="{'can-scroll-top': canScrollTop, 'can-scroll-bottom': canScrollBottom}" ref="container">
<div v-if="!disabled" class="scroll-shadow scroll-shadow-top"></div>
<slot></slot>
<div v-if="!disabled" class="scroll-shadow scroll-shadow-bottom"></div>
</div>
`;
export default {
props: {
disabled: Boolean,
},
data() {
return {
scrollTop: 0,
scrollHeight: 0,
height: 0,
};
},
mounted() {
let { container, content } = this.$refs;
container.addEventListener('scroll', this.handleScroll, { passive: true });
this.scrollHeight = container.scrollHeight;
this.height = container.clientHeight;
},
computed: {
canScrollTop() {
return !this.disabled && this.scrollTop > 1;
},
maxScrollTop() {
return this.scrollHeight - this.height;
},
canScrollBottom() {
return !this.disabled && this.scrollTop < this.maxScrollTop - 1;
},
scrollProgress() {
return this.scrollTop / this.maxScrollTop;
},
scrollProgressEnd() {
return this.scrollProgress + this.maxScrollTop / this.scrollHeight;
},
scrollBottom() {
return this.scrollHeight * this.scrollProgressEnd;
},
},
methods: {
handleScroll(event) {
let { container } = this.$refs;
this.scrollTop = container.scrollTop;
},
},
watch: {
scrollTop(value, oldValue) {
let { container } = this.$refs;
if (container && oldValue === 0) {
this.scrollHeight = container.scrollHeight;
this.height = container.clientHeight;
}
},
},
template,
components: {},
compilerOptions: {
isCustomElement: tag => tag.startsWith('wa-'),
},
};

View File

@@ -1,46 +0,0 @@
.ui-slider {
display: grid;
grid-template:
'label label label'
'min slider max';
grid-template-columns: auto 1fr auto;
align-items: center;
gap: var(--wa-space-2xs);
wa-slider {
display: block;
grid-area: slider;
width: 100%;
}
&:has(.ui-slider-min) wa-slider {
&::part(label) {
margin-inline-start: calc(-1 * (var(--wa-space-s) + 1rem + 2 * var(--wa-border-width-m)));
}
}
.clear-button {
vertical-align: middle;
margin-inline-start: var(--wa-space-xs);
font-size: var(--wa-font-size-xs);
}
}
.ui-slider-header {
grid-area: label;
}
.ui-slider-min,
.ui-slider-max {
width: min-content;
}
.ui-slider-min {
grid-area: min;
margin-inline-start: calc(-1 * var(--wa-space-s));
}
.ui-slider-max {
grid-area: max;
margin-inline-end: calc(-1 * var(--wa-space-s));
}

View File

@@ -1,86 +0,0 @@
import inputMixin from '../mixins/input.js';
import InfoTip from './info-tip.js';
let maxUid = 0;
const template = `
<div class="ui-slider">
<div class="ui-slider-header">
<label :for="sliderId">{{ label }}</label>
<info-tip v-if="clearable && (value !== defaultValue ?? initialValue)" :text="'Reset to ' + valueFormatter(defaultValue ?? initialValue)">
<wa-icon-button @click="value = defaultValue ?? initialValue" class="clear-button" name="circle-xmark" library="system" variant="regular" :label="'Reset to ' + tooltipFormatter(defaultValue ?? initialValue)"></wa-icon-button>
</info-tip>
</div>
<info-tip v-if="$slots.min" :text="'Set to min (' + valueFormatter(min) + ')'">
<wa-button class="ui-slider-min" appearance="plain" size="small" @click="value = min"><slot name="min"></slot></wa-button>
</info-tip>
<wa-slider ref="slider" :id="sliderId" class="ui-slider" :value @input="handleInput"
:min="min" :max="max" :step="step">
</wa-slider>
<info-tip v-if="$slots.max" :text="'Set to max (' + valueFormatter(max) + ')'">
<wa-button class="ui-slider-max" appearance="plain" size="small" @click="value = max"><slot name="max"></slot></wa-button>
</info-tip>
</div>
`;
export default {
mixins: [inputMixin],
props: {
label: String,
id: String,
defaultValue: Number,
min: {
type: Number,
default: 0,
},
max: {
type: Number,
default: 100,
},
step: {
type: Number,
default(rawProps) {
return (rawProps.max - rawProps.min) / 100;
},
},
format: [Function, String],
clearable: Boolean,
},
data() {
let uid = ++maxUid;
return { uid, value: this.modelValue };
},
mounted() {
if (this.format) {
this.$refs.slider.tooltipFormatter = this.valueFormatter;
}
},
computed: {
sliderId() {
return this.id || `ui-slider-${this.uid}`;
},
valueFormatter() {
if (typeof this.format === 'string') {
return v => this.format.replaceAll('{value}', v);
}
return this.format;
},
},
watch: {
tooltip() {
if (this.$refs.slider) {
this.$refs.slider.tooltipFormatter = this.tooltipFormatter;
}
},
},
template,
components: {
InfoTip,
},
compilerOptions: {
isCustomElement: tag => tag.startsWith('wa-'),
},
};

View File

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

View File

@@ -1,32 +0,0 @@
/**
* Mixin for components that behave like form controls.
*/
export default {
props: {
modelValue: {
type: [String, Number, Boolean],
},
},
data() {
return {
initialValue: this.modelValue,
value: this.modelValue,
};
},
emits: ['update:modelValue', 'input'],
methods: {
handleInput(e) {
this.value = e.target.value;
this.$emit('input', this.value);
},
},
watch: {
value(value) {
this.$emit('update:modelValue', value);
},
modelValue(value) {
this.value = value;
},
},
};

View File

@@ -1,110 +0,0 @@
import my from '/assets/scripts/my.js';
import Permalink from '/assets/scripts/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

@@ -65,18 +65,6 @@ Use the `appearance` attribute to change the badge's visual appearance.
</div>
```
### Size
Badges are sized relative to the current font size. You can set `font-size` on any badge (or an ancestor element) to change it.
```html {.example}
<wa-badge variant="brand" style="font-size: var(--wa-font-size-xs);">Brand</wa-badge>
<wa-badge variant="brand" style="font-size: var(--wa-font-size-s);">Brand</wa-badge>
<wa-badge variant="brand" style="font-size: var(--wa-font-size-m);">Brand</wa-badge>
<wa-badge variant="brand" style="font-size: var(--wa-font-size-l);">Brand</wa-badge>
<wa-badge variant="brand" style="font-size: var(--wa-font-size-xl);">Brand</wa-badge>
```
### Pill Badges
Use the `pill` attribute to give badges rounded edges.

View File

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

View File

@@ -6,7 +6,7 @@ icon: card
---
```html {.example}
<wa-card class="card-overview">
<wa-card with-image with-footer class="card-overview">
<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"
@@ -15,9 +15,9 @@ icon: card
<strong>Mittens</strong><br />
This kitten is as cute as he is playful. Bring him home today!<br />
<small class="wa-caption-m">6 weeks old</small>
<small>6 weeks old</small>
<div slot="footer" class="wa-split">
<div slot="footer">
<wa-button variant="brand" pill>More Info</wa-button>
<wa-rating label="Rating"></wa-rating>
</div>
@@ -27,6 +27,16 @@ icon: card
.card-overview {
width: 300px;
}
.card-overview small {
color: var(--wa-color-text-quiet);
}
.card-overview [slot='footer'] {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
```
@@ -54,10 +64,10 @@ Headers can be used to display titles and more.
If using SSR, you need to also use the `with-header` attribute to add a header to the card (if not, it is added automatically).
```html {.example}
<wa-card class="card-header">
<div slot="header" class="wa-split">
<wa-card with-header class="card-header">
<div slot="header">
Header Title
<wa-icon-button name="gear" variant="solid" label="Settings" class="wa-size-m"></wa-icon-button>
<wa-icon-button name="gear" variant="solid" label="Settings"></wa-icon-button>
</div>
This card has a header. You can put all sorts of things in it!
@@ -68,9 +78,19 @@ If using SSR, you need to also use the `with-header` attribute to add a header t
max-width: 300px;
}
.card-header [slot='header'] {
display: flex;
align-items: center;
justify-content: space-between;
}
.card-header h3 {
margin: 0;
}
.card-header wa-icon-button {
font-size: var(--wa-font-size-m);
}
</style>
```
@@ -80,10 +100,10 @@ Footers can be used to display actions, summaries, or other relevant content.
If using SSR, you need to also use the `with-footer` attribute to add a footer to the card (if not, it is added automatically).
```html {.example}
<wa-card class="card-footer">
<wa-card with-footer class="card-footer">
This card has a footer. You can put all sorts of things in it!
<div slot="footer" class="wa-split">
<div slot="footer">
<wa-rating></wa-rating>
<wa-button variant="brand">Preview</wa-button>
</div>
@@ -93,6 +113,12 @@ If using SSR, you need to also use the `with-footer` attribute to add a footer t
.card-footer {
max-width: 300px;
}
.card-footer [slot='footer'] {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
```
@@ -102,7 +128,7 @@ Card images are displayed atop the card and will stretch to fit.
If using SSR, you need to also use the `with-image` attribute to add an image to the card (if not, it is added automatically).
```html {.example}
<wa-card class="card-image">
<wa-card with-image class="card-image">
<img
slot="image"
src="https://images.unsplash.com/photo-1547191783-94d5f8f6d8b1?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=400&q=80"
@@ -124,60 +150,35 @@ Use the `size` attribute to change a card's size.
```html {.example}
<div class="wa-stack">
<wa-card size="small">
<wa-card with-footer size="small">
This is a small card.
<footer slot="footer" class="wa-split">
<footer slot="footer" class="wa-flank">
<wa-button variant="brand" pill>More Info</wa-button>
<wa-rating></wa-rating>
</footer>
</wa-card>
<wa-card size="medium">
<wa-card with-footer size="medium">
This is a medium card (default).
<footer slot="footer" class="wa-split">
<footer slot="footer" class="wa-flank">
<wa-button variant="brand" pill>More Info</wa-button>
<wa-rating></wa-rating>
</footer>
</wa-card>
<wa-card size="large">
<wa-card with-footer size="large">
This is a large card.
<footer slot="footer" class="wa-split">
<footer slot="footer" class="wa-flank">
<wa-button variant="brand" pill>More Info</wa-button>
<wa-rating></wa-rating>
</footer>
</wa-card>
</div>
```
### Appearance
Use the `appearance` attribute to change the card's visual appearance.
```html {.example}
<div class="wa-grid">
<wa-card>
<img
slot="image"
src="https://images.unsplash.com/photo-1559209172-0ff8f6d49ff7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=80"
alt="A kitten sits patiently between a terracotta pot and decorative grasses."
/>
<div slot="header">Outlined (default)</div>
Card content.
</wa-card>
{% for appearance in ['outlined filled', 'outlined accent', 'plain', 'filled', 'accent'] -%}
<wa-card appearance="{{ appearance }}">
<img
slot="image"
src="https://images.unsplash.com/photo-1559209172-0ff8f6d49ff7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=80"
alt="A kitten sits patiently between a terracotta pot and decorative grasses."
/>
<div slot="header">{{ appearance | capitalize }}</div>
Card content.
</wa-card>
{%- endfor %}
</div>
```
<style>
</style>

View File

@@ -2,8 +2,7 @@
title: Code Demo
description: Code demos can be used to render code examples as inline live demos.
tags: component
isPro: true
unpublished: true
noAlpha: true
---
```html {.example}
@@ -210,4 +209,4 @@ It goes without saying that this list is a rough plan and subject to change.
- Tabbed layout
- Provide a way to display CSS and JS separately
- Provide a way to customize the playground used (currently it is hardcoded to CodePen)
- Provide a way to customize the buttons shown
- Provide a way to customize the buttons shown

View File

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

View File

@@ -10,7 +10,7 @@ keywords: modal
<!-- cspell:dictionaries lorem-ipsum -->
```html {.example}
<wa-dialog label="Dialog" id="dialog-overview">
<wa-dialog label="Dialog" with-header with-footer id="dialog-overview">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<wa-button slot="footer" variant="brand" data-dialog="close">Close</wa-button>
</wa-dialog>
@@ -27,20 +27,19 @@ keywords: modal
## Examples
### Dialog without Header
### Dialog with Header
Headers are enabled by default. To render a dialog without a header, add the `without-header` attribute.
Headers can be used to display titles and more. Use the `with-header` attribute to add a header to the dialog.
```html {.example}
<wa-dialog label="Dialog" without-header class="dialog-without-header">
<wa-dialog label="Dialog" with-header class="dialog-header">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<wa-button slot="footer" variant="brand" data-dialog="close">Close</wa-button>
</wa-dialog>
<wa-button>Open Dialog</wa-button>
<script>
const dialog = document.querySelector('.dialog-without-header');
const dialog = document.querySelector('.dialog-header');
const openButton = dialog.nextElementSibling;
openButton.addEventListener('click', () => dialog.open = true);
@@ -49,10 +48,10 @@ Headers are enabled by default. To render a dialog without a header, add the `wi
### Dialog with Footer
Footers can be used to display titles and more. Use the `footer` slot to add a footer to the dialog.
Footers can be used to display titles and more. Use the `with-footer` attribute to add a footer to the dialog.
```html {.example}
<wa-dialog label="Dialog" class="dialog-footer">
<wa-dialog label="Dialog" with-footer class="dialog-footer">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<wa-button slot="footer" variant="brand" data-dialog="close">Close</wa-button>
</wa-dialog>
@@ -72,7 +71,7 @@ Footers can be used to display titles and more. Use the `footer` slot to add a f
You can add the special `data-dialog="close"` attribute to a button inside the dialog to tell it to close without additional JavaScript. Alternatively, you can set the `open` property to `false` to close the dialog programmatically.
```html {.example}
<wa-dialog label="Dialog" class="dialog-dismiss">
<wa-dialog label="Dialog" with-header with-footer class="dialog-dismiss">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<wa-button slot="footer" variant="brand" data-dialog="close">Close</wa-button>
</wa-dialog>
@@ -89,10 +88,10 @@ You can add the special `data-dialog="close"` attribute to a button inside the d
### Custom Width
Just use the `--width` custom property to set the dialog's width.
Just use the CSS `width` property to set the dialog's width.
```html {.example}
<wa-dialog label="Dialog" class="dialog-width" style="--width: 50vw;">
<wa-dialog label="Dialog" with-header with-footer class="dialog-width" style="width: 50vw;">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<wa-button slot="footer" variant="brand" data-dialog="close">Close</wa-button>
</wa-dialog>
@@ -112,7 +111,7 @@ Just use the `--width` custom property to set the dialog's width.
By design, a dialog's height will never exceed that of the viewport. As such, dialogs will not scroll with the page ensuring the header and footer are always accessible to the user.
```html {.example}
<wa-dialog label="Dialog" class="dialog-scrolling">
<wa-dialog label="Dialog" with-header with-footer class="dialog-scrolling">
<div style="height: 150vh; border: dashed 2px var(--wa-color-surface-border); padding: 0 1rem;">
<p>Scroll down and give it a try! 👇</p>
</div>
@@ -134,7 +133,7 @@ By design, a dialog's height will never exceed that of the viewport. As such, di
The header shows a functional close button by default. You can use the `header-actions` slot to add additional [icon buttons](/docs/components/icon-button) if needed.
```html {.example}
<wa-dialog label="Dialog" class="dialog-header-actions">
<wa-dialog label="Dialog" with-header with-footer class="dialog-header-actions">
<wa-icon-button class="new-window" slot="header-actions" name="arrow-up-right-from-square" variant="solid"></wa-icon-button>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<wa-button slot="footer" variant="brand" data-dialog="close">Close</wa-button>
@@ -157,7 +156,7 @@ The header shows a functional close button by default. You can use the `header-a
If you want the dialog to close when the user clicks on the overlay, add the `light-dismiss` attribute.
```html {.example}
<wa-dialog label="Dialog" light-dismiss class="dialog-light-dismiss">
<wa-dialog label="Dialog" light-dismiss with-header with-footer class="dialog-light-dismiss">
This dialog will close when you click on the overlay.
<wa-button slot="footer" variant="brand" data-dialog="close">Close</wa-button>
</wa-dialog>
@@ -181,7 +180,7 @@ To keep the dialog open in such cases, you can cancel the `wa-hide` event. When
You can use `event.detail.source` to determine which element triggered the request to close. This example prevents the dialog from closing when the overlay is clicked, but allows the close button or [[Escape]] to dismiss it.
```html {.example}
<wa-dialog label="Dialog" class="dialog-deny-close">
<wa-dialog label="Dialog" with-header with-footer class="dialog-deny-close">
This dialog will only close when you click the button below.
<wa-button slot="footer" variant="brand" data-dialog="close">Only this button will close it</wa-button>
</wa-dialog>
@@ -209,7 +208,7 @@ You can use `event.detail.source` to determine which element triggered the reque
To give focus to a specific element when the dialog opens, use the `autofocus` attribute.
```html {.example}
<wa-dialog label="Dialog" class="dialog-focus">
<wa-dialog label="Dialog" with-header with-footer class="dialog-focus">
<wa-input autofocus placeholder="I will have focus when the dialog is opened"></wa-input>
<wa-button slot="footer" variant="brand" data-dialog="close">Close</wa-button>
</wa-dialog>

View File

@@ -8,7 +8,7 @@ icon: drawer
<!-- cspell:dictionaries lorem-ipsum -->
```html {.example}
<wa-drawer label="Drawer" id="drawer-overview">
<wa-drawer label="Drawer" with-header with-footer class="drawer-overview">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<wa-button slot="footer" variant="brand" data-drawer="close">Close</wa-button>
</wa-drawer>
@@ -16,7 +16,7 @@ icon: drawer
<wa-button>Open Drawer</wa-button>
<script>
const drawer = document.querySelector('#drawer-overview');
const drawer = document.querySelector('.drawer-overview');
const openButton = drawer.nextElementSibling;
openButton.addEventListener('click', () => drawer.open = true);
@@ -25,20 +25,19 @@ icon: drawer
## Examples
### Drawer without Header
### Drawer with Header
Headers are enabled by default. To render a drawer without a header, add the `without-header` attribute.
Headers can be used to display titles and more. Use the `with-header` attribute to add a header to the drawer.
```html {.example}
<wa-drawer label="Drawer" without-header class="drawer-without-header">
<wa-drawer label="Drawer" with-header class="drawer-header">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<wa-button slot="footer" variant="brand" data-drawer="close">Close</wa-button>
</wa-drawer>
<wa-button>Open Drawer</wa-button>
<script>
const drawer = document.querySelector('.drawer-without-header');
const drawer = document.querySelector('.drawer-header');
const openButton = drawer.nextElementSibling;
openButton.addEventListener('click', () => drawer.open = true);
@@ -47,10 +46,10 @@ Headers are enabled by default. To render a drawer without a header, add the `wi
### Drawer with Footer
Footers can be used to display titles and more. Use the `footer` slot to add a footer to the drawer.
Footers can be used to display titles and more. Use the `with-footer` attribute to add a footer to the drawer.
```html {.example}
<wa-drawer label="Drawer" class="drawer-footer">
<wa-drawer label="Drawer" with-footer class="drawer-footer">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<wa-button slot="footer" variant="brand" data-drawer="close">Close</wa-button>
</wa-drawer>
@@ -70,7 +69,7 @@ Footers can be used to display titles and more. Use the `footer` slot to add a f
You can add the special `data-drawer="close"` attribute to a button inside the drawer to tell it to close without additional JavaScript. Alternatively, you can set the `open` property to `false` to close the drawer programmatically.
```html {.example}
<wa-drawer label="Drawer" class="drawer-dismiss">
<wa-drawer label="Drawer" with-header with-footer class="drawer-dismiss">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<wa-button slot="footer" variant="brand" data-drawer="close">Close</wa-button>
</wa-drawer>
@@ -90,7 +89,7 @@ You can add the special `data-drawer="close"` attribute to a button inside the d
By default, drawers slide in from the end. To make the drawer slide in from the start, set the `placement` attribute to `start`.
```html {.example}
<wa-drawer label="Drawer" placement="start" class="drawer-placement-start">
<wa-drawer label="Drawer" with-header with-footer placement="start" class="drawer-placement-start">
This drawer slides in from the start.
<wa-button slot="footer" variant="brand" data-drawer="close">Close</wa-button>
</wa-drawer>
@@ -110,7 +109,7 @@ By default, drawers slide in from the end. To make the drawer slide in from the
To make the drawer slide in from the top, set the `placement` attribute to `top`.
```html {.example}
<wa-drawer label="Drawer" placement="top" class="drawer-placement-top">
<wa-drawer label="Drawer" with-header with-footer placement="top" class="drawer-placement-top">
This drawer slides in from the top.
<wa-button slot="footer" variant="brand" data-drawer="close">Close</wa-button>
</wa-drawer>
@@ -130,7 +129,7 @@ To make the drawer slide in from the top, set the `placement` attribute to `top`
To make the drawer slide in from the bottom, set the `placement` attribute to `bottom`.
```html {.example}
<wa-drawer label="Drawer" placement="bottom" class="drawer-placement-bottom">
<wa-drawer label="Drawer" with-header with-footer placement="bottom" class="drawer-placement-bottom">
This drawer slides in from the bottom.
<wa-button slot="footer" variant="brand" data-drawer="close">Close</wa-button>
</wa-drawer>
@@ -150,7 +149,7 @@ To make the drawer slide in from the bottom, set the `placement` attribute to `b
Use the `--size` custom property to set the drawer's size. This will be applied to the drawer's width or height depending on its `placement`.
```html {.example}
<wa-drawer label="Drawer" class="drawer-custom-size" style="--size: 50vw;">
<wa-drawer label="Drawer" with-header with-footer class="drawer-custom-size" style="--size: 50vw;">
This drawer is always 50% of the viewport.
<wa-button slot="footer" variant="brand" data-drawer="close">Close</wa-button>
</wa-drawer>
@@ -170,7 +169,7 @@ Use the `--size` custom property to set the drawer's size. This will be applied
By design, a drawer's height will never exceed 100% of its container. As such, drawers will not scroll with the page to ensure the header and footer are always accessible to the user.
```html {.example}
<wa-drawer label="Drawer" class="drawer-scrolling">
<wa-drawer label="Drawer" with-header with-footer class="drawer-scrolling">
<div style="height: 150vh; border: dashed 2px var(--wa-color-surface-border); padding: 0 1rem;">
<p>Scroll down and give it a try! 👇</p>
</div>
@@ -192,7 +191,7 @@ By design, a drawer's height will never exceed 100% of its container. As such, d
The header shows a functional close button by default. You can use the `header-actions` slot to add additional [icon buttons](/docs/components/icon-button) if needed.
```html {.example}
<wa-drawer label="Drawer" class="drawer-header-actions">
<wa-drawer label="Drawer" with-header with-footer class="drawer-header-actions">
<wa-icon-button class="new-window" slot="header-actions" name="arrow-up-right-from-square" variant="solid"></wa-icon-button>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
<wa-button slot="footer" variant="brand" data-drawer="close">Close</wa-button>
@@ -215,7 +214,7 @@ The header shows a functional close button by default. You can use the `header-a
If you want the drawer to close when the user clicks on the overlay, add the `light-dismiss` attribute.
```html {.example}
<wa-drawer label="Drawer" light-dismiss class="drawer-light-dismiss">
<wa-drawer label="Drawer" light-dismiss with-header with-footer class="drawer-light-dismiss">
This drawer will close when you click on the overlay.
<wa-button slot="footer" variant="brand" data-drawer="close">Close</wa-button>
</wa-drawer>
@@ -239,7 +238,7 @@ To keep the drawer open in such cases, you can cancel the `wa-hide` event. When
You can use `event.detail.source` to determine what triggered the request to close. This example prevents the drawer from closing when the overlay is clicked, but allows the close button or [[Escape]] to dismiss it.
```html {.example}
<wa-drawer label="Drawer" class="drawer-deny-close">
<wa-drawer label="Drawer" with-header with-footer class="drawer-deny-close">
This drawer will only close when you click the button below.
<wa-button slot="footer" variant="brand" data-drawer="close">Close</wa-button>
</wa-drawer>
@@ -262,12 +261,12 @@ You can use `event.detail.source` to determine what triggered the request to clo
</script>
```
### Setting Initial Focus
### Customizing Initial Focus
To give focus to a specific element when the drawer opens, use the `autofocus` attribute.
By default, the drawer's panel will gain focus when opened. This allows a subsequent tab press to focus on the first tabbable element in the drawer. If you want a different element to have focus, add the `autofocus` attribute to it as shown below.
```html {.example}
<wa-drawer label="Drawer" class="drawer-focus">
<wa-drawer label="Drawer" with-header with-footer class="drawer-focus">
<wa-input autofocus placeholder="I will have focus when the drawer is opened"></wa-input>
<wa-button slot="footer" variant="brand" data-drawer="close">Close</wa-button>
</wa-drawer>

View File

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

View File

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

View File

@@ -1,16 +1,14 @@
---
title: Comparison
description: Compare visual differences between similar content with a sliding panel.
title: Image Comparer
description: Compare visual differences between similar photos with a sliding panel.
tags: [imagery, niche]
icon: comparison
icon: image-comparer
---
This is especially useful for comparing images, but can be used for comparing any type of content (for an example of using it to compare entire UIs, check out our [theme pages](/docs/themes/default/)).
For best results, use content that shares the same dimensions.
The slider can be controlled by dragging or pressing the left and right arrow keys. (Tip: press shift + arrows to move the slider in larger intervals, or home + end to jump to the beginning or end.)
For best results, use images that share the same dimensions. The slider can be controlled by dragging or pressing the left and right arrow keys. (Tip: press shift + arrows to move the slider in larger intervals, or home + end to jump to the beginning or end.)
```html {.example}
<wa-comparison>
<wa-image-comparer>
<img
slot="before"
src="https://images.unsplash.com/photo-1517331156700-3c241d2b4d83?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=800&q=80&sat=-100&bri=-5"
@@ -21,7 +19,7 @@ The slider can be controlled by dragging or pressing the left and right arrow ke
src="https://images.unsplash.com/photo-1517331156700-3c241d2b4d83?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=800&q=80"
alt="Color version of kittens in a basket looking around."
/>
</wa-comparison>
</wa-image-comparer>
```
## Examples
@@ -31,7 +29,7 @@ The slider can be controlled by dragging or pressing the left and right arrow ke
Use the `position` attribute to set the initial position of the slider. This is a percentage from `0` to `100`.
```html {.example}
<wa-comparison position="25">
<wa-image-comparer position="25">
<img
slot="before"
src="https://images.unsplash.com/photo-1520903074185-8eca362b3dce?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1200&q=80"
@@ -42,5 +40,5 @@ Use the `position` attribute to set the initial position of the slider. This is
src="https://images.unsplash.com/photo-1520640023173-50a135e35804?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2250&q=80"
alt="A person sitting on a yellow curb tying shoelaces on a boot."
/>
</wa-comparison>
</wa-image-comparer>
```

View File

@@ -2,10 +2,13 @@
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
override:tags: []
categories:
tags: [actions, feedback, imagery, inputs, navigation, organization, helpers]
titles:
feedback: 'Feedback & Status'
helpers: 'Utilities'
- actions
- feedback: 'Feedback & Status'
- imagery
- inputs
- navigation
- organization
- helpers: 'Utilities'
override:tags: []
---

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