Compare commits
154 Commits
isolate
...
v3.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49bb6af154 | ||
|
|
9b8d73a2ea | ||
|
|
0b4c1a5934 | ||
|
|
8b17b3d3e0 | ||
|
|
f5d1b74e00 | ||
|
|
40ea444c48 | ||
|
|
403927687e | ||
|
|
6af4e849af | ||
|
|
2c839a4225 | ||
|
|
3703ef26da | ||
|
|
3640b4c6e1 | ||
|
|
27fc269a94 | ||
|
|
a5b2fffb7a | ||
|
|
7de6a676b8 | ||
|
|
3c77d400f8 | ||
|
|
6ee10f44f4 | ||
|
|
6afc6e928c | ||
|
|
f8fcbd60ec | ||
|
|
bdd25571a8 | ||
|
|
38c13640fc | ||
|
|
00a3b1aa8d | ||
|
|
22298781c5 | ||
|
|
fe27710f57 | ||
|
|
d94589d113 | ||
|
|
3508bf6339 | ||
|
|
61e65ffcb9 | ||
|
|
aed28adbe4 | ||
|
|
15b8bde81b | ||
|
|
9ee3fb5d28 | ||
|
|
47aa376c08 | ||
|
|
69ba974a50 | ||
|
|
8dfb411e5e | ||
|
|
a35a8fd2ad | ||
|
|
2503005bbd | ||
|
|
78027170ea | ||
|
|
a20aa48992 | ||
|
|
ac8accd664 | ||
|
|
c571573063 | ||
|
|
e813440315 | ||
|
|
cfc3f181a3 | ||
|
|
7545f04c46 | ||
|
|
38bd6528fe | ||
|
|
2202ea9642 | ||
|
|
58fbcede51 | ||
|
|
971200cc88 | ||
|
|
b75d3b615c | ||
|
|
9d1c47449e | ||
|
|
003fd28cb0 | ||
|
|
2f300d8930 | ||
|
|
f13deb87bb | ||
|
|
deb9fd70b3 | ||
|
|
ff3b3d6558 | ||
|
|
6b3edb8a56 | ||
|
|
6162b8b115 | ||
|
|
cff752b600 | ||
|
|
7892a94b9b | ||
|
|
40a58ff35f | ||
|
|
0f2950c4cc | ||
|
|
b334884f57 | ||
|
|
734417d66b | ||
|
|
2cfd651d2f | ||
|
|
3e2d1b98be | ||
|
|
40f332f37c | ||
|
|
bfda64f690 | ||
|
|
883d6df2ef | ||
|
|
b4240fd321 | ||
|
|
8755a834f6 | ||
|
|
8d905296b8 | ||
|
|
8eba1e5003 | ||
|
|
21aa85acc0 | ||
|
|
404c15b303 | ||
|
|
8a26afc334 | ||
|
|
513a1e35a9 | ||
|
|
09f668fc99 | ||
|
|
d451ba98e5 | ||
|
|
fd287edd56 | ||
|
|
8424b49646 | ||
|
|
fa24c0f70e | ||
|
|
1bba87c66d | ||
|
|
0db9ca12e3 | ||
|
|
041555fe99 | ||
|
|
b41dbd2de7 | ||
|
|
7c6f31e0c7 | ||
|
|
9e84274a93 | ||
|
|
2b3803f91e | ||
|
|
faed8da3cd | ||
|
|
17cf902f53 | ||
|
|
8214ff6b2d | ||
|
|
c9979e15f8 | ||
|
|
fcfe2bde7d | ||
|
|
59dcaaff83 | ||
|
|
5bad30ec30 | ||
|
|
87c1762146 | ||
|
|
899edd1d5e | ||
|
|
872a110b1e | ||
|
|
07fe6d598e | ||
|
|
79bafc513a | ||
|
|
1d03f7bee0 | ||
|
|
a9bf1bd838 | ||
|
|
c0ca739366 | ||
|
|
a6745602d6 | ||
|
|
da4f619d95 | ||
|
|
1283a696a5 | ||
|
|
d12b97b0b0 | ||
|
|
e5c2884880 | ||
|
|
1d600a77c4 | ||
|
|
db3c568ba2 | ||
|
|
4bb9805ba6 | ||
|
|
bd935fa8d5 | ||
|
|
c3e582b47b | ||
|
|
4d094a4e19 | ||
|
|
782c404bdf | ||
|
|
f1438981b2 | ||
|
|
18b88c2f5c | ||
|
|
a2d85f49a3 | ||
|
|
be00026cd3 | ||
|
|
58ed88bc5a | ||
|
|
1d14e186f3 | ||
|
|
5f672aabc2 | ||
|
|
db08e12a32 | ||
|
|
e0fc639226 | ||
|
|
e6c662b543 | ||
|
|
d1de9a9a73 | ||
|
|
4931de8eb4 | ||
|
|
71e7227763 | ||
|
|
dd671e15aa | ||
|
|
2daeea0349 | ||
|
|
3cb6625c1d | ||
|
|
c4b5446d01 | ||
|
|
41affca083 | ||
|
|
132dbfabcc | ||
|
|
4fc6224464 | ||
|
|
4921d1c32e | ||
|
|
54d71d2319 | ||
|
|
c1ecca0169 | ||
|
|
d6a91919e0 | ||
|
|
4621094ea1 | ||
|
|
726dc73e2a | ||
|
|
4bfebf3249 | ||
|
|
99ad0abdd3 | ||
|
|
902e2b6367 | ||
|
|
7f3210b12d | ||
|
|
eee63bdecd | ||
|
|
a91fd07ed7 | ||
|
|
34aa8917a6 | ||
|
|
715c2c0def | ||
|
|
13b5385633 | ||
|
|
d25f3748c4 | ||
|
|
b6620ddf7e | ||
|
|
d70d4a91b1 | ||
|
|
bb1f7b2b7a | ||
|
|
9921c17d63 | ||
|
|
7f964f9b56 | ||
|
|
31eeea1630 |
2
.github/workflows/client_tests.js.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
- name: Lint
|
||||
run: npm run prettier
|
||||
- name: Build
|
||||
run: npm run build:alpha
|
||||
run: npm run build
|
||||
- name: Install Playwright
|
||||
run: npx playwright install --with-deps
|
||||
- name: Run CSR tests
|
||||
|
||||
11
.github/workflows/ssr_tests.js.yml
vendored
@@ -1,11 +1,12 @@
|
||||
# # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: SSR Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [next]
|
||||
# push:
|
||||
# branches: [next]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
ssr_test:
|
||||
@@ -30,7 +31,7 @@ jobs:
|
||||
run: npm run prettier
|
||||
|
||||
- name: Build
|
||||
run: npm run build:alpha
|
||||
run: npm run build
|
||||
|
||||
- name: Install Playwright
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
4
.gitignore
vendored
@@ -1,12 +1,8 @@
|
||||
_site
|
||||
.cache
|
||||
.DS_Store
|
||||
package.json
|
||||
package-lock.json
|
||||
dist/
|
||||
dist-cdn/
|
||||
docs/public/pagefind
|
||||
node_modules
|
||||
src/react
|
||||
.astro
|
||||
cdn/
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
*.hbs
|
||||
*.md
|
||||
!docs/docs/patterns/**/*.md
|
||||
docs/docs/patterns/blog-news/post-list.md
|
||||
.cache
|
||||
.github
|
||||
cspell.json
|
||||
@@ -13,4 +15,4 @@ package-lock.json
|
||||
tsconfig.json
|
||||
cdn
|
||||
_site
|
||||
docs/assets/scripts/prism.js
|
||||
docs/assets/scripts/prism-downloaded.js
|
||||
|
||||
@@ -148,6 +148,7 @@
|
||||
"scrollbars",
|
||||
"scrollend",
|
||||
"scroller",
|
||||
"Scrollers",
|
||||
"Segoe",
|
||||
"semibold",
|
||||
"shadowrootmode",
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
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 componentList from './_data/componentList.js';
|
||||
import nunjucks from 'nunjucks';
|
||||
// import componentList from './_data/componentList.js';
|
||||
import * as filters from './_utils/filters.js';
|
||||
import { outlinePlugin } from './_utils/outline.js';
|
||||
import { replaceTextPlugin } from './_utils/replace-text.js';
|
||||
@@ -16,29 +17,36 @@ import { searchPlugin } from './_utils/search.js';
|
||||
|
||||
import process from 'process';
|
||||
|
||||
const packageData = JSON.parse(await readFile('./package.json', 'utf-8'));
|
||||
const isAlpha = process.argv.includes('--alpha');
|
||||
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 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) {
|
||||
// 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');
|
||||
}
|
||||
/**
|
||||
* 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';
|
||||
|
||||
// Add template data
|
||||
for (let name in globalData) {
|
||||
@@ -55,7 +63,38 @@ export default function (eleventyConfig) {
|
||||
|
||||
// Shortcodes - {% shortCode arg1, arg2 %}
|
||||
eleventyConfig.addShortcode('cdnUrl', location => {
|
||||
return `https://early.webawesome.com/webawesome@${packageData.version}/dist/` + location.replace(/^\//, '');
|
||||
return `https://early.webawesome.com/webawesome@${packageData.version}/dist/` + (location || '').replace(/^\//, '');
|
||||
});
|
||||
|
||||
// Turns `{% server "foo" %} into `{{ server.foo | safe }}` when the WEBAWESOME_SERVER variable is set to "true"
|
||||
eleventyConfig.addShortcode('server', function (property) {
|
||||
if (serverBuild) {
|
||||
return `{{ server.${property} | safe }}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
eleventyConfig.addTransform('second-nunjucks-transform', function NunjucksTransform(content) {
|
||||
// For a server build, we expect a server to run the second transform.
|
||||
if (serverBuild) {
|
||||
return content;
|
||||
}
|
||||
|
||||
// Only run the transform on files nunjucks would transform.
|
||||
if (!this.page.inputPath.match(/.(md|html|njk)$/)) {
|
||||
return content;
|
||||
}
|
||||
|
||||
/** This largely mimics what an app would do and just stubs out what we don't care about. */
|
||||
return nunjucks.renderString(content, {
|
||||
// Stub the server EJS shortcodes.
|
||||
server: {
|
||||
head: '',
|
||||
loginOrAvatar: '',
|
||||
flashes: '',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Paired shortcodes - {% shortCode %}content{% endShortCode %}
|
||||
@@ -63,9 +102,6 @@ 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);
|
||||
|
||||
@@ -94,7 +130,7 @@ export default function (eleventyConfig) {
|
||||
eleventyConfig.addPlugin(highlightCodePlugin());
|
||||
|
||||
// Add copy code buttons to code blocks
|
||||
eleventyConfig.addPlugin(copyCodePlugin());
|
||||
eleventyConfig.addPlugin(copyCodePlugin);
|
||||
|
||||
// Various text replacements
|
||||
eleventyConfig.addPlugin(
|
||||
@@ -117,28 +153,14 @@ export default function (eleventyConfig) {
|
||||
]),
|
||||
);
|
||||
|
||||
// SSR plugin
|
||||
if (!isDev) {
|
||||
//
|
||||
// Problematic components in SSR land:
|
||||
// - animation (breaks on navigation + ssr with Turbo)
|
||||
// - mutation-observer (why SSR this?)
|
||||
// - resize-observer (why SSR this?)
|
||||
// - tooltip (why SSR this?)
|
||||
//
|
||||
const omittedModules = [];
|
||||
const componentModules = componentList
|
||||
.filter(component => !omittedModules.includes(component.tagName.split(/wa-/)[1]))
|
||||
.map(component => {
|
||||
const name = component.tagName.split(/wa-/)[1];
|
||||
return `./dist/components/${name}/${name}.js`;
|
||||
});
|
||||
eleventyConfig.addPreprocessor('unpublished', '*', (data, content) => {
|
||||
if (data.unpublished && process.env.ELEVENTY_RUN_MODE === 'build') {
|
||||
// Exclude "unpublished" pages from final builds.
|
||||
return false;
|
||||
}
|
||||
|
||||
eleventyConfig.addPlugin(litPlugin, {
|
||||
mode: 'worker',
|
||||
componentModules,
|
||||
});
|
||||
}
|
||||
return content;
|
||||
});
|
||||
|
||||
// Build the search index
|
||||
eleventyConfig.addPlugin(
|
||||
@@ -166,6 +188,31 @@ export default function (eleventyConfig) {
|
||||
eleventyConfig.addPassthroughCopy(glob);
|
||||
}
|
||||
|
||||
// // SSR plugin
|
||||
// // Make sure this is the last thing, we don't want to run the risk of accidentally transforming shadow roots with the nunjucks 2nd transform.
|
||||
// if (!isDev) {
|
||||
// //
|
||||
// // Problematic components in SSR land:
|
||||
// // - animation (breaks on navigation + ssr with Turbo)
|
||||
// // - mutation-observer (why SSR this?)
|
||||
// // - resize-observer (why SSR this?)
|
||||
// // - tooltip (why SSR this?)
|
||||
// //
|
||||
// const omittedModules = [];
|
||||
// const componentModules = componentList
|
||||
// .filter(component => !omittedModules.includes(component.tagName.split(/wa-/)[1]))
|
||||
// .map(component => {
|
||||
// const name = component.tagName.split(/wa-/)[1];
|
||||
// const componentDirectory = process.env.UNBUNDLED_DIST_DIRECTORY || path.join('.', 'dist');
|
||||
// return path.join(componentDirectory, 'components', name, `${name}.js`);
|
||||
// });
|
||||
//
|
||||
// eleventyConfig.addPlugin(litPlugin, {
|
||||
// mode: 'worker',
|
||||
// componentModules,
|
||||
// });
|
||||
// }
|
||||
|
||||
return {
|
||||
markdownTemplateEngine: 'njk',
|
||||
dir: {
|
||||
|
||||
1
docs/_data/hueRanges.js
Normal file
@@ -0,0 +1 @@
|
||||
export { hueRanges as default } from '../assets/data/index.js';
|
||||
@@ -1 +1 @@
|
||||
["red", "yellow", "green", "cyan", "blue", "indigo", "purple", "gray"]
|
||||
["red", "orange", "yellow", "green", "cyan", "blue", "indigo", "purple", "pink", "gray"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default as default } from '../../src/styles/color/palettes-analyzed.js';
|
||||
export { default as default } from '../../src/styles/color/scripts/palettes-analyzed.js';
|
||||
|
||||
61
docs/_data/themes.js
Normal file
@@ -0,0 +1,61 @@
|
||||
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', '');
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-fa-kit-code="b10bfbde90" data-cdn-url="{% cdnUrl %}">
|
||||
<html lang="en" data-fa-kit-code="b10bfbde90" data-cdn-url="{% cdnUrl %}" class="wa-cloak">
|
||||
<head>
|
||||
{% include 'head.njk' %}
|
||||
<meta name="theme-color" content="#f36944">
|
||||
|
||||
<script type="module" src="/assets/scripts/code-examples.js"></script>
|
||||
<script type="module" src="/assets/scripts/copy-code.js"></script>
|
||||
|
||||
<script type="module" src="/assets/scripts/scroll.js"></script>
|
||||
<script type="module" src="/assets/scripts/turbo.js"></script>
|
||||
<script type="module" src="/assets/scripts/search.js"></script>
|
||||
<script type="module" src="/assets/scripts/outline.js"></script>
|
||||
{% if hasSidebar %}<script type="module" src="/assets/scripts/sidebar-tweaks.js"></script>{% endif %}
|
||||
<script defer data-domain="backers.webawesome.com" src="https://plausible.io/js/script.js"></script>
|
||||
|
||||
{# Docs styles #}
|
||||
@@ -20,7 +19,7 @@
|
||||
</head>
|
||||
<body class="layout-{{ layout | stripExtension }}{{ ' page-wide' if wide }}">
|
||||
<!-- use view="desktop" as default to reduce layout jank on desktop site. -->
|
||||
<wa-page view="desktop" disable-navigation-toggle="">
|
||||
<wa-page view="desktop" disable-navigation-toggle="" mobile-breakpoint="1140">
|
||||
<header slot="header" class="wa-split">
|
||||
{# Logo #}
|
||||
<div id="docs-branding">
|
||||
@@ -33,13 +32,13 @@
|
||||
<span class="wa-desktop-only">{% include "logo.njk" %}</span>
|
||||
<span class="wa-mobile-only">{% include "logo-simple.njk" %}</span>
|
||||
</a>
|
||||
<small id="version-number" class="only-desktop">{{ package.version }}</small>
|
||||
<wa-badge variant="warning" appearance="filled" class="only-desktop">Alpha</wa-badge>
|
||||
<small id="version-number" class="wa-desktop-only">{{ package.version }}</small>
|
||||
<wa-badge variant="warning" appearance="filled" class="wa-desktop-only">Alpha</wa-badge>
|
||||
</div>
|
||||
|
||||
<div id="docs-toolbar" class="wa-cluster wa-gap-xs">
|
||||
{# Desktop selectors #}
|
||||
<div class="only-desktop wa-cluster wa-gap-xs">
|
||||
<div class="wa-desktop-only wa-cluster wa-gap-xs">
|
||||
{% include "preset-theme-selector.njk" %}
|
||||
{% include "color-scheme-selector.njk" %}
|
||||
</div>
|
||||
@@ -48,8 +47,11 @@
|
||||
<wa-button id="search-trigger" appearance="outlined" size="small" data-search>
|
||||
<wa-icon slot="prefix" name="magnifying-glass"></wa-icon>
|
||||
Search
|
||||
<kbd slot="suffix" class="only-desktop">/</kbd>
|
||||
<kbd slot="suffix" class="wa-desktop-only">/</kbd>
|
||||
</wa-button>
|
||||
|
||||
{# Login #}
|
||||
{% server "loginOrAvatar" %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -76,14 +78,19 @@
|
||||
</aside>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{# Main #}
|
||||
<main id="content">
|
||||
{# Expandable outline #}
|
||||
{% if hasOutline %}
|
||||
<nav id="outline-expandable">
|
||||
<details class="outline-links">
|
||||
<summary>On this page</summary>
|
||||
</details>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
<div id="flashes">{% server "flashes" %}</div>
|
||||
|
||||
{% block header %}
|
||||
{% include 'breadcrumbs.njk' %}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
{% set breadcrumbs = page.url | breadcrumbs %}
|
||||
{% if breadcrumbs.length > 0 %}
|
||||
{% set ancestors = page.url | ancestors %}
|
||||
|
||||
{% if ancestors.length > 0 %}
|
||||
<wa-breadcrumb id="docs-breadcrumbs">
|
||||
{% for crumb in breadcrumbs %}
|
||||
<wa-breadcrumb-item href="{{ crumb.url }}">{{ crumb.title }}</wa-breadcrumb-item>
|
||||
{% for ancestor in ancestors %}
|
||||
{% if ancestor.page.url != "/" %}
|
||||
<wa-breadcrumb-item href="{{ ancestor.page.url }}">{{ ancestor.data.title }}</wa-breadcrumb-item>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<wa-breadcrumb-item>{# Current page #}</wa-breadcrumb-item>
|
||||
</wa-breadcrumb>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<table class="colors wa-palette-{{ paletteId }}">
|
||||
<table class="colors wa-palette-{{ paletteId }} contrast-table" data-min-contrast="{{ minContrast }}">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
@@ -12,19 +12,31 @@
|
||||
</tr>
|
||||
</thead>
|
||||
{% for hue in hues -%}
|
||||
<tr>
|
||||
<tr data-hue="{{ hue }}">
|
||||
<th>{{ hue | capitalize }}</th>
|
||||
{% for tint_bg in tints -%}
|
||||
{% set color_bg = palettes[paletteId][hue][tint_bg] %}
|
||||
{% for tint_fg in tints | reverse -%}
|
||||
{% set color_fg = palettes[paletteId][hue][tint_fg] %}
|
||||
{% if (tint_fg - tint_bg) | abs == difference %}
|
||||
<td>
|
||||
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}-{{ tint_bg }}); color: var(--wa-color-{{ hue }}-{{ tint_fg }})">
|
||||
{% set contrast_wcag = '' %}
|
||||
{% if color_fg and color_bg %}
|
||||
{% set contrast_wcag = color_bg.contrast(color_fg, 'WCAG21') %}
|
||||
{% endif %}
|
||||
{% set contrast_wcag = '' %}
|
||||
{% if color_fg and color_bg -%}
|
||||
{% set contrast_wcag = color_bg.contrast(color_fg, 'WCAG21') %}
|
||||
{%- endif %}
|
||||
<td v-for="contrast of [contrasts.{{ hue }}['{{ tint_bg }}']['{{ tint_fg }}']]"
|
||||
data-tint-bg="{{ tint_bg }}" data-tint-fg="{{ tint_fg }}" data-original-contrast="{{ contrast_wcag }}">
|
||||
<div v-content:number="contrast.value"
|
||||
class="color swatch" :class="{
|
||||
'value-up': contrast.value - contrast.original > 0.0001,
|
||||
'value-down': contrast.original - contrast.value > 0.0001,
|
||||
'contrast-fail': contrast.value < {{ minContrast }}
|
||||
}"
|
||||
style="--color: var(--wa-color-{{ hue }}-{{ tint_bg }}); color: var(--wa-color-{{ hue }}-{{ tint_fg }})"
|
||||
:style="{
|
||||
'--color': contrast.bgColor,
|
||||
color: contrast.fgColor,
|
||||
}"
|
||||
>
|
||||
{% if contrast_wcag %}
|
||||
{{ contrast_wcag | number({maximumSignificantDigits: 2}) }}
|
||||
{% else %}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
{# Cards for pages listed by category #}
|
||||
|
||||
<section id="grid" class="index-grid">
|
||||
{% for category, pages in allPages | groupByTags(categories) -%}
|
||||
<h2 class="index-category">{{ category | getCategoryTitle(categories) }}</h2>
|
||||
{%- for page in pages -%}
|
||||
{%- if not page.data.parent or listChildren -%}
|
||||
{% include "page-card.njk" %}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{% set groupedPages = allPages | groupPages(categories, page) %}
|
||||
{% for category, pages in groupedPages -%}
|
||||
{% if groupedPages.meta.groupCount > 1 and pages.length > 0 %}
|
||||
<h2 class="index-category" id="{{ category | slugify }}">
|
||||
{% if pages.meta.url %}<a href="{{ pages.meta.url }}">{{ pages.meta.title }}</a>
|
||||
{% else %}
|
||||
{{ pages.meta.title }}
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% endif %}
|
||||
{%- for page in pages -%}
|
||||
{% include "page-card.njk" %}
|
||||
{%- endfor -%}
|
||||
{%- endfor -%}
|
||||
</section>
|
||||
|
||||
@@ -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 %}<meta name="robots" content="noindex">{% endif %}
|
||||
{% if noindex or unlisted %}<meta name="robots" content="noindex">{% endif %}
|
||||
|
||||
<title>{{ title }}</title>
|
||||
|
||||
@@ -23,14 +23,17 @@
|
||||
<script src="/assets/scripts/hydration-errors.js"></script>
|
||||
<link rel="stylesheet" href="/assets/styles/hydration-errors.css">
|
||||
<link rel="preconnect" href="https://cdn.jsdelivr.net">
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.10/+esm"></script>
|
||||
|
||||
{# Internal components #}
|
||||
<script type="module" src="/assets/components/scoped.js"></script>
|
||||
|
||||
{# Web Awesome #}
|
||||
<script type="module" src="/dist/webawesome.ssr-loader.js"></script>
|
||||
<script type="module" src="/dist/webawesome.loader.js"></script>
|
||||
|
||||
<script type="module" src="/assets/scripts/theme-picker.js"></script>
|
||||
{# Preset Theme #}
|
||||
{% if forceTheme %}
|
||||
{% if noTheme %}
|
||||
{% elif 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>
|
||||
@@ -47,3 +50,6 @@
|
||||
<link rel="stylesheet" href="/dist/styles/webawesome.css" />
|
||||
<link id="color-stylesheet" rel="stylesheet" href="/dist/styles/utilities.css" />
|
||||
<link rel="stylesheet" href="/dist/styles/forms.css" />
|
||||
|
||||
{# Used by Web Awesome App to inject other assets into the head. #}
|
||||
{% server "head" %}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{%- if not page.data.unlisted -%}
|
||||
<a href="{{ page.url }}"{{ page.data.keywords | attr('data-keywords') }}>
|
||||
{% if page.url %}<a href="{{ page.url }}"{{ page.data.keywords | attr('data-keywords') }}>{% endif %}
|
||||
<wa-card with-header>
|
||||
<div slot="header">
|
||||
{% include "svgs/" + (page.data.icon or "thumbnail-placeholder") + ".njk" %}
|
||||
{% include "svgs/" + (page.data.icon or "thumbnail-placeholder") + ".njk" ignore missing %}
|
||||
</div>
|
||||
<span class="page-name">{{ page.data.title }}</span>
|
||||
{% if pageSubtitle -%}
|
||||
<div class="wa-caption-s">{{ pageSubtitle }}</div>
|
||||
{%- endif %}
|
||||
</wa-card>
|
||||
</a>
|
||||
{% if page.url %}</a>{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -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 }}"{{ ' data-alpha="remove"' if theme.noAlpha }}>
|
||||
<wa-option value="{{ theme.page.fileSlug }}">
|
||||
{{ theme.data.title }}
|
||||
</wa-option>
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<wa-dialog id="site-search" no-header light-dismiss>
|
||||
<wa-dialog id="site-search" without-header light-dismiss>
|
||||
<div id="site-search-container">
|
||||
{# Header #}
|
||||
<header>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
{# Some collections (like "patterns") will not have any items in the alpha build for example. So this checks to make sure the collection exists. #}
|
||||
{% if collections[tag] -%}
|
||||
{% set groupUrl %}/docs/{{ tag }}/{% endset %}
|
||||
{% set groupItem = groupUrl | getCollectionItemFromUrl %}
|
||||
{% set children = groupItem.data.children if groupItem.data.children.length > 0 else (collections[tag] | sort) %}
|
||||
|
||||
<wa-details {{ ((tag in (tags or [])) or (groupUrl in page.url)) | attr('open') }}>
|
||||
<h2 slot="summary">
|
||||
{% if groupUrl | getCollectionItemFromUrl %}
|
||||
{% if groupItem %}
|
||||
<a href="{{ groupUrl }}" title="Overview">{{ title or (tag | capitalize) }}
|
||||
<wa-icon name="grid-2"></wa-icon>
|
||||
</a>
|
||||
@@ -12,10 +15,8 @@
|
||||
{% endif %}
|
||||
</h2>
|
||||
<ul>
|
||||
{% for page in collections[tag] | sort %}
|
||||
{% if not page.data.parent -%}
|
||||
{% for page in children %}
|
||||
{% include 'sidebar-link.njk' %}
|
||||
{%- endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</wa-details>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% if not (isAlpha and page.data.noAlpha) and page.fileSlug != tag and not page.data.unlisted -%}
|
||||
{% if page 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 %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{# Getting started #}
|
||||
<h2>Getting Started</h2>
|
||||
<ul>
|
||||
<li><a href="/docs/installation">Installation</a></li>
|
||||
<li><a href="/docs/">Installation</a></li>
|
||||
<li><a href="/docs/usage">Usage</a></li>
|
||||
<li><a href="/docs/customizing">Customizing</a></li>
|
||||
<li><a href="/docs/form-controls">Form Controls</a></li>
|
||||
|
||||
8
docs/_includes/svgs/action-panel.njk
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="96" height="57" viewBox="0 0 96 57" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 1H84C90.0751 1 95 5.92487 95 12V45C95 51.0751 90.0751 56 84 56H12C5.92487 56 1 51.0751 1 45V12C1 5.92487 5.92487 1 12 1Z" fill="white" stroke="#E4E5E9" stroke-width="2"/>
|
||||
<rect x="7" y="39" width="50" height="11" rx="4" fill="#4895FD"/>
|
||||
<rect x="7" y="8" width="41" height="5" fill="#616D8A"/>
|
||||
<rect x="7" y="19.75" width="76" height="3" fill="#D9D9D9"/>
|
||||
<rect x="7" y="25" width="76" height="3" fill="#D9D9D9"/>
|
||||
<rect x="7" y="32" width="76" height="3" fill="#D9D9D9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 587 B |
3
docs/_includes/svgs/call-to-action.njk
Normal file
@@ -0,0 +1,3 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
10
docs/_includes/svgs/category-list.njk
Normal file
@@ -0,0 +1,10 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 986 B |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
@@ -1,24 +1,20 @@
|
||||
{% set paletteId = palette.fileSlug or page.fileSlug %}
|
||||
{% set tints = [80, 60, 40, 20] %}
|
||||
{% set width = 20 %}
|
||||
{% set height = 13 %}
|
||||
{% set gap_x = 3 %}
|
||||
{% set gap_y = 3 %}
|
||||
{% set suffixes = ['-80', '', '-20'] %}
|
||||
|
||||
<svg viewBox="0 0 {{ (width + gap_x) * hues|length }} {{ (height + gap_y) * tints|length }}" fill="none" xmlns="http://www.w3.org/2000/svg" class="wa-palette-{{ paletteId }} palette-icon">
|
||||
<style>
|
||||
@import url('/dist/styles/color/{{ paletteId }}.css') layer(palette.{{ paletteId }});
|
||||
.palette-icon {
|
||||
height: 8ch;
|
||||
}
|
||||
</style>
|
||||
<wa-scoped class="palette-icon-host">
|
||||
<template>
|
||||
<link rel="stylesheet" href="/dist/styles/color/{{ paletteId }}.css">
|
||||
<link rel="stylesheet" href="/assets/styles/theme-icons.css">
|
||||
|
||||
{% for hue in hues -%}
|
||||
{% set hueIndex = loop.index0 %}
|
||||
{% for tint in tints -%}
|
||||
<rect x="{{ hueIndex * (width + gap_x) }}" y="{{ loop.index0 * (height + gap_y) }}"
|
||||
width="{{ width }}" height="{{ height }}"
|
||||
fill="var(--wa-color-{{ hue }}-{{ tint }})" rx="4" />
|
||||
{%- endfor %}
|
||||
{% endfor %}
|
||||
</svg>
|
||||
<div class="palette-icon" style="--hues: {{ hues|length }}; --suffixes: {{ suffixes|length }}">
|
||||
{% for hue in hues -%}
|
||||
{% set hueIndex = loop.index %}
|
||||
{% for suffix in suffixes -%}
|
||||
<div class="swatch"
|
||||
data-hue="{{ hue }}" data-suffix="{{ suffix }}"
|
||||
style="--color: var(--wa-color-{{ hue }}{{ suffix }}); grid-column: {{ hueIndex }}; grid-row: {{ loop.index }}"> </div>
|
||||
{%- endfor %}
|
||||
{%- endfor %}
|
||||
</div>
|
||||
</template>
|
||||
</wa-scoped>
|
||||
|
||||
10
docs/_includes/svgs/preview.njk
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="120" height="87" viewBox="0 0 240 178" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect y="39" width="67" height="63" rx="12" fill="#D9D9D9"/>
|
||||
<rect y="115" width="67" height="63" rx="12" fill="#D9D9D9"/>
|
||||
<rect width="67" height="19" rx="6" fill="#D9D9D9"/>
|
||||
<rect x="87" y="39" width="67" height="63" rx="12" fill="#D9D9D9"/>
|
||||
<rect x="174" y="39" width="67" height="63" rx="12" fill="#D9D9D9"/>
|
||||
<rect x="174" y="7" width="67" height="4.75" rx="2.375" fill="#D9D9D9"/>
|
||||
<rect x="87" y="115" width="67" height="63" rx="12" fill="#D9D9D9"/>
|
||||
<rect x="174" y="115" width="67" height="63" rx="12" fill="#D9D9D9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 631 B |
31
docs/_includes/svgs/scroller.njk
Normal file
@@ -0,0 +1,31 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -1,13 +1,13 @@
|
||||
{% set themeId = theme.fileSlug %}
|
||||
|
||||
<wa-isolate>
|
||||
<wa-scoped class="theme-icon-host theme-color-icon-host">
|
||||
<template>
|
||||
<link rel="stylesheet" href="/dist/styles/utilities.css">
|
||||
<link rel="stylesheet" href="/dist/styles/themes/{{ page.fileSlug or 'default' }}.css">
|
||||
<link rel="stylesheet" href="/dist/styles/themes/{{ themeId }}/color.css">
|
||||
<link rel="stylesheet" href="/assets/styles/theme-icons.css">
|
||||
|
||||
<div class="theme-color-icon wa-theme-{{ themeId }}">
|
||||
<div class="theme-icon theme-color-icon wa-theme-{{ themeId }}">
|
||||
<div class="wa-brand wa-accent">A</div>
|
||||
<div class="wa-brand wa-outlined">A</div>
|
||||
<div class="wa-brand wa-filled">A</div>
|
||||
@@ -21,4 +21,4 @@
|
||||
{# <div class="wa-warning wa-outlined wa-filled"><wa-icon slot="icon" name="triangle-exclamation" variant="regular"></wa-icon></div> #}
|
||||
</div>
|
||||
</template>
|
||||
</wa-isolate>
|
||||
</wa-scoped>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% set themeId = theme.fileSlug %}
|
||||
{% set themeId = theme.fileSlug or page.fileSlug %}
|
||||
|
||||
<wa-isolate>
|
||||
<wa-scoped class="theme-icon-host theme-typography-icon-host">
|
||||
<template>
|
||||
<link rel="stylesheet" href="/dist/styles/native/content.css">
|
||||
<link rel="stylesheet" href="/dist/styles/native/blockquote.css">
|
||||
@@ -8,9 +8,9 @@
|
||||
<link rel="stylesheet" href="/dist/styles/themes/{{ themeId }}/typography.css">
|
||||
<link rel="stylesheet" href="/assets/styles/theme-icons.css">
|
||||
|
||||
<div class="theme-typography-icon wa-theme-{{ themeId }}" data-no-outline data-no-anchor role="presentation">
|
||||
<div class="theme-icon theme-typography-icon wa-theme-{{ themeId }}" data-no-outline data-no-anchor role="presentation">
|
||||
<h3>Title</h3>
|
||||
<p>Body text</p>
|
||||
</div>
|
||||
</template>
|
||||
</wa-isolate>
|
||||
</wa-scoped>
|
||||
|
||||
29
docs/_includes/svgs/theme.njk
Normal file
@@ -0,0 +1,29 @@
|
||||
{% set themeId = theme.fileSlug or page.fileSlug %}
|
||||
|
||||
|
||||
<wa-scoped class="theme-icon-host theme-overall-icon-host">
|
||||
<template>
|
||||
<link rel="stylesheet" href="/dist/styles/utilities.css">
|
||||
<link rel="stylesheet" href="/dist/styles/native/content.css">
|
||||
<link rel="stylesheet" href="/dist/styles/themes/{{ themeId }}.css">
|
||||
<link rel="stylesheet" href="/assets/styles/theme-icons.css">
|
||||
|
||||
<div class="theme-icon theme-overall-icon" role="presentation" data-no-anchor data-no-outline>
|
||||
<div class="row row-1">
|
||||
<h2>Aa</h2>
|
||||
<div class="swatches">
|
||||
<div class="wa-brand"></div>
|
||||
|
||||
<div class="wa-success"></div>
|
||||
<div class="wa-warning"></div>
|
||||
<div class="wa-danger"></div>
|
||||
<div class="wa-neutral"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-2">
|
||||
<wa-input value="Input" size="small" inert></wa-input>
|
||||
<wa-button size="small" variant="brand" inert>Go</wa-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</wa-scoped>
|
||||
@@ -1,14 +1,14 @@
|
||||
<div class="showcase-examples-wrapper" aria-hidden="true" data-no-outline>
|
||||
<div class="showcase-examples">
|
||||
<wa-card with-header with-footer>
|
||||
<wa-card>
|
||||
<div slot="header" class="wa-split">
|
||||
<h3 class="wa-heading-m">Your Cart</h3>
|
||||
<wa-icon-button name="xmark" tabindex="-1"></wa-icon-button>
|
||||
</div>
|
||||
<div class="wa-stack wa-gap-xl">
|
||||
<div class="wa-flank">
|
||||
<wa-avatar shape="rounded" style="--size: 3em; --background-color: var(--wa-color-green-60); --text-color: var(--wa-color-green-95);">
|
||||
<wa-icon slot="icon" name="sword-laser" family="duotone" style="font-size: 1.5em;"></wa-icon>
|
||||
<wa-avatar 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>
|
||||
<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="--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 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>
|
||||
<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" family="duotone" style="font-size: 1.75em;"></wa-icon>
|
||||
<wa-icon slot="icon" name="hat-wizard" style="font-size: 1.75em;"></wa-icon>
|
||||
</wa-avatar>
|
||||
<p class="wa-body-l" style="margin: 0;">“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.”</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 with-footer>
|
||||
<wa-card>
|
||||
<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" read-only></wa-rating>
|
||||
<wa-rating value="4.6" readonly tabindex="-1"></wa-rating>
|
||||
<strong>4.6</strong>
|
||||
<span>(419 reviews)</span>
|
||||
</div>
|
||||
@@ -140,5 +140,196 @@
|
||||
</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 haven’t 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>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<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 }}">
|
||||
|
||||
|
||||
@@ -255,7 +255,7 @@
|
||||
{# Importing #}
|
||||
<h2>Importing</h2>
|
||||
<p>
|
||||
The <a href="/docs/installation/#quick-start-autoloading-via-cdn">autoloader</a> is the recommended way to import components. If you prefer to do it manually, use one of the following code snippets.
|
||||
The <a href="/docs/#quick-start-autoloading-via-cdn">autoloader</a> is the recommended way to import components. If you prefer to do it manually, use one of the following code snippets.
|
||||
</p>
|
||||
|
||||
<wa-tab-group label="How would you like to import this component?">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
layout: page-outline
|
||||
tags: ["overview"]
|
||||
---
|
||||
{% set forTag = forTag or (page.url | split('/') | last) %}
|
||||
{% if description %}
|
||||
@@ -13,8 +12,10 @@ tags: ["overview"]
|
||||
</wa-input>
|
||||
</div>
|
||||
|
||||
{% set allPages = collections[forTag] %}
|
||||
{% set allPages = allPages or collections[forTag] %}
|
||||
{% if allPages and allPages.length > 0 %}
|
||||
{% include "grouped-pages.njk" %}
|
||||
{% endif %}
|
||||
|
||||
<link href="/assets/styles/filter.css" rel="stylesheet">
|
||||
<script type="module" src="/assets/scripts/filter.js"></script>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{% set hasSidebar = true %}
|
||||
{% set hasOutline = false %}
|
||||
{% if hasSidebar == undefined %}
|
||||
{% set hasSidebar = true %}
|
||||
{% endif %}
|
||||
|
||||
{% if hasOutline == undefined %}
|
||||
{% set hasOutline = false %}
|
||||
{% endif %}
|
||||
|
||||
{% extends "../_includes/base.njk" %}
|
||||
|
||||
@@ -1,17 +1,85 @@
|
||||
{% set hasSidebar = true %}
|
||||
{% set hasOutline = true %}
|
||||
{# {% set forceTheme = page.fileSlug %} #}
|
||||
|
||||
{% extends '../_layouts/block.njk' %}
|
||||
|
||||
{% set paletteId = page.fileSlug %}
|
||||
|
||||
{% block afterContent %}
|
||||
<style>@import url('/dist/styles/color/{{ paletteId }}.css') layer(palette.{{ paletteId }});</style>
|
||||
|
||||
{% set tints = ["95", "90", "80", "70", "60", "50", "40", "30", "20", "10", "05"] %}
|
||||
|
||||
<table class="colors wa-palette-{{ paletteId }}">
|
||||
{% extends '../_includes/base.njk' %}
|
||||
|
||||
{% block head %}
|
||||
<style>@import url('/dist/styles/color/{{ paletteId }}.css') layer(palette.{{ paletteId }});</style>
|
||||
<link href="{{ page.url }}../tweak.css" rel="stylesheet">
|
||||
<script type="module" src="{{ page.url }}../tweak.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<div id="palette-app" data-palette-id="{{ paletteId }}">
|
||||
<div
|
||||
:class="{
|
||||
tweaking: tweaking.chroma,
|
||||
'tweaking-chroma': tweaking.chroma,
|
||||
'tweaking-hue': tweaking.chroma,
|
||||
'tweaking-gray-chroma': tweaking.grayChroma,
|
||||
'tweaked-chroma': tweaked?.chroma,
|
||||
'tweaked-hue': tweaked?.hue,
|
||||
'tweaked-any': tweaked
|
||||
}"
|
||||
:style="{
|
||||
'--chroma-scale': chromaScale,
|
||||
'--gray-chroma': tweaked?.grayChroma ? grayChroma : '',
|
||||
}">
|
||||
|
||||
{% include 'breadcrumbs.njk' %}
|
||||
|
||||
<h1 class="title">
|
||||
<span v-content="title">{{ title }}</span>
|
||||
<template v-if="saved || tweaked">
|
||||
<wa-icon-button name="pencil" label="Rename palette" @click="rename"></wa-icon-button>
|
||||
<wa-icon-button v-if="saved" class="delete" name="trash" label="Delete palette" @click="deleteSaved"></wa-icon-button>
|
||||
<wa-button @click="save()" :disabled="!unsavedChanges"
|
||||
:variant="unsavedChanges ? 'success' : 'neutral'" size="small" :appearance="unsavedChanges ? 'accent' : 'outlined'">
|
||||
<span slot="prefix" class="icon-modifier">
|
||||
<wa-icon name="sidebar" variant="regular"></wa-icon>
|
||||
<wa-icon name="circle-plus" class="modifier" style="color: light-dark(var(--wa-color-green-70), var(--wa-color-green-60));"></wa-icon>
|
||||
</span>
|
||||
<span v-content="unsavedChanges ? 'Save' : 'Saved'">Save</span>
|
||||
</wa-button>
|
||||
</template>
|
||||
</h1>
|
||||
|
||||
<div class="block-info">
|
||||
<code class="class">.wa-palette-{{ paletteId }}</code>
|
||||
{% include '../_includes/status.njk' %}
|
||||
{% if not isPro %}
|
||||
<wa-badge class="pro" v-if="tweaked">PRO</wa-badge>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if description %}
|
||||
<p class="summary">
|
||||
{{ description | inlineMarkdown | safe }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block afterContent %}
|
||||
|
||||
{% set maxChroma = 0 %}
|
||||
|
||||
<wa-callout size="small" class="tweaked-callout" variant="warning">
|
||||
<wa-icon name="sliders-simple" slot="icon" variant="regular"></wa-icon>
|
||||
This palette has been tweaked.
|
||||
<div class="wa-cluster wa-gap-xs">
|
||||
<wa-tag v-for="tweakHumanReadable, param in tweaksHumanReadable" removable @wa-remove="reset(param)" v-content="tweakHumanReadable"></wa-tag>
|
||||
</div>
|
||||
|
||||
<wa-button @click="reset()" appearance="outlined" variant="danger">
|
||||
<span slot="prefix" class="icon-modifier">
|
||||
<wa-icon name="circle-xmark" variant="regular"></wa-icon>
|
||||
</span>
|
||||
Reset
|
||||
</wa-button>
|
||||
</wa-callout>
|
||||
|
||||
<table class="colors main wa-palette-{{ paletteId }}">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
@@ -21,18 +89,89 @@
|
||||
{%- endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
{# Initialize to last hue before gray #}
|
||||
{%- set hueBefore = hues[hues|length - 2] -%}
|
||||
{% for hue in hues -%}
|
||||
<tr>
|
||||
<th>{{ hue | capitalize }}</th>
|
||||
<td class="core-column">
|
||||
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}); color: var(--wa-color-{{ hue }}-{{ '05' if palettes[paletteId][hue].maxChromaTint > 60 else '95' }});">
|
||||
{{ palettes[paletteId][hue].maxChromaTint }}
|
||||
<wa-copy-button value="--wa-color-{{ hue }}" copy-label="--wa-color-{{ hue }}"></wa-copy-button>
|
||||
</div>
|
||||
{% set coreTint = palettes[paletteId][hue].maxChromaTint %}
|
||||
{%- set coreColor = palettes[paletteId][hue][coreTint] -%}
|
||||
{%- set maxChroma = coreColor.c if coreColor.c > maxChroma else maxChroma -%}
|
||||
{% if hue === 'gray' %}
|
||||
<tr data-hue="{{ hue }}" class="color-scale"
|
||||
:class="{tweaking: tweaking.grayChroma, tweaked: tweaked.grayChroma || tweaked.grayColor }">
|
||||
{% else %}
|
||||
<tr data-hue="{{ hue }}" class="color-scale"
|
||||
:class="{tweaking: tweaking.{{ hue }}, tweaked: hueShifts.{{ hue }} }"
|
||||
:style="{ '--hue-shift': hueShifts.{{ hue }} || '' }">
|
||||
{% endif %}
|
||||
<th>
|
||||
{{ hue | capitalize }}
|
||||
</th>
|
||||
<td class="core-column"
|
||||
style="--color: var(--wa-color-{{ hue }})"
|
||||
:style="{
|
||||
'--color-tweaked': colors.{{ hue }}[{{ coreTint }}],
|
||||
'--color-gray-undertone': colors[grayColor][{{coreTint}}],
|
||||
'--color-tweaked-no-gray-chroma': colorsMinusGrayChroma.{{ hue }}[{{ coreTint }}],
|
||||
}">
|
||||
<wa-dropdown>
|
||||
<div slot="trigger" id="core-{{ hue }}-swatch" data-tint="core" class="color swatch"
|
||||
style="background-color: var(--wa-color-{{ hue }}); color: var(--wa-color-{{ hue }}-{{ '05' if palettes[paletteId][hue].maxChromaTint > 60 else '95' }});"
|
||||
>
|
||||
{{ palettes[paletteId][hue].maxChromaTint }}
|
||||
<wa-icon name="sliders-simple" class="tweak-icon"></wa-icon>
|
||||
</div>
|
||||
<div class="popup">
|
||||
{% if hue === 'gray' %}
|
||||
<swatch-select label="Gray undertone" shape="circle" :values="hues" v-model="grayColor"></swatch-select>
|
||||
|
||||
<div class="decorated-slider gray-chroma-slider" :style="{'--max': maxGrayChroma}">
|
||||
<wa-slider name="gray-chroma" v-model="grayChroma" ref="grayChromaSlider"
|
||||
value="0" min="0" :max="maxGrayChroma" step="0.01"
|
||||
@input="tweaking.grayChroma = true" @change="tweaking.grayChroma = false">
|
||||
<div slot="label">
|
||||
Gray colorfulness
|
||||
<wa-icon-button @click="grayChroma = originalGrayChroma" class="clear-button" name="circle-xmark" library="system" variant="regular" label="Reset"></wa-icon-button>
|
||||
</div>
|
||||
</wa-slider>
|
||||
<div class="label-min">Neutral</div>
|
||||
<div class="label-max" v-content="moreHue[grayColor]">Warmer/Cooler</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{%- set hueAfter = hues[loop.index0 + 1] -%}
|
||||
{%- set hueAfter = hues[0] if hueAfter == 'gray' else hueAfter -%}
|
||||
{%- set minShift = hueRanges[hue].min - coreColor.h | round -%}
|
||||
{%- set maxShift = hueRanges[hue].max - coreColor.h | round -%}
|
||||
|
||||
<div class="decorated-slider hue-shift-slider" style="--min: {{ minShift }}; --max: {{ maxShift }};">
|
||||
<wa-slider name="{{ hue }}-shift" v-model="hueShifts.{{ hue }}" value="0"
|
||||
min="{{ minShift }}" max="{{ maxShift }}" step="1"
|
||||
@input="tweaking.hue = tweaking.{{hue}} = true"
|
||||
@change="tweaking.hue = tweaking.{{ hue }} = false">
|
||||
<div slot="label">
|
||||
Tweak {{ hue }} hue
|
||||
<wa-icon-button @click="hueShifts.{{ hue }} = 0" class="clear-button" name="circle-xmark" library="system" variant="regular" label="Reset"></wa-icon-button>
|
||||
</div>
|
||||
</wa-slider>
|
||||
<div class="label-min">More {{hueBefore}}</div>
|
||||
<div class="label-max">More {{hueAfter}}</div>
|
||||
</div>
|
||||
{%- set hueBefore = hue -%}
|
||||
{% endif %}
|
||||
<div class="wa-gap-s">
|
||||
<code>--wa-color-{{ hue }}</code>
|
||||
<wa-copy-button value="--wa-color-{{ hue }}" copy-label="--wa-color-{{ hue }}"></wa-copy-button>
|
||||
</div>
|
||||
</div>`
|
||||
</wa-dropdown>
|
||||
</td>
|
||||
{% for tint in tints -%}
|
||||
<td>
|
||||
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}-{{ tint }})">
|
||||
{%- set color = palettes[paletteId][hue][tint] -%}
|
||||
<td data-tint="{{ tint }}" style="--color: var(--wa-color-{{ hue }}-{{ tint }})"
|
||||
:style="{
|
||||
'--color-tweaked': colors.{{ hue }}[{{ tint }}],
|
||||
'--color-tweaked-no-gray-chroma': colorsMinusGrayChroma.{{ hue }}[{{ tint }}],
|
||||
}">
|
||||
<div class="color swatch" style="--color: var(--wa-color-{{ hue }}-{{ tint }})">
|
||||
<wa-copy-button value="--wa-color-{{ hue }}-{{ tint }}" copy-label="--wa-color-{{ hue }}-{{ tint }}"></wa-copy-button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -41,6 +180,26 @@
|
||||
{%- endfor %}
|
||||
</table>
|
||||
|
||||
{% set chromaScaleBounds = [
|
||||
(0.08 / maxChroma) | number({maximumFractionDigits: 2}),
|
||||
(0.3 / maxChroma]) | number({maximumFractionDigits: 2}) -%}
|
||||
<div class="decorated-slider chroma-scale-slider wa-palette-{{ paletteId }}"
|
||||
:class="{ tweaked: chromaScale !== 1 }"
|
||||
style="--min: {{ chromaScaleBounds[0] }}; --max: {{ chromaScaleBounds[1] }};">
|
||||
<wa-slider name="chroma-scale" ref="chromaScaleSlider"
|
||||
v-model="chromaScale" value="1" step="0.01"
|
||||
min="{{ chromaScaleBounds[0] }}" max="{{ chromaScaleBounds[1] }}"
|
||||
@input="tweaking.chroma = true"
|
||||
@change="tweaking.chroma = false">
|
||||
<div slot="label">
|
||||
Overall colorfulness
|
||||
<wa-icon-button @click="chromaScale = 1" class="clear-button" name="circle-xmark" library="system" variant="regular" label="Reset"></wa-icon-button>
|
||||
</div>
|
||||
</wa-slider>
|
||||
<div class="label-min">More muted</div>
|
||||
<div class="label-max">More vibrant</div>
|
||||
</div>
|
||||
|
||||
<h2>Used By</h2>
|
||||
|
||||
<section class="index-grid">
|
||||
@@ -65,6 +224,7 @@ A difference of `40` ensures a minimum **3:1** contrast ratio, suitable for larg
|
||||
{% endmarkdown %}
|
||||
|
||||
{% set difference = 40 %}
|
||||
{% set minContrast = 3 %}
|
||||
{% include "contrast-table.njk" %}
|
||||
|
||||
{% markdown %}
|
||||
@@ -84,6 +244,7 @@ A difference of `50` ensures a minimum **4.5:1** contrast ratio, suitable for no
|
||||
{% endmarkdown %}
|
||||
|
||||
{% set difference = 50 %}
|
||||
{% set minContrast = 4.5 %}
|
||||
{% include "contrast-table.njk" %}
|
||||
|
||||
{% markdown %}
|
||||
@@ -102,6 +263,7 @@ A difference of `60` ensures a minimum **7:1** contrast ratio, suitable for all
|
||||
{% endmarkdown %}
|
||||
|
||||
{% set difference = 60 %}
|
||||
{% set minContrast = 7 %}
|
||||
{% include "contrast-table.njk" %}
|
||||
|
||||
{% markdown %}
|
||||
@@ -114,13 +276,34 @@ This also goes for a difference of `65`:
|
||||
{% include "contrast-table.njk" %}
|
||||
|
||||
{% markdown %}
|
||||
## How to use this palette
|
||||
## How to use this palette { #usage }
|
||||
|
||||
If you are using a Web Awesome theme that uses this palette, it will already be included.
|
||||
To use a different palette than a theme default, or to use it in a custom theme, you can import this palette directly from the Web Awesome CDN.
|
||||
|
||||
{% set stylesheet = 'styles/color/' + page.fileSlug + '.css' %}
|
||||
{% include 'import-stylesheet-code.md.njk' %}
|
||||
<wa-tab-group class="import-stylesheet-code">
|
||||
<wa-tab panel="html">In HTML</wa-tab>
|
||||
<wa-tab panel="css">In CSS</wa-tab>
|
||||
<wa-tab-panel name="html">
|
||||
|
||||
Add the following code to the `<head>` of your page:
|
||||
```html { v-content:html="code.html.highlighted" }
|
||||
<link rel="stylesheet" href="{% cdnUrl stylesheet %}" />
|
||||
```
|
||||
</wa-tab-panel>
|
||||
<wa-tab-panel name="css">
|
||||
|
||||
Add the following code at the top of your CSS file:
|
||||
```css { v-content:html="code.css.highlighted" }
|
||||
@import url('{% cdnUrl stylesheet %}');
|
||||
```
|
||||
</wa-tab-panel>
|
||||
</wa-tab-group>
|
||||
|
||||
|
||||
{% endmarkdown %}
|
||||
</div></div> {# end palette app #}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
5
docs/_layouts/patterns.njk
Normal file
@@ -0,0 +1,5 @@
|
||||
{% extends '../_layouts/block.njk' %}
|
||||
|
||||
{% block head %}
|
||||
<link href="/docs/patterns/patterns.css" rel="stylesheet">
|
||||
{% endblock %}
|
||||
@@ -5,35 +5,22 @@
|
||||
{% 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 src="{{ page.url }}../remix.js" type="module"></script>
|
||||
<script type="module" src="{{ page.url }}../edit/index.js"></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 src='{{ page.url }}demo.html' id="demo"></iframe>
|
||||
<iframe ref="preview" :src="'{{ page.url }}demo.html' + urlParams" src='{{ page.url }}demo.html' id="demo"></iframe>
|
||||
|
||||
<wa-details id="mix_and_match" class="wa-gap-m" >
|
||||
{% if page.fileSlug !== 'custom' %}
|
||||
<wa-details id="mix_and_match" class="wa-gap-m" :open="saved || unsavedChanges">
|
||||
<h4 slot="summary" data-no-anchor data-no-outline id="remix">
|
||||
<wa-icon name="arrows-rotate"></wa-icon>
|
||||
Remix this theme
|
||||
@@ -41,92 +28,64 @@ wa_data.palettes = {
|
||||
<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="colors" label="Colors from…" value="" clearable>
|
||||
<wa-icon name="palette" slot="prefix" variant="regular"></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-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>
|
||||
{% endfor %}
|
||||
</wa-select>
|
||||
|
||||
<wa-select name="palette" label="Palette" clearable>
|
||||
<wa-select name="palette" label="Color palette" clearable v-model="theme.palette">
|
||||
<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-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>
|
||||
|
||||
<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 %}
|
||||
<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-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>
|
||||
</wa-option>
|
||||
{% endfor %}
|
||||
</template>
|
||||
</wa-select>
|
||||
|
||||
<wa-select name="typography" label="Typography from…" clearable>
|
||||
<wa-select name="typography" label="Typography from…" clearable v-model="theme.typography">
|
||||
<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-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>
|
||||
</wa-details>
|
||||
|
||||
{% endif %}
|
||||
<h2>Color</h2>
|
||||
{% set paletteURL = '/docs/palettes/' + palette + '/' %}
|
||||
|
||||
|
||||
<div class="index-grid">
|
||||
{% 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 }}">
|
||||
{% 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}">
|
||||
<div slot="header"></div>
|
||||
<div class="page-name">{{ brand | capitalize }}</div>
|
||||
<div class="wa-caption-s">Default brand color</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>
|
||||
</wa-card>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -139,7 +98,25 @@ wa_data.palettes = {
|
||||
You can import this theme from the Web Awesome CDN.
|
||||
|
||||
{% set stylesheet = 'styles/themes/' + page.fileSlug + '.css' %}
|
||||
{% include 'import-stylesheet-code.md.njk' %}
|
||||
|
||||
<wa-tab-group class="import-stylesheet-code">
|
||||
<wa-tab panel="html">In HTML</wa-tab>
|
||||
<wa-tab panel="css">In CSS</wa-tab>
|
||||
<wa-tab-panel name="html">
|
||||
|
||||
Add the following code to the `<head>` of your page:
|
||||
```html { v-content:html="code.html.highlighted" }
|
||||
<link rel="stylesheet" href="{% cdnUrl stylesheet %}" />
|
||||
```
|
||||
</wa-tab-panel>
|
||||
<wa-tab-panel name="css">
|
||||
|
||||
Add the following code at the top of your CSS file:
|
||||
```css { v-content:html="code.css.highlighted" }
|
||||
@import url('{% cdnUrl stylesheet %}');
|
||||
```
|
||||
</wa-tab-panel>
|
||||
</wa-tab-group>
|
||||
|
||||
## Dark mode
|
||||
|
||||
@@ -203,5 +180,6 @@ systemDark.addEventListener('change', applyDark);
|
||||
applyDark();
|
||||
```
|
||||
|
||||
</div> {# end theme app #}
|
||||
{% endmarkdown %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -102,12 +102,7 @@ const templates = {
|
||||
export function codeExamplesPlugin(eleventyConfig, options = {}) {
|
||||
const defaultOptions = {
|
||||
container: 'body',
|
||||
defaultOpen: (code, { outputPathIndex }) => {
|
||||
return (
|
||||
outputPathIndex === 1 && // is first
|
||||
code.textContent.length < 500
|
||||
); // is short
|
||||
},
|
||||
defaultOpen: () => false,
|
||||
};
|
||||
options = { ...defaultOptions, ...options };
|
||||
|
||||
|
||||
@@ -3,30 +3,39 @@ import { parse } from 'node-html-parser';
|
||||
/**
|
||||
* Eleventy plugin to add copy buttons to code blocks.
|
||||
*/
|
||||
export function copyCodePlugin(options = {}) {
|
||||
export function copyCodePlugin(eleventyConfig, options = {}) {
|
||||
options = {
|
||||
container: 'body',
|
||||
...options,
|
||||
};
|
||||
|
||||
return function (eleventyConfig) {
|
||||
eleventyConfig.addTransform('copy-code', content => {
|
||||
const doc = parse(content, { blockTextElements: { code: true } });
|
||||
const container = doc.querySelector(options.container);
|
||||
let codeCount = 0;
|
||||
eleventyConfig.addTransform('copy-code', content => {
|
||||
const doc = parse(content, { blockTextElements: { code: true } });
|
||||
const container = doc.querySelector(options.container);
|
||||
|
||||
if (!container) {
|
||||
return content;
|
||||
if (!container) {
|
||||
return content;
|
||||
}
|
||||
|
||||
// Look for code blocks
|
||||
container.querySelectorAll('pre > code').forEach(code => {
|
||||
const pre = code.closest('pre');
|
||||
let preId = pre.getAttribute('id') || `code-block-${++codeCount}`;
|
||||
let codeId = code.getAttribute('id') || `${preId}-inner`;
|
||||
|
||||
if (!code.getAttribute('id')) {
|
||||
code.setAttribute('id', codeId);
|
||||
}
|
||||
if (!pre.getAttribute('id')) {
|
||||
pre.setAttribute('id', preId);
|
||||
}
|
||||
|
||||
// Look for code blocks
|
||||
container.querySelectorAll('pre > code').forEach(code => {
|
||||
const pre = code.closest('pre');
|
||||
|
||||
// Add a copy button (we set the copy data at runtime to reduce page bloat)
|
||||
pre.innerHTML = `<wa-copy-button class="copy-button" hoist></wa-copy-button>` + pre.innerHTML;
|
||||
});
|
||||
|
||||
return doc.toString();
|
||||
// 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>`;
|
||||
});
|
||||
};
|
||||
|
||||
return doc.toString();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ function getCollection(name) {
|
||||
}
|
||||
|
||||
export function getCollectionItemFromUrl(url, collection) {
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
collection ??= getCollection.call(this, 'all') || [];
|
||||
return collection.find(item => item.url === url);
|
||||
}
|
||||
@@ -42,35 +45,33 @@ export function split(text, separator) {
|
||||
return (text + '').split(separator).filter(Boolean);
|
||||
}
|
||||
|
||||
export function breadcrumbs(url, { withCurrent = false } = {}) {
|
||||
const parts = split(url, '/');
|
||||
const ret = [];
|
||||
export function ancestors(url, { withCurrent = false, withRoot = false } = {}) {
|
||||
let ret = [];
|
||||
let currentUrl = url;
|
||||
let currentItem = getCollectionItemFromUrl.call(this, url);
|
||||
|
||||
while (parts.length) {
|
||||
let partialUrl = '/' + parts.join('/') + '/';
|
||||
let item = getCollectionItemFromUrl.call(this, partialUrl);
|
||||
|
||||
if (item && (partialUrl !== url || withCurrent)) {
|
||||
let title = item.data.title;
|
||||
if (title) {
|
||||
ret.unshift({ url: partialUrl, title });
|
||||
}
|
||||
}
|
||||
|
||||
parts.pop();
|
||||
|
||||
if (item?.data.parent) {
|
||||
let parentURL = item.data.parent;
|
||||
if (!item.data.parent.startsWith('/')) {
|
||||
// Parent is in the same directory
|
||||
parts.push(item.data.parent);
|
||||
parentURL = '/' + parts.join('/') + '/';
|
||||
}
|
||||
|
||||
let parentBreadcrumbs = breadcrumbs.call(this, parentURL, { withCurrent: true });
|
||||
return [...parentBreadcrumbs, ...ret];
|
||||
if (!currentItem) {
|
||||
// Might have eleventyExcludeFromCollections, jump to parent
|
||||
let parentUrl = this.ctx.parentUrl;
|
||||
if (parentUrl) {
|
||||
url = parentUrl;
|
||||
}
|
||||
}
|
||||
|
||||
for (let item; (item = getCollectionItemFromUrl.call(this, url)); url = item.data.parentUrl) {
|
||||
ret.unshift(item);
|
||||
}
|
||||
|
||||
if (!withRoot && ret[0]?.page.url === '/') {
|
||||
// Remove root
|
||||
ret.shift();
|
||||
}
|
||||
|
||||
if (!withCurrent && ret.at(-1)?.page.url === currentUrl) {
|
||||
// Remove current page
|
||||
ret.pop();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -180,69 +181,185 @@ export function sort(arr, by = { 'data.order': 1, 'data.title': '' }) {
|
||||
/**
|
||||
* Group an 11ty collection (or any array of objects with a `data.tags` property) by certain tags.
|
||||
* @param {object[]} collection
|
||||
* @param { Object<string, string> | (string | Object<string, string>)[]} [tags] The tags to group by. If not provided/empty, defaults to grouping by all tags.
|
||||
* @returns { Object.<string, object[]> } An object with keys for each tag, and an array of items for each tag.
|
||||
* @param { Object<string, string> | string[]} [options] Options object or array of tags to group by.
|
||||
* @param {string[] | true} [options.tags] Tags to group by. If true, groups by all tags.
|
||||
* If not provided/empty, defaults to grouping by page hierarchy, with any pages with more than 1 children becoming groups.
|
||||
* @param {string[]} [options.groups] The groups to use if only a subset or a specific order is desired. Defaults to `options.tags`.
|
||||
* @param {string[]} [options.titles] Any title overrides for groups.
|
||||
* @param {string | false} [options.other="Other"] The title to use for the "Other" group. If `false`, the "Other" group is removed..
|
||||
* @returns { Object.<string, object[]> } An object of group ids to arrays of page objects.
|
||||
*/
|
||||
export function groupByTags(collection, tags) {
|
||||
export function groupPages(collection, options = {}, page) {
|
||||
if (!collection) {
|
||||
console.error(`Empty collection passed to groupByTags() to group by ${JSON.stringify(tags)}`);
|
||||
}
|
||||
if (!tags) {
|
||||
// Default to grouping by union of all tags
|
||||
tags = Array.from(new Set(collection.flatMap(item => item.data.tags)));
|
||||
} else if (Array.isArray(tags)) {
|
||||
// May contain objects of one-off tag -> label mappings
|
||||
tags = tags.map(tag => (typeof tag === 'object' ? Object.keys(tag)[0] : tag));
|
||||
} else if (typeof tags === 'object') {
|
||||
// tags is an object of tags to labels, so we just want the keys
|
||||
tags = Object.keys(tags);
|
||||
console.error(`Empty collection passed to groupPages() to group by ${JSON.stringify(options)}`);
|
||||
}
|
||||
|
||||
let ret = Object.fromEntries(tags.map(tag => [tag, []]));
|
||||
ret.other = [];
|
||||
if (Array.isArray(options)) {
|
||||
options = { tags: options };
|
||||
}
|
||||
|
||||
let { tags, groups, titles = {}, other = 'Other' } = options;
|
||||
|
||||
if (groups === undefined && Array.isArray(tags)) {
|
||||
groups = tags;
|
||||
}
|
||||
|
||||
let grouping;
|
||||
|
||||
if (tags) {
|
||||
grouping = {
|
||||
isGroup: item => undefined,
|
||||
getCandidateGroups: item => item.data.tags,
|
||||
getGroupMeta: group => ({}),
|
||||
};
|
||||
} else {
|
||||
grouping = {
|
||||
isGroup: item => (item.data.children.length >= 2 ? item.page.url : undefined),
|
||||
getCandidateGroups: item => {
|
||||
let parentUrl = item.data.parentUrl;
|
||||
if (page?.url === parentUrl) {
|
||||
return [];
|
||||
}
|
||||
return [parentUrl];
|
||||
},
|
||||
getGroupMeta: group => {
|
||||
let item = byUrl[group] || getCollectionItemFromUrl.call(this, group);
|
||||
return {
|
||||
title: item?.data.title,
|
||||
url: group,
|
||||
item,
|
||||
};
|
||||
},
|
||||
sortGroups: groups => sort(groups.map(url => byUrl[url]).filter(Boolean)).map(item => item.page.url),
|
||||
};
|
||||
}
|
||||
|
||||
let byUrl = {};
|
||||
let byParentUrl = {};
|
||||
|
||||
for (let item of collection) {
|
||||
let categorized = false;
|
||||
let url = item.page.url;
|
||||
let parentUrl = item.data.parentUrl;
|
||||
|
||||
for (let tag of tags) {
|
||||
if (item.data.tags.includes(tag)) {
|
||||
ret[tag].push(item);
|
||||
categorized = true;
|
||||
}
|
||||
}
|
||||
byUrl[url] = item;
|
||||
|
||||
if (!categorized) {
|
||||
ret.other.push(item);
|
||||
if (parentUrl) {
|
||||
byParentUrl[parentUrl] ??= [];
|
||||
byParentUrl[parentUrl].push(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove empty categories
|
||||
for (let category in ret) {
|
||||
if (ret[category].length === 0) {
|
||||
delete ret[category];
|
||||
let urlToGroups = {};
|
||||
|
||||
for (let item of collection) {
|
||||
let url = item.page.url;
|
||||
let parentUrl = item.data.parentUrl;
|
||||
|
||||
if (grouping.isGroup(item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let parentItem = byUrl[parentUrl];
|
||||
if (parentItem && !grouping.isGroup(parentItem)) {
|
||||
// Their parent is also here and is not a group
|
||||
continue;
|
||||
}
|
||||
|
||||
let candidateGroups = grouping.getCandidateGroups(item);
|
||||
|
||||
if (groups) {
|
||||
candidateGroups = candidateGroups.filter(group => groups.includes(group));
|
||||
}
|
||||
|
||||
urlToGroups[url] ??= [];
|
||||
|
||||
for (let group of candidateGroups) {
|
||||
urlToGroups[url].push(group);
|
||||
}
|
||||
}
|
||||
|
||||
let ret = {};
|
||||
|
||||
for (let url in urlToGroups) {
|
||||
let groups = urlToGroups[url];
|
||||
let item = byUrl[url];
|
||||
|
||||
if (groups.length === 0) {
|
||||
// Not filtered out but also not categorized
|
||||
groups = ['other'];
|
||||
}
|
||||
|
||||
for (let group of groups) {
|
||||
ret[group] ??= [];
|
||||
ret[group].push(item);
|
||||
|
||||
if (!ret[group].meta) {
|
||||
if (group === 'other') {
|
||||
ret[group].meta = { title: other };
|
||||
} else {
|
||||
ret[group].meta = grouping.getGroupMeta(group);
|
||||
ret[group].meta.title = titles[group] ?? ret[group].meta.title ?? capitalize(group);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (other === false) {
|
||||
delete ret.other;
|
||||
}
|
||||
|
||||
// Sort
|
||||
let sortedGroups = groups ?? grouping.sortGroups?.(Object.keys(ret));
|
||||
|
||||
if (sortedGroups) {
|
||||
ret = sortObject(ret, sortedGroups);
|
||||
} else {
|
||||
// At least make sure other is last
|
||||
if (ret.other) {
|
||||
let otherGroup = ret.other;
|
||||
delete ret.other;
|
||||
ret.other = otherGroup;
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(ret, 'meta', {
|
||||
value: {
|
||||
groupCount: Object.keys(ret).length,
|
||||
},
|
||||
enumerable: false,
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort an object by its keys
|
||||
* @param {*} obj
|
||||
* @param {function | string[]} order
|
||||
*/
|
||||
function sortObject(obj, order) {
|
||||
let ret = {};
|
||||
let sortedKeys = Array.isArray(order) ? order : Object.keys(obj).sort(order);
|
||||
|
||||
for (let key of sortedKeys) {
|
||||
if (key in obj) {
|
||||
ret[key] = obj[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Add any keys that weren't in the order
|
||||
for (let key in obj) {
|
||||
if (!(key in ret)) {
|
||||
ret[key] = obj[key];
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function getCategoryTitle(category, categories) {
|
||||
let title;
|
||||
if (Array.isArray(categories)) {
|
||||
// Find relevant entry
|
||||
// [{id: "Title"}, id2, ...]
|
||||
title = categories.find(entry => typeof entry === 'object' && entry?.[category])?.[category];
|
||||
} else if (typeof categories === 'object') {
|
||||
// {id: "Title", id2: "Title 2", ...}
|
||||
title = categories[category];
|
||||
}
|
||||
|
||||
if (title) {
|
||||
return title;
|
||||
}
|
||||
|
||||
// Capitalized
|
||||
return category.charAt(0).toUpperCase() + category.slice(1);
|
||||
function capitalize(str) {
|
||||
str += '';
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
const IDENTITY = x => x;
|
||||
@@ -282,3 +399,12 @@ 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);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export function outlinePlugin(options = {}) {
|
||||
}
|
||||
|
||||
// Create a clone of the heading so we can remove links and [data-no-outline] elements from the text content
|
||||
clone.querySelectorAll('a').forEach(a => a.remove());
|
||||
clone.querySelectorAll('.wa-visually-hidden, [hidden], [aria-hidden="true"]').forEach(el => el.remove());
|
||||
clone.querySelectorAll('[data-no-outline]').forEach(el => el.remove());
|
||||
|
||||
// Generate the link
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
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();
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
import { mkdir, writeFile } from 'fs/promises';
|
||||
import lunr from 'lunr';
|
||||
import { parse } from 'node-html-parser';
|
||||
import * as path from 'path';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
function collapseWhitespace(string) {
|
||||
@@ -23,9 +24,23 @@ export function searchPlugin(options = {}) {
|
||||
};
|
||||
|
||||
return function (eleventyConfig) {
|
||||
const pagesToIndex = [];
|
||||
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;
|
||||
});
|
||||
|
||||
eleventyConfig.addTransform('search', function (content) {
|
||||
if (!pagesToIndex.has(this.page.inputPath)) {
|
||||
return content;
|
||||
}
|
||||
|
||||
const doc = parse(content, {
|
||||
blockTextElements: {
|
||||
script: false,
|
||||
@@ -41,7 +56,7 @@ export function searchPlugin(options = {}) {
|
||||
doc.querySelectorAll(selector).forEach(el => el.remove());
|
||||
});
|
||||
|
||||
pagesToIndex.push({
|
||||
pagesToIndex.set(this.page.inputPath, {
|
||||
title: collapseWhitespace(options.getTitle(doc)),
|
||||
description: collapseWhitespace(options.getDescription(doc)),
|
||||
headings: options.getHeadings(doc).map(collapseWhitespace),
|
||||
@@ -52,8 +67,9 @@ export function searchPlugin(options = {}) {
|
||||
return content;
|
||||
});
|
||||
|
||||
eleventyConfig.on('eleventy.after', ({ dir }) => {
|
||||
const outputFilename = join(dir.output, 'search.json');
|
||||
eleventyConfig.on('eleventy.after', ({ directories }) => {
|
||||
const { output } = directories;
|
||||
const outputFilename = path.resolve(join(output, 'search.json'));
|
||||
const map = [];
|
||||
const searchIndex = lunr(async function () {
|
||||
let index = 0;
|
||||
@@ -63,7 +79,7 @@ export function searchPlugin(options = {}) {
|
||||
this.field('h', { boost: 10 });
|
||||
this.field('c');
|
||||
|
||||
for (const page of pagesToIndex) {
|
||||
for (const [_inputPath, 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++;
|
||||
|
||||
172
docs/assets/components/scoped.js
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
47
docs/assets/data/colors.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Data related to palettes and colors.
|
||||
* Must work in both browser and Node.js
|
||||
*/
|
||||
|
||||
export const tints = ['05', '10', '20', '30', '40', '50', '60', '70', '80', '90', '95'];
|
||||
|
||||
export const hueRanges = {
|
||||
red: { min: 5, max: 35 }, // 30
|
||||
orange: { min: 35, max: 60 }, // 25
|
||||
yellow: { min: 60, max: 112 }, // 45
|
||||
green: { min: 112, max: 170 }, // 55
|
||||
cyan: { min: 170, max: 220 }, // 50
|
||||
blue: { min: 220, max: 265 }, // 45
|
||||
indigo: { min: 265, max: 290 }, // 25
|
||||
purple: { min: 290, max: 320 }, // 30
|
||||
pink: { min: 320, max: 365 }, // 45
|
||||
};
|
||||
|
||||
export const 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/
|
||||
yellow: 'Yellower',
|
||||
green: 'Greener',
|
||||
cyan: 'More cyan',
|
||||
blue: 'Bluer',
|
||||
indigo: 'More indigo',
|
||||
pink: 'Pinker',
|
||||
};
|
||||
|
||||
/**
|
||||
* Max gray chroma (% of chroma of undertone) per hue
|
||||
*/
|
||||
export const maxGrayChroma = {
|
||||
red: 0.2,
|
||||
orange: 0.2,
|
||||
yellow: 0.25,
|
||||
green: 0.25,
|
||||
cyan: 0.3,
|
||||
blue: 0.35,
|
||||
indigo: 0.35,
|
||||
purple: 0.3,
|
||||
pink: 0.25,
|
||||
};
|
||||
65
docs/assets/data/fonts.js
Normal file
@@ -0,0 +1,65 @@
|
||||
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));
|
||||
7
docs/assets/data/icons.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export const iconLibraries = {
|
||||
default: {
|
||||
title: 'Font Awesome',
|
||||
family: ['classic', 'sharp', 'duotone', 'sharp-duotone'],
|
||||
style: ['solid', 'regular', 'light', 'thin'],
|
||||
},
|
||||
};
|
||||
6
docs/assets/data/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
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/';
|
||||
57
docs/assets/data/palettes.js.njk
Normal file
@@ -0,0 +1,57 @@
|
||||
---
|
||||
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;
|
||||
27
docs/assets/data/themes.js.njk
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
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 %}
|
||||
};
|
||||
118
docs/assets/data/theming.js
Normal file
@@ -0,0 +1,118 @@
|
||||
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'),
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
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();
|
||||
}
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
function setCopyValue() {
|
||||
document.querySelectorAll('.copy-button').forEach(copyButton => {
|
||||
const pre = copyButton.closest('pre');
|
||||
const code = pre?.querySelector('code');
|
||||
|
||||
if (code) {
|
||||
copyButton.value = code.textContent;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Set data for all copy buttons when the page loads
|
||||
setCopyValue();
|
||||
|
||||
document.addEventListener('turbo:load', setCopyValue);
|
||||
@@ -1,3 +1,11 @@
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function (...args) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
function updateResults(input) {
|
||||
const filter = input.value.toLowerCase().trim();
|
||||
let filtered = Boolean(filter);
|
||||
@@ -18,8 +26,10 @@ function updateResults(input) {
|
||||
}
|
||||
}
|
||||
|
||||
const debouncedUpdateResults = debounce(updateResults, 300);
|
||||
|
||||
document.documentElement.addEventListener('input', e => {
|
||||
if (e.target?.matches('#block-filter wa-input')) {
|
||||
updateResults(e.target);
|
||||
debouncedUpdateResults(e.target);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<button class="diff-dialog-toggle">
|
||||
Show Hydration Mismatch
|
||||
</button>
|
||||
<wa-dialog class="diff-dialog" with-header light-dismiss>
|
||||
<wa-dialog class="diff-dialog" light-dismiss>
|
||||
<div class="diff-grid">
|
||||
<div>
|
||||
<div>Server</div>
|
||||
|
||||
172
docs/assets/scripts/my.js
Normal file
@@ -0,0 +1,172 @@
|
||||
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',
|
||||
});
|
||||
178
docs/assets/scripts/permalink.js
Normal file
@@ -0,0 +1,178 @@
|
||||
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 there’s already a search, replace it.
|
||||
// We don’t want to clog the user’s 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];
|
||||
}
|
||||
8
docs/assets/scripts/prism-downloaded.js
Normal file
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* Get import code for remixed themes.
|
||||
*/
|
||||
export const urls = {
|
||||
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`,
|
||||
};
|
||||
|
||||
function getImport(url, options = {}) {
|
||||
let { language = 'html', cdnUrl = '/dist/', attributes } = options;
|
||||
url = cdnUrl + url;
|
||||
|
||||
if (language === 'css') {
|
||||
return `@import url('${url}');`;
|
||||
} else {
|
||||
attributes = attributes ? ` ${attributes}` : '';
|
||||
return `<link rel="stylesheet" href="${url}"${attributes} />`;
|
||||
}
|
||||
}
|
||||
|
||||
export function getCode(base, params, options) {
|
||||
let ret = [];
|
||||
|
||||
if (base) {
|
||||
ret.push(`styles/themes/${base}.css`);
|
||||
}
|
||||
|
||||
ret.push(
|
||||
...Object.entries(params)
|
||||
.filter(([aspect, id]) => Boolean(id))
|
||||
.map(([aspect, id]) => urls[aspect](id)),
|
||||
);
|
||||
|
||||
return ret.map(url => getImport(url, options)).join('\n');
|
||||
}
|
||||
120
docs/assets/scripts/sidebar-tweaks.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import my from '/assets/scripts/my.js';
|
||||
|
||||
const sidebar = {
|
||||
addChild(a, parentA) {
|
||||
let parentLi = parentA.closest('li');
|
||||
let ul = parentLi.querySelector(':scope > ul');
|
||||
ul ??= parentLi.appendChild(document.createElement('ul'));
|
||||
let li = document.createElement('li');
|
||||
li.append(a);
|
||||
ul.appendChild(li);
|
||||
|
||||
// If we are on the same page, update the current link
|
||||
let url = location.href.replace(/#.+$/, '');
|
||||
if (url.startsWith(a.href)) {
|
||||
// Remove existing current
|
||||
for (let current of document.querySelectorAll('#sidebar a.current')) {
|
||||
current.classList.remove('current');
|
||||
}
|
||||
|
||||
a.classList.add('current');
|
||||
}
|
||||
|
||||
return a;
|
||||
},
|
||||
|
||||
removeLink(a) {
|
||||
if (!a || !a.isConnected) {
|
||||
// Link doesn't exist or is already removed
|
||||
return;
|
||||
}
|
||||
|
||||
let li = a?.closest('li');
|
||||
let ul = li?.closest('ul');
|
||||
let parentA = ul?.closest('li')?.querySelector(':scope > a');
|
||||
|
||||
li?.remove();
|
||||
if (ul?.children.length === 0) {
|
||||
ul.remove();
|
||||
}
|
||||
|
||||
if (a.classList.contains('current')) {
|
||||
// If the deleted palette was the current one, the current one is now the parent
|
||||
parentA.classList.add('current');
|
||||
}
|
||||
},
|
||||
|
||||
findEntity(entity) {
|
||||
return document.querySelector(`#sidebar a[href^="${entity.baseUrl}"][data-uid="${entity.uid}"]`);
|
||||
},
|
||||
|
||||
renderEntity(entity) {
|
||||
let { url, parentUrl } = entity;
|
||||
|
||||
// Find parent
|
||||
let parentA = document.querySelector(`#sidebar a[href="${parentUrl}"]`);
|
||||
let parentLi = parentA?.closest('li');
|
||||
|
||||
if (!parentLi) {
|
||||
throw new Error(`Cannot find parent url ${parentUrl}`);
|
||||
}
|
||||
|
||||
// Find existing
|
||||
let a = this.findEntity(entity);
|
||||
let alreadyExisted = !!a;
|
||||
|
||||
a ??= document.createElement('a');
|
||||
|
||||
a.textContent = entity.title;
|
||||
a.href = url;
|
||||
|
||||
if (!alreadyExisted) {
|
||||
a.dataset.uid = entity.uid;
|
||||
|
||||
a = sidebar.addChild(a, parentA);
|
||||
|
||||
// This is mainly to port Pro badges
|
||||
let badges = Array.from(parentLi.querySelectorAll(':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);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
globalThis.sidebar = sidebar;
|
||||
|
||||
// Update sidebar when my saved stuff changes
|
||||
my.addEventListener('delete', e => sidebar.removeLink(sidebar.findEntity(e.detail)));
|
||||
my.addEventListener('save', e => sidebar.renderEntity(e.detail));
|
||||
|
||||
sidebar.render();
|
||||
window.addEventListener('turbo:render', () => sidebar.render());
|
||||
@@ -1,14 +1,5 @@
|
||||
// Helper for view transitions
|
||||
export function domChange(fn, { behavior = 'smooth' } = {}) {
|
||||
const canUseViewTransitions =
|
||||
document.startViewTransition && !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
if (canUseViewTransitions && behavior === 'smooth') {
|
||||
document.startViewTransition(fn);
|
||||
} else {
|
||||
fn(true);
|
||||
}
|
||||
}
|
||||
import { domChange } from './util/dom-change.js';
|
||||
export { domChange };
|
||||
|
||||
export function nextFrame() {
|
||||
return new Promise(resolve => requestAnimationFrame(resolve));
|
||||
@@ -100,6 +91,7 @@ const colorScheme = new ThemeAspect({
|
||||
domChange(() => {
|
||||
let dark = this.computedValue === 'dark';
|
||||
document.documentElement.classList.toggle(`wa-dark`, dark);
|
||||
document.documentElement.dispatchEvent(new CustomEvent('wa-color-scheme-change', { detail: { dark } }));
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -114,6 +106,6 @@ document.addEventListener('keydown', event => {
|
||||
!event.composedPath().some(el => ['input', 'textarea'].includes(el?.tagName?.toLowerCase()))
|
||||
) {
|
||||
event.preventDefault();
|
||||
colorScheme.set(theming.colorScheme.resolvedValue === 'dark' ? 'light' : 'dark');
|
||||
colorScheme.set(colorScheme.get() === 'dark' ? 'light' : 'dark');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import 'https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.10/+esm';
|
||||
import { preventTurboFouce } from '/dist/webawesome.js';
|
||||
|
||||
if (!window.___turboScrollPositions___) {
|
||||
window.___turboScrollPositions___ = {};
|
||||
}
|
||||
@@ -70,3 +73,4 @@ function fixDSD(e) {
|
||||
window.addEventListener('turbo:before-cache', saveScrollPosition);
|
||||
window.addEventListener('turbo:before-render', restoreScrollPosition);
|
||||
window.addEventListener('turbo:render', restoreScrollPosition);
|
||||
preventTurboFouce();
|
||||
|
||||
6
docs/assets/scripts/tweak.js
Normal file
@@ -0,0 +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';
|
||||
91
docs/assets/scripts/tweak/code.js
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
export function cssImport(url, options = {}) {
|
||||
let { language = 'html', cdnUrl = '/dist/', attributes } = options;
|
||||
url = cdnUrl + url;
|
||||
|
||||
if (language === 'css') {
|
||||
return `@import url('${url}');`;
|
||||
} else {
|
||||
attributes = attributes ? ` ${attributes}` : '';
|
||||
return `<link rel="stylesheet" href="${url}"${attributes} />`;
|
||||
}
|
||||
}
|
||||
|
||||
export function cssLiteral(value, options = {}) {
|
||||
let { language = 'html' } = options;
|
||||
|
||||
if (language === 'css') {
|
||||
return value;
|
||||
} else {
|
||||
return `<style${options.attributes ?? ''}>\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';
|
||||
|
||||
deepEach(themeConfig, (config, aspect, obj, path) => {
|
||||
if (!config?.default) {
|
||||
// We're not in a config object
|
||||
return;
|
||||
}
|
||||
|
||||
let value = deepGet(theme, [...path, aspect]);
|
||||
|
||||
if (!value && value !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function cssRule(selector, declarations, { indent = ' ' } = {}) {
|
||||
selector = Array.isArray(selector) ? selector.flat().join(',\n') : selector;
|
||||
declarations = Array.isArray(declarations) ? declarations.flat() : declarations;
|
||||
declarations = declarations.map(declaration => indent + declaration.trim()).join('\n');
|
||||
return `${selector} {\n${declarations.trimEnd()}\n}`;
|
||||
}
|
||||
36
docs/assets/scripts/tweak/util.js
Normal file
@@ -0,0 +1,36 @@
|
||||
export function normalizeAngles(angles) {
|
||||
// First, normalize
|
||||
angles = angles.map(h => ((h % 360) + 360) % 360);
|
||||
|
||||
// Remove top and bottom 25% and find average
|
||||
let averageHue =
|
||||
angles
|
||||
.toSorted((a, b) => a - b)
|
||||
.slice(angles.length / 4, -angles.length / 4)
|
||||
.reduce((a, b) => a + b, 0) / angles.length;
|
||||
|
||||
for (let i = 0; i < angles.length; i++) {
|
||||
let h = angles[i];
|
||||
let prevHue = angles[i - 1];
|
||||
let delta = h - prevHue;
|
||||
|
||||
if (Math.abs(delta) > 180) {
|
||||
let equivalent = [h + 360, h - 360];
|
||||
// Offset hue to minimize difference in the direction that brings it closer to the average
|
||||
let delta = h - averageHue;
|
||||
|
||||
if (Math.abs(equivalent[0] - prevHue) <= Math.abs(equivalent[1] - prevHue)) {
|
||||
angles[i] = equivalent[0];
|
||||
} else {
|
||||
angles[i] = equivalent[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return angles;
|
||||
}
|
||||
|
||||
export function subtractAngles(θ1, θ2) {
|
||||
let [a, b] = normalizeAngles([θ1, θ2]);
|
||||
return a - b;
|
||||
}
|
||||
17
docs/assets/scripts/util/array.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 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];
|
||||
}
|
||||
180
docs/assets/scripts/util/deep.js
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
39
docs/assets/scripts/util/dom-change.js
Normal file
@@ -0,0 +1,39 @@
|
||||
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;
|
||||
42
docs/assets/scripts/util/string.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -27,3 +27,19 @@ wa-copy-button.copy-button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.block-link-icon {
|
||||
position: absolute;
|
||||
inset-block-start: 0;
|
||||
inset-inline-end: calc(100% + var(--wa-space-s));
|
||||
|
||||
transition: var(--wa-transition-slow);
|
||||
|
||||
&:not(:hover, :focus) {
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
:not(:hover, :focus-within) > & {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -156,7 +156,7 @@ wa-page > header {
|
||||
}
|
||||
|
||||
/* Pro badges */
|
||||
wa-badge.pro::part(base) {
|
||||
wa-badge.pro {
|
||||
background-color: var(--wa-brand-orange);
|
||||
border-color: var(--wa-brand-orange);
|
||||
}
|
||||
@@ -188,6 +188,29 @@ wa-badge.pro::part(base) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wa-icon-button.delete {
|
||||
vertical-align: -0.2em;
|
||||
margin-inline-start: var(--wa-space-xs);
|
||||
|
||||
&:not(li:hover > *, :focus) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wa-icon-button.delete {
|
||||
&:hover {
|
||||
color: var(--wa-color-danger-on-quiet);
|
||||
}
|
||||
|
||||
&::part(base):hover {
|
||||
background: var(--wa-color-danger-fill-quiet);
|
||||
}
|
||||
|
||||
&:not(:hover, :focus) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
#sidebar-close-button {
|
||||
@@ -232,16 +255,23 @@ wa-page > main {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
h1.title wa-badge {
|
||||
vertical-align: middle;
|
||||
|
||||
&::part(base) {
|
||||
h1.title {
|
||||
wa-badge {
|
||||
vertical-align: middle;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.block-info {
|
||||
display: flex;
|
||||
gap: var(--wa-space-xs);
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-block-end: var(--wa-flow-spacing);
|
||||
|
||||
code {
|
||||
line-height: var(--wa-line-height-condensed);
|
||||
}
|
||||
}
|
||||
|
||||
/* Current link */
|
||||
@@ -331,29 +361,29 @@ wa-page > main:has(> .index-grid) {
|
||||
|
||||
.index-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(22ch, 100%), 1fr));
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--wa-space-2xl);
|
||||
margin-block-end: var(--wa-space-3xl);
|
||||
|
||||
@media screen and (max-width: 1470px) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 960px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
}
|
||||
|
||||
a {
|
||||
border-radius: var(--wa-border-radius-l);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -361,18 +391,17 @@ 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-name {
|
||||
font-size: var(--wa-font-size-s);
|
||||
font-weight: var(--wa-font-weight-action);
|
||||
}
|
||||
wa-card .page-name {
|
||||
font-size: var(--wa-font-size-s);
|
||||
font-weight: var(--wa-font-weight-action);
|
||||
}
|
||||
|
||||
.index-category {
|
||||
@@ -381,6 +410,146 @@ wa-page > main:has(> .index-grid) {
|
||||
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;
|
||||
@@ -400,9 +569,18 @@ wa-page > main:has(> .index-grid) {
|
||||
|
||||
&.color {
|
||||
border-color: transparent;
|
||||
transition: background var(--wa-transition-slow);
|
||||
background: linear-gradient(var(--color-2, transparent) 0% 100%) no-repeat border-box var(--color,);
|
||||
background-position: var(--color-2-position, bottom);
|
||||
background-size: var(--color-2-width, 100%) var(--color-2-height, 50%);
|
||||
|
||||
&.contrast-fail {
|
||||
outline: 1px dashed var(--wa-color-red);
|
||||
outline-offset: calc(-1 * var(--wa-space-2xs));
|
||||
}
|
||||
}
|
||||
|
||||
wa-copy-button {
|
||||
> wa-copy-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -463,6 +641,55 @@ table.colors {
|
||||
}
|
||||
}
|
||||
|
||||
.value-up,
|
||||
.value-down {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: ' ' var(--icon);
|
||||
position: absolute;
|
||||
margin-inline-start: 3em;
|
||||
scale: 1 0.6;
|
||||
color: color-mix(in oklch, oklch(from var(--icon-color) none c h) 0%, oklch(from currentColor l none none));
|
||||
font-size: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
.value-down {
|
||||
--icon: '▼';
|
||||
--icon-color: var(--wa-color-danger-fill-quiet);
|
||||
|
||||
&::after {
|
||||
margin-block-end: -0.2em;
|
||||
}
|
||||
}
|
||||
|
||||
.value-up {
|
||||
--icon: '▲';
|
||||
--icon-color: var(--wa-color-success-fill-quiet);
|
||||
}
|
||||
|
||||
.icon-modifier {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
|
||||
.modifier {
|
||||
position: absolute;
|
||||
bottom: -0.1em;
|
||||
right: -0.3em;
|
||||
font-size: 60%;
|
||||
|
||||
&::part(svg) {
|
||||
stroke: var(--background-color, var(--wa-color-surface-default));
|
||||
stroke-width: 100px;
|
||||
paint-order: stroke;
|
||||
overflow: visible;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Layout Examples */
|
||||
.layout-example-boundary {
|
||||
border: var(--wa-border-width-s) dashed var(--wa-color-neutral-border-normal);
|
||||
@@ -499,13 +726,6 @@ table.colors {
|
||||
margin-block-end: var(--wa-flow-spacing);
|
||||
}
|
||||
|
||||
/** mobile */
|
||||
@media screen and (max-width: 768px) {
|
||||
wa-page .only-desktop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/** desktop */
|
||||
@media screen and not (max-width: 768px) {
|
||||
/* Navigation sidebar */
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
|
||||
&::part(base) {
|
||||
margin-block: 10rem;
|
||||
&::part(dialog) {
|
||||
margin-block-start: 10vh;
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
&::part(body) {
|
||||
@@ -23,26 +24,26 @@
|
||||
@media screen and (max-width: 900px) {
|
||||
max-width: calc(100% - 2rem);
|
||||
|
||||
&::part(base) {
|
||||
&::part(dialog) {
|
||||
margin-block: 1rem;
|
||||
}
|
||||
|
||||
#site-search-container {
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#site-search-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - 20rem);
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
max-height: calc(100dvh - 2rem);
|
||||
}
|
||||
max-height: calc(100vh - 18rem);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
header {
|
||||
#site-search-container header {
|
||||
flex: 0 0 auto;
|
||||
align-items: middle;
|
||||
align-items: center;
|
||||
/* 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);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,86 @@
|
||||
.theme-color-icon {
|
||||
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;
|
||||
gap: var(--wa-space-xs);
|
||||
grid-template-columns: repeat(4, auto);
|
||||
min-width: 15ch;
|
||||
background: var(--wa-color-surface-lowered);
|
||||
|
||||
& + & {
|
||||
border-start-start-radius: 0;
|
||||
border-start-end-radius: 0;
|
||||
}
|
||||
|
||||
div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -14,21 +91,127 @@
|
||||
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 {
|
||||
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-typography-icon {
|
||||
.theme-icon.theme-dimensionality-icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--wa-space-xs);
|
||||
flex-flow: column;
|
||||
gap: var(--wa-space-2xs);
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-height: 6.75rem;
|
||||
box-sizing: border-box;
|
||||
|
||||
h3,
|
||||
p {
|
||||
margin-block: 0;
|
||||
padding: 0;
|
||||
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);
|
||||
gap: var(--wa-space-xs);
|
||||
place-items: center;
|
||||
place-content: center;
|
||||
|
||||
& wa-icon {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
145
docs/assets/styles/ui.css
Normal file
@@ -0,0 +1,145 @@
|
||||
/* 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%;
|
||||
}
|
||||
}
|
||||
78
docs/assets/vue/components/color-select.js
Normal file
@@ -0,0 +1,78 @@
|
||||
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-'),
|
||||
},
|
||||
};
|
||||
86
docs/assets/vue/components/editable-text.js
Normal file
@@ -0,0 +1,86 @@
|
||||
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-'),
|
||||
},
|
||||
};
|
||||
132
docs/assets/vue/components/fonts-card.js
Normal file
@@ -0,0 +1,132 @@
|
||||
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-'),
|
||||
},
|
||||
};
|
||||
175
docs/assets/vue/components/icons-card.js
Normal file
@@ -0,0 +1,175 @@
|
||||
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-'),
|
||||
},
|
||||
};
|
||||
13
docs/assets/vue/components/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
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';
|
||||
39
docs/assets/vue/components/info-tip.js
Normal file
@@ -0,0 +1,39 @@
|
||||
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-'),
|
||||
},
|
||||
};
|
||||
83
docs/assets/vue/components/page-card.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 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-'),
|
||||
},
|
||||
};
|
||||
63
docs/assets/vue/components/palette-card.js
Normal file
@@ -0,0 +1,63 @@
|
||||
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
|
||||
}"> </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-'),
|
||||
},
|
||||
};
|
||||
167
docs/assets/vue/components/panel.css
Normal file
@@ -0,0 +1,167 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
89
docs/assets/vue/components/scrollable.css
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 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)));
|
||||
}
|
||||
}
|
||||
54
docs/assets/vue/components/swatch-select.js
Normal file
@@ -0,0 +1,54 @@
|
||||
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-'),
|
||||
},
|
||||
};
|
||||
119
docs/assets/vue/components/theme-card.js
Normal file
@@ -0,0 +1,119 @@
|
||||
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-'),
|
||||
},
|
||||
};
|
||||
120
docs/assets/vue/components/ui-panel-container.js
Normal file
@@ -0,0 +1,120 @@
|
||||
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-'),
|
||||
},
|
||||
};
|
||||
73
docs/assets/vue/components/ui-panel.js
Normal file
@@ -0,0 +1,73 @@
|
||||
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-'),
|
||||
},
|
||||
};
|
||||
77
docs/assets/vue/components/ui-scrollable.js
Normal file
@@ -0,0 +1,77 @@
|
||||
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-'),
|
||||
},
|
||||
};
|
||||
46
docs/assets/vue/components/ui-slider.css
Normal file
@@ -0,0 +1,46 @@
|
||||
.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));
|
||||
}
|
||||
86
docs/assets/vue/components/ui-slider.js
Normal file
@@ -0,0 +1,86 @@
|
||||
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-'),
|
||||
},
|
||||
};
|
||||
22
docs/assets/vue/directives/content.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// Like v-text, but doesn't complain if the element has content,
|
||||
// making it possible to use in a PE fashion, with the contents being the fallback
|
||||
export default function content(el, { value, arg }) {
|
||||
if (!el.dataset.fallback) {
|
||||
// Store the original content as a fallback the first time
|
||||
el.dataset.fallback = el.textContent;
|
||||
}
|
||||
|
||||
if (value === '') {
|
||||
value = el.dataset.fallback;
|
||||
} else {
|
||||
if (arg === 'number') {
|
||||
value = Number(value).toLocaleString(undefined, { maximumSignificantDigits: 2 });
|
||||
}
|
||||
}
|
||||
|
||||
if (arg === 'html') {
|
||||
el.innerHTML = value;
|
||||
} else {
|
||||
el.textContent = value;
|
||||
}
|
||||
}
|
||||
32
docs/assets/vue/mixins/input.js
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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;
|
||||
},
|
||||
},
|
||||
};
|
||||
110
docs/assets/vue/mixins/saved.js
Normal file
@@ -0,0 +1,110 @@
|
||||
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();
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -65,6 +65,18 @@ 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.
|
||||
|
||||
@@ -167,7 +167,7 @@ Other elements can also be placed inside button groups:
|
||||
<wa-button-group label="Example Button Group">
|
||||
<wa-button>Button</wa-button>
|
||||
<button>Native Button</button>
|
||||
<wa-dropdown hoist>
|
||||
<wa-dropdown>
|
||||
<wa-button slot="trigger" caret>Dropdown</wa-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Item 1</wa-menu-item>
|
||||
@@ -185,7 +185,7 @@ Create a split button using a button and a dropdown. Use a [visually hidden](/do
|
||||
```html {.example}
|
||||
<wa-button-group label="Example Button Group">
|
||||
<wa-button variant="brand">Save</wa-button>
|
||||
<wa-dropdown placement="bottom-end" hoist>
|
||||
<wa-dropdown placement="bottom-end">
|
||||
<wa-button slot="trigger" variant="brand" caret>
|
||||
<span class="wa-visually-hidden">More options</span>
|
||||
</wa-button>
|
||||
|
||||