mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-11 20:08:56 +00:00
Themer rework (#1048)
* remove experimental pages * typo * untying the knots * whitespace * fix input bubbling * refactor * reimplement color scheme selector * rename * fix comments * revert premature abstraction * host vars * rework theme selector * feed themes from global data * add back component headers * reimplement component index * make headings optional * add back color palette page sans instructions * fix selector * fix search list with turbo * always dispatch after update * add back theme preview page * fix * fix errors * fix theme selector * fix themes * improve * make transition smooth * revert * revert * skip * fix borders * fix fade in * remove unused blocks * we know this guy * fix event timing * remove header * remove unused styles * add better nunjucks extension * update header * fix layout * add theme linke * fix autocomplete bug * correct description * consistency between palettes and tokens docs * better extension * add colorPalettes data * typo * typos * fix progress bar height * remove due to errors; works without * move pro palettes to pro * consolidate themer data * themer data * add font packs * reorg * restructure theme data * add word * detect the reference slot instead of requiring with-references * Remove `:root` selector and `@import` rules from themes (#1061) * remove `:root` selector and imports from themes * re-add `base.css` to free * update how themes are assigned * update showcase location * update * fix palettes * add font href * fix theme page in turbo * fix color palettes * remove rose * fix shadow * update docs * update docs * rename eyedropper to fix spelling * rename eyedropper * disable turbo * remove unused import * update themer data * add get/set icon family * revert example * fix color palette data * add brand color to theme data * fix sharp duotone name * fix typo * update changelog for merged branch * update changelog * allow default theme to inherit color scheme when nested * make font packs more exciting * update serif typeface * add examples * update font weights * set initial selection instantly * set brand and palette with theme in theme selector * add palette stylesheets * fix icon slot * fix theme descriptions * minor style touch up * tweak example button text * group callout examples * add 'create theme' button * tweak spacing, fix sneaky plain card background * prettier * ultra tiny tweak to showcase example * show usage instructions * prevent error when theme selector isn't present --------- Co-authored-by: lindsaym-fa <dev@lindsaym.design> Co-authored-by: Lindsay M <126139086+lindsaym-fa@users.noreply.github.com>
This commit is contained in:
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -3,6 +3,7 @@
|
|||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"bierner.lit-html",
|
"bierner.lit-html",
|
||||||
"streetsidesoftware.code-spell-checker"
|
"streetsidesoftware.code-spell-checker",
|
||||||
|
"ronnidc.nunjucks"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
15
cspell.json
15
cspell.json
@@ -8,8 +8,10 @@
|
|||||||
"APG",
|
"APG",
|
||||||
"apos",
|
"apos",
|
||||||
"atrule",
|
"atrule",
|
||||||
|
"autocapitalize",
|
||||||
"autocorrect",
|
"autocorrect",
|
||||||
"autofix",
|
"autofix",
|
||||||
|
"autofocus",
|
||||||
"autoload",
|
"autoload",
|
||||||
"autoloader",
|
"autoloader",
|
||||||
"autoloading",
|
"autoloading",
|
||||||
@@ -43,16 +45,22 @@
|
|||||||
"csspart",
|
"csspart",
|
||||||
"cssproperty",
|
"cssproperty",
|
||||||
"cssstate",
|
"cssstate",
|
||||||
|
"datalist",
|
||||||
"datetime",
|
"datetime",
|
||||||
"describedby",
|
"describedby",
|
||||||
|
"dictsort",
|
||||||
"Docsify",
|
"Docsify",
|
||||||
"dogfood",
|
"dogfood",
|
||||||
"dropdowns",
|
"dropdowns",
|
||||||
"easings",
|
"easings",
|
||||||
"ecommerce",
|
"ecommerce",
|
||||||
|
"eleventy",
|
||||||
|
"elif",
|
||||||
"endfor",
|
"endfor",
|
||||||
|
"endmarkdown",
|
||||||
"endraw",
|
"endraw",
|
||||||
"endregion",
|
"endregion",
|
||||||
|
"endset",
|
||||||
"enterkeyhint",
|
"enterkeyhint",
|
||||||
"eqeqeq",
|
"eqeqeq",
|
||||||
"erroneou",
|
"erroneou",
|
||||||
@@ -60,6 +68,7 @@
|
|||||||
"esbuild",
|
"esbuild",
|
||||||
"exportmaps",
|
"exportmaps",
|
||||||
"exportparts",
|
"exportparts",
|
||||||
|
"fetchpriority",
|
||||||
"fieldsets",
|
"fieldsets",
|
||||||
"focusin",
|
"focusin",
|
||||||
"focusout",
|
"focusout",
|
||||||
@@ -78,6 +87,7 @@
|
|||||||
"giga",
|
"giga",
|
||||||
"globby",
|
"globby",
|
||||||
"Grayscale",
|
"Grayscale",
|
||||||
|
"groupby",
|
||||||
"haspopup",
|
"haspopup",
|
||||||
"heroicons",
|
"heroicons",
|
||||||
"hexa",
|
"hexa",
|
||||||
@@ -88,6 +98,7 @@
|
|||||||
"inputmode",
|
"inputmode",
|
||||||
"ionicon",
|
"ionicon",
|
||||||
"ionicons",
|
"ionicons",
|
||||||
|
"jank",
|
||||||
"jsDelivr",
|
"jsDelivr",
|
||||||
"jsfiddle",
|
"jsfiddle",
|
||||||
"keydown",
|
"keydown",
|
||||||
@@ -123,7 +134,9 @@
|
|||||||
"noindex",
|
"noindex",
|
||||||
"noopener",
|
"noopener",
|
||||||
"noreferrer",
|
"noreferrer",
|
||||||
|
"noscript",
|
||||||
"novalidate",
|
"novalidate",
|
||||||
|
"nowrap",
|
||||||
"Numberish",
|
"Numberish",
|
||||||
"nums",
|
"nums",
|
||||||
"oklab",
|
"oklab",
|
||||||
@@ -159,6 +172,7 @@
|
|||||||
"scroller",
|
"scroller",
|
||||||
"Scrollers",
|
"Scrollers",
|
||||||
"Segoe",
|
"Segoe",
|
||||||
|
"selectattr",
|
||||||
"semibold",
|
"semibold",
|
||||||
"shadowrootmode",
|
"shadowrootmode",
|
||||||
"Shortcode",
|
"Shortcode",
|
||||||
@@ -192,6 +206,7 @@
|
|||||||
"typeof",
|
"typeof",
|
||||||
"unbundles",
|
"unbundles",
|
||||||
"unbundling",
|
"unbundling",
|
||||||
|
"Uncategorized",
|
||||||
"unicons",
|
"unicons",
|
||||||
"unsanitized",
|
"unsanitized",
|
||||||
"unsupportive",
|
"unsupportive",
|
||||||
|
|||||||
@@ -1,66 +1,98 @@
|
|||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
|
import { parse } from 'path';
|
||||||
import { anchorHeadingsPlugin } from './_utils/anchor-headings.js';
|
import { anchorHeadingsPlugin } from './_utils/anchor-headings.js';
|
||||||
import { codeExamplesPlugin } from './_utils/code-examples.js';
|
import { codeExamplesPlugin } from './_utils/code-examples.js';
|
||||||
import { copyCodePlugin } from './_utils/copy-code.js';
|
import { copyCodePlugin } from './_utils/copy-code.js';
|
||||||
import { currentLink } from './_utils/current-link.js';
|
import { currentLink } from './_utils/current-link.js';
|
||||||
import { highlightCodePlugin } from './_utils/highlight-code.js';
|
import { highlightCodePlugin } from './_utils/highlight-code.js';
|
||||||
|
import { getComponents } from './_utils/manifest.js';
|
||||||
import { markdown } from './_utils/markdown.js';
|
import { markdown } from './_utils/markdown.js';
|
||||||
// import { formatCodePlugin } from './_utils/format-code.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 { readFile } from 'fs/promises';
|
||||||
import nunjucks from 'nunjucks';
|
import nunjucks from 'nunjucks';
|
||||||
// import componentList from './_data/componentList.js';
|
import process from 'process';
|
||||||
import * as filters from './_utils/filters.js';
|
import * as url from 'url';
|
||||||
import { outlinePlugin } from './_utils/outline.js';
|
import { outlinePlugin } from './_utils/outline.js';
|
||||||
import { replaceTextPlugin } from './_utils/replace-text.js';
|
import { replaceTextPlugin } from './_utils/replace-text.js';
|
||||||
import { searchPlugin } from './_utils/search.js';
|
import { searchPlugin } from './_utils/search.js';
|
||||||
|
|
||||||
import process from 'process';
|
|
||||||
|
|
||||||
import * as url from 'url';
|
|
||||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
const __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 isDev = process.argv.includes('--develop');
|
||||||
|
const packageData = JSON.parse(await readFile(path.join(__dirname, '..', 'package.json'), 'utf-8'));
|
||||||
|
const docsDir = path.join(process.env.BASE_DIR || '.', 'docs');
|
||||||
|
const passThroughExtensions = ['js', 'css', 'png', 'svg', 'jpg', 'mp4'];
|
||||||
|
const allComponents = getComponents();
|
||||||
|
|
||||||
const globalData = {
|
/**
|
||||||
package: packageData,
|
* If you plan to add or remove any of these extensions, make sure to let either Konnor or Cory know as these
|
||||||
layout: 'page.njk',
|
* passthrough extensions will also need to be updated in the Web Awesome App.
|
||||||
server: {
|
*/
|
||||||
|
const passThrough = [...passThroughExtensions.map(ext => path.join(docsDir, '**/*.' + ext))];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the guard we use for now to make sure our final built files don't 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';
|
||||||
|
|
||||||
|
export default async function (eleventyConfig) {
|
||||||
|
//
|
||||||
|
// Set all global template data here
|
||||||
|
//
|
||||||
|
eleventyConfig.addGlobalData('package', packageData);
|
||||||
|
eleventyConfig.addGlobalData('layout', 'page.njk');
|
||||||
|
eleventyConfig.addGlobalData('server', {
|
||||||
head: '',
|
head: '',
|
||||||
loginOrAvatar: '',
|
loginOrAvatar: '',
|
||||||
flashes: '',
|
flashes: '',
|
||||||
},
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export default async function (eleventyConfig) {
|
|
||||||
/**
|
|
||||||
* If you plan to add or remove any of these extensions, make sure to let either Konnor or Cory know as these passthrough extensions
|
|
||||||
* will also need to be updated in the Web Awesome App.
|
|
||||||
*/
|
|
||||||
const passThroughExtensions = ['js', 'css', 'png', 'svg', 'jpg', 'mp4'];
|
|
||||||
|
|
||||||
const docsDir = path.join(process.env.BASE_DIR || '.', 'docs');
|
|
||||||
const passThrough = [...passThroughExtensions.map(ext => path.join(docsDir, '**/*.' + 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) {
|
|
||||||
eleventyConfig.addGlobalData(name, globalData[name]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Template filters - {{ content | filter }}
|
// Template filters - {{ content | filter }}
|
||||||
eleventyConfig.addFilter('inlineMarkdown', content => markdown.renderInline(content || ''));
|
eleventyConfig.addFilter('inlineMarkdown', content => markdown.renderInline(content || ''));
|
||||||
eleventyConfig.addFilter('markdown', content => markdown.render(content || ''));
|
eleventyConfig.addFilter('markdown', content => markdown.render(content || ''));
|
||||||
|
eleventyConfig.addFilter('stripExtension', string => parse(string + '').name);
|
||||||
|
eleventyConfig.addFilter('stripPrefix', content => content.replace(/^wa-/, ''));
|
||||||
|
// Trims whitespace and pipes from the start and end of a string. Useful for CEM types, which can be pipe-delimited.
|
||||||
|
// With Prettier 3, this means a leading pipe will exist be present when the line wraps.
|
||||||
|
eleventyConfig.addFilter('trimPipes', content => {
|
||||||
|
return typeof content === 'string' ? content.replace(/^(\s|\|)/g, '').replace(/(\s|\|)$/g, '') : content;
|
||||||
|
});
|
||||||
|
|
||||||
for (let name in filters) {
|
// Custom filter to sort with a priority item first, e.g.
|
||||||
eleventyConfig.addFilter(name, filters[name]);
|
// {{ collection | sortWithFirst('fileSlug', 'default') }} => the item with the fileSlug of 'default' will be first
|
||||||
}
|
eleventyConfig.addFilter('sortWithFirst', function (collection, property, firstValue) {
|
||||||
|
const items = [...collection]; // Create a copy to avoid mutating original
|
||||||
|
return items.sort((a, b) => {
|
||||||
|
const aValue = property ? a[property] : a;
|
||||||
|
const bValue = property ? b[property] : b;
|
||||||
|
if (aValue === firstValue) return -1;
|
||||||
|
if (bValue === firstValue) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// Add the componentPages collection
|
||||||
|
//
|
||||||
|
eleventyConfig.addCollection('componentPages', function (collectionApi) {
|
||||||
|
const componentPages = collectionApi.getFilteredByGlob(
|
||||||
|
path.join(eleventyConfig.directories.input, 'docs/components/**/*.md'),
|
||||||
|
);
|
||||||
|
|
||||||
|
return componentPages.map(page => {
|
||||||
|
const componentName = path.basename(page.inputPath, '.md');
|
||||||
|
const tagName = `wa-${componentName}`;
|
||||||
|
const component = allComponents.find(c => c.tagName === tagName);
|
||||||
|
|
||||||
|
// Add component to the page's data
|
||||||
|
if (component) {
|
||||||
|
page.data.component = component;
|
||||||
|
}
|
||||||
|
|
||||||
|
return page;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Shortcodes - {% shortCode arg1, arg2 %}
|
// Shortcodes - {% shortCode arg1, arg2 %}
|
||||||
eleventyConfig.addShortcode('cdnUrl', location => {
|
eleventyConfig.addShortcode('cdnUrl', location => {
|
||||||
@@ -102,6 +134,15 @@ export default async function (eleventyConfig) {
|
|||||||
eleventyConfig.addPairedShortcode('markdown', content => markdown.render(content || ''));
|
eleventyConfig.addPairedShortcode('markdown', content => markdown.render(content || ''));
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
|
eleventyConfig.addNunjucksGlobal('getComponent', tagName => {
|
||||||
|
const component = allComponents.find(c => c.tagName === tagName);
|
||||||
|
if (!component) {
|
||||||
|
throw new Error(
|
||||||
|
`Unable to find "<${tagName}>". Make sure the file name is the same as the tag name (without prefix).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return component;
|
||||||
|
});
|
||||||
|
|
||||||
// Use our own markdown instance
|
// Use our own markdown instance
|
||||||
eleventyConfig.setLibrary('md', markdown);
|
eleventyConfig.setLibrary('md', markdown);
|
||||||
@@ -184,7 +225,8 @@ export default async function (eleventyConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// // SSR plugin
|
// // 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.
|
// // 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) {
|
// if (!isDev) {
|
||||||
// //
|
// //
|
||||||
// // Problematic components in SSR land:
|
// // Problematic components in SSR land:
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
/**
|
|
||||||
* @module components Fetches components from custom-elements.json and exposes them in a saner format.
|
|
||||||
*/
|
|
||||||
import { readFileSync } from 'fs';
|
|
||||||
import { dirname, join, resolve } from 'path';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
||||||
const customElementsJSON = process.env.DIST_DIR
|
|
||||||
? join(process.env.DIST_DIR, 'custom-elements.json')
|
|
||||||
: resolve(__dirname, '../../dist/custom-elements.json');
|
|
||||||
|
|
||||||
const manifest = JSON.parse(readFileSync(customElementsJSON), 'utf-8');
|
|
||||||
|
|
||||||
const components = manifest.modules.flatMap(module => {
|
|
||||||
return module.declarations
|
|
||||||
.filter(c => c?.customElement)
|
|
||||||
.map(declaration => {
|
|
||||||
// Generate the dist path based on the src path and attach it to the component
|
|
||||||
declaration.path = module.path.replace(/^src\//, 'dist/').replace(/\.ts$/, '.js');
|
|
||||||
|
|
||||||
// Remove private members and those that lack a description
|
|
||||||
const members = declaration.members?.filter(member => member.description && member.privacy !== 'private');
|
|
||||||
|
|
||||||
const methods = members?.filter(prop => prop.kind === 'method' && prop.privacy !== 'private');
|
|
||||||
const attributes = declaration.attributes ?? [];
|
|
||||||
const properties = members?.filter(prop => {
|
|
||||||
// Look for a corresponding attribute
|
|
||||||
const attribute = attributes?.find(attr => attr.fieldName === prop.name);
|
|
||||||
if (attribute) {
|
|
||||||
prop.attribute = attribute.name || attribute.fieldName;
|
|
||||||
}
|
|
||||||
|
|
||||||
return prop.kind === 'field' && prop.privacy !== 'private';
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...declaration,
|
|
||||||
slug: declaration.tagName.replace(/^wa-/, ''),
|
|
||||||
methods,
|
|
||||||
attributes,
|
|
||||||
properties,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Build dependency graphs
|
|
||||||
components.forEach(component => {
|
|
||||||
const dependencies = [];
|
|
||||||
|
|
||||||
// Recursively fetch sub-dependencies
|
|
||||||
function getDependencies(tag) {
|
|
||||||
const cmp = components.find(c => c.tagName === tag);
|
|
||||||
if (!cmp || !Array.isArray(component.dependencies)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cmp.dependencies?.forEach(dependentTag => {
|
|
||||||
if (!dependencies.includes(dependentTag)) {
|
|
||||||
dependencies.push(dependentTag);
|
|
||||||
}
|
|
||||||
getDependencies(dependentTag);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getDependencies(component.tagName);
|
|
||||||
|
|
||||||
component.dependencies = dependencies.sort();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sort by name
|
|
||||||
components.sort((a, b) => {
|
|
||||||
if (a.name < b.name) return -1;
|
|
||||||
if (a.name > b.name) return 1;
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
export default components;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import componentList from './componentList.js';
|
|
||||||
|
|
||||||
export default Object.fromEntries(componentList.map(component => [component.slug, component]));
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { hueRanges as default } from '../assets/data/index.js';
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
["red", "orange", "yellow", "green", "cyan", "blue", "indigo", "purple", "pink", "gray"]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default as default } from '../../src/styles/color/scripts/palettes-analyzed.js';
|
|
||||||
735
packages/webawesome/docs/_data/themer.js
Normal file
735
packages/webawesome/docs/_data/themer.js
Normal file
@@ -0,0 +1,735 @@
|
|||||||
|
/**
|
||||||
|
* All themes used in the themer.
|
||||||
|
*/
|
||||||
|
export const themes = [
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// #region Default
|
||||||
|
//
|
||||||
|
name: 'Default',
|
||||||
|
description: 'Your trusty companion, like a perfectly broken-in pair of jeans.',
|
||||||
|
filename: 'default.css',
|
||||||
|
isPro: false,
|
||||||
|
fonts: {
|
||||||
|
body: {
|
||||||
|
name: 'OS Default',
|
||||||
|
css: 'ui-sans-serif, system-ui, sans-serif',
|
||||||
|
href: null,
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
name: 'OS Default',
|
||||||
|
css: 'ui-sans-serif, system-ui, sans-serif',
|
||||||
|
href: null,
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
name: 'OS Default',
|
||||||
|
css: 'ui-monospace, monospace',
|
||||||
|
href: null,
|
||||||
|
},
|
||||||
|
longform: {
|
||||||
|
name: 'OS Default',
|
||||||
|
css: 'ui-serif, serif',
|
||||||
|
href: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icons: {
|
||||||
|
family: 'classic',
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
palette: {
|
||||||
|
name: 'Default',
|
||||||
|
filename: 'default.css',
|
||||||
|
},
|
||||||
|
colorBrand: {
|
||||||
|
color: 'blue',
|
||||||
|
},
|
||||||
|
tokens: {
|
||||||
|
// Fonts
|
||||||
|
'--wa-font-family-body': 'ui-sans-serif, system-ui, sans-serif',
|
||||||
|
'--wa-font-family-heading': 'var(--wa-font-family-body)',
|
||||||
|
'--wa-font-family-code': 'ui-monospace, monospace',
|
||||||
|
'--wa-font-family-longform': 'ui-serif, serif',
|
||||||
|
'--wa-font-weight-body': 400,
|
||||||
|
'--wa-font-weight-heading': 600,
|
||||||
|
'--wa-font-weight-code': 400,
|
||||||
|
'--wa-font-weight-longform': 400,
|
||||||
|
|
||||||
|
// Elements
|
||||||
|
'--wa-border-radius-scale': 1,
|
||||||
|
'--wa-space-scale': 1,
|
||||||
|
'--wa-border-width-scale': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
//
|
||||||
|
// #region Awesome
|
||||||
|
//
|
||||||
|
{
|
||||||
|
name: 'Awesome',
|
||||||
|
description: 'Punchy and vibrant, the rock star of themes.',
|
||||||
|
filename: 'awesome.css',
|
||||||
|
isPro: false,
|
||||||
|
fonts: {
|
||||||
|
body: {
|
||||||
|
name: 'Quicksand',
|
||||||
|
css: 'Quicksand, sans-serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Crimson+Pro:ital,wght@0,200..900;1,200..900&family=Quicksand:wght@300..700&display=swap',
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
name: 'Quicksand',
|
||||||
|
css: 'Quicksand, sans-serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Crimson+Pro:ital,wght@0,200..900;1,200..900&family=Quicksand:wght@300..700&display=swap',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
name: 'OS Default',
|
||||||
|
css: 'ui-monospace, monospace',
|
||||||
|
href: null,
|
||||||
|
},
|
||||||
|
longform: {
|
||||||
|
name: 'Crimson Pro',
|
||||||
|
css: '"Crimson Pro", serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Crimson+Pro:ital,wght@0,200..900;1,200..900&family=Quicksand:wght@300..700&display=swap',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icons: {
|
||||||
|
family: 'classic',
|
||||||
|
weight: 2,
|
||||||
|
},
|
||||||
|
palette: {
|
||||||
|
name: 'Bright',
|
||||||
|
filename: 'bright.css',
|
||||||
|
},
|
||||||
|
colorBrand: {
|
||||||
|
color: 'blue',
|
||||||
|
},
|
||||||
|
tokens: {
|
||||||
|
// Fonts
|
||||||
|
'--wa-font-family-body': 'Quicksand, sans-serif',
|
||||||
|
'--wa-font-family-heading': 'var(--wa-font-family-body)',
|
||||||
|
'--wa-font-family-code': 'ui-monospace, monospace',
|
||||||
|
'--wa-font-family-longform': '"Crimson Pro", serif',
|
||||||
|
'--wa-font-weight-body': 500,
|
||||||
|
'--wa-font-weight-heading': 700,
|
||||||
|
'--wa-font-weight-code': 500,
|
||||||
|
'--wa-font-weight-longform': 500,
|
||||||
|
|
||||||
|
// Elements
|
||||||
|
'--wa-border-radius-scale': 1.5,
|
||||||
|
'--wa-space-scale': 1,
|
||||||
|
'--wa-border-width-scale': 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
//
|
||||||
|
// #region Shoelace
|
||||||
|
//
|
||||||
|
{
|
||||||
|
name: 'Shoelace',
|
||||||
|
description: 'The original, familiar look you know and love from Shoelace.',
|
||||||
|
filename: 'shoelace.css',
|
||||||
|
isPro: false,
|
||||||
|
fonts: {
|
||||||
|
body: {
|
||||||
|
name: 'OS Default',
|
||||||
|
css: 'ui-sans-serif, system-ui, sans-serif',
|
||||||
|
href: null,
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
name: 'OS Default',
|
||||||
|
css: 'ui-sans-serif, system-ui, sans-serif',
|
||||||
|
href: null,
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
name: 'OS Default',
|
||||||
|
css: 'ui-monospace, monospace',
|
||||||
|
href: null,
|
||||||
|
},
|
||||||
|
longform: {
|
||||||
|
name: 'OS Default',
|
||||||
|
css: 'ui-serif, serif',
|
||||||
|
href: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icons: {
|
||||||
|
family: 'classic',
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
palette: {
|
||||||
|
name: 'Shoelace',
|
||||||
|
filename: 'shoelace.css',
|
||||||
|
},
|
||||||
|
colorBrand: {
|
||||||
|
color: 'blue',
|
||||||
|
},
|
||||||
|
tokens: {
|
||||||
|
// Fonts
|
||||||
|
'--wa-font-family-body': 'ui-sans-serif, system-ui, sans-serif',
|
||||||
|
'--wa-font-family-heading': 'var(--wa-font-family-body)',
|
||||||
|
'--wa-font-family-code': 'ui-monospace, monospace',
|
||||||
|
'--wa-font-family-longform': 'ui-serif, serif',
|
||||||
|
'--wa-font-weight-body': 400,
|
||||||
|
'--wa-font-weight-heading': 600,
|
||||||
|
'--wa-font-weight-code': 400,
|
||||||
|
'--wa-font-weight-longform': 400,
|
||||||
|
|
||||||
|
// Elements
|
||||||
|
'--wa-border-radius-scale': 0.7,
|
||||||
|
'--wa-space-scale': 1,
|
||||||
|
'--wa-border-width-scale': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
//
|
||||||
|
// #region Active
|
||||||
|
//
|
||||||
|
{
|
||||||
|
name: 'Active',
|
||||||
|
description: 'Energetic and tactile, always in motion.',
|
||||||
|
filename: 'active.css',
|
||||||
|
isPro: true,
|
||||||
|
fonts: {
|
||||||
|
body: {
|
||||||
|
name: 'Inter',
|
||||||
|
css: 'Inter, sans-serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Aleo:ital,wght@0,100..900;1,100..900&family=Geist+Mono:wght@100..900&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
name: 'Inter',
|
||||||
|
css: 'Inter, sans-serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Aleo:ital,wght@0,100..900;1,100..900&family=Geist+Mono:wght@100..900&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
name: 'Geist Mono',
|
||||||
|
css: '"Geist Mono", monospace',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Aleo:ital,wght@0,100..900;1,100..900&family=Geist+Mono:wght@100..900&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
|
||||||
|
},
|
||||||
|
longform: {
|
||||||
|
name: 'Aleo',
|
||||||
|
css: 'Aleo, serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Aleo:ital,wght@0,100..900;1,100..900&family=Geist+Mono:wght@100..900&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icons: {
|
||||||
|
family: 'classic',
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
palette: {
|
||||||
|
name: 'Rudimentary',
|
||||||
|
filename: 'rudimentary.css',
|
||||||
|
},
|
||||||
|
colorBrand: {
|
||||||
|
color: 'green',
|
||||||
|
},
|
||||||
|
tokens: {
|
||||||
|
// Fonts
|
||||||
|
'--wa-font-family-body': 'Inter, sans-serif',
|
||||||
|
'--wa-font-family-heading': 'var(--wa-font-family-body)',
|
||||||
|
'--wa-font-family-code': '"Geist Mono", monospace',
|
||||||
|
'--wa-font-family-longform': 'Aleo, serif',
|
||||||
|
'--wa-font-weight-body': 400,
|
||||||
|
'--wa-font-weight-heading': 650,
|
||||||
|
'--wa-font-weight-code': 400,
|
||||||
|
'--wa-font-weight-longform': 400,
|
||||||
|
|
||||||
|
// Elements
|
||||||
|
'--wa-border-radius-scale': 1.75,
|
||||||
|
'--wa-space-scale': 1,
|
||||||
|
'--wa-border-width-scale': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
//
|
||||||
|
// #region Brutalist
|
||||||
|
//
|
||||||
|
{
|
||||||
|
name: 'Brutalist',
|
||||||
|
description: 'Sharp, square, and unapologetically bold.',
|
||||||
|
filename: 'brutalist.css',
|
||||||
|
isPro: true,
|
||||||
|
fonts: {
|
||||||
|
body: {
|
||||||
|
name: 'Space Grotesk',
|
||||||
|
css: '"Space Grotesk", sans-serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Condensed:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=Podkova:wght@400..800&family=Space+Grotesk:wght@300..700&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap',
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
name: 'IBM Plex Sans Condensed',
|
||||||
|
css: '"IBM Plex Sans Condensed", sans-serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Condensed:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=Podkova:wght@400..800&family=Space+Grotesk:wght@300..700&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
name: 'Space Mono',
|
||||||
|
css: '"Space Mono", monospace',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Condensed:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=Podkova:wght@400..800&family=Space+Grotesk:wght@300..700&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap',
|
||||||
|
},
|
||||||
|
longform: {
|
||||||
|
name: 'Podkova',
|
||||||
|
css: 'Podkova, serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Condensed:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=Podkova:wght@400..800&family=Space+Grotesk:wght@300..700&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icons: {
|
||||||
|
family: 'classic',
|
||||||
|
weight: 2,
|
||||||
|
},
|
||||||
|
palette: {
|
||||||
|
name: 'Default',
|
||||||
|
filename: 'default.css',
|
||||||
|
},
|
||||||
|
colorBrand: {
|
||||||
|
color: 'blue',
|
||||||
|
},
|
||||||
|
tokens: {
|
||||||
|
// Fonts
|
||||||
|
'--wa-font-family-body': 'Space Grotesk, sans-serif',
|
||||||
|
'--wa-font-family-heading': 'IBM Plex Sans Condensed, sans-serif',
|
||||||
|
'--wa-font-family-code': 'Space Mono, monospace',
|
||||||
|
'--wa-font-family-longform': 'Podkova, serif',
|
||||||
|
'--wa-font-weight-body': 400,
|
||||||
|
'--wa-font-weight-heading': 500,
|
||||||
|
'--wa-font-weight-code': 400,
|
||||||
|
'--wa-font-weight-longform': 400,
|
||||||
|
|
||||||
|
// Elements
|
||||||
|
'--wa-border-radius-scale': 0,
|
||||||
|
'--wa-space-scale': 1.125,
|
||||||
|
'--wa-border-width-scale': 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
//
|
||||||
|
// #region Glossy
|
||||||
|
//
|
||||||
|
{
|
||||||
|
name: 'Glossy',
|
||||||
|
description: 'Bustling with plenty of luster and shine.',
|
||||||
|
filename: 'glossy.css',
|
||||||
|
isPro: true,
|
||||||
|
fonts: {
|
||||||
|
body: {
|
||||||
|
name: 'Figtree',
|
||||||
|
css: 'Figtree, sans-serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Chivo+Mono:ital,wght@0,100..900;1,100..900&family=Figtree:ital,wght@0,300..900;1,300..900&family=Fraunces:ital,opsz,wght@0,9..144,100..900;1,9..144,100..900&display=swap',
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
name: 'Figtree',
|
||||||
|
css: 'Figtree, sans-serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Chivo+Mono:ital,wght@0,100..900;1,100..900&family=Figtree:ital,wght@0,300..900;1,300..900&family=Fraunces:ital,opsz,wght@0,9..144,100..900;1,9..144,100..900&display=swap',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
name: 'Chivo Mono',
|
||||||
|
css: '"Chivo Mono", monospace',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Chivo+Mono:ital,wght@0,100..900;1,100..900&family=Figtree:ital,wght@0,300..900;1,300..900&family=Fraunces:ital,opsz,wght@0,9..144,100..900;1,9..144,100..900&display=swap',
|
||||||
|
},
|
||||||
|
longform: {
|
||||||
|
name: 'Fraunces',
|
||||||
|
css: 'Fraunces, serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Chivo+Mono:ital,wght@0,100..900;1,100..900&family=Figtree:ital,wght@0,300..900;1,300..900&family=Fraunces:ital,opsz,wght@0,9..144,100..900;1,9..144,100..900&display=swap',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icons: {
|
||||||
|
family: 'classic',
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
palette: {
|
||||||
|
name: 'Elegant',
|
||||||
|
filename: 'elegant.css',
|
||||||
|
},
|
||||||
|
colorBrand: {
|
||||||
|
color: 'indigo',
|
||||||
|
},
|
||||||
|
tokens: {
|
||||||
|
// Fonts
|
||||||
|
'--wa-font-family-body': 'Figtree, sans-serif',
|
||||||
|
'--wa-font-family-heading': 'var(--wa-font-family-body)',
|
||||||
|
'--wa-font-family-code': '"Chivo Mono", monospace',
|
||||||
|
'--wa-font-family-longform': 'Fraunces, serif',
|
||||||
|
'--wa-font-weight-body': 400,
|
||||||
|
'--wa-font-weight-heading': 800,
|
||||||
|
'--wa-font-weight-code': 400,
|
||||||
|
'--wa-font-weight-longform': 350,
|
||||||
|
|
||||||
|
// Elements
|
||||||
|
'--wa-border-radius-scale': 1.33,
|
||||||
|
'--wa-space-scale': 1.125,
|
||||||
|
'--wa-border-width-scale': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
//
|
||||||
|
// #region Matter
|
||||||
|
//
|
||||||
|
{
|
||||||
|
name: 'Matter',
|
||||||
|
description: 'Digital design inspired by the real world.',
|
||||||
|
filename: 'matter.css',
|
||||||
|
isPro: true,
|
||||||
|
fonts: {
|
||||||
|
body: {
|
||||||
|
name: 'Wix Madefor Text',
|
||||||
|
css: '"Wix Madefor Text", sans-serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto+Serif:ital,opsz,wght@0,8..144,100..900;1,8..144,100..900&family=Roboto:ital,wght@0,100..900;1,100..900&family=Wix+Madefor+Text:ital,wght@0,400..800;1,400..800&display=swap',
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
name: 'Wix Madefor Text',
|
||||||
|
css: '"Wix Madefor Text", sans-serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto+Serif:ital,opsz,wght@0,8..144,100..900;1,8..144,100..900&family=Roboto:ital,wght@0,100..900;1,100..900&family=Wix+Madefor+Text:ital,wght@0,400..800;1,400..800&display=swap',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
name: 'Roboto Mono',
|
||||||
|
css: '"Roboto Mono", monospace',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto+Serif:ital,opsz,wght@0,8..144,100..900;1,8..144,100..900&family=Roboto:ital,wght@0,100..900;1,100..900&family=Wix+Madefor+Text:ital,wght@0,400..800;1,400..800&display=swap',
|
||||||
|
},
|
||||||
|
longform: {
|
||||||
|
name: 'Roboto Serif',
|
||||||
|
css: '"Roboto Serif", serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto+Serif:ital,opsz,wght@0,8..144,100..900;1,8..144,100..900&family=Roboto:ital,wght@0,100..900;1,100..900&family=Wix+Madefor+Text:ital,wght@0,400..800;1,400..800&display=swap',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icons: {
|
||||||
|
family: 'classic',
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
palette: {
|
||||||
|
name: 'Mild',
|
||||||
|
filename: 'mild.css',
|
||||||
|
},
|
||||||
|
colorBrand: {
|
||||||
|
color: 'purple',
|
||||||
|
},
|
||||||
|
tokens: {
|
||||||
|
// Fonts
|
||||||
|
'--wa-font-family-body': 'Wix Madefor Text, sans-serif',
|
||||||
|
'--wa-font-family-heading': 'var(--wa-font-family-body)',
|
||||||
|
'--wa-font-family-code': 'Roboto Mono, monospace',
|
||||||
|
'--wa-font-family-longform': 'Roboto Serif, serif',
|
||||||
|
'--wa-font-weight-body': 400,
|
||||||
|
'--wa-font-weight-heading': 500,
|
||||||
|
'--wa-font-weight-code': 400,
|
||||||
|
'--wa-font-weight-longform': 400,
|
||||||
|
|
||||||
|
// Elements
|
||||||
|
'--wa-border-radius-scale': 1.33,
|
||||||
|
'--wa-space-scale': 1,
|
||||||
|
'--wa-border-width-scale': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
//
|
||||||
|
// #region Mellow
|
||||||
|
//
|
||||||
|
{
|
||||||
|
name: 'Mellow',
|
||||||
|
description: 'Soft and soothing, like a lazy Sunday morning.',
|
||||||
|
filename: 'mellow.css',
|
||||||
|
isPro: true,
|
||||||
|
fonts: {
|
||||||
|
body: {
|
||||||
|
name: 'Mulish',
|
||||||
|
css: 'Mulish, sans-serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400..700;1,400..700&family=Mulish:ital,wght@0,200..1000;1,200..1000&display=swap',
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
name: 'Lora',
|
||||||
|
css: 'Lora, serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400..700;1,400..700&family=Mulish:ital,wght@0,200..1000;1,200..1000&display=swap',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
name: 'OS Default',
|
||||||
|
css: 'ui-monospace, monospace',
|
||||||
|
href: null,
|
||||||
|
},
|
||||||
|
longform: {
|
||||||
|
name: 'Lora',
|
||||||
|
css: 'Lora, serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400..700;1,400..700&family=Mulish:ital,wght@0,200..1000;1,200..1000&display=swap',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icons: {
|
||||||
|
family: 'classic',
|
||||||
|
weight: 1.5,
|
||||||
|
},
|
||||||
|
palette: {
|
||||||
|
name: 'Natural',
|
||||||
|
filename: 'natural.css',
|
||||||
|
},
|
||||||
|
colorBrand: {
|
||||||
|
color: 'blue',
|
||||||
|
},
|
||||||
|
tokens: {
|
||||||
|
// Fonts
|
||||||
|
'--wa-font-family-body': 'Mulish, sans-serif',
|
||||||
|
'--wa-font-family-heading': 'Lora, serif',
|
||||||
|
'--wa-font-family-code': 'ui-monospace, monospace',
|
||||||
|
'--wa-font-family-longform': 'Lora, serif',
|
||||||
|
'--wa-font-weight-body': 400,
|
||||||
|
'--wa-font-weight-heading': 700,
|
||||||
|
'--wa-font-weight-code': 400,
|
||||||
|
'--wa-font-weight-longform': 400,
|
||||||
|
|
||||||
|
// Elements
|
||||||
|
'--wa-border-radius-scale': 1,
|
||||||
|
'--wa-space-scale': 1.125,
|
||||||
|
'--wa-border-width-scale': 1.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
//
|
||||||
|
// #region Playful
|
||||||
|
//
|
||||||
|
{
|
||||||
|
name: 'Playful',
|
||||||
|
description: 'Cheerful and engaging, like a playground on screen.',
|
||||||
|
filename: 'playful.css',
|
||||||
|
isPro: true,
|
||||||
|
fonts: {
|
||||||
|
body: {
|
||||||
|
name: 'Nunito',
|
||||||
|
css: 'Nunito, sans-serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Azeret+Mono:ital,wght@0,100..900;1,100..900&family=BioRhyme:wght@200..800&family=Fredoka:wght@300..700&family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap',
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
name: 'Fredoka',
|
||||||
|
css: 'Fredoka, sans-serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Azeret+Mono:ital,wght@0,100..900;1,100..900&family=BioRhyme:wght@200..800&family=Fredoka:wght@300..700&family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
name: 'Azeret Mono',
|
||||||
|
css: '"Azeret Mono", monospace',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Azeret+Mono:ital,wght@0,100..900;1,100..900&family=BioRhyme:wght@200..800&family=Fredoka:wght@300..700&family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap',
|
||||||
|
},
|
||||||
|
longform: {
|
||||||
|
name: 'BioRhyme',
|
||||||
|
css: 'BioRhyme, serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Azeret+Mono:ital,wght@0,100..900;1,100..900&family=BioRhyme:wght@200..800&family=Fredoka:wght@300..700&family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icons: {
|
||||||
|
family: 'classic',
|
||||||
|
weight: 3,
|
||||||
|
},
|
||||||
|
palette: {
|
||||||
|
name: 'Rudimentary',
|
||||||
|
filename: 'rudimentary.css',
|
||||||
|
},
|
||||||
|
colorBrand: {
|
||||||
|
color: 'purple',
|
||||||
|
},
|
||||||
|
tokens: {
|
||||||
|
// Fonts
|
||||||
|
'--wa-font-family-body': 'Nunito, sans-serif',
|
||||||
|
'--wa-font-family-heading': 'Fredoka, sans-serif',
|
||||||
|
'--wa-font-family-code': 'Azeret Mono, monospace',
|
||||||
|
'--wa-font-family-longform': 'BioRhyme, serif',
|
||||||
|
'--wa-font-weight-body': 500,
|
||||||
|
'--wa-font-weight-heading': 600,
|
||||||
|
'--wa-font-weight-code': 400,
|
||||||
|
'--wa-font-weight-longform': 400,
|
||||||
|
|
||||||
|
// Elements
|
||||||
|
'--wa-border-radius-scale': 2,
|
||||||
|
'--wa-space-scale': 1,
|
||||||
|
'--wa-border-width-scale': 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
//
|
||||||
|
// #region Premium
|
||||||
|
//
|
||||||
|
{
|
||||||
|
name: 'Premium',
|
||||||
|
description: 'The ultimate in sophistication and style.',
|
||||||
|
filename: 'premium.css',
|
||||||
|
isPro: true,
|
||||||
|
fonts: {
|
||||||
|
body: {
|
||||||
|
name: 'DM Sans',
|
||||||
|
css: '"DM Sans", sans-serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=Playfair+Display:ital,wght@0,400..900;1,400..900&family=Playfair:ital,opsz,wght@0,5..1200,300..900;1,5..1200,300..900&display=swap',
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
name: 'Playfair Display',
|
||||||
|
css: '"Playfair Display", serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=Playfair+Display:ital,wght@0,400..900;1,400..900&family=Playfair:ital,opsz,wght@0,5..1200,300..900;1,5..1200,300..900&display=swap',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
name: 'OS Default',
|
||||||
|
css: 'ui-monospace, monospace',
|
||||||
|
href: null,
|
||||||
|
},
|
||||||
|
longform: {
|
||||||
|
name: 'Playfair',
|
||||||
|
css: 'Playfair, serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=Playfair+Display:ital,wght@0,400..900;1,400..900&family=Playfair:ital,opsz,wght@0,5..1200,300..900;1,5..1200,300..900&display=swap',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icons: {
|
||||||
|
family: 'classic',
|
||||||
|
weight: 1.5,
|
||||||
|
},
|
||||||
|
palette: {
|
||||||
|
name: 'Anodized',
|
||||||
|
filename: 'anodized.css',
|
||||||
|
},
|
||||||
|
colorBrand: {
|
||||||
|
color: 'cyan',
|
||||||
|
},
|
||||||
|
tokens: {
|
||||||
|
// Fonts
|
||||||
|
'--wa-font-family-body': 'DM Sans, sans-serif',
|
||||||
|
'--wa-font-family-heading': 'Playfair Display, serif',
|
||||||
|
'--wa-font-family-code': 'ui-monospace, monospace',
|
||||||
|
'--wa-font-family-longform': 'Playfair, serif',
|
||||||
|
'--wa-font-weight-body': 400,
|
||||||
|
'--wa-font-weight-heading': 500,
|
||||||
|
'--wa-font-weight-code': 400,
|
||||||
|
'--wa-font-weight-longform': 400,
|
||||||
|
|
||||||
|
// Elements
|
||||||
|
'--wa-border-radius-scale': 0.5,
|
||||||
|
'--wa-space-scale': 1,
|
||||||
|
'--wa-border-width-scale': 1.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
//
|
||||||
|
// #region Tailspin
|
||||||
|
//
|
||||||
|
{
|
||||||
|
name: 'Tailspin',
|
||||||
|
description: 'Like a bird in flight, guiding you from there to here.',
|
||||||
|
filename: 'tailspin.css',
|
||||||
|
isPro: true,
|
||||||
|
fonts: {
|
||||||
|
body: {
|
||||||
|
name: 'Inter',
|
||||||
|
css: 'Inter, sans-serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
name: 'Inter',
|
||||||
|
css: 'Inter, sans-serif',
|
||||||
|
href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
name: 'OS Default',
|
||||||
|
css: 'ui-monospace, monospace',
|
||||||
|
href: null,
|
||||||
|
},
|
||||||
|
longform: {
|
||||||
|
name: 'OS Default',
|
||||||
|
css: 'ui-serif, serif',
|
||||||
|
href: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icons: {
|
||||||
|
family: 'classic',
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
palette: {
|
||||||
|
name: 'Vogue',
|
||||||
|
filename: 'vogue.css',
|
||||||
|
},
|
||||||
|
colorBrand: {
|
||||||
|
color: 'indigo',
|
||||||
|
},
|
||||||
|
tokens: {
|
||||||
|
// Fonts
|
||||||
|
'--wa-font-family-body': 'Inter, sans-serif',
|
||||||
|
'--wa-font-family-heading': 'var(--wa-font-family-body)',
|
||||||
|
'--wa-font-family-code': 'ui-monospace, monospace',
|
||||||
|
'--wa-font-family-longform': 'ui-serif, serif',
|
||||||
|
'--wa-font-weight-body': 400,
|
||||||
|
'--wa-font-weight-heading': 700,
|
||||||
|
'--wa-font-weight-code': 400,
|
||||||
|
'--wa-font-weight-longform': 400,
|
||||||
|
|
||||||
|
// Elements
|
||||||
|
'--wa-border-radius-scale': 1,
|
||||||
|
'--wa-space-scale': 0.875,
|
||||||
|
'--wa-border-width-scale': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All fonts used by themes, collected from the four font categories.
|
||||||
|
*/
|
||||||
|
export const fonts = themes
|
||||||
|
.flatMap(theme => [theme.fonts.body, theme.fonts.heading, theme.fonts.code, theme.fonts.longform])
|
||||||
|
.filter(
|
||||||
|
(font, index, array) =>
|
||||||
|
array.findIndex(f => f.name === font.name && f.css === font.css && f.href === font.href) === index,
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Font presets derived from themes, with unique font names in order: heading > body > code > longform
|
||||||
|
*/
|
||||||
|
export const fontPresets = themes
|
||||||
|
.map(theme => {
|
||||||
|
const fontNames = [
|
||||||
|
theme.fonts.heading.name,
|
||||||
|
theme.fonts.body.name,
|
||||||
|
theme.fonts.code.name,
|
||||||
|
theme.fonts.longform.name,
|
||||||
|
];
|
||||||
|
const uniqueFonts = fontNames.filter((name, index) => fontNames.indexOf(name) === index);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: theme.name,
|
||||||
|
displayName: uniqueFonts.join(' · '),
|
||||||
|
fontFamilyBody: theme.fonts.body.css,
|
||||||
|
fontFamilyHeading: theme.fonts.heading.css,
|
||||||
|
fontFamilyCode: theme.fonts.code.css,
|
||||||
|
fontFamilyLongform: theme.fonts.longform.css,
|
||||||
|
fontWeightBody: theme.tokens['--wa-font-weight-body'],
|
||||||
|
fontWeightHeading: theme.tokens['--wa-font-weight-heading'],
|
||||||
|
fontWeightCode: theme.tokens['--wa-font-weight-code'],
|
||||||
|
fontWeightLongform: theme.tokens['--wa-font-weight-longform'],
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((preset, index, array) => array.findIndex(p => p.displayName === preset.displayName) === index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Element presets derived from themes.
|
||||||
|
*/
|
||||||
|
export const elementPresets = themes.map(theme => ({
|
||||||
|
name: theme.name,
|
||||||
|
borderRadiusScale: theme.tokens['--wa-border-radius-scale'],
|
||||||
|
spaceScale: theme.tokens['--wa-space-scale'],
|
||||||
|
borderWidthScale: theme.tokens['--wa-border-width-scale'],
|
||||||
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All palettes used by themes in a simple array.
|
||||||
|
*/
|
||||||
|
export const palettes = themes
|
||||||
|
.map(theme => theme.palette)
|
||||||
|
.filter(
|
||||||
|
(palette, index, array) =>
|
||||||
|
array.findIndex(p => p.name === palette.name && p.filename === palette.filename) === index,
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Available icons.
|
||||||
|
*/
|
||||||
|
export const icons = [
|
||||||
|
{ name: 'Classic', libraryName: 'classic' },
|
||||||
|
{ name: 'Sharp', libraryName: 'sharp' },
|
||||||
|
{ name: 'Duotone', libraryName: 'duotone' },
|
||||||
|
{ name: 'Sharp Duotone', libraryName: 'sharp-duotone' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const colors = ['red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'indigo', 'purple', 'pink', 'gray'];
|
||||||
|
export const tints = ['95', '90', '80', '70', '60', '50', '40', '30', '20', '10', '05'];
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
// import { inlined } from '../../dist/components/icon/library.wa.js';
|
|
||||||
const distDirectory = process.env.UNBUNDLED_DIST_DIRECTORY || path.join(path.resolve(), 'dist');
|
|
||||||
|
|
||||||
const THEME_DIR = path.join(distDirectory, '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', '');
|
|
||||||
}
|
|
||||||
@@ -8,15 +8,39 @@
|
|||||||
<script type="module" src="/assets/scripts/scroll.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/turbo.js"></script>
|
||||||
<script type="module" src="/assets/scripts/search.js"></script>
|
<script type="module" src="/assets/scripts/search.js"></script>
|
||||||
|
<script type="module" src="/assets/scripts/search-list.js"></script>
|
||||||
<script type="module" src="/assets/scripts/outline.js"></script>
|
<script type="module" src="/assets/scripts/outline.js"></script>
|
||||||
|
<script type="module" src="/assets/scripts/color-scheme.js"></script>
|
||||||
|
<script type="module" src="/assets/scripts/theme.js"></script>
|
||||||
{% if hasSidebar %}<script type="module" src="/assets/scripts/sidebar.js"></script>{% endif %}
|
{% if hasSidebar %}<script type="module" src="/assets/scripts/sidebar.js"></script>{% endif %}
|
||||||
{% 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>
|
<script defer data-domain="backers.webawesome.com" src="https://plausible.io/js/script.js"></script>
|
||||||
|
|
||||||
{# Docs styles #}
|
{# Docs styles #}
|
||||||
<link rel="stylesheet" href="/assets/styles/docs.css" />
|
<link rel="stylesheet" href="/assets/styles/docs.css" />
|
||||||
|
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
// Set the initial color scheme before the page renders to prevent flashing
|
||||||
|
const value = localStorage.getItem('color-scheme') || 'auto';
|
||||||
|
const isDark = value === 'dark' || (value === 'auto' && matchMedia('(prefers-color-scheme: dark)').matches);
|
||||||
|
document.documentElement.classList.toggle('wa-dark', isDark);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
// Set the initial theme before the page renders to prevent flashing
|
||||||
|
const savedTheme = localStorage.getItem('theme') || 'default';
|
||||||
|
|
||||||
|
// Update the theme stylesheet link first
|
||||||
|
const themeStylesheet = document.getElementById('theme-stylesheet');
|
||||||
|
if (themeStylesheet) {
|
||||||
|
themeStylesheet.href = `/dist/styles/themes/${savedTheme}.css`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedTheme !== 'default') {
|
||||||
|
document.documentElement.classList.add(`wa-theme-${savedTheme}`);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body class="layout-{{ layout | stripExtension }}{{ ' page-wide' if wide }}">
|
<body class="layout-{{ layout | stripExtension }}{{ ' page-wide' if wide }}">
|
||||||
<!-- use view="desktop" as default to reduce layout jank on desktop site. -->
|
<!-- use view="desktop" as default to reduce layout jank on desktop site. -->
|
||||||
@@ -40,7 +64,7 @@
|
|||||||
<div id="docs-toolbar" class="wa-cluster wa-gap-xs">
|
<div id="docs-toolbar" class="wa-cluster wa-gap-xs">
|
||||||
{# Desktop selectors #}
|
{# Desktop selectors #}
|
||||||
<div class="wa-desktop-only wa-cluster wa-gap-xs">
|
<div class="wa-desktop-only wa-cluster wa-gap-xs">
|
||||||
{% include "preset-theme-selector.njk" %}
|
{% include "theme-selector.njk" %}
|
||||||
{% include "color-scheme-selector.njk" %}
|
{% include "color-scheme-selector.njk" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -61,7 +85,7 @@
|
|||||||
{# Mobile selectors #}
|
{# Mobile selectors #}
|
||||||
<div class="wa-mobile-only" slot="navigation-header">
|
<div class="wa-mobile-only" slot="navigation-header">
|
||||||
<div class="wa-cluster wa-gap-xs">
|
<div class="wa-cluster wa-gap-xs">
|
||||||
{% include "preset-theme-selector.njk" %}
|
{% include "theme-selector.njk" %}
|
||||||
{% include "color-scheme-selector.njk" %}
|
{% include "color-scheme-selector.njk" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,7 +103,6 @@
|
|||||||
</aside>
|
</aside>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
{# Main #}
|
{# Main #}
|
||||||
<main id="content">
|
<main id="content">
|
||||||
{# Expandable outline #}
|
{# Expandable outline #}
|
||||||
@@ -94,10 +117,11 @@
|
|||||||
<div id="flashes">{% server "flashes" %}</div>
|
<div id="flashes">{% server "flashes" %}</div>
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
{% include 'breadcrumbs.njk' %}
|
|
||||||
<h1 class="title">{{ title }}</h1>
|
<h1 class="title">{{ title }}</h1>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block beforeContent %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ content | safe }}
|
{{ content | safe }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
{% set ancestors = page.url | ancestors %}
|
|
||||||
|
|
||||||
{% if ancestors.length > 0 %}
|
|
||||||
<wa-breadcrumb id="docs-breadcrumbs">
|
|
||||||
{% for ancestor in ancestors %}
|
|
||||||
{% if ancestor.page.url != "/" %}
|
|
||||||
<wa-breadcrumb-item href="{{ ancestor.page.url }}">{{ ancestor.data.title }}</wa-breadcrumb-item>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
<wa-breadcrumb-item>{# Current page #}</wa-breadcrumb-item>
|
|
||||||
</wa-breadcrumb>
|
|
||||||
{% else %}
|
|
||||||
{% endif %}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
{# Color scheme selector #}
|
|
||||||
<wa-select class="color-scheme-selector" appearance="filled" size="small" value="auto" pill title="Press \ to toggle">
|
<wa-select class="color-scheme-selector" appearance="filled" size="small" value="auto" pill title="Press \ to toggle">
|
||||||
<wa-icon class="only-light" slot="start" name="sun" variant="regular"></wa-icon>
|
<wa-icon class="only-light" slot="start" name="sun" variant="regular"></wa-icon>
|
||||||
<wa-icon class="only-dark" slot="start" name="moon" variant="regular"></wa-icon>
|
<wa-icon class="only-dark" slot="start" name="moon" variant="regular"></wa-icon>
|
||||||
@@ -17,3 +16,10 @@
|
|||||||
System
|
System
|
||||||
</wa-option>
|
</wa-option>
|
||||||
</wa-select>
|
</wa-select>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Immediately set the correct value from storage
|
||||||
|
document.querySelectorAll('wa-select.color-scheme-selector').forEach(el => {
|
||||||
|
el.setAttribute('value', localStorage.getItem('color-scheme') || 'auto');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
<table class="colors wa-palette-{{ paletteId }} contrast-table" data-min-contrast="{{ minContrast }}">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
{% for tint_bg in tints -%}
|
|
||||||
{% for tint_fg in tints | reverse -%}
|
|
||||||
{% if (tint_fg - tint_bg) | abs == difference %}
|
|
||||||
<th>{{ tint_fg }} on {{ tint_bg }}</th>
|
|
||||||
{% endif %}
|
|
||||||
{%- endfor -%}
|
|
||||||
{%- endfor %}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
{% for hue in hues -%}
|
|
||||||
<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 %}
|
|
||||||
{% 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 %}
|
|
||||||
{{ tint_fg }} on {{ tint_bg }}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
{% endif %}
|
|
||||||
{%- endfor -%}
|
|
||||||
{%- endfor -%}
|
|
||||||
</tr>
|
|
||||||
{%- endfor %}
|
|
||||||
</table>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{# Cards for pages listed by category #}
|
|
||||||
|
|
||||||
<section id="grid" class="index-grid">
|
|
||||||
{% set groupedPages = allPages | groupPages(categories, page) %}
|
|
||||||
{% for category, pages in groupedPages -%}
|
|
||||||
{% if groupedPages.meta.groupCount > 1 and pages.length > 0 %}
|
|
||||||
<h2 class="index-category" id="{{ category | slugify }}">
|
|
||||||
{% if pages.meta.url %}<a href="{{ pages.meta.url }}">{{ pages.meta.title }}</a>
|
|
||||||
{% else %}
|
|
||||||
{{ pages.meta.title }}
|
|
||||||
{% endif %}
|
|
||||||
</h2>
|
|
||||||
{% endif %}
|
|
||||||
{%- for page in pages -%}
|
|
||||||
{% include "page-card.njk" %}
|
|
||||||
{%- endfor -%}
|
|
||||||
{%- endfor -%}
|
|
||||||
</section>
|
|
||||||
@@ -17,51 +17,13 @@
|
|||||||
|
|
||||||
<link rel="icon" href="/assets/images/webawesome-logo.svg" />
|
<link rel="icon" href="/assets/images/webawesome-logo.svg" />
|
||||||
<link rel="apple-touch-icon" href="/assets/images/app-icon.png">
|
<link rel="apple-touch-icon" href="/assets/images/app-icon.png">
|
||||||
|
|
||||||
{# Scripts #}
|
|
||||||
{# Hydration stuff #}
|
|
||||||
<script src="/assets/scripts/hydration-errors.js"></script>
|
|
||||||
<link rel="stylesheet" href="/assets/styles/hydration-errors.css">
|
|
||||||
<link rel="preconnect" href="https://cdn.jsdelivr.net">
|
<link rel="preconnect" href="https://cdn.jsdelivr.net">
|
||||||
|
|
||||||
{# Internal components #}
|
|
||||||
<script type="module" src="/assets/components/scoped.js"></script>
|
|
||||||
|
|
||||||
{# Web Awesome #}
|
|
||||||
<script type="module" src="/dist/webawesome.loader.js"></script>
|
<script type="module" src="/dist/webawesome.loader.js"></script>
|
||||||
|
|
||||||
{# Fallback loading when using the free repo #}
|
<link id="theme-stylesheet" rel="stylesheet" href="/dist/styles/themes/default.css" render="blocking" fetchpriority="high" />
|
||||||
<link rel="preconnect" href="https://early.webawesome.com">
|
{% for palette in themer.palettes %}
|
||||||
<script type="module">
|
<link rel="stylesheet" href="/dist/styles/color/palettes/{{palette.filename}}" />
|
||||||
document.addEventListener("wa-discovery-complete", loadLayout)
|
{% endfor %}
|
||||||
function loadLayout () {
|
|
||||||
if (!customElements.get("wa-page")) {
|
|
||||||
import("https://early.webawesome.com/webawesome@3.0.0-alpha.13/dist/components/page/page.js")
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e)
|
|
||||||
// known errors with dual registration. This is only a thing in the free repo.
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script type="module" src="/assets/scripts/theme-picker.js"></script>
|
|
||||||
{# Preset Theme #}
|
|
||||||
{% 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>
|
|
||||||
<script>
|
|
||||||
{
|
|
||||||
let preset = localStorage.presetTheme ?? 'default';
|
|
||||||
let script = document.currentScript;
|
|
||||||
script.insertAdjacentHTML('beforebegin', `<link id="theme-stylesheet" rel="stylesheet" id="theme-stylesheet" href="/dist/styles/themes/${ preset }.css" render="blocking" fetchpriority="high" />`);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script type="module" src="/assets/scripts/preset-theme-picker.js"></script>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/dist/styles/webawesome.css" />
|
<link rel="stylesheet" href="/dist/styles/webawesome.css" />
|
||||||
|
|
||||||
{# Used by Web Awesome App to inject other assets into the head. #}
|
{# Used by Web Awesome App to inject other assets into the head. #}
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
<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
|
|
||||||
<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
|
|
||||||
@import url('{% cdnUrl stylesheet %}');
|
|
||||||
```
|
|
||||||
</wa-tab-panel>
|
|
||||||
</wa-tab-group>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{%- if not page.data.unlisted -%}
|
|
||||||
{% 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" ignore missing %}
|
|
||||||
</div>
|
|
||||||
<span class="page-name">{{ page.data.title }}</span>
|
|
||||||
{% if pageSubtitle -%}
|
|
||||||
<div class="wa-caption-s">{{ pageSubtitle }}</div>
|
|
||||||
{%- endif %}
|
|
||||||
</wa-card>
|
|
||||||
{% if page.url %}</a>{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<div id="page_slots_demo">
|
|
||||||
<link rel="stylesheet" href="/docs/components/page/demo.css">
|
|
||||||
{% set slots = components.page.slots %}
|
|
||||||
|
|
||||||
<fieldset id="page_slots_fieldset">
|
|
||||||
<legend>Slots</legend>
|
|
||||||
<div class="options">
|
|
||||||
{% for slot in slots %}
|
|
||||||
{% if (slot.name != "skip-to-content") and (slot.name != "navigation-toggle-icon") %}
|
|
||||||
<wa-checkbox name="slot" value="{{ slot.name }}" {{ 'checked' if slot.name != "menu" and slot.name != 'navigation-toggle' | safe}} class="{{ 'default' if not slot.name }}"
|
|
||||||
data-description="{{ slot.description | inlineMarkdown }}" title="{{ slot.description | inlineMarkdown | striptags | safe }}">
|
|
||||||
{{ slot.name or "(default)" }}
|
|
||||||
</wa-checkbox>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<wa-zoomable-frame srcdoc="" zoom="0.5" id="page_slots_iframe"></wa-zoomable-frame>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="module">
|
|
||||||
const cacheBust = new Date().toString()
|
|
||||||
import(`/docs/components/page/demo.js?${cacheBust}`)
|
|
||||||
</script>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{# Preset theme selector #}
|
|
||||||
<wa-select appearance="filled" size="small" value="default" pill class="preset-theme-selector">
|
|
||||||
<wa-icon slot="start" name="paintbrush" variant="regular"></wa-icon>
|
|
||||||
{% for theme in collections.theme | sort %}
|
|
||||||
<wa-option value="{{ theme.page.fileSlug }}">
|
|
||||||
{{ theme.data.title }}
|
|
||||||
</wa-option>
|
|
||||||
{% endfor %}
|
|
||||||
</wa-select>
|
|
||||||
@@ -20,58 +20,6 @@
|
|||||||
<li><a href="/docs/resources/visual-tests">Visual Tests</a></li>
|
<li><a href="/docs/resources/visual-tests">Visual Tests</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<!-- Themes -->
|
|
||||||
<wa-details appearance="outlined">
|
|
||||||
<h2 slot="summary">
|
|
||||||
<a href="/docs/themes/" title="Overview">
|
|
||||||
Themes
|
|
||||||
<wa-icon name="grid-2" aria-hidden="true"></wa-icon>
|
|
||||||
</a>
|
|
||||||
</h2>
|
|
||||||
<ul>
|
|
||||||
<li><a href="/docs/themes/default/">Default</a></li>
|
|
||||||
<li><a href="/docs/themes/shoelace/">Shoelace</a></li>
|
|
||||||
<li><a href="/docs/themes/awesome/">Awesome</a></li>
|
|
||||||
<li>
|
|
||||||
<a href="/docs/themes/active/">Active</a>
|
|
||||||
<wa-badge class="pro" appearance="accent" attention="none">PRO</wa-badge>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/docs/themes/brutalist/">Brutalist</a>
|
|
||||||
<wa-badge class="pro" appearance="accent" attention="none">PRO</wa-badge>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/docs/themes/glossy/">Glossy</a>
|
|
||||||
<wa-badge class="pro" appearance="accent" attention="none">PRO</wa-badge>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/docs/themes/matter/">Matter</a>
|
|
||||||
<wa-badge class="pro" appearance="accent" attention="none">PRO</wa-badge>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/docs/themes/mellow/">Mellow</a>
|
|
||||||
<wa-badge class="pro" appearance="accent" attention="none">PRO</wa-badge>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/docs/themes/playful/">Playful</a>
|
|
||||||
<wa-badge class="pro" appearance="accent" attention="none">PRO</wa-badge>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/docs/themes/premium/">Premium</a>
|
|
||||||
<wa-badge class="pro" appearance="accent" attention="none">PRO</wa-badge>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/docs/themes/tailspin/">Tailspin</a>
|
|
||||||
<wa-badge class="pro" appearance="accent" attention="none">PRO</wa-badge>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/docs/themes/custom/">Custom</a>
|
|
||||||
<wa-badge class="pro" appearance="accent" attention="none">PRO</wa-badge>
|
|
||||||
</li>
|
|
||||||
<li><a href="/docs/themes/creating/">Creating Themes</a></li>
|
|
||||||
</ul>
|
|
||||||
</wa-details>
|
|
||||||
|
|
||||||
<!-- Components -->
|
<!-- Components -->
|
||||||
<wa-details appearance="outlined">
|
<wa-details appearance="outlined">
|
||||||
<h2 slot="summary">
|
<h2 slot="summary">
|
||||||
@@ -367,44 +315,13 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</wa-details>
|
</wa-details>
|
||||||
|
|
||||||
<!-- Color palettes -->
|
<!-- Color Palettes & Themes -->
|
||||||
<wa-details appearance="outlined">
|
<h2>Color Palettes & Themes</h2>
|
||||||
<h2 slot="summary">
|
<ul>
|
||||||
<a href="/docs/palettes/" title="Overview">
|
<li><a href="/docs/color-palettes">Color Palettes</a></li>
|
||||||
Color Palettes
|
<li><a href="/docs/themes">Themes</a></li>
|
||||||
<wa-icon name="grid-2" aria-hidden="true"></wa-icon>
|
<li><a href="/themer" data-turbo="false">Theme Builder</a></li>
|
||||||
</a>
|
</ul>
|
||||||
</h2>
|
|
||||||
<ul>
|
|
||||||
<li><a href="/docs/palettes/default/">Default</a></li>
|
|
||||||
<li>
|
|
||||||
<a href="/docs/palettes/anodized/">Anodized</a>
|
|
||||||
<wa-badge class="pro" appearance="accent" attention="none">PRO</wa-badge>
|
|
||||||
</li>
|
|
||||||
<li><a href="/docs/palettes/bright/">Bright</a></li>
|
|
||||||
<li>
|
|
||||||
<a href="/docs/palettes/elegant/">Elegant</a>
|
|
||||||
<wa-badge class="pro" appearance="accent" attention="none">PRO</wa-badge>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/docs/palettes/mild/">Mild</a>
|
|
||||||
<wa-badge class="pro" appearance="accent" attention="none">PRO</wa-badge>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/docs/palettes/natural/">Natural</a>
|
|
||||||
<wa-badge class="pro" appearance="accent" attention="none">PRO</wa-badge>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/docs/palettes/rudimentary/">Rudimentary</a>
|
|
||||||
<wa-badge class="pro" appearance="accent" attention="none">PRO</wa-badge>
|
|
||||||
</li>
|
|
||||||
<li><a href="/docs/palettes/shoelace/">Shoelace</a></li>
|
|
||||||
<li>
|
|
||||||
<a href="/docs/palettes/vogue/">Vogue</a>
|
|
||||||
<wa-badge class="pro" appearance="accent" attention="none">PRO</wa-badge>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</wa-details>
|
|
||||||
|
|
||||||
<!-- Design tokens -->
|
<!-- Design tokens -->
|
||||||
<wa-details appearance="outlined">
|
<wa-details appearance="outlined">
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
{% if since -%}
|
|
||||||
<wa-badge variant="neutral">Since {{ since }}</wa-badge>
|
|
||||||
{% endif -%}
|
|
||||||
|
|
||||||
{%- if status %}
|
|
||||||
{%- if status == "experimental" %}
|
|
||||||
<wa-badge variant="warning">
|
|
||||||
<wa-icon name="flask"></wa-icon>
|
|
||||||
Experimental
|
|
||||||
</wa-badge>
|
|
||||||
{%- elif status == "stable" %}
|
|
||||||
<wa-badge variant="brand">Stable</wa-badge>
|
|
||||||
{%- else %}
|
|
||||||
<wa-badge>{{ status}}</wa-badge>
|
|
||||||
{%- endif -%}
|
|
||||||
{%- endif %}
|
|
||||||
|
|
||||||
{%- if isPro %}
|
|
||||||
<wa-badge class="pro">PRO</wa-badge>
|
|
||||||
{%- endif -%}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{% set paletteId = palette.fileSlug or page.fileSlug %}
|
|
||||||
{% set suffixes = ['-80', '', '-20'] %}
|
|
||||||
|
|
||||||
<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">
|
|
||||||
|
|
||||||
<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>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
{% set themeId = theme.fileSlug %}
|
|
||||||
|
|
||||||
<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-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>
|
|
||||||
<div class="wa-brand wa-plain">A</div>
|
|
||||||
{# <div class="wa-danger wa-outlined wa-filled"><wa-icon slot="icon" name="circle-exclamation" variant="regular"></wa-icon></div> #}
|
|
||||||
|
|
||||||
<div class="wa-neutral wa-accent">A</div>
|
|
||||||
<div class="wa-neutral wa-outlined">A</div>
|
|
||||||
<div class="wa-neutral wa-filled">A</div>
|
|
||||||
<div class="wa-neutral wa-plain">A</div>
|
|
||||||
{# <div class="wa-warning wa-outlined wa-filled"><wa-icon slot="icon" name="triangle-exclamation" variant="regular"></wa-icon></div> #}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</wa-scoped>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{% set themeId = theme.fileSlug or page.fileSlug %}
|
|
||||||
|
|
||||||
<wa-scoped class="theme-icon-host theme-typography-icon-host">
|
|
||||||
<template>
|
|
||||||
<link rel="stylesheet" href="/dist/styles/native/content.css">
|
|
||||||
<link rel="stylesheet" href="/dist/styles/native/blockquote.css">
|
|
||||||
<link rel="stylesheet" href="/dist/styles/themes/{{ page.fileSlug or 'default' }}.css">
|
|
||||||
<link rel="stylesheet" href="/dist/styles/themes/{{ themeId }}/typography.css">
|
|
||||||
<link rel="stylesheet" href="/assets/styles/theme-icons.css">
|
|
||||||
|
|
||||||
<div class="theme-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-scoped>
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
{% set themeId = theme.fileSlug or page.fileSlug %}
|
|
||||||
|
|
||||||
|
|
||||||
<wa-scoped class="theme-icon-host theme-overall-icon-host">
|
|
||||||
<template>
|
|
||||||
<link rel="stylesheet" href="/dist/styles/utilities.css">
|
|
||||||
<link rel="stylesheet" href="/dist/styles/native/content.css">
|
|
||||||
<link rel="stylesheet" href="/dist/styles/themes/{{ themeId }}.css">
|
|
||||||
<link rel="stylesheet" href="/assets/styles/theme-icons.css">
|
|
||||||
|
|
||||||
<div class="theme-icon theme-overall-icon" role="presentation" data-no-anchor data-no-outline>
|
|
||||||
<div class="row row-1">
|
|
||||||
<h2>Aa</h2>
|
|
||||||
<div class="swatches">
|
|
||||||
<div class="wa-brand"></div>
|
|
||||||
|
|
||||||
<div class="wa-success"></div>
|
|
||||||
<div class="wa-warning"></div>
|
|
||||||
<div class="wa-danger"></div>
|
|
||||||
<div class="wa-neutral"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row row-2">
|
|
||||||
<wa-input value="Input" size="small" inert></wa-input>
|
|
||||||
<wa-button size="small" variant="brand" inert>Go</wa-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</wa-scoped>
|
|
||||||
41
packages/webawesome/docs/_includes/theme-selector.njk
Normal file
41
packages/webawesome/docs/_includes/theme-selector.njk
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<wa-select appearance="filled" size="small" value="default" pill class="theme-selector">
|
||||||
|
<wa-icon slot="start" name="paintbrush" variant="regular"></wa-icon>
|
||||||
|
|
||||||
|
{# Free themes #}
|
||||||
|
{% for theme in themer.themes %}
|
||||||
|
{% if not theme.isPro %}
|
||||||
|
<wa-option
|
||||||
|
value="{{ theme.filename | stripExtension }}"
|
||||||
|
data-brand="{{ theme.colorBrand.color }}"
|
||||||
|
data-palette="{{ theme.palette.filename | stripExtension }}"
|
||||||
|
>
|
||||||
|
{{ theme.name }}
|
||||||
|
</wa-option>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{# Pro themes #}
|
||||||
|
{% for theme in themer.themes %}
|
||||||
|
{% if loop.first %}<wa-divider></wa-divider>{% endif %}
|
||||||
|
{% if theme.isPro %}
|
||||||
|
<wa-option
|
||||||
|
value="{{ theme.filename | stripExtension }}"
|
||||||
|
data-brand="{{ theme.colorBrand.color }}"
|
||||||
|
data-palette="{{ theme.palette.filename | stripExtension }}"
|
||||||
|
>
|
||||||
|
{{ theme.name }}
|
||||||
|
</wa-option>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</wa-select>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Immediately set the correct values from storage
|
||||||
|
document.querySelectorAll('wa-select.theme-selector').forEach(el => {
|
||||||
|
el.setAttribute('value', localStorage.getItem('theme') || 'default');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply saved brand and palette classes
|
||||||
|
document.documentElement.classList.add(`wa-brand-${localStorage.getItem('brand') || 'blue'}`);
|
||||||
|
document.documentElement.classList.add(`wa-palette-${localStorage.getItem('palette') || 'default'}`);
|
||||||
|
</script>
|
||||||
@@ -1,347 +0,0 @@
|
|||||||
<div class="showcase-examples-wrapper" aria-hidden="true" data-no-outline>
|
|
||||||
<div class="showcase-examples">
|
|
||||||
<wa-card>
|
|
||||||
<div slot="header" class="wa-split">
|
|
||||||
<h3 class="wa-heading-m">Your Cart</h3>
|
|
||||||
<wa-button appearance="plain" size="small" tabindex="-1">
|
|
||||||
<wa-icon name="xmark" label="Close"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
</div>
|
|
||||||
<div class="wa-stack wa-gap-xl">
|
|
||||||
<div class="wa-flank">
|
|
||||||
<wa-avatar shape="rounded" style="--background-color: var(--wa-color-green-60); --text-color: var(--wa-color-green-95);">
|
|
||||||
<wa-icon slot="icon" name="sword-laser"></wa-icon>
|
|
||||||
</wa-avatar>
|
|
||||||
<div class="wa-stack wa-gap-2xs">
|
|
||||||
<div class="wa-split wa-gap-2xs">
|
|
||||||
<strong>Initiate Saber</strong>
|
|
||||||
<strong>$179.99</strong>
|
|
||||||
</div>
|
|
||||||
<div class="wa-split wa-gap-2xs wa-caption-m">
|
|
||||||
<span>Green</span>
|
|
||||||
<a href="#" tabindex="-1">Remove</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<wa-divider></wa-divider>
|
|
||||||
<div class="wa-flank">
|
|
||||||
<wa-avatar shape="rounded" style="--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">
|
|
||||||
<strong>Repair Droid</strong>
|
|
||||||
<strong>$3,049.99</strong>
|
|
||||||
</div>
|
|
||||||
<div class="wa-split wa-gap-2xs wa-caption-m">
|
|
||||||
<span>R-series</span>
|
|
||||||
<a href="#" tabindex="-1">Remove</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div slot="footer" class="wa-stack">
|
|
||||||
<div class="wa-split">
|
|
||||||
<strong>Subtotal</strong>
|
|
||||||
<strong>$3,229.98</strong>
|
|
||||||
</div>
|
|
||||||
<span class="wa-caption-m">Shipping and taxes calculated at checkout.</span>
|
|
||||||
<wa-button tabindex="-1" variant="brand">
|
|
||||||
<wa-icon slot="start" name="shopping-bag"></wa-icon>
|
|
||||||
Checkout
|
|
||||||
</wa-button>
|
|
||||||
</div>
|
|
||||||
</wa-card>
|
|
||||||
<wa-card>
|
|
||||||
<wa-avatar shape="rounded" style="--size: 1.9lh; float: left; margin-right: var(--wa-space-m);">
|
|
||||||
<wa-icon slot="icon" name="hat-wizard" 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>
|
|
||||||
<wa-card>
|
|
||||||
<div class="wa-stack">
|
|
||||||
<h3 class="wa-heading-m">Sign In</h3>
|
|
||||||
<wa-input tabindex="-1" label="Email" placeholder="ddjarin@mandalore.gov">
|
|
||||||
<wa-icon slot="start" name="envelope" variant="regular"></wa-icon>
|
|
||||||
</wa-input>
|
|
||||||
<wa-input tabindex="-1" label="Password" type="password">
|
|
||||||
<wa-icon slot="start" name="lock" variant="regular"></wa-icon>
|
|
||||||
</wa-input>
|
|
||||||
<wa-button tabindex="-1" variant="brand">Sign In</wa-button>
|
|
||||||
<a href="#" tabindex="-1" class="wa-body-s">I forgot my password</a>
|
|
||||||
</div>
|
|
||||||
</wa-card>
|
|
||||||
<wa-card>
|
|
||||||
<div class="wa-stack">
|
|
||||||
<div class="wa-split">
|
|
||||||
<h3 class="wa-heading-m">To-Do</h3>
|
|
||||||
<wa-button appearance="plain" size="small" tabindex="-1">
|
|
||||||
<wa-icon name="plus" label="Add task"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
</div>
|
|
||||||
<wa-checkbox tabindex="-1" checked>Umbrella for Adelard</wa-checkbox>
|
|
||||||
<wa-checkbox tabindex="-1" checked>Waste-paper basket for Dora</wa-checkbox>
|
|
||||||
<wa-checkbox tabindex="-1" checked>Pen and ink for Milo</wa-checkbox>
|
|
||||||
<wa-checkbox tabindex="-1">Mirror for Angelica</wa-checkbox>
|
|
||||||
<wa-checkbox tabindex="-1">Silver spoons for Lobelia</wa-checkbox>
|
|
||||||
</div>
|
|
||||||
<div slot="footer">
|
|
||||||
<a href="" tabindex="-1">View all completed</a>
|
|
||||||
</div>
|
|
||||||
</wa-card>
|
|
||||||
<wa-card>
|
|
||||||
<div class="wa-stack">
|
|
||||||
<div class="wa-frame wa-border-radius-m" style="align-self: center; max-inline-size: 25ch;">
|
|
||||||
<img src="https://images.unsplash.com/photo-1667514627762-521b1c815a89?q=20" alt="Album art">
|
|
||||||
</div>
|
|
||||||
<div class="wa-flank:end wa-align-items-start">
|
|
||||||
<div class="wa-stack wa-gap-3xs">
|
|
||||||
<div class="wa-cluster wa-gap-xs" style="height: 2.25em;">
|
|
||||||
<strong>The Stone Troll</strong>
|
|
||||||
<small><wa-badge variant="neutral" appearance="filled">E</wa-badge></small>
|
|
||||||
</div>
|
|
||||||
<span class="wa-caption-m">Samwise G</span>
|
|
||||||
</div>
|
|
||||||
<wa-button appearance="plain" size="small" tabindex="-1">
|
|
||||||
<wa-icon name="ellipsis" label="Options"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
</div>
|
|
||||||
<div class="wa-stack wa-gap-2xs">
|
|
||||||
<wa-progress-bar value="34" style="height: 0.5em"></wa-progress-bar>
|
|
||||||
<div class="wa-split">
|
|
||||||
<span class="wa-caption-xs">1:01</span>
|
|
||||||
<span class="wa-caption-xs">-1:58</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="wa-grid wa-align-items-center" style="--min-column-size: 1em; justify-items: center;">
|
|
||||||
<wa-button appearance="plain" tabindex="-1">
|
|
||||||
<wa-icon name="backward" label="Skip backward"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
<wa-button appearance="plain" size="large" tabindex="-1">
|
|
||||||
<wa-icon name="pause" label="Pause"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
<wa-button appearance="plain" tabindex="-1">
|
|
||||||
<wa-icon name="forward" label="Skip forward"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</wa-card>
|
|
||||||
<wa-card>
|
|
||||||
<div class="wa-stack">
|
|
||||||
<h3 class="wa-heading-m">Chalmun's Spaceport Cantina</h3>
|
|
||||||
<div class="wa-cluster wa-gap-xs">
|
|
||||||
<wa-rating value="4.6" readonly tabindex="-1"></wa-rating>
|
|
||||||
<strong>4.6</strong>
|
|
||||||
<span>(419 reviews)</span>
|
|
||||||
</div>
|
|
||||||
<div class="wa-cluster wa-gap-xs">
|
|
||||||
<div class="wa-cluster wa-gap-3xs">
|
|
||||||
<wa-icon name="dollar" style="color: var(--wa-color-green-60);"></wa-icon>
|
|
||||||
<wa-icon name="dollar" style="color: var(--wa-color-green-60);"></wa-icon>
|
|
||||||
<wa-icon name="dollar" style="color: var(--wa-color-green-60);"></wa-icon>
|
|
||||||
</div>
|
|
||||||
<span class="wa-caption-m">•</span>
|
|
||||||
<wa-tag size="small">Cocktail Bar</wa-tag>
|
|
||||||
<wa-tag size="small">Gastropub</wa-tag>
|
|
||||||
<wa-tag size="small">Local Fare</wa-tag>
|
|
||||||
<wa-tag size="small">Gluten Free</wa-tag>
|
|
||||||
</div>
|
|
||||||
<div class="wa-flank wa-gap-xs">
|
|
||||||
<wa-icon name="location-dot"></wa-icon>
|
|
||||||
<a href="#" class="wa-caption-m" tabindex="-1">Mos Eisley, Tatooine</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</wa-card>
|
|
||||||
<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="start" name="at"></wa-icon>
|
|
||||||
Email
|
|
||||||
</wa-button>
|
|
||||||
<wa-button appearance="outlined" tabindex="-1">
|
|
||||||
<wa-icon slot="start" 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-button id="more-actions-2" slot="trigger" appearance="plain" size="small" tabindex="-1">
|
|
||||||
<wa-icon name="ellipsis-vertical" label="View menu"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
<wa-dropdown-item>Copy link</wa-dropdown-item>
|
|
||||||
<wa-dropdown-item>Rename</wa-dropdown-item>
|
|
||||||
<wa-dropdown-item>Move to trash</wa-dropdown-item>
|
|
||||||
</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>
|
|
||||||
@@ -5,14 +5,8 @@
|
|||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body class="layout-{{ layout | stripExtension }}">
|
<body class="layout-{{ layout | stripExtension }}">
|
||||||
|
{% block content %}
|
||||||
{% block header %}{% endblock %}
|
{{ content | safe }}
|
||||||
|
{% endblock %}
|
||||||
{% block content %}
|
|
||||||
{{ content | safe }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block afterContent %}{% endblock %}
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
{% set hasSidebar = true %}
|
|
||||||
{% set hasOutline = true %}
|
|
||||||
|
|
||||||
{% extends '../_includes/base.njk' %}
|
|
||||||
|
|
||||||
{# Component header #}
|
|
||||||
{% block header %}
|
|
||||||
{% include 'breadcrumbs.njk' %}
|
|
||||||
<h1 class="title">{{ title }}</h1>
|
|
||||||
<div class="block-info">
|
|
||||||
{% set snippets = (elements or element or snippets or snippet) | dict %}
|
|
||||||
|
|
||||||
{% for snippet, link in snippets %}
|
|
||||||
{% if snippet %}
|
|
||||||
<code class="class">
|
|
||||||
{%- if link -%}
|
|
||||||
<a href="{{ link }}">{{ snippet }}</a>
|
|
||||||
{%- else -%}
|
|
||||||
{{ snippet }}
|
|
||||||
{%- endif-%}
|
|
||||||
</code>
|
|
||||||
{%- endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% include '../_includes/status.njk' %}
|
|
||||||
</div>
|
|
||||||
{% if description %}
|
|
||||||
<p class="summary">
|
|
||||||
{{ description | inlineMarkdown | safe }}
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
{% block notes %}{% endblock %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{# Content #}
|
|
||||||
{% block content %}
|
|
||||||
{{ content | safe }}
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,5 +1,22 @@
|
|||||||
{% extends '../_layouts/block.njk' %}
|
{% extends '../_layouts/docs.njk' %}
|
||||||
{% set component = components[page.fileSlug] %}
|
{% set component = getComponent('wa-' + page.fileSlug) %}
|
||||||
|
|
||||||
|
{# Component header #}
|
||||||
|
{% block beforeContent %}
|
||||||
|
<div class="component-info">
|
||||||
|
<code class="component-tag"><{{ component.tagName }}></code>
|
||||||
|
<wa-badge variant="neutral">Since {{ component.since }}</wa-badge>
|
||||||
|
<wa-badge
|
||||||
|
{% if component.status == 'stable' %}variant="brand"{% endif %}
|
||||||
|
{% if component.status == 'experimental' %}variant="warning"{% endif %}
|
||||||
|
>
|
||||||
|
{{ component.status }}
|
||||||
|
</wa-badge>
|
||||||
|
</div>
|
||||||
|
<p class="component-summary">
|
||||||
|
{{ component.summary | inlineMarkdown | safe }}
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{# Component API #}
|
{# Component API #}
|
||||||
{% block afterContent %}
|
{% block afterContent %}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
{% extends '../_layouts/block.njk' %}
|
|
||||||
|
|
||||||
{# Component header #}
|
|
||||||
{% block notes %}
|
|
||||||
{% if component %}
|
|
||||||
<wa-callout variant="success">
|
|
||||||
<wa-icon slot="icon" name="lightbulb" variant="regular"></wa-icon>
|
|
||||||
Want to do more?
|
|
||||||
Check out the {% for name in (component | toList) -%}
|
|
||||||
{{ ' and ' if loop.last and not loop.first }}<a href="/docs/components/{{ name }}"><code><wa-{{ name }}></code></a>{{ ', ' if not loop.last }}
|
|
||||||
{%- endfor %} component{{ 's' if (component | isList) }}</a>!
|
|
||||||
</wa-callout>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block afterContent %}
|
|
||||||
{% if file %}
|
|
||||||
{% markdown %}
|
|
||||||
## Opting In to Native {{ title }} Styles
|
|
||||||
|
|
||||||
If you want to use the Native {{ title }} styles **without including the entirety of Web Awesome Native Styles**,
|
|
||||||
you can include the following CSS files from the Web Awesome CDN.
|
|
||||||
|
|
||||||
{% set stylesheet = file %}
|
|
||||||
{% include 'import-stylesheet-code.md.njk' %}
|
|
||||||
|
|
||||||
To use all of Web Awesome Native styles, follow the [instructions on the Native Styles overview page](../).
|
|
||||||
|
|
||||||
{% endmarkdown %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
---
|
|
||||||
layout: page-outline
|
|
||||||
---
|
|
||||||
{% set forTag = forTag or (page.url | split('/') | last) %}
|
|
||||||
{% if description %}
|
|
||||||
<div class="index-summary">{{ description | markdown | safe }}</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div id="block-filter">
|
|
||||||
<wa-input type="search" placeholder="Search {{ title }}" with-clear autofocus>
|
|
||||||
<wa-icon slot="start" name="search"></wa-icon>
|
|
||||||
</wa-input>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% 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>
|
|
||||||
|
|
||||||
{% if content | trim %}
|
|
||||||
<wa-divider style="--spacing: var(--wa-space-3xl)"></wa-divider> {# Temp fix for spacing issue #}
|
|
||||||
{{ content | safe }}
|
|
||||||
{% endif %}
|
|
||||||
@@ -1,313 +0,0 @@
|
|||||||
{% set hasSidebar = true %}
|
|
||||||
{% set hasOutline = true %}
|
|
||||||
{% set paletteId = page.fileSlug %}
|
|
||||||
{% set tints = ["95", "90", "80", "70", "60", "50", "40", "30", "20", "10", "05"] %}
|
|
||||||
|
|
||||||
{% extends '../_includes/base.njk' %}
|
|
||||||
|
|
||||||
{% block head %}
|
|
||||||
<style>@import url('/dist/styles/color/{{ paletteId }}.css') layer(palette.{{ paletteId }});</style>
|
|
||||||
<link href="{{ page.url }}../tweak.css" rel="stylesheet">
|
|
||||||
<script type="module" src="{{ page.url }}../tweak.js"></script>
|
|
||||||
{% 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-button appearance="plain" size="small" @click="rename">
|
|
||||||
<wa-icon name="pencil" label="Rename palette"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
<wa-button appearance="plain" size="small" v-if="saved" class="delete" @click="deleteSaved">
|
|
||||||
<wa-icon name="trash" label="Delete palette"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
<wa-button @click="save()" :disabled="!unsavedChanges"
|
|
||||||
:variant="unsavedChanges ? 'success' : 'neutral'" size="small" :appearance="unsavedChanges ? 'accent' : 'outlined'">
|
|
||||||
<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" with-remove @wa-remove="reset(param)" v-content="tweakHumanReadable"></wa-tag>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<wa-button @click="reset()" appearance="outlined" variant="danger" size="small">
|
|
||||||
<wa-icon slot="start" name="circle-xmark" variant="regular"></wa-icon>
|
|
||||||
Reset
|
|
||||||
</wa-button>
|
|
||||||
</wa-callout>
|
|
||||||
|
|
||||||
<table class="colors main wa-palette-{{ paletteId }}">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th class="core-column">Core tint</th>
|
|
||||||
{% for tint in tints -%}
|
|
||||||
<th>{{ tint }}</th>
|
|
||||||
{%- endfor %}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
{# Initialize to last hue before gray #}
|
|
||||||
{%- set hueBefore = hues[hues|length - 2] -%}
|
|
||||||
{% for hue in hues -%}
|
|
||||||
{% set coreTint = palettes[paletteId][hue].maxChromaTint %}
|
|
||||||
{%- set coreColor = palettes[paletteId][hue][coreTint] -%}
|
|
||||||
{%- set maxChroma = coreColor.c if coreColor.c > maxChroma else maxChroma -%}
|
|
||||||
{% if hue === 'gray' %}
|
|
||||||
<tr data-hue="{{ hue }}" class="color-scale"
|
|
||||||
:class="{tweaking: tweaking.grayChroma, tweaked: tweaked.grayChroma || tweaked.grayColor }">
|
|
||||||
{% else %}
|
|
||||||
<tr data-hue="{{ hue }}" class="color-scale"
|
|
||||||
:class="{tweaking: tweaking.{{ hue }}, tweaked: hueShifts.{{ hue }} }"
|
|
||||||
:style="{ '--hue-shift': hueShifts.{{ hue }} || '' }">
|
|
||||||
{% endif %}
|
|
||||||
<th>
|
|
||||||
{{ hue | capitalize }}
|
|
||||||
</th>
|
|
||||||
<td class="core-column"
|
|
||||||
style="--color: var(--wa-color-{{ hue }})"
|
|
||||||
:style="{
|
|
||||||
'--color-tweaked': colors.{{ hue }}[{{ coreTint }}],
|
|
||||||
'--color-gray-undertone': colors[grayColor][{{coreTint}}],
|
|
||||||
'--color-tweaked-no-gray-chroma': colorsMinusGrayChroma.{{ hue }}[{{ coreTint }}],
|
|
||||||
}">
|
|
||||||
<wa-dropdown>
|
|
||||||
<div slot="trigger" id="core-{{ hue }}-swatch" data-tint="core" class="color swatch"
|
|
||||||
style="background-color: var(--wa-color-{{ hue }}); color: var(--wa-color-{{ hue }}-{{ '05' if palettes[paletteId][hue].maxChromaTint > 60 else '95' }});"
|
|
||||||
>
|
|
||||||
{{ palettes[paletteId][hue].maxChromaTint }}
|
|
||||||
<wa-icon name="sliders-simple" class="tweak-icon"></wa-icon>
|
|
||||||
</div>
|
|
||||||
<div class="popup">
|
|
||||||
{% if hue === 'gray' %}
|
|
||||||
<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-button appearance="plain" @click="grayChroma = originalGrayChroma" class="clear-button">
|
|
||||||
<wa-icon name="circle-xmark" label="Reset" variant="regular"></wa-icon>
|
|
||||||
</wa-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-button appearance="plain" @click="hueShifts.{{ hue }} = 0" class="clear-button">
|
|
||||||
<wa-icon name="circle-xmark" label="Reset" variant="regular"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
</div>
|
|
||||||
</wa-slider>
|
|
||||||
<div class="label-min">More {{hueBefore}}</div>
|
|
||||||
<div class="label-max">More {{hueAfter}}</div>
|
|
||||||
</div>
|
|
||||||
{%- set hueBefore = hue -%}
|
|
||||||
{% endif %}
|
|
||||||
<div class="wa-gap-s">
|
|
||||||
<code>--wa-color-{{ hue }}</code>
|
|
||||||
<wa-copy-button value="--wa-color-{{ hue }}" copy-label="--wa-color-{{ hue }}"></wa-copy-button>
|
|
||||||
</div>
|
|
||||||
</div>`
|
|
||||||
</wa-dropdown>
|
|
||||||
</td>
|
|
||||||
{% for tint in tints -%}
|
|
||||||
{%- set color = palettes[paletteId][hue][tint] -%}
|
|
||||||
<td data-tint="{{ tint }}" style="--color: var(--wa-color-{{ hue }}-{{ tint }})"
|
|
||||||
: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>
|
|
||||||
{%- endfor -%}
|
|
||||||
</tr>
|
|
||||||
{%- 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-button appearance="plain" @click="chromaScale = 1" class="clear-button">
|
|
||||||
<wa-icon name="circle-xmark" label="Reset" variant="regular"></wa-icon>
|
|
||||||
</wa-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">
|
|
||||||
{% for page in collections.theme %}
|
|
||||||
{%- if page.data.palette == paletteId -%}
|
|
||||||
{% include "page-card.njk" %}
|
|
||||||
{%- endif -%}
|
|
||||||
{% endfor %}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{% markdown %}
|
|
||||||
## Color Contrast
|
|
||||||
|
|
||||||
Web Awesome color scales are designed to guarantee certain contrast ratios,
|
|
||||||
both per [WCAG 2.1 success criteria](https://www.w3.org/TR/WCAG21/#contrast-minimum)
|
|
||||||
as well as the emergent APCA specification _(planned)_,
|
|
||||||
so you can ensure that text is both legible to all users, and legally conformant.
|
|
||||||
|
|
||||||
### Level 1
|
|
||||||
|
|
||||||
A difference of `40` ensures a minimum **3:1** contrast ratio, suitable for large text and icons (AA).
|
|
||||||
{% endmarkdown %}
|
|
||||||
|
|
||||||
{% set difference = 40 %}
|
|
||||||
{% set minContrast = 3 %}
|
|
||||||
{% include "contrast-table.njk" %}
|
|
||||||
|
|
||||||
{% markdown %}
|
|
||||||
|
|
||||||
This also goes for a difference of `45`:
|
|
||||||
|
|
||||||
{% endmarkdown %}
|
|
||||||
|
|
||||||
{% set difference = 45 %}
|
|
||||||
{% include "contrast-table.njk" %}
|
|
||||||
|
|
||||||
{% markdown %}
|
|
||||||
|
|
||||||
### Level 2
|
|
||||||
|
|
||||||
A difference of `50` ensures a minimum **4.5:1** contrast ratio, suitable for normal text (AA) and large text (AAA)
|
|
||||||
{% endmarkdown %}
|
|
||||||
|
|
||||||
{% set difference = 50 %}
|
|
||||||
{% set minContrast = 4.5 %}
|
|
||||||
{% include "contrast-table.njk" %}
|
|
||||||
|
|
||||||
{% markdown %}
|
|
||||||
|
|
||||||
This also goes for a difference of `55`:
|
|
||||||
|
|
||||||
{% endmarkdown %}
|
|
||||||
|
|
||||||
{% set difference = 55 %}
|
|
||||||
{% include "contrast-table.njk" %}
|
|
||||||
|
|
||||||
{% markdown %}
|
|
||||||
### Level 3
|
|
||||||
|
|
||||||
A difference of `60` ensures a minimum **7:1** contrast ratio, suitable for all text (AAA)
|
|
||||||
{% endmarkdown %}
|
|
||||||
|
|
||||||
{% set difference = 60 %}
|
|
||||||
{% set minContrast = 7 %}
|
|
||||||
{% include "contrast-table.njk" %}
|
|
||||||
|
|
||||||
{% markdown %}
|
|
||||||
|
|
||||||
This also goes for a difference of `65`:
|
|
||||||
|
|
||||||
{% endmarkdown %}
|
|
||||||
|
|
||||||
{% set difference = 65 %}
|
|
||||||
{% include "contrast-table.njk" %}
|
|
||||||
|
|
||||||
{% markdown %}
|
|
||||||
## 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' %}
|
|
||||||
<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 %}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{% extends '../_layouts/block.njk' %}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
{% set hasSidebar = true %}
|
|
||||||
{% set hasOutline = true %}
|
|
||||||
{# {% set forceTheme = page.fileSlug %} #}
|
|
||||||
|
|
||||||
{% extends '../_includes/base.njk' %}
|
|
||||||
|
|
||||||
{% block head %}
|
|
||||||
<link href="{{ page.url }}../remix.css" rel="stylesheet">
|
|
||||||
<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 ref="preview" :src="'{{ page.url }}demo.html' + urlParams" src='{{ page.url }}demo.html' id="demo"></iframe>
|
|
||||||
|
|
||||||
{% if page.fileSlug !== 'custom' %}
|
|
||||||
<wa-details id="mix_and_match" class="wa-gap-m" :open="saved || unsavedChanges">
|
|
||||||
<h4 slot="summary" data-no-anchor data-no-outline id="remix">
|
|
||||||
<wa-icon name="arrows-rotate"></wa-icon>
|
|
||||||
Remix this theme
|
|
||||||
<wa-icon id="what-is-remixing" href="#remixing" name="circle-question" slot="end" variant="regular"></wa-icon>
|
|
||||||
<wa-tooltip for="what-is-remixing">Customize this theme by changing its colors and/or remixing it with design elements from other themes!</wa-tooltip>
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
<wa-select name="palette" label="Color palette" with-clear v-model="theme.palette">
|
|
||||||
<wa-icon name="swatchbook" slot="start" variant="regular"></wa-icon>
|
|
||||||
<wa-option v-for="(palette, paletteId) in palettes" :label="palette.title" :value="paletteId === baseTheme.palette ? '' : paletteId">
|
|
||||||
<palette-card :palette="paletteId" size="small">
|
|
||||||
<template #extra>
|
|
||||||
<wa-badge v-if="paletteId === baseTheme.palette" variant="neutral" appearance="outlined">Theme default</wa-badge>
|
|
||||||
</template>
|
|
||||||
</palette-card>
|
|
||||||
</wa-option>
|
|
||||||
</wa-select>
|
|
||||||
|
|
||||||
<color-select :model-value="computed.brand" @update:model-value="value => theme.brand = value" label="Brand color"
|
|
||||||
:values="hues"></color-select>
|
|
||||||
|
|
||||||
<wa-select name="colors" class="theme-colors-select" label="Color contrast from…" value="" with-clear v-model="theme.colors">
|
|
||||||
<wa-icon name="palette" slot="start" 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>
|
|
||||||
</template>
|
|
||||||
</wa-select>
|
|
||||||
|
|
||||||
<wa-select name="typography" label="Typography from…" with-clear v-model="theme.typography">
|
|
||||||
<wa-icon name="font-case" slot="start"></wa-icon>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<div class="index-grid">
|
|
||||||
{% if page.fileSlug === 'custom' %}
|
|
||||||
<palette-card :palette="computed.palette" subtitle="Color palette"></palette-card>
|
|
||||||
{% else %}
|
|
||||||
{% set themePage = page %}
|
|
||||||
{% set paletteURL = '/docs/palettes/' + palette + '/' %}
|
|
||||||
{% set page = paletteURL | getCollectionItemFromUrl %}
|
|
||||||
{% set pageSubtitle = "Default color palette" %}
|
|
||||||
{% include 'page-card.njk' %}
|
|
||||||
{% set page = themePage %}
|
|
||||||
{% endif %}
|
|
||||||
<wa-card class="wa-palette-{{ palette }}" style="--header-background: var(--wa-color-{{ brand }})"
|
|
||||||
:class="`wa-palette-${computed.palette}`" :style="{'--header-background': palettes[computed.palette]?.colors[computed.brand]?.key}">
|
|
||||||
<div slot="header"></div>
|
|
||||||
<div class="page-name" v-content="capitalize(computed.brand)">{{ brand | capitalize }}</div>
|
|
||||||
<div class="wa-caption-s">{{ 'Brand color' if page.fileSlug === 'custom' else 'Default brand color' }}</div>
|
|
||||||
</wa-card>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block afterContent %}
|
|
||||||
|
|
||||||
<h2 id="usage">How to use this theme</h2>
|
|
||||||
|
|
||||||
{% markdown %}
|
|
||||||
You can import this theme from the Web Awesome CDN.
|
|
||||||
|
|
||||||
{% set stylesheet = 'styles/themes/' + page.fileSlug + '.css' %}
|
|
||||||
|
|
||||||
<wa-tab-group class="import-stylesheet-code">
|
|
||||||
<wa-tab panel="html">In HTML</wa-tab>
|
|
||||||
<wa-tab panel="css">In CSS</wa-tab>
|
|
||||||
<wa-tab-panel name="html">
|
|
||||||
|
|
||||||
Add the following code to the `<head>` of your page:
|
|
||||||
```html { v-content:html="code.html.highlighted" }
|
|
||||||
<link rel="stylesheet" href="{% cdnUrl stylesheet %}" />
|
|
||||||
```
|
|
||||||
</wa-tab-panel>
|
|
||||||
<wa-tab-panel name="css">
|
|
||||||
|
|
||||||
Add the following code at the top of your CSS file:
|
|
||||||
```css { v-content:html="code.css.highlighted" }
|
|
||||||
@import url('{% cdnUrl stylesheet %}');
|
|
||||||
```
|
|
||||||
</wa-tab-panel>
|
|
||||||
</wa-tab-group>
|
|
||||||
|
|
||||||
## Dark mode
|
|
||||||
|
|
||||||
To activate the dark color scheme of the theme on any element and its contents, apply the class `wa-dark` to it.
|
|
||||||
This means you can use different color schemes throughout the page.
|
|
||||||
Here, we use the default theme with a dark sidebar:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" href="path/to/web-awesome/dist/styles/themes/default.css" />
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<nav class="wa-dark">
|
|
||||||
<!-- dark-themed sidebar -->
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- light-themed content -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
You can apply the class to the `<html>` element on your page to activate the dark color scheme for the entire page.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<html class="wa-dark">
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" href="path/to/web-awesome/dist/styles/themes/{{ page.fileSlug }}.css" />
|
|
||||||
<!-- other links, scripts, and metadata -->
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- page content -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
### Detecting Color Scheme Preference
|
|
||||||
|
|
||||||
Web Awesome's themes have both light and dark styles built in.
|
|
||||||
However, Web Awesome doesn't try to auto-detect the user's light/dark mode preference.
|
|
||||||
This should be done at the application level.
|
|
||||||
|
|
||||||
As a best practice, to provide a dark theme in your app, you should:
|
|
||||||
|
|
||||||
- Check for [`prefers-color-scheme`](https://stackoverflow.com/a/57795495/567486) and use its value by default
|
|
||||||
- Allow the user to override the setting in your app
|
|
||||||
- Remember the user's preference and restore it on subsequent logins
|
|
||||||
|
|
||||||
Web Awesome avoids using the `prefers-color-scheme` media query because not all apps support dark mode, and it would break things for the ones that don't.
|
|
||||||
|
|
||||||
Assuming the user's preference is in a variable called `colorScheme` (values: `auto`, `light`, `dark`),
|
|
||||||
you can use the following JS snippet to apply the `wa-dark` class to the `<html>` element accordingly:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const systemDark = window.matchMedia('(prefers-color-scheme: dark)');
|
|
||||||
const applyDark = function (event = systemDark) {
|
|
||||||
const isDark = colorScheme === 'auto' ? event.matches : colorScheme === 'dark';
|
|
||||||
document.documentElement.classList.toggle('wa-dark', isDark);
|
|
||||||
};
|
|
||||||
systemDark.addEventListener('change', applyDark);
|
|
||||||
applyDark();
|
|
||||||
```
|
|
||||||
|
|
||||||
</div> {# end theme app #}
|
|
||||||
{% endmarkdown %}
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{% extends '../_layouts/block.njk' %}
|
|
||||||
|
|
||||||
{% block afterContent %}
|
|
||||||
{% if file %}
|
|
||||||
{% markdown %}
|
|
||||||
## Opting In
|
|
||||||
|
|
||||||
If you want to use this utility **only** without [all others](../), you can include the following CSS file from the Web Awesome CDN.
|
|
||||||
|
|
||||||
{% set stylesheet = file %}
|
|
||||||
{% include 'import-stylesheet-code.md.njk' %}
|
|
||||||
|
|
||||||
Want them all?
|
|
||||||
Follow the [instructions on the Utilities overview page](../) to get all Web Awesome utilities.
|
|
||||||
|
|
||||||
{% endmarkdown %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,410 +0,0 @@
|
|||||||
import { parse } from 'path';
|
|
||||||
|
|
||||||
export function stripExtension(string) {
|
|
||||||
return parse(string).name;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stripPrefix(content) {
|
|
||||||
return content.replace(/^wa-/, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trims whitespace and pipes from the start and end of a string. Useful for CEM types, which can be pipe-delimited.
|
|
||||||
// With Prettier 3, this means a leading pipe will exist be present when the line wraps.
|
|
||||||
export function trimPipes(content) {
|
|
||||||
return typeof content === 'string' ? content.replace(/^(\s|\|)/g, '').replace(/(\s|\|)$/g, '') : content;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function keys(obj) {
|
|
||||||
return Object.keys(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function log(firstArg, ...rest) {
|
|
||||||
console.log(firstArg, ...rest);
|
|
||||||
return firstArg;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCollection(name) {
|
|
||||||
// From https://github.com/11ty/eleventy/blob/d3d24ccddb804e6e14773501d8c4e07e2c4b9c2b/src/Filters/GetLocaleCollectionItem.js#L39-L43
|
|
||||||
return this.collections?.[name] || this.ctx?.collections?.[name] || this.context?.environments?.collections?.[name];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCollectionItemFromUrl(url, collection) {
|
|
||||||
if (!url) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
collection ??= getCollection.call(this, 'all') || [];
|
|
||||||
return collection.find(item => item.url === url);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTitleFromUrl(url, collection) {
|
|
||||||
const item = getCollectionItemFromUrl.call(this, url, collection);
|
|
||||||
return item?.data.title || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function split(text, separator) {
|
|
||||||
return (text + '').split(separator).filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ancestors(url, { withCurrent = false, withRoot = false } = {}) {
|
|
||||||
let ret = [];
|
|
||||||
let currentUrl = url;
|
|
||||||
let currentItem = getCollectionItemFromUrl.call(this, url);
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isObject(value) {
|
|
||||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isList(value) {
|
|
||||||
return Array.isArray(value) || value instanceof Set;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get an Array or Set */
|
|
||||||
export function toList(value) {
|
|
||||||
return isList(value) ? value : [value];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert any value to something that can be iterated over with a for key, value loop.
|
|
||||||
* Arrays and sets will be converted to a Map of value -> undefined
|
|
||||||
*/
|
|
||||||
export function dict(value) {
|
|
||||||
if (value instanceof Map || isObject(value)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
let list = toList(value);
|
|
||||||
return new Map([...list].map(item => [item, undefined]));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deepValue(obj, key) {
|
|
||||||
key = Array.isArray(key) ? key : key.split('.');
|
|
||||||
return key.reduce((subObj, property) => subObj?.[property], obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function number(value, options) {
|
|
||||||
if (typeof value !== 'number' && isNaN(value)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
let lang = options?.lang ?? 'en';
|
|
||||||
if (options?.lang) {
|
|
||||||
delete options.lang;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!options || Object.keys(options).length === 0) {
|
|
||||||
options = { maximumSignificantDigits: 3 };
|
|
||||||
}
|
|
||||||
|
|
||||||
return Number(value).toLocaleString(lang, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isNumeric(value) {
|
|
||||||
return typeof value === 'number' || (typeof value === 'string' && !isNaN(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isString(value) {
|
|
||||||
return typeof value === 'string';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isEmpty(value) {
|
|
||||||
return value === null || value === undefined || value === '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function compare(a, b) {
|
|
||||||
let isEmptyA = isEmpty(a);
|
|
||||||
let isEmptyB = isEmpty(b);
|
|
||||||
|
|
||||||
if (isEmptyA) {
|
|
||||||
if (isEmptyB) {
|
|
||||||
return 0;
|
|
||||||
} else {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
} else if (isEmptyB) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Both strings, and at least one non-numeric
|
|
||||||
if (isNumeric(a) || isNumeric(b)) {
|
|
||||||
return a - b;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (a + '').localeCompare(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sort an array of objects by one or more of their properties */
|
|
||||||
export function sort(arr, by = { 'data.order': 1, 'data.title': '' }) {
|
|
||||||
let keys = Array.isArray(by) ? by : Object.keys(by);
|
|
||||||
|
|
||||||
return arr.sort((a, b) => {
|
|
||||||
let aValues = keys.map(key => deepValue(a, key) ?? by[key]);
|
|
||||||
let bValues = keys.map(key => deepValue(b, key) ?? by[key]);
|
|
||||||
|
|
||||||
for (let i = 0; i < aValues.length; i++) {
|
|
||||||
let aVal = aValues[i];
|
|
||||||
let bVal = bValues[i];
|
|
||||||
let result = compare(aVal, bVal);
|
|
||||||
|
|
||||||
// They are not equal in terms of comparison OR we're at the last key
|
|
||||||
if (result !== 0 || i === aValues.length - 1) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group an 11ty collection (or any array of objects with a `data.tags` property) by certain tags.
|
|
||||||
* @param {object[]} collection
|
|
||||||
* @param { Object<string, string> | string[]} [options] Options object or array of tags to group by.
|
|
||||||
* @param {string[] | true} [options.tags] Tags to group by. If true, groups by all tags.
|
|
||||||
* If not provided/empty, defaults to grouping by page hierarchy, with any pages with more than 1 children becoming groups.
|
|
||||||
* @param {string[]} [options.groups] The groups to use if only a subset or a specific order is desired. Defaults to `options.tags`.
|
|
||||||
* @param {string[]} [options.titles] Any title overrides for groups.
|
|
||||||
* @param {string | false} [options.other="Other"] The title to use for the "Other" group. If `false`, the "Other" group is removed..
|
|
||||||
* @returns { Object.<string, object[]> } An object of group ids to arrays of page objects.
|
|
||||||
*/
|
|
||||||
export function groupPages(collection, options = {}, page) {
|
|
||||||
if (!collection) {
|
|
||||||
console.error(`Empty collection passed to groupPages() to group by ${JSON.stringify(options)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 url = item.page.url;
|
|
||||||
let parentUrl = item.data.parentUrl;
|
|
||||||
|
|
||||||
byUrl[url] = item;
|
|
||||||
|
|
||||||
if (parentUrl) {
|
|
||||||
byParentUrl[parentUrl] ??= [];
|
|
||||||
byParentUrl[parentUrl].push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let urlToGroups = {};
|
|
||||||
|
|
||||||
for (let item of collection) {
|
|
||||||
let url = item.page.url;
|
|
||||||
let parentUrl = item.data.parentUrl;
|
|
||||||
|
|
||||||
if (grouping.isGroup(item)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let parentItem = byUrl[parentUrl];
|
|
||||||
if (parentItem && !grouping.isGroup(parentItem)) {
|
|
||||||
// Their parent is also here and is not a group
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let candidateGroups = grouping.getCandidateGroups(item);
|
|
||||||
|
|
||||||
if (groups) {
|
|
||||||
candidateGroups = candidateGroups.filter(group => groups.includes(group));
|
|
||||||
}
|
|
||||||
|
|
||||||
urlToGroups[url] ??= [];
|
|
||||||
|
|
||||||
for (let group of candidateGroups) {
|
|
||||||
urlToGroups[url].push(group);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let ret = {};
|
|
||||||
|
|
||||||
for (let url in urlToGroups) {
|
|
||||||
let groups = urlToGroups[url];
|
|
||||||
let item = byUrl[url];
|
|
||||||
|
|
||||||
if (groups.length === 0) {
|
|
||||||
// Not filtered out but also not categorized
|
|
||||||
groups = ['other'];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let group of groups) {
|
|
||||||
ret[group] ??= [];
|
|
||||||
ret[group].push(item);
|
|
||||||
|
|
||||||
if (!ret[group].meta) {
|
|
||||||
if (group === 'other') {
|
|
||||||
ret[group].meta = { title: other };
|
|
||||||
} else {
|
|
||||||
ret[group].meta = grouping.getGroupMeta(group);
|
|
||||||
ret[group].meta.title = titles[group] ?? ret[group].meta.title ?? capitalize(group);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function capitalize(str) {
|
|
||||||
str += '';
|
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const IDENTITY = x => x;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to print out one or more HTML attributes, especially conditional ones.
|
|
||||||
* Usage in 11ty:
|
|
||||||
* - Single attribute: `<foo{{ value | attr(name) }}>`
|
|
||||||
* - Multiple attributes: `<foo{{ { name1: value1, name2: value2 } | attr }}>`
|
|
||||||
*
|
|
||||||
* @overload
|
|
||||||
* @param {any} value - The attribute value If falsey, the attribute is not printed. If `true` the attribute is printed without a value.
|
|
||||||
* @param {string} name - The name of the attribute
|
|
||||||
*
|
|
||||||
* @overload
|
|
||||||
* @param {Object<string, any>} obj - Map of attribute names to values
|
|
||||||
*
|
|
||||||
* @returns {string} The attribute string. No `| safe` is needed.
|
|
||||||
*/
|
|
||||||
export function attr(value, name) {
|
|
||||||
const safe = this?.env.filters.safe ?? IDENTITY;
|
|
||||||
|
|
||||||
if (arguments.length === 1 && value && typeof value === 'object') {
|
|
||||||
// Called with a single object argument of names to values
|
|
||||||
let ret = Object.entries(obj)
|
|
||||||
.map(([name, value]) => attr(value, name))
|
|
||||||
.join('');
|
|
||||||
return safe(ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
// false, "", null, undefined
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
let ret = ' ' + name + (value === true ? '' : `="${value}"`);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
71
packages/webawesome/docs/_utils/manifest.js
Normal file
71
packages/webawesome/docs/_utils/manifest.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { dirname, resolve } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
const manifest = JSON.parse(readFileSync(resolve(__dirname, '../../dist/custom-elements.json'), 'utf-8'));
|
||||||
|
/**
|
||||||
|
* @returns Fetches components from custom-elements.json and returns them in more sane format.
|
||||||
|
*/
|
||||||
|
export function getComponents() {
|
||||||
|
const components = [];
|
||||||
|
|
||||||
|
manifest.modules?.forEach(module => {
|
||||||
|
module.declarations?.forEach(declaration => {
|
||||||
|
if (declaration.customElement) {
|
||||||
|
// Generate the dist path based on the src path and attach it to the component
|
||||||
|
declaration.path = module.path.replace(/^src\//, 'dist/').replace(/\.ts$/, '.js');
|
||||||
|
|
||||||
|
// Remove private members and those that lack a description
|
||||||
|
const members = declaration.members?.filter(member => member.description && member.privacy !== 'private');
|
||||||
|
const methods = members?.filter(prop => prop.kind === 'method' && prop.privacy !== 'private');
|
||||||
|
const properties = members?.filter(prop => {
|
||||||
|
// Look for a corresponding attribute
|
||||||
|
const attribute = declaration.attributes?.find(attr => attr.fieldName === prop.name);
|
||||||
|
if (attribute) {
|
||||||
|
prop.attribute = attribute.name || attribute.fieldName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prop.kind === 'field' && prop.privacy !== 'private';
|
||||||
|
});
|
||||||
|
components.push({
|
||||||
|
...declaration,
|
||||||
|
methods,
|
||||||
|
properties,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build dependency graphs
|
||||||
|
components.forEach(component => {
|
||||||
|
const dependencies = [];
|
||||||
|
|
||||||
|
// Recursively fetch sub-dependencies
|
||||||
|
function getDependencies(tag) {
|
||||||
|
const cmp = components.find(c => c.tagName === tag);
|
||||||
|
if (!cmp || !Array.isArray(component.dependencies)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cmp.dependencies?.forEach(dependentTag => {
|
||||||
|
if (!dependencies.includes(dependentTag)) {
|
||||||
|
dependencies.push(dependentTag);
|
||||||
|
}
|
||||||
|
getDependencies(dependentTag);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getDependencies(component.tagName);
|
||||||
|
|
||||||
|
component.dependencies = dependencies.sort();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort by name
|
||||||
|
return components.sort((a, b) => {
|
||||||
|
if (a.name < b.name) return -1;
|
||||||
|
if (a.name > b.name) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
/**
|
|
||||||
* Low-level utility to encapsulate a bit of HTML (mainly to apply certain stylesheets to it without them leaking to the rest of the page)
|
|
||||||
* Usage: <wa-scoped><template><!-- your HTML here --></template></wa-scoped>
|
|
||||||
*/
|
|
||||||
import { discover } from '/dist/webawesome.js';
|
|
||||||
|
|
||||||
const imports = new Set();
|
|
||||||
const fontFaceRules = new Set();
|
|
||||||
|
|
||||||
export default class WaScoped extends HTMLElement {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.attachShadow({ mode: 'open' });
|
|
||||||
|
|
||||||
this.observer = new MutationObserver(records => this.render(records));
|
|
||||||
this.observer.observe(this, { childList: true, subtree: true, characterData: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
this.render();
|
|
||||||
this.ownerDocument.documentElement.addEventListener('wa-color-scheme-change', e =>
|
|
||||||
this.#applyDarkMode(e.detail.dark),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render(records) {
|
|
||||||
this.observer.takeRecords();
|
|
||||||
this.observer.disconnect();
|
|
||||||
|
|
||||||
this.shadowRoot.innerHTML = '';
|
|
||||||
|
|
||||||
// To avoid mutating this.childNodes while iterating over it
|
|
||||||
let nodes = [];
|
|
||||||
|
|
||||||
for (let template of this.childNodes) {
|
|
||||||
if (!(template instanceof HTMLTemplateElement)) {
|
|
||||||
if (template.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
console.warn('<wa-scoped> can only contain <template> elements');
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (template.content.childNodes.length > 0) {
|
|
||||||
nodes.push(template.content.cloneNode(true));
|
|
||||||
} else if (template.childNodes.length > 0) {
|
|
||||||
// Fake template, suck its children out of the light DOM
|
|
||||||
nodes.push(...template.childNodes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.shadowRoot.append(...nodes);
|
|
||||||
|
|
||||||
this.#fixStyles();
|
|
||||||
this.#applyDarkMode();
|
|
||||||
|
|
||||||
discover(this.shadowRoot);
|
|
||||||
|
|
||||||
this.observer.observe(this, { childList: true, subtree: true, characterData: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
#applyDarkMode(isDark = getComputedStyle(this).colorScheme === 'dark') {
|
|
||||||
// Hack to make dark mode work
|
|
||||||
// NOTE If any child nodes actually have .wa-dark, this will override it
|
|
||||||
for (let node of this.shadowRoot.children) {
|
|
||||||
node.classList.toggle('wa-dark', isDark);
|
|
||||||
}
|
|
||||||
this.classList.toggle('wa-dark', isDark);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @font-face does not work in shadow DOM in Chrome & FF, as of March 2025 https://issues.chromium.org/issues/41085401
|
|
||||||
* This works around this issue by traversing the shadow DOM CSS looking
|
|
||||||
* for @font-face rules or CSS imports to known font providers and copies them to the main document
|
|
||||||
*/
|
|
||||||
async #fixStyles() {
|
|
||||||
let styleElements = [...this.shadowRoot.querySelectorAll('link[rel="stylesheet"], style')];
|
|
||||||
|
|
||||||
let loadStates = styleElements.map(element => {
|
|
||||||
try {
|
|
||||||
if (element.sheet?.cssRules) {
|
|
||||||
// Already loaded
|
|
||||||
return Promise.resolve(element.sheet);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// CORS
|
|
||||||
return Promise.resolve(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
element.addEventListener('load', e => resolve(element.sheet));
|
|
||||||
element.addEventListener('error', e => reject(null));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.allSettled(loadStates);
|
|
||||||
|
|
||||||
let fontRules = findFontFaceRules(...this.shadowRoot.styleSheets);
|
|
||||||
|
|
||||||
if (!fontRules.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let doc = this.ownerDocument;
|
|
||||||
// Why not adoptedStyleSheets? Can't have @import in those yet
|
|
||||||
let id = `wa-scoped-hoisted-fonts`;
|
|
||||||
let style = doc.head.querySelector('style#' + id);
|
|
||||||
if (!style) {
|
|
||||||
style = Object.assign(doc.createElement('style'), { id, textContent: ' ' });
|
|
||||||
doc.head.append(style);
|
|
||||||
}
|
|
||||||
let sheet = style.sheet;
|
|
||||||
|
|
||||||
for (let rule of fontRules) {
|
|
||||||
let cssText = rule.cssText;
|
|
||||||
if (rule.type === CSSRule.FONT_FACE_RULE) {
|
|
||||||
if (fontFaceRules.has(cssText)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
fontFaceRules.add(cssText);
|
|
||||||
sheet.insertRule(cssText);
|
|
||||||
} else if (rule.type === CSSRule.IMPORT_RULE) {
|
|
||||||
if (imports.has(rule.href)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
imports.add(rule.href);
|
|
||||||
sheet.insertRule(cssText, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static observedAttributes = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define('wa-scoped', WaScoped);
|
|
||||||
|
|
||||||
export const WEB_FONT_HOSTS = [
|
|
||||||
'fonts.googleapis.com',
|
|
||||||
'fonts.gstatic.com',
|
|
||||||
'use.typekit.net',
|
|
||||||
'fonts.adobe.com',
|
|
||||||
'kit.fontawesome.com',
|
|
||||||
'pro.fontawesome.com',
|
|
||||||
'cdn.materialdesignicons.com',
|
|
||||||
];
|
|
||||||
|
|
||||||
function findFontFaceRules(...stylesheets) {
|
|
||||||
let ret = [];
|
|
||||||
|
|
||||||
for (let sheet of stylesheets) {
|
|
||||||
let rules;
|
|
||||||
try {
|
|
||||||
rules = sheet.cssRules;
|
|
||||||
} catch (e) {
|
|
||||||
// CORS
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let rule of rules) {
|
|
||||||
if (rule.type === CSSRule.FONT_FACE_RULE) {
|
|
||||||
ret.push(rule);
|
|
||||||
} else if (rule.type === CSSRule.IMPORT_RULE) {
|
|
||||||
if (WEB_FONT_HOSTS.some(host => rule.href.includes(host))) {
|
|
||||||
ret.push(rule);
|
|
||||||
} else if (rule.styleSheet) {
|
|
||||||
ret.push(...findFontFaceRules(rule.styleSheet));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import { deepEntries } from '../scripts/util/deep.js';
|
|
||||||
import { themeConfig } from './theming.js';
|
|
||||||
import themes from '/assets/data/themes.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map of font pairings (body + heading) to the first theme that uses them.
|
|
||||||
*/
|
|
||||||
export const pairings = {};
|
|
||||||
|
|
||||||
// NOTE Do not use Symbols, we want these to be enumerable when used as keys
|
|
||||||
export const sameAs = { body: '$body' };
|
|
||||||
|
|
||||||
export const fontNames = {
|
|
||||||
'system-ui': 'OS Default',
|
|
||||||
'ui-serif': 'OS Default Serif',
|
|
||||||
'ui-sans-serif': 'OS Default Sans Serif',
|
|
||||||
'ui-monospace': 'OS Default Code Font',
|
|
||||||
'ui-monospace': 'OS Default Code Font',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function defaultTitle(fonts) {
|
|
||||||
let { body, heading = sameAs.body } = fonts;
|
|
||||||
let names = [body];
|
|
||||||
|
|
||||||
if (heading !== sameAs.body) {
|
|
||||||
names.unshift(heading);
|
|
||||||
}
|
|
||||||
|
|
||||||
return names.map(name => fontNames[name] ?? name).join(' • ');
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let id in themes) {
|
|
||||||
let theme = themes[id];
|
|
||||||
let { fonts } = theme;
|
|
||||||
|
|
||||||
if (fonts) {
|
|
||||||
let { body, heading = sameAs.body } = fonts;
|
|
||||||
|
|
||||||
pairings[body] ??= {};
|
|
||||||
pairings[body][heading] ??= {
|
|
||||||
id, // First theme that uses this pairing
|
|
||||||
ids: new Set([id]), // All themes that use this pairing
|
|
||||||
url: themeConfig.typography.url(id), // Stylesheet URL
|
|
||||||
fonts,
|
|
||||||
get title() {
|
|
||||||
return defaultTitle(this.fonts);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
pairings[body][heading].ids.add(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const pairingsEntries = deepEntries(pairings, {
|
|
||||||
descend(value, key, parent, path) {
|
|
||||||
if (value?.fonts) {
|
|
||||||
return false; // Don't recurse into pairing objects
|
|
||||||
}
|
|
||||||
},
|
|
||||||
filter(value, key, parent, path) {
|
|
||||||
// Only keep 2 levels (body → heading → pairing)
|
|
||||||
return path.length === 1;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const pairingsList = pairingsEntries.map(arg => arg.at(-1));
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export const iconLibraries = {
|
|
||||||
default: {
|
|
||||||
title: 'Font Awesome',
|
|
||||||
family: ['classic', 'sharp', 'duotone', 'sharp-duotone'],
|
|
||||||
style: ['solid', 'regular', 'light', 'thin'],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export * from './colors.js';
|
|
||||||
// export * from './fonts.js';
|
|
||||||
export * from './icons.js';
|
|
||||||
export * from './theming.js';
|
|
||||||
|
|
||||||
export const cdnUrl = globalThis.document ? document.documentElement.dataset.cdnUrl : '/dist/';
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
---
|
|
||||||
layout: null
|
|
||||||
permalink: '/assets/data/palettes.js'
|
|
||||||
eleventyExcludeFromCollections: true
|
|
||||||
---
|
|
||||||
import Color from 'https://colorjs.io/dist/color.js';
|
|
||||||
|
|
||||||
const palettes = {
|
|
||||||
{%- for palette in collections.palette | sort %}
|
|
||||||
{%- if not palette.data.unlisted %}
|
|
||||||
{% set paletteId = palette.fileSlug -%}
|
|
||||||
{%- set colors = palettes[paletteId] -%}
|
|
||||||
'{{ paletteId }}': {
|
|
||||||
id: '{{ paletteId }}',
|
|
||||||
title: '{{ palette.data.title }}',
|
|
||||||
colors: {
|
|
||||||
{% for hue, tints in colors -%}
|
|
||||||
'{{ hue }}': {
|
|
||||||
{% for tint, value in tints -%}
|
|
||||||
{%- if tint != '05' -%}
|
|
||||||
'{{ '05' if tint == '5' else tint }}': '{{ value | safe }}',
|
|
||||||
{%- endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
get key() {
|
|
||||||
return this[this.maxChromaTint];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{% endfor -%} // end colors
|
|
||||||
}
|
|
||||||
}, // end palette
|
|
||||||
{%- endif -%}
|
|
||||||
{% endfor %}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create Color instances for each color
|
|
||||||
for (let palette in palettes) {
|
|
||||||
for (let hue in palettes[palette].colors) {
|
|
||||||
let scale = palettes[palette].colors[hue];
|
|
||||||
|
|
||||||
for (let tint in scale) {
|
|
||||||
let color = scale[tint];
|
|
||||||
try {
|
|
||||||
if (Array.isArray(color)) {
|
|
||||||
scale[tint] = new Color('oklch', color);
|
|
||||||
}
|
|
||||||
else if (typeof color === 'string' && isNaN(color)) {
|
|
||||||
scale[tint] = new Color(color);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default palettes;
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
---
|
|
||||||
layout: null
|
|
||||||
permalink: '/assets/data/themes.js'
|
|
||||||
eleventyExcludeFromCollections: true
|
|
||||||
---
|
|
||||||
|
|
||||||
export default {
|
|
||||||
{%- for theme in collections.theme | sort %}
|
|
||||||
{%- if not theme.data.unlisted and theme.fileSlug !== 'edit' and theme.fileSlug !== 'custom' %}
|
|
||||||
{% set themeId = theme.fileSlug -%}
|
|
||||||
{%- set themeMeta = themes[themeId] -%}
|
|
||||||
'{{ themeId }}': {
|
|
||||||
id: '{{ themeId }}',
|
|
||||||
title: '{{ theme.data.title }}',
|
|
||||||
palette: '{{ themeMeta.palette }}',
|
|
||||||
brand: '{{ themeMeta.brand }}',
|
|
||||||
isPro: {{ theme.data.isPro or 'pro' in theme.data.tags }},
|
|
||||||
fonts: {{ (theme.data.fonts | json or 'null') | safe }},
|
|
||||||
icons: {{ (themeMeta.icons | json or 'null') | safe }},
|
|
||||||
rounding: {{ themeMeta.rounding }},
|
|
||||||
spacing: {{ themeMeta.spacing }},
|
|
||||||
borderWidth: {{ themeMeta.borderWidth }},
|
|
||||||
dimension: {{ (theme.data.dimension or themeMeta.dimension or false) | json | safe }},
|
|
||||||
},
|
|
||||||
{%- endif %}
|
|
||||||
{% endfor %}
|
|
||||||
};
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import { deepEach, isPlainObject } from '../scripts/util/deep.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Data related to themes, theme remixing
|
|
||||||
* Must work in both browser and Node.js
|
|
||||||
*/
|
|
||||||
export const cdnUrl = globalThis.document ? document.documentElement.dataset.cdnUrl : '/dist/';
|
|
||||||
|
|
||||||
// This should eventually replace all uses of `urls` and `themeParams`
|
|
||||||
export const themeConfig = {
|
|
||||||
base: { url: id => `styles/themes/${id}.css`, default: 'default' },
|
|
||||||
colors: {
|
|
||||||
url: id => `styles/themes/${id}/color.css`,
|
|
||||||
docs: '/docs/themes/',
|
|
||||||
icon: 'palette',
|
|
||||||
default() {
|
|
||||||
return this.base;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
palette: {
|
|
||||||
url: id => `styles/color/${id}.css`,
|
|
||||||
docs: '/docs/palette/',
|
|
||||||
icon: 'swatchbook',
|
|
||||||
default(baseTheme) {
|
|
||||||
return baseTheme?.palette;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
brand: {
|
|
||||||
url: id => `styles/brand/${id}.css`,
|
|
||||||
icon: 'droplet',
|
|
||||||
default(baseTheme) {
|
|
||||||
return baseTheme?.brand;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
typography: {
|
|
||||||
url: id => `styles/themes/${id}/typography.css`,
|
|
||||||
docs: '/docs/themes/',
|
|
||||||
icon: 'font-case',
|
|
||||||
default() {
|
|
||||||
return this.base;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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,289 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en" class="wa-cloak">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Web Awesome Page Demo 1</title>
|
|
||||||
<link rel="stylesheet" href="/dist/styles/themes/default.css" />
|
|
||||||
<link rel="stylesheet" href="/dist/styles/webawesome.css" />
|
|
||||||
<script type="module" src="/dist/webawesome.loader.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<wa-page mobile-breakpoint="920">
|
|
||||||
<div slot="banner" class="wa-body-s">
|
|
||||||
<a href="#" class="wa-cluster wa-align-items-baseline wa-gap-xs" style="flex-wrap: nowrap">
|
|
||||||
<wa-icon name="gift"></wa-icon>
|
|
||||||
<span>Give a Hoot for the Holidays: Donate now and double your impact.</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<header slot="header" class="wa-split">
|
|
||||||
<div class="wa-cluster">
|
|
||||||
<wa-icon name="feather-pointed" style="color: var(--wa-color-brand-fill-loud); font-size: 1.5em"></wa-icon>
|
|
||||||
<span id="brand-name" class="wa-heading-s wa-desktop-only">Audubon Worldwide</span>
|
|
||||||
<a href="#">Our Work</a>
|
|
||||||
<a href="#">About Us</a>
|
|
||||||
<a href="#">Discover</a>
|
|
||||||
<a href="#">Get Involved</a>
|
|
||||||
</div>
|
|
||||||
<div class="wa-cluster wa-gap-xs">
|
|
||||||
<wa-button size="small" variant="brand" appearance="outlined">Find Your Local Audubon</wa-button>
|
|
||||||
<wa-button size="small" variant="brand">Donate</wa-button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<nav slot="subheader">
|
|
||||||
<div class="wa-cluster" style="flex-wrap: nowrap">
|
|
||||||
<wa-button data-toggle-nav appearance="plain" size="small">
|
|
||||||
<wa-icon name="bars" label="Menu"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
<wa-breadcrumb style="font-size: var(--wa-font-size-s)">
|
|
||||||
<wa-breadcrumb-item>Field Guides</wa-breadcrumb-item>
|
|
||||||
<wa-breadcrumb-item>Owls</wa-breadcrumb-item>
|
|
||||||
<wa-breadcrumb-item>Great Horned Owl</wa-breadcrumb-item>
|
|
||||||
</wa-breadcrumb>
|
|
||||||
</div>
|
|
||||||
<wa-input id="search" class="wa-desktop-only" placeholder="Search" size="small" style="max-inline-size: 12rem">
|
|
||||||
<wa-icon slot="start" name="magnifying-glass"></wa-icon>
|
|
||||||
</wa-input>
|
|
||||||
</nav>
|
|
||||||
<nav slot="navigation-header">
|
|
||||||
<div class="wa-flank">
|
|
||||||
<wa-avatar image="https://images.unsplash.com/photo-1544648720-132573cb590d?q=20" label=""></wa-avatar>
|
|
||||||
<div class="wa-stack wa-gap-3xs">
|
|
||||||
<span class="wa-heading-s">Great Horned Owl</span>
|
|
||||||
<span class="wa-caption-s" lang="la"><em>Bubo virginianus</em></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
<nav slot="navigation">
|
|
||||||
<a href="#identification">Identification</a>
|
|
||||||
<a href="#range">Range and Habitat</a>
|
|
||||||
<a href="#behavior">Behavior</a>
|
|
||||||
<a href="#conservation">Conservation</a>
|
|
||||||
</nav>
|
|
||||||
<nav slot="navigation-footer">
|
|
||||||
<a href="#" class="wa-flank">
|
|
||||||
<wa-icon name="camera"></wa-icon>
|
|
||||||
<span>Photo Gallery</span>
|
|
||||||
</a>
|
|
||||||
<a href="#" class="wa-flank">
|
|
||||||
<wa-icon name="map-location-dot"></wa-icon>
|
|
||||||
<span>Interactive Range Map</span>
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
<header slot="main-header">
|
|
||||||
<div
|
|
||||||
class="wa-flank:end wa-border-radius-l wa-dark"
|
|
||||||
style="
|
|
||||||
background-color: var(--wa-color-surface-lowered);
|
|
||||||
--content-percentage: 35%;
|
|
||||||
padding: var(--wa-space-m);
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div class="wa-stack" style="margin: var(--wa-space-2xl)">
|
|
||||||
<h1>Great Horned Owl</h1>
|
|
||||||
<wa-divider></wa-divider>
|
|
||||||
<div class="wa-cluster wa-gap-xs">
|
|
||||||
<wa-tag size="small">Owls</wa-tag>
|
|
||||||
<wa-tag size="small">Birds of Prey</wa-tag>
|
|
||||||
<wa-tag size="small">Pleistocene Birds</wa-tag>
|
|
||||||
</div>
|
|
||||||
<div class="wa-flank">
|
|
||||||
<wa-icon name="ruler"></wa-icon>
|
|
||||||
<span class="wa-caption-m">L 21.5" | WS 48.5"</span>
|
|
||||||
</div>
|
|
||||||
<div class="wa-flank">
|
|
||||||
<wa-icon name="earth-americas"></wa-icon>
|
|
||||||
<span class="wa-caption-m"
|
|
||||||
>North America (Widespread), Central America (Limited), South America (Limited)</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="wa-flank">
|
|
||||||
<wa-icon name="shield-heart"></wa-icon>
|
|
||||||
<span class="wa-caption-m">Least Concern</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="wa-frame" style="border-radius: var(--wa-border-radius-l); max-inline-size: 40ch">
|
|
||||||
<img src="https://images.unsplash.com/photo-1544648720-132573cb590d?q=20" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main class="wa-body-l">
|
|
||||||
<h2 id="identification">Identification</h2>
|
|
||||||
<p>
|
|
||||||
Lorem ipsum odor amet, consectetuer adipiscing elit. Eget habitant scelerisque lectus ultrices nascetur
|
|
||||||
aliquet sapien primis. Cursus sapien fusce semper nulla elit sociosqu lectus per sem. Sem ad porttitor dictum
|
|
||||||
nisl pharetra tortor convallis. Sit molestie hendrerit porta dictum tortor posuere euismod magna. Mauris
|
|
||||||
suspendisse pharetra finibus; eleifend etiam ridiculus.
|
|
||||||
</p>
|
|
||||||
<h2 id="range">Range and Habitat</h2>
|
|
||||||
<p>
|
|
||||||
Diam sed ipsum pretium porttitor class cubilia elementum. Blandit felis ligula habitant ultricies vulputate
|
|
||||||
rutrum lacus commodo pulvinar. Nostra semper placerat lectus in dis eu. Sagittis ipsum placerat rhoncus lacus
|
|
||||||
id eget. Erat pharetra aptent enim, augue accumsan ultricies inceptos habitasse. Senectus id maximus
|
|
||||||
parturient tellus; fermentum posuere vulputate luctus. Ac tempus dapibus vehicula ligula ullamcorper sit duis.
|
|
||||||
</p>
|
|
||||||
<h2 id="behavior">Behavior</h2>
|
|
||||||
<p>
|
|
||||||
Erat vitae luctus arcu taciti malesuada pretium arcu justo primis. Cubilia vitae maecenas congue velit id
|
|
||||||
netus arcu. Dictum vel pellentesque taciti fermentum risus consectetur amet. Faucibus commodo habitasse sem
|
|
||||||
maximus praesent purus, dignissim tristique porta. Platea magna justo ipsum ut metus ac facilisi. Imperdiet
|
|
||||||
laoreet pharetra maximus lacus tortor suscipit. Nam quisque iaculis orci porttitor pellentesque rhoncus.
|
|
||||||
Molestie sagittis tincidunt quisque nisi non urna conubia.
|
|
||||||
</p>
|
|
||||||
<h2 id="conservation">Conservation</h2>
|
|
||||||
<p>
|
|
||||||
Nullam magna quam quisque eu varius integer. Inceptos donec facilisi risus himenaeos semper mollis habitasse.
|
|
||||||
Vehicula lacus vivamus euismod pharetra mollis dictum. Ante ex tortor elementum eleifend habitasse orci
|
|
||||||
aliquam. Fames erat senectus fames etiam dapibus cursus.
|
|
||||||
</p>
|
|
||||||
</main>
|
|
||||||
<footer slot="main-footer">
|
|
||||||
<section>
|
|
||||||
<h2 class="wa-heading-m">Sources</h2>
|
|
||||||
<ul class="wa-body-s">
|
|
||||||
<li>
|
|
||||||
<cite
|
|
||||||
><a href="https://www.audubon.org/field-guide/bird/great-horned-owl" target="_blank" rel="noopener"
|
|
||||||
>Great Horned Owl</a
|
|
||||||
></cite
|
|
||||||
>, National Audubon Society. Retrieved 5 December 2024.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<cite
|
|
||||||
><a href="https://www.allaboutbirds.org/guide/Great_Horned_Owl/" target="_blank" rel="noopener"
|
|
||||||
>Great Horned Owl</a
|
|
||||||
></cite
|
|
||||||
>, All About Birds by CornellLab. Retrieved 5 December 2024.
|
|
||||||
</li>
|
|
||||||
<li>Armistead, G. L. (2015). <cite>Field guide to birds of Pennsylvania</cite>. Scott & Nix, Inc.</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
</footer>
|
|
||||||
<aside slot="aside" class="wa-desktop-only">
|
|
||||||
<h2 class="wa-heading-m">Discover More Birds</h2>
|
|
||||||
<wa-card>
|
|
||||||
<div slot="media" class="wa-frame">
|
|
||||||
<img src="https://images.unsplash.com/photo-1635254859323-65b78408dcca?q=20" alt="" />
|
|
||||||
</div>
|
|
||||||
<div class="wa-stack wa-gap-3xs">
|
|
||||||
<span class="wa-heading-s">Long-eared Owl</span>
|
|
||||||
<span class="wa-caption-s" lang="la"><em>Asio otus</em></span>
|
|
||||||
</div>
|
|
||||||
</wa-card>
|
|
||||||
<wa-card>
|
|
||||||
<div slot="media" class="wa-frame">
|
|
||||||
<img src="https://images.unsplash.com/photo-1661350356618-f5915c7b6a3c?q=20" alt="" />
|
|
||||||
</div>
|
|
||||||
<div class="wa-stack wa-gap-3xs">
|
|
||||||
<span class="wa-heading-s">Northen Hawk Owl</span>
|
|
||||||
<span class="wa-caption-s" lang="la"><em>Surnia ulula</em></span>
|
|
||||||
</div>
|
|
||||||
</wa-card>
|
|
||||||
<wa-card>
|
|
||||||
<div slot="media" class="wa-frame">
|
|
||||||
<img src="https://images.unsplash.com/photo-1660307777355-f08bced145d3?q=20" alt="" />
|
|
||||||
</div>
|
|
||||||
<div class="wa-stack wa-gap-3xs">
|
|
||||||
<span class="wa-heading-s">Golden Eagle</span>
|
|
||||||
<span class="wa-caption-s" lang="la"><em>Aquila chrysaetos</em></span>
|
|
||||||
</div>
|
|
||||||
</wa-card>
|
|
||||||
</aside>
|
|
||||||
<footer slot="footer" class="wa-grid wa-gap-xl">
|
|
||||||
<div class="wa-cluster" style="flex-wrap: nowrap">
|
|
||||||
<wa-icon name="feather-pointed" style="font-size: 1.5em"></wa-icon>
|
|
||||||
<span class="wa-heading-s">Audubon Worldwide</span>
|
|
||||||
</div>
|
|
||||||
<div class="wa-stack">
|
|
||||||
<h3 class="wa-heading-xs">Our Work</h3>
|
|
||||||
<a href="#">Habitat Restoration</a>
|
|
||||||
<a href="#">Migration Science</a>
|
|
||||||
<a href="#">Advocacy</a>
|
|
||||||
</div>
|
|
||||||
<div class="wa-stack">
|
|
||||||
<h3 class="wa-heading-xs">About Us</h3>
|
|
||||||
<a href="#">Our History</a>
|
|
||||||
<a href="#">Leadership</a>
|
|
||||||
<a href="#">Fiscal Reports</a>
|
|
||||||
</div>
|
|
||||||
<div class="wa-stack">
|
|
||||||
<h3 class="wa-heading-xs">Discover</h3>
|
|
||||||
<a href="#">Field Guides</a>
|
|
||||||
<a href="#">Photo Search</a>
|
|
||||||
<a href="#">Gear and Resources</a>
|
|
||||||
</div>
|
|
||||||
<div class="wa-stack">
|
|
||||||
<h3 class="wa-heading-xs">Get Involved</h3>
|
|
||||||
<a href="#">Adopt a Bird</a>
|
|
||||||
<a href="#">Your Local Audubon</a>
|
|
||||||
<a href="#">Youth Audubon Camps</a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</wa-page>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
wa-page {
|
|
||||||
--menu-width: 15rem;
|
|
||||||
--aside-width: 15rem;
|
|
||||||
}
|
|
||||||
wa-page[view='desktop'] {
|
|
||||||
[slot*='navigation'] {
|
|
||||||
border-inline-end: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wa-page[view='mobile'] {
|
|
||||||
--menu-width: auto;
|
|
||||||
--aside-width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
[slot='banner'] {
|
|
||||||
--wa-color-text-link: var(--wa-color-neutral-on-loud);
|
|
||||||
background-color: var(--wa-color-neutral-fill-loud);
|
|
||||||
}
|
|
||||||
[slot='header'] {
|
|
||||||
--wa-link-decoration-default: none;
|
|
||||||
border-block-end: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
|
|
||||||
}
|
|
||||||
[slot*='header'] a {
|
|
||||||
font-weight: var(--wa-font-weight-action);
|
|
||||||
}
|
|
||||||
[slot='subheader'] {
|
|
||||||
background-color: var(--wa-color-surface-lowered);
|
|
||||||
border-block-end: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
|
|
||||||
}
|
|
||||||
[slot='navigation-header'] {
|
|
||||||
border-block-end: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
[slot*='navigation'] a {
|
|
||||||
--wa-color-text-link: var(--wa-color-text-normal);
|
|
||||||
}
|
|
||||||
[slot='navigation-footer'] {
|
|
||||||
border-block-start: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
|
|
||||||
|
|
||||||
.wa-flank {
|
|
||||||
--flank-size: 1.25em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
[slot='main-header'],
|
|
||||||
main,
|
|
||||||
[slot='main-footer'] {
|
|
||||||
max-inline-size: 60rem;
|
|
||||||
margin-inline: auto;
|
|
||||||
}
|
|
||||||
[slot='main-footer'] {
|
|
||||||
border-block-start: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
|
|
||||||
}
|
|
||||||
[slot='footer'] {
|
|
||||||
--wa-color-text-link: var(--wa-color-text-quiet);
|
|
||||||
background-color: var(--wa-color-surface-lowered);
|
|
||||||
font-size: var(--wa-font-size-s);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const sectionAnchors = document.querySelectorAll("[slot*='navigation'] a[href*='#']");
|
|
||||||
sectionAnchors.forEach(sectionAnchor => sectionAnchor.setAttribute('data-drawer', 'close'));
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,484 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en" class="wa-cloak">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Web Awesome Page Demo 2</title>
|
|
||||||
<link rel="stylesheet" href="/dist/styles/themes/default.css" />
|
|
||||||
<link rel="stylesheet" href="/dist/styles/webawesome.css" />
|
|
||||||
<script type="module" src="/dist/webawesome.loader.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<wa-page class="wa-dark">
|
|
||||||
<header slot="header">
|
|
||||||
<div class="wa-cluster">
|
|
||||||
<wa-button data-toggle-nav appearance="plain" size="small">
|
|
||||||
<wa-icon name="bars" label="Menu"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
<wa-icon name="record-vinyl"></wa-icon>
|
|
||||||
<span class="wa-heading-m">radiogaga</span>
|
|
||||||
</div>
|
|
||||||
<wa-input id="search-header" placeholder="Search" class="wa-desktop-only" style="max-inline-size: 100%">
|
|
||||||
<wa-icon slot="start" name="magnifying-glass"></wa-icon>
|
|
||||||
</wa-input>
|
|
||||||
<div class="wa-cluster">
|
|
||||||
<wa-button appearance="outlined">Log In</wa-button>
|
|
||||||
<wa-button>Sign Up</wa-button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div slot="navigation-header" class="wa-split">
|
|
||||||
<wa-input id="search-nav-drawer" placeholder="Search" style="max-inline-size: 100%" class="wa-mobile-only">
|
|
||||||
<wa-icon slot="start" name="magnifying-glass"></wa-icon>
|
|
||||||
</wa-input>
|
|
||||||
<div class="wa-split">
|
|
||||||
<h2 class="wa-heading-s">For You</h2>
|
|
||||||
<wa-button id="settings" appearance="plain" size="small">
|
|
||||||
<wa-icon name="gear" label="Settings"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
<wa-tooltip for="settings">Settings</wa-tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<nav slot="navigation">
|
|
||||||
<h3 class="wa-heading-xs">Discover</h3>
|
|
||||||
<ul class="wa-stack wa-gap-0">
|
|
||||||
<li>
|
|
||||||
<a href="#" class="wa-flank">
|
|
||||||
<wa-icon name="house"></wa-icon>
|
|
||||||
<span>Home</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#" class="wa-flank">
|
|
||||||
<wa-icon name="star"></wa-icon>
|
|
||||||
<span>New</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#" class="wa-flank">
|
|
||||||
<wa-icon name="tower-broadcast"></wa-icon>
|
|
||||||
<span>Stations</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<h3 class="wa-heading-xs">Library</h3>
|
|
||||||
<ul class="wa-stack wa-gap-0">
|
|
||||||
<li>
|
|
||||||
<a href="#" class="wa-flank">
|
|
||||||
<wa-icon name="heart"></wa-icon>
|
|
||||||
<span>Favorites</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#" class="wa-flank">
|
|
||||||
<wa-icon name="bars-staggered"></wa-icon>
|
|
||||||
<span>Playlists</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#" class="wa-flank">
|
|
||||||
<wa-icon name="microphone-lines"></wa-icon>
|
|
||||||
<span>Artists</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#" class="wa-flank">
|
|
||||||
<wa-icon name="layer-group"></wa-icon>
|
|
||||||
<span>Albums</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#" class="wa-flank">
|
|
||||||
<wa-icon name="podcast"></wa-icon>
|
|
||||||
<span>Podcasts</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<h3 class="wa-heading-xs">Recently Played</h3>
|
|
||||||
<ul id="recent" class="wa-stack wa-gap-0">
|
|
||||||
<li>
|
|
||||||
<a href="#" class="wa-flank">
|
|
||||||
<wa-icon name="radio" style="background: var(--wa-color-red-90); color: var(--wa-color-red-60)"></wa-icon>
|
|
||||||
<span>Lo-Fi Station</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#" class="wa-flank">
|
|
||||||
<wa-icon
|
|
||||||
name="font-awesome"
|
|
||||||
style="background: var(--wa-color-blue-30); color: var(--wa-color-yellow-90)"
|
|
||||||
></wa-icon>
|
|
||||||
<span>Podcast Awesome</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="#" class="wa-flank">
|
|
||||||
<wa-icon
|
|
||||||
name="seedling"
|
|
||||||
style="background: var(--wa-color-green-70); color: var(--wa-color-green-90)"
|
|
||||||
></wa-icon>
|
|
||||||
<div class="wa-stack wa-gap-0">
|
|
||||||
<span>Seasons</span>
|
|
||||||
<span class="wa-caption-s">Blister Soul</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
<div slot="main-header">
|
|
||||||
<wa-button id="back" appearance="plain" size="small">
|
|
||||||
<wa-icon name="chevron-left" label="Back"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
<wa-tooltip for="back" placement="bottom" distance="2">Back</wa-tooltip>
|
|
||||||
<div class="wa-cluster">
|
|
||||||
<wa-button id="favorite" appearance="plain" size="small">
|
|
||||||
<wa-icon name="heart" label="Favorite" variant="regular"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
<wa-tooltip for="favorite" placement="bottom" distance="2">Favorite</wa-tooltip>
|
|
||||||
<wa-button id="options" appearance="plain" size="small">
|
|
||||||
<wa-icon name="ellipsis" label="Options"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
<wa-tooltip for="options" placement="bottom" distance="2">Options</wa-tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<main>
|
|
||||||
<div class="wa-stack wa-gap-3xl">
|
|
||||||
<div class="wa-flank wa-gap-3xl" style="--content-percentage: 40%">
|
|
||||||
<div class="wa-frame wa-border-radius-l" style="max-inline-size: 40ch">
|
|
||||||
<img
|
|
||||||
src="https://images.unsplash.com/photo-1732430579016-8d5e5ebd3c99?q=20"
|
|
||||||
alt="Home for the Holidays album artwork"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="wa-split:column wa-align-items-start">
|
|
||||||
<div class="wa-stack" style="margin-block: auto">
|
|
||||||
<h1 class="wa-heading-3xl">Home for the Holidays</h1>
|
|
||||||
<a href="#" class="wa-heading-m">The Shire Choir</a>
|
|
||||||
<div class="wa-cluster wa-caption-m wa-gap-2xs">
|
|
||||||
<span>Holiday</span>
|
|
||||||
<span>•</span>
|
|
||||||
<span>2024</span>
|
|
||||||
<span>•</span>
|
|
||||||
<span>12 songs, 41 minutes 9 seconds</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="play-controls" class="wa-split wa-gap-xl">
|
|
||||||
<div class="wa-cluster wa-gap-xl">
|
|
||||||
<wa-button variant="brand" size="large">
|
|
||||||
<wa-icon name="play" label="Play"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
<wa-button appearance="plain">
|
|
||||||
<wa-icon name="shuffle" label="Shuffle"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
</div>
|
|
||||||
<wa-button appearance="plain">
|
|
||||||
<wa-icon name="plus" label="Add to Library"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ol class="wa-stack wa-gap-0">
|
|
||||||
<li class="wa-split">
|
|
||||||
<span class="wa-flank">
|
|
||||||
<wa-icon name="1"></wa-icon>
|
|
||||||
<span>Fa-La-La-Fellowship</span>
|
|
||||||
</span>
|
|
||||||
<span class="wa-cluster">
|
|
||||||
<span class="wa-caption-m">3:27</span>
|
|
||||||
<wa-button appearance="plain" size="small">
|
|
||||||
<wa-icon name="ellipsis" label="Song Options"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li class="wa-split">
|
|
||||||
<span class="wa-flank">
|
|
||||||
<wa-icon name="2"></wa-icon>
|
|
||||||
<span>Sleigh Ride</span>
|
|
||||||
</span>
|
|
||||||
<span class="wa-cluster">
|
|
||||||
<span class="wa-caption-m">2:36</span>
|
|
||||||
<wa-button appearance="plain" size="small">
|
|
||||||
<wa-icon name="ellipsis" label="Song Options"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li class="wa-split">
|
|
||||||
<span class="wa-flank">
|
|
||||||
<wa-icon name="3"></wa-icon>
|
|
||||||
<span>All I Want For Christmas Is Stew</span>
|
|
||||||
</span>
|
|
||||||
<span class="wa-cluster">
|
|
||||||
<span class="wa-caption-m">2:51</span>
|
|
||||||
<wa-button appearance="plain" size="small">
|
|
||||||
<wa-icon name="ellipsis" label="Song Options"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li class="wa-split">
|
|
||||||
<span class="wa-flank">
|
|
||||||
<wa-icon name="4"></wa-icon>
|
|
||||||
<span>Rockin' Around the Christmas Ent</span>
|
|
||||||
</span>
|
|
||||||
<span class="wa-cluster">
|
|
||||||
<span class="wa-caption-m">3:05</span>
|
|
||||||
<wa-button appearance="plain" size="small">
|
|
||||||
<wa-icon name="ellipsis" label="Song Options"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li class="wa-split">
|
|
||||||
<span class="wa-flank">
|
|
||||||
<wa-icon name="5"></wa-icon>
|
|
||||||
<span>Merry, Did You Know?</span>
|
|
||||||
</span>
|
|
||||||
<span class="wa-cluster">
|
|
||||||
<span class="wa-caption-m">1:56</span>
|
|
||||||
<wa-button appearance="plain" size="small">
|
|
||||||
<wa-icon name="ellipsis" label="Song Options"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li class="wa-split">
|
|
||||||
<span class="wa-flank">
|
|
||||||
<wa-icon name="6"></wa-icon>
|
|
||||||
<span>Run Run Shadowfax</span>
|
|
||||||
</span>
|
|
||||||
<span class="wa-cluster">
|
|
||||||
<span class="wa-caption-m">3:32</span>
|
|
||||||
<wa-button appearance="plain" size="small">
|
|
||||||
<wa-icon name="ellipsis" label="Song Options"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li class="wa-split">
|
|
||||||
<span class="wa-flank">
|
|
||||||
<wa-icon name="7"></wa-icon>
|
|
||||||
<span>You're a Mean One, Mr. Grima</span>
|
|
||||||
</span>
|
|
||||||
<span class="wa-cluster">
|
|
||||||
<span class="wa-caption-m">2:46</span>
|
|
||||||
<wa-button appearance="plain" size="small">
|
|
||||||
<wa-icon name="ellipsis" label="Song Options"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li class="wa-split">
|
|
||||||
<span class="wa-flank">
|
|
||||||
<wa-icon name="8"></wa-icon>
|
|
||||||
<span>O Come, All Ye Faithful</span>
|
|
||||||
</span>
|
|
||||||
<span class="wa-cluster">
|
|
||||||
<span class="wa-caption-m">3:27</span>
|
|
||||||
<wa-button appearance="plain" size="small">
|
|
||||||
<wa-icon name="ellipsis" label="Song Options"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li class="wa-split">
|
|
||||||
<span class="wa-flank">
|
|
||||||
<wa-icon name="9"></wa-icon>
|
|
||||||
<span>Do You Hear What I Hear</span>
|
|
||||||
</span>
|
|
||||||
<span class="wa-cluster">
|
|
||||||
<span class="wa-caption-m">2:13</span>
|
|
||||||
<wa-button appearance="plain" size="small">
|
|
||||||
<wa-icon name="ellipsis" label="Song Options"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li class="wa-split">
|
|
||||||
<span class="wa-flank">
|
|
||||||
<span class="wa-cluster wa-gap-3xs">
|
|
||||||
<wa-icon name="1"></wa-icon>
|
|
||||||
<wa-icon name="0"></wa-icon>
|
|
||||||
</span>
|
|
||||||
<span>Carol of the Horns</span>
|
|
||||||
</span>
|
|
||||||
<span class="wa-cluster">
|
|
||||||
<span class="wa-caption-m">2:55</span>
|
|
||||||
<wa-button appearance="plain" size="small">
|
|
||||||
<wa-icon name="ellipsis" label="Song Options"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li class="wa-split">
|
|
||||||
<span class="wa-flank">
|
|
||||||
<span class="wa-cluster wa-gap-3xs">
|
|
||||||
<wa-icon name="1"></wa-icon>
|
|
||||||
<wa-icon name="1"></wa-icon>
|
|
||||||
</span>
|
|
||||||
<span>Silent Night</span>
|
|
||||||
</span>
|
|
||||||
<span class="wa-cluster">
|
|
||||||
<span class="wa-caption-m">3:10</span>
|
|
||||||
<wa-button appearance="plain" size="small">
|
|
||||||
<wa-icon name="ellipsis" label="Song Options"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li class="wa-split">
|
|
||||||
<span class="wa-flank">
|
|
||||||
<span class="wa-cluster wa-gap-3xs">
|
|
||||||
<wa-icon name="1"></wa-icon>
|
|
||||||
<wa-icon name="2"></wa-icon>
|
|
||||||
</span>
|
|
||||||
<span>Wizard Wonderland</span>
|
|
||||||
</span>
|
|
||||||
<span class="wa-cluster">
|
|
||||||
<span class="wa-caption-m">3:22</span>
|
|
||||||
<wa-button appearance="plain" size="small">
|
|
||||||
<wa-icon name="ellipsis" label="Song Options"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<div slot="main-footer" class="wa-grid wa-gap-xl wa-align-items-center">
|
|
||||||
<h2 class="wa-heading-2xl">More You Might Like</h2>
|
|
||||||
<div class="wa-stack wa-gap-xs">
|
|
||||||
<div class="wa-frame wa-border-radius-l">
|
|
||||||
<img src="https://images.unsplash.com/photo-1675219119611-40323b738563?q=20" alt="" />
|
|
||||||
</div>
|
|
||||||
<span class="wa-heading-s">Festival of Lights</span>
|
|
||||||
<span class="wa-caption-s">Station</span>
|
|
||||||
</div>
|
|
||||||
<div class="wa-stack wa-gap-xs">
|
|
||||||
<div class="wa-frame wa-border-radius-l">
|
|
||||||
<img src="https://images.unsplash.com/photo-1481930916222-5ec4696fc0f2?q=20" alt="" />
|
|
||||||
</div>
|
|
||||||
<span class="wa-heading-s">Holiday Cheer</span>
|
|
||||||
<span class="wa-caption-s">Essential Playlist</span>
|
|
||||||
</div>
|
|
||||||
<div class="wa-stack wa-gap-xs">
|
|
||||||
<div class="wa-frame wa-border-radius-l">
|
|
||||||
<img src="https://images.unsplash.com/photo-1667514627762-521b1c815a89?q=20" alt="" />
|
|
||||||
</div>
|
|
||||||
<span class="wa-heading-s">Nursery Rhymes from the Shire</span>
|
|
||||||
<span class="wa-caption-s">The Shire Choir</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</wa-page>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
wa-page {
|
|
||||||
--menu-width: 30ch;
|
|
||||||
--wa-tooltip-arrow-size: 0;
|
|
||||||
background-color: var(--wa-color-surface-lowered);
|
|
||||||
}
|
|
||||||
|
|
||||||
wa-page[view='mobile'] {
|
|
||||||
--menu-width: auto;
|
|
||||||
|
|
||||||
[slot*='main'],
|
|
||||||
main {
|
|
||||||
padding: var(--wa-space-xl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wa-page,
|
|
||||||
[slot='header'],
|
|
||||||
wa-page[view='desktop'] [slot*='navigation'] {
|
|
||||||
background-color: var(--wa-color-surface-lowered);
|
|
||||||
}
|
|
||||||
wa-page[view='mobile'] [slot*='navigation'] {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
wa-page::part(base) {
|
|
||||||
background-color: var(--wa-color-surface-lowered);
|
|
||||||
}
|
|
||||||
[slot='header'] {
|
|
||||||
background: linear-gradient(to bottom, var(--wa-color-surface-raised), var(--wa-color-surface-lowered));
|
|
||||||
}
|
|
||||||
[slot='navigation-header'],
|
|
||||||
[slot='main-header'] {
|
|
||||||
padding-block-end: 0 !important;
|
|
||||||
padding-block-start: var(--wa-space-3xl);
|
|
||||||
}
|
|
||||||
[slot='navigation'] {
|
|
||||||
a {
|
|
||||||
--wa-color-text-link: var(--wa-color-text-normal);
|
|
||||||
--wa-link-decoration-default: none;
|
|
||||||
--wa-link-decoration-hover: none;
|
|
||||||
--flank-size: 2rem;
|
|
||||||
font-weight: var(--wa-font-weight-action);
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
a {
|
|
||||||
border-radius: var(--wa-border-radius-m);
|
|
||||||
padding: var(--wa-space-xs);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: color-mix(in oklab, var(--wa-color-surface-default), var(--wa-color-brand-fill-quiet));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wa-icon {
|
|
||||||
align-items: center;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
color: var(--wa-color-brand-fill-loud);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
padding-block: 0.5em;
|
|
||||||
}
|
|
||||||
#recent wa-icon {
|
|
||||||
border-radius: var(--wa-border-radius-s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[slot='main-header'] {
|
|
||||||
border-block-start: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
|
|
||||||
border-inline: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
|
|
||||||
border-radius: var(--wa-border-radius-l) var(--wa-border-radius-l) 0 0;
|
|
||||||
}
|
|
||||||
main,
|
|
||||||
[slot*='main'] {
|
|
||||||
margin-inline: var(--wa-space-m);
|
|
||||||
}
|
|
||||||
main ol li {
|
|
||||||
padding: var(--wa-space-m);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: color-mix(in oklab, var(--wa-color-surface-default), var(--wa-color-brand-fill-quiet));
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(:first-child) {
|
|
||||||
border-block-start: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wa-flank {
|
|
||||||
--flank-size: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
main,
|
|
||||||
[slot='main-footer'] {
|
|
||||||
border-inline: var(--wa-border-width-s) var(--wa-border-style) var(--wa-color-surface-border);
|
|
||||||
}
|
|
||||||
main,
|
|
||||||
[slot='main-header'] {
|
|
||||||
background-color: var(--wa-color-surface-raised);
|
|
||||||
}
|
|
||||||
#play-controls wa-button::part(base) {
|
|
||||||
border: var(--wa-border-width-l) var(--wa-border-style) currentColor;
|
|
||||||
border-radius: var(--wa-border-radius-circle);
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
#play-controls wa-button:has(wa-icon[name='play'])::part(base) {
|
|
||||||
border: none;
|
|
||||||
font-size: 2.5rem;
|
|
||||||
padding: 0.5em 0.45em 0.5em 0.55em;
|
|
||||||
}
|
|
||||||
[slot='main-footer'].wa-grid > * {
|
|
||||||
max-inline-size: 30ch;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const sectionAnchors = document.querySelectorAll("[slot*='navigation'] a[href*='#']");
|
|
||||||
sectionAnchors.forEach(sectionAnchor => sectionAnchor.setAttribute('data-drawer', 'close'));
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
38
packages/webawesome/docs/assets/scripts/color-scheme.js
Normal file
38
packages/webawesome/docs/assets/scripts/color-scheme.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { doViewTransition } from '../scripts/view-transitions.js';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Updates the color scheme when a color scheme selector changes
|
||||||
|
//
|
||||||
|
function updateTheme(value) {
|
||||||
|
localStorage.setItem('color-scheme', value);
|
||||||
|
|
||||||
|
const isDark = value === 'dark' || (value === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||||
|
|
||||||
|
doViewTransition(() => {
|
||||||
|
document.documentElement.classList.toggle('wa-dark', isDark);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync all selectors
|
||||||
|
document.querySelectorAll('.color-scheme-selector').forEach(el => (el.value = value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle changes
|
||||||
|
document.addEventListener('input', e => {
|
||||||
|
if (e.target.matches('.color-scheme-selector')) {
|
||||||
|
updateTheme(e.target.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle backslash key toggle
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
if (e.key === '\\' && !e.composedPath().some(el => el.tagName === 'INPUT')) {
|
||||||
|
const current = localStorage.getItem('color-scheme') || 'auto';
|
||||||
|
const isDark =
|
||||||
|
current === 'dark' || (current === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||||
|
updateTheme(isDark ? 'light' : 'dark');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
const saved = localStorage.getItem('color-scheme') || 'auto';
|
||||||
|
updateTheme(saved);
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
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);
|
|
||||||
|
|
||||||
for (let grid of document.querySelectorAll('.index-grid')) {
|
|
||||||
grid.classList.toggle('filtered', filtered);
|
|
||||||
|
|
||||||
for (let item of grid.querySelectorAll('a:has(> wa-card)')) {
|
|
||||||
let isMatch = true;
|
|
||||||
|
|
||||||
if (filter) {
|
|
||||||
const content = item.textContent.toLowerCase() + ' ' + (item.getAttribute('data-keywords') + ' ');
|
|
||||||
isMatch = content.includes(filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
item.hidden = !isMatch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const debouncedUpdateResults = debounce(updateResults, 300);
|
|
||||||
|
|
||||||
document.documentElement.addEventListener('input', e => {
|
|
||||||
if (e.target?.matches('#block-filter wa-input')) {
|
|
||||||
debouncedUpdateResults(e.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
(async () => {
|
(async () => {
|
||||||
const hostname = new URL(document.baseURI).hostname;
|
const hostname = new URL(document.baseURI).hostname;
|
||||||
|
|
||||||
// Only diff on localhost. We dont need to show hydration errors on main site. Only locally.
|
// Only diff on localhost. We don't need to show hydration errors on main site. Only locally.
|
||||||
if (hostname !== 'localhost') {
|
if (hostname !== 'localhost') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
const my = (globalThis.my = new EventTarget());
|
|
||||||
export default my;
|
|
||||||
|
|
||||||
class PersistedArray extends Array {
|
|
||||||
constructor(key) {
|
|
||||||
super();
|
|
||||||
this.key = key;
|
|
||||||
|
|
||||||
if (this.key) {
|
|
||||||
this.fromLocalStorage();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Items were updated in another tab
|
|
||||||
addEventListener('storage', event => {
|
|
||||||
if (event.key === this.key || !event.key) {
|
|
||||||
this.fromLocalStorage();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update data from local storage
|
|
||||||
*/
|
|
||||||
fromLocalStorage() {
|
|
||||||
// First, empty the array
|
|
||||||
this.splice(0, this.length);
|
|
||||||
|
|
||||||
// Then, fill it with the data from local storage
|
|
||||||
let saved = localStorage[this.key] ? JSON.parse(localStorage[this.key]) : null;
|
|
||||||
|
|
||||||
if (saved) {
|
|
||||||
this.push(...saved);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write data to local storage
|
|
||||||
*/
|
|
||||||
toLocalStorage() {
|
|
||||||
if (this.length > 0) {
|
|
||||||
localStorage[this.key] = JSON.stringify(this);
|
|
||||||
} else {
|
|
||||||
delete localStorage[this.key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SavedEntities extends EventTarget {
|
|
||||||
constructor({ key, type, url }) {
|
|
||||||
super();
|
|
||||||
this.key = key;
|
|
||||||
this.type = type;
|
|
||||||
this.url = url ?? type + 's';
|
|
||||||
this.saved = new PersistedArray(key);
|
|
||||||
|
|
||||||
let all = this;
|
|
||||||
this.entityPrototype = {
|
|
||||||
type: this.type,
|
|
||||||
baseUrl: this.baseUrl,
|
|
||||||
|
|
||||||
get url() {
|
|
||||||
return all.getURL(this);
|
|
||||||
},
|
|
||||||
|
|
||||||
get parentUrl() {
|
|
||||||
return all.getParentURL(this);
|
|
||||||
},
|
|
||||||
|
|
||||||
delete() {
|
|
||||||
all.delete(this);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getUid() {
|
|
||||||
if (this.saved.length === 0) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let uids = new Set(this.saved.map(p => p.uid));
|
|
||||||
|
|
||||||
// Find first available number
|
|
||||||
for (let i = 1; i <= this.saved.length + 1; i++) {
|
|
||||||
if (!uids.has(i)) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get baseUrl() {
|
|
||||||
return `/docs/${this.url}/`;
|
|
||||||
}
|
|
||||||
|
|
||||||
getURL(entity) {
|
|
||||||
return this.getParentURL(entity) + entity.search;
|
|
||||||
}
|
|
||||||
|
|
||||||
getParentURL(entity) {
|
|
||||||
return this.baseUrl + entity.id + '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
getObject(entity) {
|
|
||||||
let ret = Object.create(this.entityPrototype, Object.getOwnPropertyDescriptors(entity));
|
|
||||||
// debugger;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save an entity, either by updating its existing entry or creating a new one
|
|
||||||
* @param {object} entity
|
|
||||||
*/
|
|
||||||
save(entity) {
|
|
||||||
if (!entity.uid) {
|
|
||||||
// First time saving
|
|
||||||
entity.uid = this.getUid();
|
|
||||||
}
|
|
||||||
|
|
||||||
let savedPalettes = this.saved;
|
|
||||||
let existingIndex = entity.uid ? this.saved.findIndex(p => p.uid === entity.uid) : -1;
|
|
||||||
let newIndex = existingIndex > -1 ? existingIndex : savedPalettes.length;
|
|
||||||
|
|
||||||
this.saved.splice(newIndex, 1, entity);
|
|
||||||
|
|
||||||
this.saved.toLocalStorage();
|
|
||||||
|
|
||||||
this.dispatchEvent(new CustomEvent('save', { detail: this.getObject(entity) }));
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(entity) {
|
|
||||||
let count = this.saved.length;
|
|
||||||
|
|
||||||
if (count === 0 || !entity?.uid) {
|
|
||||||
// No stored entities or this entity has not been saved
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO improve UX of this
|
|
||||||
if (!confirm(`Are you sure you want to delete ${this.type} “${entity.title}”?`)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let index; (index = this.saved.findIndex(p => p.uid === entity.uid)) > -1; ) {
|
|
||||||
this.saved.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.saved.length === count) {
|
|
||||||
// Nothing was removed
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.saved.toLocalStorage();
|
|
||||||
|
|
||||||
this.dispatchEvent(new CustomEvent('delete', { detail: this.getObject(entity) }));
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatchEvent(event) {
|
|
||||||
super.dispatchEvent(event);
|
|
||||||
my.dispatchEvent(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
my.palettes = new SavedEntities({
|
|
||||||
key: 'savedPalettes',
|
|
||||||
type: 'palette',
|
|
||||||
});
|
|
||||||
|
|
||||||
my.themes = new SavedEntities({
|
|
||||||
key: 'savedThemes',
|
|
||||||
type: 'theme',
|
|
||||||
});
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { domChange, ThemeAspect } from './theme-picker.js';
|
|
||||||
|
|
||||||
const presetTheme = new ThemeAspect({
|
|
||||||
defaultValue: 'default',
|
|
||||||
key: 'presetTheme',
|
|
||||||
picker: 'wa-select.preset-theme-selector',
|
|
||||||
|
|
||||||
applyChange(options = {}) {
|
|
||||||
const oldStylesheets = [...document.querySelectorAll('#theme-stylesheet')];
|
|
||||||
const oldStylesheet = oldStylesheets.pop();
|
|
||||||
|
|
||||||
if (oldStylesheets.length > 0) {
|
|
||||||
// Remove all but the last one
|
|
||||||
for (let stylesheet of oldStylesheets) {
|
|
||||||
stylesheet.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const href = `/dist/styles/themes/${this.value}.css`;
|
|
||||||
|
|
||||||
if (!oldStylesheet || oldStylesheet.getAttribute('href') !== href) {
|
|
||||||
const newStylesheet = document.createElement('link');
|
|
||||||
Object.assign(newStylesheet, { href, id: 'theme-stylesheet', rel: 'preload', as: 'style' });
|
|
||||||
oldStylesheet.after(newStylesheet);
|
|
||||||
|
|
||||||
newStylesheet.addEventListener(
|
|
||||||
'load',
|
|
||||||
e => {
|
|
||||||
domChange(
|
|
||||||
async instant => {
|
|
||||||
// Swap stylesheets
|
|
||||||
newStylesheet.rel = 'stylesheet';
|
|
||||||
|
|
||||||
if (instant) {
|
|
||||||
// If no VT, delay by 1 frame to make it smoother
|
|
||||||
await new Promise(requestAnimationFrame);
|
|
||||||
}
|
|
||||||
|
|
||||||
oldStylesheet.remove();
|
|
||||||
},
|
|
||||||
{ behavior: 'smooth', ...options },
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{ once: true },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('turbo:render', e => {
|
|
||||||
presetTheme.applyChange({ behavior: 'instant' });
|
|
||||||
});
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,8 +0,0 @@
|
|||||||
globalThis.Prism = globalThis.Prism || {};
|
|
||||||
globalThis.Prism.manual = true;
|
|
||||||
|
|
||||||
await import('./prism-downloaded.js');
|
|
||||||
|
|
||||||
Prism.plugins.customClass.prefix('code-');
|
|
||||||
|
|
||||||
export default Prism;
|
|
||||||
82
packages/webawesome/docs/assets/scripts/search-list.js
Normal file
82
packages/webawesome/docs/assets/scripts/search-list.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* Live search functionality for component lists
|
||||||
|
*
|
||||||
|
* Required HTML structure:
|
||||||
|
* <div class="search-list">
|
||||||
|
* <input class="search-list-input" type="search" placeholder="Search...">
|
||||||
|
*
|
||||||
|
* <h2>Category Name</h2> <!-- Optional heading; h1-h6 all work -->
|
||||||
|
* <section class="search-list-grid">
|
||||||
|
* <a href="...">
|
||||||
|
* <span class="page-name">Component Title</span>
|
||||||
|
* </a>
|
||||||
|
* </section>
|
||||||
|
*
|
||||||
|
* <div class="search-list-empty" hidden>No results found</div>
|
||||||
|
* </div>
|
||||||
|
*
|
||||||
|
* Usage: import './search-list.js'
|
||||||
|
*/
|
||||||
|
export function enableSearchLists() {
|
||||||
|
document.querySelectorAll('.search-list').forEach(container => {
|
||||||
|
const input = container.querySelector('.search-list-input');
|
||||||
|
const emptyState = container.querySelector('.search-list-empty');
|
||||||
|
|
||||||
|
if (!input || !emptyState) return;
|
||||||
|
|
||||||
|
let timeout;
|
||||||
|
|
||||||
|
input.addEventListener('input', e => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
const query = e.target.value.toLowerCase().trim();
|
||||||
|
let totalVisible = 0;
|
||||||
|
|
||||||
|
// Handle sections with headings
|
||||||
|
container.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(heading => {
|
||||||
|
const section = heading.nextElementSibling;
|
||||||
|
if (!section) return;
|
||||||
|
|
||||||
|
let sectionVisible = 0;
|
||||||
|
|
||||||
|
section.querySelectorAll('a').forEach(card => {
|
||||||
|
const title = card.querySelector('.page-name')?.textContent?.toLowerCase() || '';
|
||||||
|
const visible = !query || title.includes(query);
|
||||||
|
card.style.display = visible ? '' : 'none';
|
||||||
|
if (visible) sectionVisible++;
|
||||||
|
});
|
||||||
|
|
||||||
|
heading.style.display = sectionVisible > 0 ? '' : 'none';
|
||||||
|
section.style.display = sectionVisible > 0 ? '' : 'none';
|
||||||
|
totalVisible += sectionVisible;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle standalone sections without headings
|
||||||
|
container.querySelectorAll('.search-list-grid').forEach(section => {
|
||||||
|
const prevElement = section.previousElementSibling;
|
||||||
|
const hasHeading = prevElement && /^H[1-6]$/.test(prevElement.tagName);
|
||||||
|
|
||||||
|
if (!hasHeading) {
|
||||||
|
section.querySelectorAll('a').forEach(card => {
|
||||||
|
const title = card.querySelector('.page-name')?.textContent?.toLowerCase() || '';
|
||||||
|
const visible = !query || title.includes(query);
|
||||||
|
card.style.display = visible ? '' : 'none';
|
||||||
|
if (visible) totalVisible++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
emptyState.hidden = totalVisible > 0;
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize when DOM is ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', enableSearchLists);
|
||||||
|
} else {
|
||||||
|
enableSearchLists();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('turbo:load', enableSearchLists);
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
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-button'), {
|
|
||||||
appearance: 'plain',
|
|
||||||
variant: 'danger',
|
|
||||||
size: 'small',
|
|
||||||
className: 'delete',
|
|
||||||
innerHTML: '<wa-icon name="trash" label="Delete"></wa-icon>',
|
|
||||||
});
|
|
||||||
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,37 +0,0 @@
|
|||||||
/**
|
|
||||||
* Sync iframe height with its content page (for same-origin iframes)
|
|
||||||
* NOT CURRENTLY USED ANYWHERE
|
|
||||||
*/
|
|
||||||
for (let iframe of document.querySelectorAll('iframe')) {
|
|
||||||
if (iframe.contentDocument) {
|
|
||||||
// Already loaded
|
|
||||||
syncIframeHeight(iframe);
|
|
||||||
}
|
|
||||||
|
|
||||||
iframe.onload = () => {
|
|
||||||
console.log('iframe loaded');
|
|
||||||
if (iframe.contentDocument) {
|
|
||||||
// Same origin
|
|
||||||
iframe.contentWindow.iframe = iframe;
|
|
||||||
syncIframeHeight(iframe);
|
|
||||||
const resizeObserver = new ResizeObserver(entries => {
|
|
||||||
for (let entry of entries) {
|
|
||||||
if (entry.target === iframe.contentDocument.body) {
|
|
||||||
syncIframeHeight(iframe);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
resizeObserver.observe(iframe.contentDocument.body);
|
|
||||||
window.addEventListener('turbo:render', syncIframeHeight(iframe));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncIframeHeight(iframe) {
|
|
||||||
iframe.style.height = '0px';
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
iframe.style.height = iframe.contentDocument.body.scrollHeight + 'px';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
import { domChange } from './util/dom-change.js';
|
|
||||||
export { domChange };
|
|
||||||
|
|
||||||
export class ThemeAspect {
|
|
||||||
constructor(options) {
|
|
||||||
Object.assign(this, options);
|
|
||||||
this.set();
|
|
||||||
this.syncIframes();
|
|
||||||
|
|
||||||
// Update when local storage changes.
|
|
||||||
// That way changes in one window will propagate to others (including iframes).
|
|
||||||
window.addEventListener('storage', event => {
|
|
||||||
if (event.key === this.key) {
|
|
||||||
this.set();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for selections
|
|
||||||
document.addEventListener('change', event => {
|
|
||||||
const picker = event.target.closest(this.picker);
|
|
||||||
if (picker) {
|
|
||||||
this.set(picker.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
['turbo:before-render', 'turbo:before-stream-render', 'turbo:before-frame-render'].forEach(eventName => {
|
|
||||||
document.addEventListener(eventName, e => {
|
|
||||||
const newElement = e.detail.newBody || e.detail.newFrame || e.detail.newStream;
|
|
||||||
if (newElement) {
|
|
||||||
this.syncUI(newElement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get() {
|
|
||||||
return localStorage.getItem(this.key) ?? this.defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
computed = {};
|
|
||||||
|
|
||||||
get computedValue() {
|
|
||||||
if (this.value in this.computed) {
|
|
||||||
return this.computed[this.value];
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
set(value = this.get()) {
|
|
||||||
if (value === this.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value = value;
|
|
||||||
|
|
||||||
if (this.value === this.defaultValue) {
|
|
||||||
localStorage.removeItem(this.key);
|
|
||||||
} else {
|
|
||||||
localStorage.setItem(this.key, this.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.applyChange();
|
|
||||||
this.syncUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
async syncIframes() {
|
|
||||||
await customElements.whenDefined('wa-zoomable-frame');
|
|
||||||
await new Promise(requestAnimationFrame);
|
|
||||||
|
|
||||||
// Sync to wa-zoomable-frame iframes
|
|
||||||
let dark = this.computedValue === 'dark';
|
|
||||||
for (let zoomableEl of document.querySelectorAll('wa-zoomable-frame')) {
|
|
||||||
const iframe = zoomableEl.iframe;
|
|
||||||
|
|
||||||
const applyToIframe = () => {
|
|
||||||
try {
|
|
||||||
iframe.contentDocument.documentElement.classList.toggle('wa-dark', dark);
|
|
||||||
} catch (e) {
|
|
||||||
// Silently handle access issues
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Try immediately
|
|
||||||
applyToIframe();
|
|
||||||
// Also listen for load in case it wasn't ready
|
|
||||||
iframe.addEventListener('load', applyToIframe, { once: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
syncUI(container = document) {
|
|
||||||
for (let picker of container.querySelectorAll(this.picker)) {
|
|
||||||
picker.setAttribute('value', this.value);
|
|
||||||
picker.value = this.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const colorScheme = new ThemeAspect({
|
|
||||||
defaultValue: 'auto',
|
|
||||||
key: 'colorScheme',
|
|
||||||
picker: 'wa-select.color-scheme-selector',
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
get auto() {
|
|
||||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
applyChange() {
|
|
||||||
// Toggle the dark mode class with view transition
|
|
||||||
const updateTheme = () => {
|
|
||||||
let dark = this.computedValue === 'dark';
|
|
||||||
document.documentElement.classList.toggle(`wa-dark`, dark);
|
|
||||||
document.documentElement.dispatchEvent(new CustomEvent('wa-color-scheme-change', { detail: { dark } }));
|
|
||||||
this.syncIframes();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (document.startViewTransition) {
|
|
||||||
document.startViewTransition(() => domChange(updateTheme));
|
|
||||||
} else {
|
|
||||||
domChange(updateTheme);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update the color scheme when the preference changes
|
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => colorScheme.set());
|
|
||||||
|
|
||||||
// Toggle color scheme with backslash
|
|
||||||
document.addEventListener('keydown', event => {
|
|
||||||
if (
|
|
||||||
event.key === '\\' &&
|
|
||||||
!event.composedPath().some(el => ['input', 'textarea'].includes(el?.tagName?.toLowerCase()))
|
|
||||||
) {
|
|
||||||
event.preventDefault();
|
|
||||||
colorScheme.set(colorScheme.get() === 'dark' ? 'light' : 'dark');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
81
packages/webawesome/docs/assets/scripts/theme.js
Normal file
81
packages/webawesome/docs/assets/scripts/theme.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { doViewTransition } from '../scripts/view-transitions.js';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Updates the theme when a theme selector changes
|
||||||
|
//
|
||||||
|
async function updateTheme(value, isInitialLoad = false) {
|
||||||
|
const body = document.body;
|
||||||
|
|
||||||
|
if (!isInitialLoad) {
|
||||||
|
// Add fade-out class
|
||||||
|
body.classList.add('theme-transitioning');
|
||||||
|
|
||||||
|
// Wait for fade-out to complete
|
||||||
|
await new Promise(resolve => {
|
||||||
|
const handleTransitionEnd = event => {
|
||||||
|
if (event.target === body && event.propertyName === 'opacity') {
|
||||||
|
body.removeEventListener('transitionend', handleTransitionEnd);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
body.addEventListener('transitionend', handleTransitionEnd);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('theme', value);
|
||||||
|
|
||||||
|
// Get brand and palette from the selected option
|
||||||
|
const themeSelector = document.querySelector('.theme-selector');
|
||||||
|
const selectedOption = themeSelector?.querySelector(`wa-option[value="${value}"]`);
|
||||||
|
const brand = selectedOption?.getAttribute('data-brand') || 'blue';
|
||||||
|
const palette = selectedOption?.getAttribute('data-palette') || 'default';
|
||||||
|
const htmlElement = document.documentElement;
|
||||||
|
|
||||||
|
localStorage.setItem('brand', brand);
|
||||||
|
localStorage.setItem('palette', palette);
|
||||||
|
|
||||||
|
// Update theme classes
|
||||||
|
const classesToRemove = Array.from(htmlElement.classList).filter(
|
||||||
|
className =>
|
||||||
|
className.startsWith('wa-theme-') || className.startsWith('wa-brand-') || className.startsWith('wa-palette-'),
|
||||||
|
);
|
||||||
|
const themeStylesheet = document.getElementById('theme-stylesheet');
|
||||||
|
const href = `/dist/styles/themes/${value}.css`;
|
||||||
|
|
||||||
|
doViewTransition(() => {
|
||||||
|
// Update the theme
|
||||||
|
if (themeStylesheet) {
|
||||||
|
themeStylesheet.href = href;
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlElement.classList.remove(...classesToRemove);
|
||||||
|
|
||||||
|
// Add the new theme, brand, and palette classes
|
||||||
|
htmlElement.classList.add(`wa-theme-${value}`);
|
||||||
|
htmlElement.classList.add(`wa-brand-${brand}`);
|
||||||
|
htmlElement.classList.add(`wa-palette-${palette}`);
|
||||||
|
|
||||||
|
// Sync all theme selectors
|
||||||
|
document.querySelectorAll('.theme-selector').forEach(el => (el.value = value));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isInitialLoad) {
|
||||||
|
// Waiting for the stylesheet and all it's imports to load is tricky. Preloading doesn't work for most themes
|
||||||
|
// because applying the new stylesheet to the document, even without adding the `wa-theme-*` class, causes jank.
|
||||||
|
// Suggestions welcome.
|
||||||
|
setTimeout(() => {
|
||||||
|
body.classList.remove('theme-transitioning');
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle changes
|
||||||
|
document.addEventListener('input', event => {
|
||||||
|
if (event.target.matches('.theme-selector')) {
|
||||||
|
updateTheme(event.target.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
const savedTheme = localStorage.getItem('theme') || 'default';
|
||||||
|
updateTheme(savedTheme, true);
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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';
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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}`;
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
/**
|
|
||||||
* Picks a random element from an array.
|
|
||||||
* @param {any[]} arr
|
|
||||||
*/
|
|
||||||
export function sample(arr) {
|
|
||||||
if (!Array.isArray(arr)) {
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (arr.length < 2) {
|
|
||||||
return arr[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
let index = Math.floor(Math.random() * arr.length);
|
|
||||||
|
|
||||||
return arr[index];
|
|
||||||
}
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
/**
|
|
||||||
* @typedef { string | number | Symbol } Property
|
|
||||||
* @typedef { (value: any, key: Property, parent: object, path: Property[]) => any } EachCallback
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function isPlainObject(obj) {
|
|
||||||
return isObject(obj, 'Object');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isObject(obj, type) {
|
|
||||||
if (!obj || typeof obj !== 'object') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let proto = Object.getPrototypeOf(obj);
|
|
||||||
return proto.constructor?.name === type;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deepMerge(target, source, options = {}) {
|
|
||||||
let {
|
|
||||||
emptyValues = [undefined],
|
|
||||||
containers = ['Object', 'EventTarget'],
|
|
||||||
isContainer = value => containers.some(type => isObject(value, type)),
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
if (isContainer(target) && isContainer(source)) {
|
|
||||||
for (let key in source) {
|
|
||||||
if (key in target && isContainer(target[key]) && isContainer(source[key])) {
|
|
||||||
target[key] = deepMerge(target[key], source[key], options);
|
|
||||||
} else if (!emptyValues.includes(source[key])) {
|
|
||||||
target[key] = source[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
|
|
||||||
return target ?? source;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterate over a deep array, recursively for plain objects
|
|
||||||
* @param { any } obj The object to iterate over. Can be an array or a plain object, or even a primitive value.
|
|
||||||
* @param { EachCallback } callback. value is === parent[key]
|
|
||||||
* @param { object } [parentObj] The parent object of the current value Mainly used internally to facilitate recursion.
|
|
||||||
* @param { Property } [key] The key of the current value. Mainly used internally to facilitate recursion.
|
|
||||||
* @param { Property[] } [path] Any existing path (not including the key). Mainly used internally to facilitate recursion.
|
|
||||||
*/
|
|
||||||
export function deepEach(obj, callback, parentObj, key, path = []) {
|
|
||||||
if (key !== undefined) {
|
|
||||||
let ret = callback(obj, key, parentObj, path);
|
|
||||||
|
|
||||||
if (ret !== undefined) {
|
|
||||||
if (ret === false) {
|
|
||||||
// Do not descend further
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overwrite value
|
|
||||||
parentObj[key] = ret;
|
|
||||||
obj = ret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let newPath = key !== undefined ? [...path, key] : path;
|
|
||||||
|
|
||||||
if (Array.isArray(obj)) {
|
|
||||||
for (let i = 0; i < obj.length; i++) {
|
|
||||||
deepEach(obj[i], callback, obj, i, newPath);
|
|
||||||
}
|
|
||||||
} else if (isPlainObject(obj)) {
|
|
||||||
for (let key in obj) {
|
|
||||||
deepEach(obj[key], callback, obj, key, newPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a value from a deeply nested object
|
|
||||||
* @param {*} obj
|
|
||||||
* @param {PropertyPath} path
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function deepGet(obj, path) {
|
|
||||||
if (path.length === 0) {
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
let ret = obj;
|
|
||||||
|
|
||||||
for (let key of path) {
|
|
||||||
if (ret === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
ret = ret[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a value in a deep object, creating object literals as needed
|
|
||||||
* @param { * } obj
|
|
||||||
* @param { Property[] } path
|
|
||||||
* @param { any } value
|
|
||||||
*/
|
|
||||||
export function deepSet(obj, path, value) {
|
|
||||||
if (path.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = path.pop();
|
|
||||||
|
|
||||||
let ret = path.reduce((acc, property) => {
|
|
||||||
if (acc[property] === undefined) {
|
|
||||||
acc[property] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc[property];
|
|
||||||
}, obj);
|
|
||||||
|
|
||||||
ret[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deepClone(obj) {
|
|
||||||
if (!obj) {
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
let ret = obj;
|
|
||||||
|
|
||||||
if (Array.isArray(obj)) {
|
|
||||||
ret = obj.map(item => deepClone(item));
|
|
||||||
} else if (isPlainObject(obj)) {
|
|
||||||
ret = { ...obj };
|
|
||||||
|
|
||||||
for (let key in obj) {
|
|
||||||
ret[key] = deepClone(obj[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Like Object.entries, but for deeply nested objects.
|
|
||||||
* For shallow objects the output is the same as Object.entries.
|
|
||||||
* @param {*} obj
|
|
||||||
* @param { object } options
|
|
||||||
* @param { EachCallback } each - If this returns false, the entry is not added to the result and the recursion is stopped.
|
|
||||||
* @param { EachCallback } filter - If this returns false, the entry is not added to the result.
|
|
||||||
* @param { EachCallback } descend - If this returns false, recursion is stopped.
|
|
||||||
* @returns {any[][]}
|
|
||||||
*/
|
|
||||||
export function deepEntries(obj, options = {}) {
|
|
||||||
let { each, filter, descend } = options;
|
|
||||||
let entries = [];
|
|
||||||
|
|
||||||
deepEach(obj, (value, key, parent, path) => {
|
|
||||||
let ret = each?.(value, key, parent, path);
|
|
||||||
|
|
||||||
if (ret !== false) {
|
|
||||||
let included = filter?.(value, key, parent, path) ?? true;
|
|
||||||
|
|
||||||
if (included) {
|
|
||||||
entries.push([...path, key, value]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let descendRet = descend?.(value, key, parent, path);
|
|
||||||
if (descendRet === false) {
|
|
||||||
return false; // Stop recursion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
});
|
|
||||||
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
let initialPageLoadComplete = document.readyState === 'complete';
|
|
||||||
|
|
||||||
if (!initialPageLoadComplete) {
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
initialPageLoadComplete = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper for performing a DOM change using a view transition, wherever supported and reduced motion is not desired.
|
|
||||||
* @param {function} fn - Function to perform the DOM change. If async, must resolve when the change is complete.
|
|
||||||
* @param {object} [options] - Options for the transition
|
|
||||||
* @param {'smooth' | 'instant'} [options.behavior] - Transition behavior. Defaults to 'smooth'. 'instant' will skip the transition.
|
|
||||||
* @param {boolean} [options.ignoreInitialLoad] - If true, will skip the transition on initial page load. Defaults to true.
|
|
||||||
*/
|
|
||||||
export function domChange(fn, { behavior = 'smooth', ignoreInitialLoad = true } = {}) {
|
|
||||||
const canUseViewTransitions =
|
|
||||||
document.startViewTransition && !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
||||||
|
|
||||||
// Skip transitions on initial page load
|
|
||||||
if (!initialPageLoadComplete && ignoreInitialLoad) {
|
|
||||||
fn(false);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (canUseViewTransitions && behavior === 'smooth') {
|
|
||||||
const transition = document.startViewTransition(() => {
|
|
||||||
fn(true);
|
|
||||||
// Wait a brief delay before finishing the transition to prevent jumpiness
|
|
||||||
return new Promise(resolve => setTimeout(resolve, 200));
|
|
||||||
});
|
|
||||||
return transition;
|
|
||||||
} else {
|
|
||||||
fn(false);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default domChange;
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
/**
|
|
||||||
* Make the first letter of a string uppercase
|
|
||||||
* @param {*} str
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function capitalize(str) {
|
|
||||||
str += '';
|
|
||||||
return str[0]?.toUpperCase() + str.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a readable string to a slug.
|
|
||||||
* @param {*} str - Input string. If argument is not a string, it will be stringified.
|
|
||||||
* @returns {string} - The slugified string
|
|
||||||
*/
|
|
||||||
export function slugify(str) {
|
|
||||||
return (str + '')
|
|
||||||
.normalize('NFD')
|
|
||||||
.replace(/[\u0300-\u036f]/g, '') // Convert accented letters to ASCII
|
|
||||||
.replace(/[^\w\s-]/g, '') // Remove remaining non-ASCII characters
|
|
||||||
.trim()
|
|
||||||
.replace(/\s+/g, '-') // Convert whitespace to hyphens
|
|
||||||
.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a string to camel case.
|
|
||||||
* @param {string} str - The string to convert.
|
|
||||||
* @returns {string} The camel case string.
|
|
||||||
*/
|
|
||||||
export function camelCase(str) {
|
|
||||||
return str.replace(/-([a-z])/g, (_, letter) => letter?.toUpperCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a string to kebab case.
|
|
||||||
* @param {string} str - The string to convert.
|
|
||||||
* @returns {string} The kebab case string.
|
|
||||||
*/
|
|
||||||
export function kebabCase(str) {
|
|
||||||
return str.replace(/([A-Z])/g, '-$1')?.toLowerCase();
|
|
||||||
}
|
|
||||||
27
packages/webawesome/docs/assets/scripts/view-transitions.js
Normal file
27
packages/webawesome/docs/assets/scripts/view-transitions.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
let initialPageLoadComplete = document.readyState === 'complete';
|
||||||
|
|
||||||
|
if (!initialPageLoadComplete) {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
initialPageLoadComplete = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper around `document.startViewTransition()` that fails gracefully in unsupportive browsers.
|
||||||
|
*/
|
||||||
|
export async function doViewTransition(callback, { ignoreInitialLoad = true } = {}) {
|
||||||
|
// Skip transitions on initial page load
|
||||||
|
if (!initialPageLoadComplete && ignoreInitialLoad) {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canUseViewTransitions =
|
||||||
|
document.startViewTransition && !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
|
||||||
|
if (canUseViewTransitions) {
|
||||||
|
await document.startViewTransition(callback).finished;
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,16 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 200ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.theme-transitioning {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 200ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
wa-page {
|
wa-page {
|
||||||
--wa-flow-spacing: var(--wa-space-xl);
|
--wa-flow-spacing: var(--wa-space-xl);
|
||||||
}
|
}
|
||||||
@@ -265,7 +275,7 @@ h1.title {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-info {
|
.component-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--wa-space-xs);
|
gap: var(--wa-space-xs);
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -359,52 +369,54 @@ h1.title {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Index Pages */
|
/* Search list pages */
|
||||||
wa-page > main:has(> .index-grid) {
|
wa-page > main:has(> .search-list) {
|
||||||
max-width: 120ch;
|
max-width: 120ch;
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.index-summary {
|
.search-list {
|
||||||
max-inline-size: 80ch;
|
.search-list-input {
|
||||||
}
|
margin-block-end: var(--wa-space-3xl);
|
||||||
|
|
||||||
.index-grid {
|
|
||||||
display: grid;
|
|
||||||
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) {
|
.search-list-grid {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
display: grid;
|
||||||
}
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: var(--wa-space-2xl);
|
||||||
|
margin-block-end: var(--wa-space-3xl);
|
||||||
|
|
||||||
@media screen and (max-width: 500px) {
|
@media screen and (max-width: 1470px) {
|
||||||
grid-template-columns: repeat(1, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
border-radius: var(--wa-border-radius-l);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
wa-card {
|
|
||||||
--spacing: var(--wa-space-m);
|
|
||||||
|
|
||||||
[slot='header'] {
|
|
||||||
display: flex;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&::part(header) {
|
@media screen and (max-width: 960px) {
|
||||||
background-color: var(--header-background, var(--wa-color-neutral-fill-quiet));
|
grid-template-columns: repeat(2, 1fr);
|
||||||
display: flex;
|
}
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
@media screen and (max-width: 500px) {
|
||||||
min-block-size: calc(6rem + var(--spacing));
|
grid-template-columns: repeat(1, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
border-radius: var(--wa-border-radius-l);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
wa-card {
|
||||||
|
--spacing: var(--wa-space-m);
|
||||||
|
|
||||||
|
[slot='header'] {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::part(header) {
|
||||||
|
background-color: var(--header-background, var(--wa-color-neutral-fill-quiet));
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-block-size: calc(6rem + var(--spacing));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -414,12 +426,6 @@ wa-card .page-name {
|
|||||||
font-weight: var(--wa-font-weight-action);
|
font-weight: var(--wa-font-weight-action);
|
||||||
}
|
}
|
||||||
|
|
||||||
.index-category {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
margin-block-end: 0;
|
|
||||||
margin-block-start: var(--wa-space-2xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Swatches */
|
/* Swatches */
|
||||||
.swatch {
|
.swatch {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
import { capitalize } from '../../scripts/util/string.js';
|
|
||||||
|
|
||||||
const template = `
|
|
||||||
<wa-select class="color-select" name="brand" :label="label" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)"
|
|
||||||
:style="{'--color': getColor(modelValue)}">
|
|
||||||
<template v-for="values, group in computedGroups">
|
|
||||||
<template v-if="group">
|
|
||||||
<wa-divider v-if="group !== firstGroup"></wa-divider>
|
|
||||||
<small>{{ group }}</small>
|
|
||||||
</template>
|
|
||||||
<wa-option v-if="values?.length" v-for="value of values" :label="getLabel(value)" :value="value" :style="{'--color': getColor(value)}" v-html="getContent?.(value) ?? getLabel(value)"></wa-option>
|
|
||||||
</template>
|
|
||||||
<slot></slot>
|
|
||||||
</wa-select>
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
modelValue: String,
|
|
||||||
label: String,
|
|
||||||
getLabel: {
|
|
||||||
type: Function,
|
|
||||||
default: capitalize,
|
|
||||||
},
|
|
||||||
getContent: {
|
|
||||||
type: Function,
|
|
||||||
},
|
|
||||||
getColor: {
|
|
||||||
type: Function,
|
|
||||||
default: value => `var(--wa-color-${value})`,
|
|
||||||
},
|
|
||||||
values: {
|
|
||||||
type: Array,
|
|
||||||
default: [],
|
|
||||||
},
|
|
||||||
groups: {
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ['update:modelValue', 'input'],
|
|
||||||
data() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
computedGroups() {
|
|
||||||
let ret = {};
|
|
||||||
|
|
||||||
if (this.values?.length) {
|
|
||||||
ret[''] = this.values;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.groups) {
|
|
||||||
for (let group in this.groups) {
|
|
||||||
if (this.groups[group]?.length) {
|
|
||||||
ret[group] = this.groups[group];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
|
|
||||||
firstGroup() {
|
|
||||||
return Object.keys(this.computedGroups)[0];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
capitalize,
|
|
||||||
handleInput(e) {
|
|
||||||
this.$emit('input', this.modelValue);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
template,
|
|
||||||
compilerOptions: {
|
|
||||||
isCustomElement: tag => tag.startsWith('wa-'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import inputMixin from '../mixins/input.js';
|
|
||||||
|
|
||||||
const template = `
|
|
||||||
<span class="editable-text">
|
|
||||||
<template v-if="isEditing">
|
|
||||||
<input ref="input" class="wa-size-s" :aria-label="label" :value="value" @input="handleInput" @keydown.enter="done" @keydown.esc="cancel" @blur="handleBlur" />
|
|
||||||
<wa-button appearance="plain" v-if="blur !== 'done'" @click="done">
|
|
||||||
<wa-icon name="check" label="Done editing"></wa-icon>
|
|
||||||
</wa-button>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<span class="text" ref="wrapper" @focus="edit" @click="edit" tabindex="0">{{ value }}</span>
|
|
||||||
<wa-button appearance="plain" @click="edit">
|
|
||||||
<wa-icon name="pencil" :label="'Edit ' + label"></wa-icon>
|
|
||||||
</wa-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-'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
import themes from '../../data/themes.js';
|
|
||||||
import PageCard from './page-card.js';
|
|
||||||
import { defaultTitle, pairings, sameAs } from '/assets/data/fonts.js';
|
|
||||||
import { themeConfig } from '/assets/data/theming.js';
|
|
||||||
import { cssImport, getThemeCode } from '/assets/scripts/tweak/code.js';
|
|
||||||
|
|
||||||
const template = `
|
|
||||||
<page-card class="fonts-card" :info="computedPairing">
|
|
||||||
<template #icon>
|
|
||||||
<wa-scoped slot="header" class="fonts-icon-host" inert :key="html">
|
|
||||||
<template v-html="html"></template>
|
|
||||||
<template>
|
|
||||||
<link rel="stylesheet" href="/dist/styles/native/content.css">
|
|
||||||
<link rel="stylesheet" href="/assets/styles/theme-icons.css">
|
|
||||||
|
|
||||||
<div class="fonts-icon" role="presentation">
|
|
||||||
<h2>When my six o'clock alarm buzzes, I require a pot of good java.</h2>
|
|
||||||
<p>By quarter past seven, I've jotted hazy musings in a flax-bound notebook, sipping lukewarm espresso.</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</wa-scoped>
|
|
||||||
</template>
|
|
||||||
<slot></slot>
|
|
||||||
<template #extra>
|
|
||||||
<slot name="extra" />
|
|
||||||
</template>
|
|
||||||
</page-card>
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
theme: String,
|
|
||||||
src: String,
|
|
||||||
fonts: Object,
|
|
||||||
pairing: Object,
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
content() {
|
|
||||||
let pairingTitle = this.computedPairing.title;
|
|
||||||
// let themeTitle = this.themeId ? `As seen in ${this.themeMeta.title}` : '';
|
|
||||||
|
|
||||||
if (this.title) {
|
|
||||||
return { title: this.title, subtitle: this.subtitle ?? pairingTitle };
|
|
||||||
} else {
|
|
||||||
return { title: pairingTitle, subtitle: this.subtitle };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
url() {
|
|
||||||
let ret = this.src ?? this.pairing?.url;
|
|
||||||
|
|
||||||
if (!ret && this.theme) {
|
|
||||||
return themeConfig.typography.url(this.theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
|
|
||||||
themeId() {
|
|
||||||
return this.theme ?? this.pairing?.id;
|
|
||||||
},
|
|
||||||
|
|
||||||
themeMeta() {
|
|
||||||
return themes[this.themeId] ?? {};
|
|
||||||
},
|
|
||||||
|
|
||||||
computedFonts() {
|
|
||||||
let ret = this.fonts ?? this.pairing?.fonts ?? this.themeMeta?.fonts;
|
|
||||||
let defaults = themes.default.fonts;
|
|
||||||
return Object.assign({}, defaults, { ...ret });
|
|
||||||
},
|
|
||||||
|
|
||||||
computedPairing() {
|
|
||||||
let ret;
|
|
||||||
|
|
||||||
if (this.pairing) {
|
|
||||||
ret = { ...this.pairing };
|
|
||||||
} else {
|
|
||||||
// Get from theme
|
|
||||||
let fonts = this.computedFonts;
|
|
||||||
let { body, heading = sameAs.body } = fonts;
|
|
||||||
let pairing = pairings[body]?.[heading];
|
|
||||||
ret = Object.assign({ fonts }, pairing);
|
|
||||||
}
|
|
||||||
|
|
||||||
ret.url = this.url;
|
|
||||||
ret.title ??= defaultTitle(fonts);
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
|
|
||||||
computed() {
|
|
||||||
let ret = { fonts: this.computedFonts };
|
|
||||||
|
|
||||||
for (let key in ret.fonts) {
|
|
||||||
if (ret.fonts[key] === sameAs.body) {
|
|
||||||
ret.fonts[key] = ret.fonts.body;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ret.pairing = this.computedPairing;
|
|
||||||
ret.theme = this.themeId;
|
|
||||||
ret.url = this.url;
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
|
|
||||||
html() {
|
|
||||||
let { id, url } = this.computedPairing;
|
|
||||||
|
|
||||||
if (id) {
|
|
||||||
let theme = { typography: id };
|
|
||||||
|
|
||||||
return getThemeCode(theme, { id, language: 'html' });
|
|
||||||
} else {
|
|
||||||
return cssImport(url, { language: 'html' });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
template,
|
|
||||||
components: {
|
|
||||||
PageCard,
|
|
||||||
},
|
|
||||||
compilerOptions: {
|
|
||||||
isCustomElement: tag => tag.startsWith('wa-'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
import { sample } from '../../scripts/util/array.js';
|
|
||||||
import { capitalize } from '../../scripts/util/string.js';
|
|
||||||
import PageCard from './page-card.js';
|
|
||||||
import { iconLibraries } from '/assets/data/icons.js';
|
|
||||||
|
|
||||||
const iconNames = [
|
|
||||||
'user',
|
|
||||||
'paper-plane',
|
|
||||||
'face-laugh',
|
|
||||||
'pen-to-square',
|
|
||||||
'trash',
|
|
||||||
'cart-shopping',
|
|
||||||
'link',
|
|
||||||
'sun',
|
|
||||||
'bookmark',
|
|
||||||
'sparkles',
|
|
||||||
'thumbs-up',
|
|
||||||
'gear',
|
|
||||||
];
|
|
||||||
const brands = new Set(['web-awesome', 'font-awesome']);
|
|
||||||
const ICON_GRID = { columns: 6, rows: 2 };
|
|
||||||
const TOTAL_ICONS = ICON_GRID.columns * ICON_GRID.rows;
|
|
||||||
|
|
||||||
const template = `
|
|
||||||
<page-card class="icons-card" :class="'icons-' + type + '-card'" :pro="$slots.default ? false : iconsMeta.isPro" :info="iconsMeta">
|
|
||||||
<template #icon>
|
|
||||||
<div slot="header" class="icons-icon" :class="'icons-' + type + '-icon'" :style="{ '--columns': ICON_GRID.columns }">
|
|
||||||
<template v-for="icon of icons">
|
|
||||||
<wa-icon v-bind="icon"></wa-icon>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<slot></slot>
|
|
||||||
</page-card>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const defaultDefaults = {
|
|
||||||
library: 'default',
|
|
||||||
family: 'classic',
|
|
||||||
style: 'solid',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
library: String,
|
|
||||||
family: String,
|
|
||||||
style: String,
|
|
||||||
defaults: Object,
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
validate(value) {
|
|
||||||
return ['library', 'family', 'style'].includes(value);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
vary: {
|
|
||||||
type: [Array, String],
|
|
||||||
validate(value) {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value.every(v => ['family', 'style'].includes(v));
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['family', 'style'].includes(value);
|
|
||||||
},
|
|
||||||
default() {
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
Object.assign(this, { iconNames, brands, ICON_GRID });
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
computedLibrary() {
|
|
||||||
return this.library ?? 'default';
|
|
||||||
},
|
|
||||||
|
|
||||||
libraryMeta() {
|
|
||||||
return iconLibraries[this.computedLibrary] ?? {};
|
|
||||||
},
|
|
||||||
|
|
||||||
defaultTitle() {
|
|
||||||
let titles = {};
|
|
||||||
for (let key in this.computed) {
|
|
||||||
let value = this.computed[key];
|
|
||||||
|
|
||||||
if (key === 'library') {
|
|
||||||
titles[key] = iconLibraries[value]?.title;
|
|
||||||
}
|
|
||||||
|
|
||||||
titles[key] ??= capitalize(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.type) {
|
|
||||||
return titles[this.type];
|
|
||||||
} else {
|
|
||||||
return titles.library + ' ' + titles.family + ' • ' + titles.style;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
icons() {
|
|
||||||
let { family, style } = this.computed;
|
|
||||||
let library = this.libraryMeta;
|
|
||||||
let vary = Array.isArray(this.vary) ? this.vary : [this.vary];
|
|
||||||
|
|
||||||
let ret = [];
|
|
||||||
|
|
||||||
if (vary.length > 0) {
|
|
||||||
for (let param of vary) {
|
|
||||||
let allValues = library[param];
|
|
||||||
if (!allValues) return;
|
|
||||||
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-'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
export { default as ColorSelect } from './color-select.js';
|
|
||||||
export { default as EditableText } from './editable-text.js';
|
|
||||||
export { default as FontsCard } from './fonts-card.js';
|
|
||||||
export { default as IconsCard } from './icons-card.js';
|
|
||||||
export { default as InfoTip } from './info-tip.js';
|
|
||||||
export { default as PageCard } from './page-card.js';
|
|
||||||
export { default as PaletteCard } from './palette-card.js';
|
|
||||||
export { default as SwatchSelect } from './swatch-select.js';
|
|
||||||
export { default as ThemeCard } from './theme-card.js';
|
|
||||||
export { default as UiPanelContainer } from './ui-panel-container.js';
|
|
||||||
export { default as UiPanel } from './ui-panel.js';
|
|
||||||
export { default as UiScrollable } from './ui-scrollable.js';
|
|
||||||
export { default as UiSlider } from './ui-slider.js';
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
const template = `
|
|
||||||
<slot>
|
|
||||||
<wa-icon :slot class="info-tip-default-trigger" :id="id" name="circle-question" variant="regular" tabindex="0"></wa-icon>
|
|
||||||
</slot>
|
|
||||||
<wa-tooltip :slot :for="id" ref="tooltip"><slot name="content">{{ text }}</slot></wa-tooltip>
|
|
||||||
`;
|
|
||||||
|
|
||||||
let maxUid = 0;
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
slot: String,
|
|
||||||
text: String,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
let uid = ++maxUid;
|
|
||||||
return { uid, id: 'info-tip-' + uid };
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
let tooltip = this.$refs.tooltip;
|
|
||||||
if (tooltip) {
|
|
||||||
// Find trigger
|
|
||||||
let trigger = tooltip.previousElementSibling;
|
|
||||||
if (trigger) {
|
|
||||||
if (trigger.id) {
|
|
||||||
// Already has id
|
|
||||||
this.id = trigger.id;
|
|
||||||
} else {
|
|
||||||
trigger.id = this.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {},
|
|
||||||
template,
|
|
||||||
compilerOptions: {
|
|
||||||
isCustomElement: tag => tag.startsWith('wa-'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
/**
|
|
||||||
* Generic component for displaying a (possibly interactive) card that represents a page
|
|
||||||
* For more specific use cases check out theme-card, icons-card, etc.
|
|
||||||
*/
|
|
||||||
export const ICON_PLACEHOLDER = `
|
|
||||||
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M1 7C1 3.68629 3.68629 1 7 1H43C46.3137 1 49 3.68629 49 7V43C49 46.3137 46.3137 49 43 49H7C3.68629 49 1 46.3137 1 43V7Z" stroke="var(--wa-color-surface-border)" stroke-width="2" stroke-linecap="round" stroke-dasharray="6 6"/>
|
|
||||||
<path d="M14.1566 18.7199L21.5367 16.7424C22.6036 16.4565 23.7003 17.0896 23.9862 18.1566L26.8463 28.8306C27.1322 29.8975 26.499 30.9942 25.4321 31.2801L18.052 33.2576C16.985 33.5435 15.8884 32.9103 15.6025 31.8434L12.7424 21.1694C12.4565 20.1024 13.0897 19.0057 14.1566 18.7199Z" stroke="var(--wa-color-neutral-border-normal)" stroke-width="2"/>
|
|
||||||
<path d="M33.8449 16.3273H26.2045C23.9953 16.3273 22.2045 18.1181 22.2045 20.3273V31.3778C22.2045 33.587 23.9953 35.3778 26.2045 35.3778H33.8449C36.0541 35.3778 37.8449 33.587 37.8449 31.3778V20.3273C37.8449 18.1181 36.0541 16.3273 33.8449 16.3273Z" fill="var(--wa-color-neutral-border-normal)" stroke="var(--wa-color-neutral-fill-quiet)" stroke-width="2"/>
|
|
||||||
</svg>`;
|
|
||||||
|
|
||||||
const template = `
|
|
||||||
<wa-card with-header class="page-card" :aria-disabled="disabled ? 'true' : null" :inert="disabled"
|
|
||||||
@click="handleClick" @keyup.enter="handleClick" @keyup.space="handleClick"
|
|
||||||
:role="action ? 'button' : null" :tabindex="action? 0 : null">
|
|
||||||
<slot name="icon" slot="header">
|
|
||||||
<div slot="header" v-html="icon || ICON_PLACEHOLDER"></div>
|
|
||||||
</slot>
|
|
||||||
|
|
||||||
<div class="page-name">
|
|
||||||
<div>
|
|
||||||
<slot>
|
|
||||||
{{ content.title }}
|
|
||||||
<wa-badge class="pro" v-if="pro">PRO</wa-badge>
|
|
||||||
<div v-if="content.subtitle" class="wa-caption-m">{{ content.subtitle }}</div>
|
|
||||||
</slot>
|
|
||||||
</div>
|
|
||||||
<slot name="extra"></slot>
|
|
||||||
<wa-icon v-if="action" name="angle-right" class="angle-right" variant="regular"></wa-icon>
|
|
||||||
</div>
|
|
||||||
</wa-card>
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
title: String,
|
|
||||||
subtitle: String,
|
|
||||||
info: Object,
|
|
||||||
icon: String,
|
|
||||||
pro: Boolean,
|
|
||||||
disabled: Boolean,
|
|
||||||
action: Function,
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
Object.assign(this, { ICON_PLACEHOLDER });
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
content() {
|
|
||||||
let defaultTitle = this.info?.title ?? {};
|
|
||||||
|
|
||||||
if (this.title) {
|
|
||||||
return { title: this.title, subtitle: this.subtitle ?? defaultTitle };
|
|
||||||
} else {
|
|
||||||
return { title: defaultTitle, subtitle: this.subtitle };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
handleClick(event) {
|
|
||||||
if (this.disabled) {
|
|
||||||
event.stopImmediatePropagation();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.action) {
|
|
||||||
this.action(event);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
template,
|
|
||||||
components: {},
|
|
||||||
compilerOptions: {
|
|
||||||
isCustomElement: tag => tag.startsWith('wa-'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import palettes from '../../data/palettes.js';
|
|
||||||
import PageCard from './page-card.js';
|
|
||||||
import { hues } from '/assets/data/index.js';
|
|
||||||
|
|
||||||
// TODO import from data.js once available
|
|
||||||
const allHues = [...hues, 'gray'];
|
|
||||||
|
|
||||||
const template = `
|
|
||||||
<page-card class="palette-card" :pro="$slots.default ? false : paletteMeta.isPro" :info="paletteMeta">
|
|
||||||
<template #icon>
|
|
||||||
<wa-scoped slot="header" class="palette-icon-host">
|
|
||||||
<template>
|
|
||||||
<link rel="stylesheet" :href="'/dist/styles/color/' + palette + '.css'">
|
|
||||||
<link rel="stylesheet" href="/assets/styles/theme-icons.css">
|
|
||||||
|
|
||||||
<div class="palette-icon" style="--hues: {{ hues|length }}; --suffixes: {{ suffixes|length }}">
|
|
||||||
<template v-for="(hue, hueIndex) of hues">
|
|
||||||
<div class="swatch" v-for="(suffix, suffixIndex) of suffixes"
|
|
||||||
:data-hue="hue" :data-suffix="suffix"
|
|
||||||
:style="{
|
|
||||||
'--color': 'var(--wa-color-' + hue + suffix + ')',
|
|
||||||
gridColumn: hueIndex + 1,
|
|
||||||
gridRow: suffixIndex + 1
|
|
||||||
}"> </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-'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
.sidebar.panel-container {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
gap: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 32ch;
|
|
||||||
overflow: hidden;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
|
||||||
/* Remove the uniform spacing used in wa-details */
|
|
||||||
--spacing: 0;
|
|
||||||
/* Specify value to manually set spacing where needed */
|
|
||||||
--panel-spacing: var(--wa-space-2xl);
|
|
||||||
--panel-background: var(--wa-color-surface-default);
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
max-height: 100%;
|
|
||||||
margin-bottom: 0;
|
|
||||||
position: relative;
|
|
||||||
background: var(--panel-background);
|
|
||||||
border: none;
|
|
||||||
transition:
|
|
||||||
translate var(--wa-transition-slow) allow-discrete,
|
|
||||||
opacity var(--wa-transition-slow) 50ms allow-discrete;
|
|
||||||
/* Ensure horizontal scrollbar isn't visible when translate takes effect */
|
|
||||||
overflow-x: hidden !important;
|
|
||||||
|
|
||||||
@starting-style {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-header {
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
justify-content: start;
|
|
||||||
gap: var(--wa-space-xs);
|
|
||||||
cursor: pointer;
|
|
||||||
background: var(--panel-background);
|
|
||||||
color: var(--wa-color-text-normal);
|
|
||||||
padding-block-end: var(--panel-spacing);
|
|
||||||
padding-inline: var(--panel-spacing);
|
|
||||||
transition: inherit;
|
|
||||||
transition-property: all;
|
|
||||||
margin-block: 0;
|
|
||||||
font-size: inherit;
|
|
||||||
|
|
||||||
[data-step='0'] &,
|
|
||||||
.previous & {
|
|
||||||
padding-block-start: var(--panel-spacing);
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-icon {
|
|
||||||
vertical-align: -0.15em;
|
|
||||||
margin-inline-end: var(--wa-space-xs);
|
|
||||||
font-size: var(--wa-font-size-m);
|
|
||||||
transition: transform var(--wa-transition-normal) var(--wa-transition-easing);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .back-icon {
|
|
||||||
transform: translateX(-0.25em);
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
pointer-events: none;
|
|
||||||
font: inherit;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-content {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
gap: var(--panel-spacing);
|
|
||||||
padding-block-end: var(--panel-spacing);
|
|
||||||
padding-inline: var(--panel-spacing);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6 {
|
|
||||||
margin-block-end: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.open) {
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
&:not(.previous, .next) {
|
|
||||||
/* Hide all but the immediately preceding or following steps */
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.next {
|
|
||||||
height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.next {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.next {
|
|
||||||
translate: 100% 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-header {
|
|
||||||
font-size: var(--wa-font-size-s);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-content {
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
content-visibility: hidden;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.open {
|
|
||||||
flex: 1;
|
|
||||||
opacity: 1;
|
|
||||||
|
|
||||||
.panel-header {
|
|
||||||
font-size: var(--wa-font-size-l);
|
|
||||||
|
|
||||||
.back-icon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-content {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
transition: inherit;
|
|
||||||
|
|
||||||
@starting-style {
|
|
||||||
display: flex;
|
|
||||||
content-visibility: visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.open) {
|
|
||||||
&.previous {
|
|
||||||
.panel-content {
|
|
||||||
opacity: 0;
|
|
||||||
translate: -100% 0%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.next {
|
|
||||||
.panel-content {
|
|
||||||
opacity: 0;
|
|
||||||
translate: inherit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
/**
|
|
||||||
* Scrollable element in a vertical flex container
|
|
||||||
* Showing shadows as an indicator of scrollability (PE wherever scroll-timeline is supported for now, can be polyfilled with JS later)
|
|
||||||
*/
|
|
||||||
|
|
||||||
.scrollable {
|
|
||||||
--scroll-shadow-height: 0.5em;
|
|
||||||
|
|
||||||
flex-shrink: 1;
|
|
||||||
min-height: 0;
|
|
||||||
overflow: auto;
|
|
||||||
position: relative;
|
|
||||||
scrollbar-width: inherit;
|
|
||||||
|
|
||||||
&:is(.panel-content > div) {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
gap: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-shadow {
|
|
||||||
position: sticky;
|
|
||||||
z-index: 1;
|
|
||||||
inset-inline: 0;
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset-inline: 0;
|
|
||||||
height: var(--scroll-shadow-height);
|
|
||||||
pointer-events: none;
|
|
||||||
mix-blend-mode: multiply;
|
|
||||||
background: radial-gradient(farthest-side, var(--wa-color-shadow) 10%, transparent) center / 120% 200%;
|
|
||||||
transition: var(--wa-transition-slow);
|
|
||||||
/* transition-property: opacity, transform, display, height, min-height; */
|
|
||||||
transition-behavior: allow-discrete;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.can-scroll-top) .scroll-shadow-top,
|
|
||||||
&:not(.can-scroll-bottom) .scroll-shadow-bottom {
|
|
||||||
opacity: 0;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.can-scroll-top) .scroll-shadow-top {
|
|
||||||
&::before {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-shadow-top {
|
|
||||||
top: 0;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
background-position: bottom;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.can-scroll-bottom) .scroll-shadow-bottom {
|
|
||||||
&::before {
|
|
||||||
transform: translateY(100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-shadow-bottom {
|
|
||||||
top: 100%;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
bottom: 0;
|
|
||||||
background-position: top;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollable:where(.panel-content) {
|
|
||||||
.scroll-shadow-top {
|
|
||||||
/* TODO convert this magic number to a token that explains what it is */
|
|
||||||
margin-bottom: -18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-shadow-bottom {
|
|
||||||
transform: translateY(var(--padding-bottom, var(--panel-spacing)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { capitalize } from '../../scripts/util/string.js';
|
|
||||||
import inputMixin from '../mixins/input.js';
|
|
||||||
import InfoTip from './info-tip.js';
|
|
||||||
|
|
||||||
const template = `
|
|
||||||
<wa-radio-group :label class="swatch-select" :class="'swatch-shape-' + shape" orientation="horizontal" :value @input="handleInput">
|
|
||||||
<info-tip v-for="value in values">
|
|
||||||
<wa-radio appearance="button" :value :label="getLabel(value)" :style="{'--color': getColor(value)}"></wa-radio>
|
|
||||||
<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-'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import themes from '../../data/themes.js';
|
|
||||||
import { capitalize } from '../../scripts/util/string.js';
|
|
||||||
import PageCard from './page-card.js';
|
|
||||||
import { getThemeCode } from '/assets/scripts/tweak/code.js';
|
|
||||||
|
|
||||||
const iconTemplates = {
|
|
||||||
colors: `
|
|
||||||
<div class="theme-icon theme-color-icon" role="presentation">
|
|
||||||
<div style="background: var(--wa-color-brand-fill-loud); border-color: var(--wa-color-brand-border-loud); color: var(--wa-color-brand-on-loud);">A</div>
|
|
||||||
<div style="background: var(--wa-color-brand-fill-normal); border-color: var(--wa-color-brand-border-normal); color: var(--wa-color-brand-on-normal);">A</div>
|
|
||||||
<div style="background: var(--wa-color-brand-fill-quiet); border-color: var(--wa-color-brand-border-quiet); color: var(--wa-color-brand-on-quiet);">A</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="wa-invert theme-icon theme-color-icon" role="presentation">
|
|
||||||
<div style="background: var(--wa-color-brand-fill-loud); border-color: var(--wa-color-brand-border-loud); color: var(--wa-color-brand-on-loud);">A</div>
|
|
||||||
<div style="background: var(--wa-color-brand-fill-normal); border-color: var(--wa-color-brand-border-normal); color: var(--wa-color-brand-on-normal);">A</div>
|
|
||||||
<div style="background: var(--wa-color-brand-fill-quiet); border-color: var(--wa-color-brand-border-quiet); color: var(--wa-color-brand-on-quiet);">A</div>
|
|
||||||
</div>`,
|
|
||||||
dimensionality: `
|
|
||||||
<wa-card size="small">
|
|
||||||
<wa-input value="Input" size="small"></wa-input>
|
|
||||||
<wa-button size="small" variant="brand">Go</wa-button>
|
|
||||||
</wa-card>
|
|
||||||
`,
|
|
||||||
overall: `
|
|
||||||
<div class="row row-1">
|
|
||||||
<h2>Aa</h2>
|
|
||||||
<div class="swatches">
|
|
||||||
<div class="wa-brand"></div>
|
|
||||||
<div class="wa-success"></div>
|
|
||||||
<div class="wa-warning"></div>
|
|
||||||
<div class="wa-danger"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row row-2">
|
|
||||||
<wa-input value="Input" size="small"></wa-input>
|
|
||||||
<wa-button size="small" variant="brand">Go</wa-button>
|
|
||||||
</div>`,
|
|
||||||
};
|
|
||||||
|
|
||||||
const template = `
|
|
||||||
<page-card class="theme-card" :class="type + '-card'" :info="themeMeta" :data-theme="theme">
|
|
||||||
<template #icon>
|
|
||||||
<wa-scoped slot="header" class="theme-icon-host" inert :key="themeCode">
|
|
||||||
<template v-html="themeCode"></template>
|
|
||||||
<template>
|
|
||||||
<link rel="stylesheet" href="/dist/styles/utilities.css">
|
|
||||||
<link rel="stylesheet" href="/dist/styles/native/content.css">
|
|
||||||
<link rel="stylesheet" href="/assets/styles/theme-icons.css">
|
|
||||||
|
|
||||||
<template v-if="type === 'colors'">
|
|
||||||
${iconTemplates.colors}
|
|
||||||
</template>
|
|
||||||
<div v-else-if="type in iconTemplates && type !== 'overall'" class="theme-icon" :class="'theme-' + type + '-icon'" v-html="iconTemplates[type]" role="presentation">
|
|
||||||
</div>
|
|
||||||
<div v-else class="theme-icon theme-overall-icon" :class="'wa-theme-' + theme" role="presentation">
|
|
||||||
${iconTemplates.overall}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</wa-scoped>
|
|
||||||
</template>
|
|
||||||
<slot></slot>
|
|
||||||
<template #extra>
|
|
||||||
<slot name="extra" />
|
|
||||||
</template>
|
|
||||||
</page-card>
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
theme: String,
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
validator(value) {
|
|
||||||
return !value || value in iconTemplates;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rest: Object,
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.iconTemplates = iconTemplates;
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
themeMeta() {
|
|
||||||
let ret = themes[this.theme] ? { ...themes[this.theme] } : {};
|
|
||||||
// if (this.type === 'dimensionality' && typeof ret.dimension === 'string') {
|
|
||||||
// ret.title = capitalize(ret.dimension);
|
|
||||||
// }
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
|
|
||||||
themeCode() {
|
|
||||||
let theme = { ...(this.rest || {}), [this.type || 'base']: this.theme };
|
|
||||||
theme.base ||= 'default';
|
|
||||||
|
|
||||||
// if (theme.dimensionality) {
|
|
||||||
// if (!themes[theme.dimensionality]?.dimension || theme.dimensionality === theme.base) {
|
|
||||||
// theme.dimensionality = '';
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
return getThemeCode(theme, { id: this.theme, language: 'html', cdnUrl: '/dist/' });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
template,
|
|
||||||
components: {
|
|
||||||
PageCard,
|
|
||||||
},
|
|
||||||
compilerOptions: {
|
|
||||||
isCustomElement: tag => tag.startsWith('wa-'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
const template = `
|
|
||||||
<section class="panel-container" ref="container" :style="{'--panel-step': step}" @open="handleOpen">
|
|
||||||
<slot ref="panels"></slot>
|
|
||||||
</section>
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
/** Currently selected id */
|
|
||||||
modelValue: String,
|
|
||||||
},
|
|
||||||
emits: ['update:modelValue'],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
value: '',
|
|
||||||
previousValue: '',
|
|
||||||
step: 0,
|
|
||||||
trail: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
let { container } = this.$refs;
|
|
||||||
let activePanel = container.querySelector(':scope > .open');
|
|
||||||
|
|
||||||
if (activePanel) {
|
|
||||||
let { step, value } = activePanel.dataset;
|
|
||||||
this.step = Number(step);
|
|
||||||
this.value = value;
|
|
||||||
this.$emit('update:modelValue', this.value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
panels() {
|
|
||||||
if (!this.$refs.container) {
|
|
||||||
return new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
let { container } = this.$refs;
|
|
||||||
|
|
||||||
return new Map(
|
|
||||||
[...container.querySelectorAll(':scope > .panel')].map(panel => [
|
|
||||||
panel.dataset.value,
|
|
||||||
Number(panel.dataset.step),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
handleOpen(e) {
|
|
||||||
let { value, step } = e.detail;
|
|
||||||
this.value = value;
|
|
||||||
this.step = step;
|
|
||||||
},
|
|
||||||
|
|
||||||
updatePanels() {
|
|
||||||
let { container } = this.$refs;
|
|
||||||
|
|
||||||
if (!container) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { step, value } = this;
|
|
||||||
|
|
||||||
if (this.panels.get(value) !== step) {
|
|
||||||
// Hasn't stabilized yet
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let previousValue = this.trail.findLast(panel => this.panels.get(panel) === step - 1);
|
|
||||||
|
|
||||||
for (let panel of container.querySelectorAll(':scope > .panel')) {
|
|
||||||
let panelStep = Number(panel.dataset.step);
|
|
||||||
let panelValue = panel.dataset.value;
|
|
||||||
let isPrevious = previousValue ? panelValue === previousValue : panelStep === step - 1;
|
|
||||||
let isOpen = panelValue === value;
|
|
||||||
let isNext = panelStep === step + 1;
|
|
||||||
|
|
||||||
panel.classList.toggle('previous', isPrevious);
|
|
||||||
panel.classList.toggle('open', isOpen);
|
|
||||||
panel.classList.toggle('next', isNext);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
value() {
|
|
||||||
if (this.value !== this.modelValue) {
|
|
||||||
this.$emit('update:modelValue', this.value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
modelValue: {
|
|
||||||
immediate: true,
|
|
||||||
async handler(value, previousValue) {
|
|
||||||
if (this.value !== this.modelValue) {
|
|
||||||
this.value = this.modelValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previousValue) {
|
|
||||||
this.trail.push(previousValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updatePanels();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
step() {
|
|
||||||
this.updatePanels();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
template,
|
|
||||||
components: {},
|
|
||||||
compilerOptions: {
|
|
||||||
isCustomElement: tag => tag.startsWith('wa-'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import UiScrollable from './ui-scrollable.js';
|
|
||||||
|
|
||||||
const template = `
|
|
||||||
<ui-scrollable :disabled="!open" role="group" :name="name || 'panel'" :data-value="value" :data-step="step" class="panel" :class="{open}">
|
|
||||||
<h2 :inert="open" class="panel-header" @click="openPanel" ref="panelHeader">
|
|
||||||
<wa-icon name="chevron-left" class="back-icon" />
|
|
||||||
<slot name="title">{{ title }}</slot>
|
|
||||||
</h2>
|
|
||||||
<div class="panel-content">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</ui-scrollable>
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
title: String,
|
|
||||||
name: String,
|
|
||||||
step: Number,
|
|
||||||
|
|
||||||
/** Id of this panel */
|
|
||||||
value: String,
|
|
||||||
|
|
||||||
/** Currently selected id */
|
|
||||||
modelValue: String,
|
|
||||||
},
|
|
||||||
emits: ['update:modelValue', 'open'],
|
|
||||||
data() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
if (this.open) {
|
|
||||||
this.$refs.panelHeader.dispatchEvent(
|
|
||||||
new CustomEvent('open', { detail: { value: this.value, step: this.step }, bubbles: true }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
open() {
|
|
||||||
return this.value === this.modelValue;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
openPanel() {
|
|
||||||
let wasOpen = this.open;
|
|
||||||
this.$emit('update:modelValue', wasOpen ? '' : this.value);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
open: {
|
|
||||||
immediate: true,
|
|
||||||
handler(open) {
|
|
||||||
if (open && this.$refs.panelHeader) {
|
|
||||||
this.$refs.panelHeader.dispatchEvent(
|
|
||||||
new CustomEvent('open', { detail: { value: this.value, step: this.step }, bubbles: true }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
template,
|
|
||||||
components: {
|
|
||||||
UiScrollable,
|
|
||||||
},
|
|
||||||
compilerOptions: {
|
|
||||||
isCustomElement: tag => tag.startsWith('wa-'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
const template = `
|
|
||||||
<div class="scrollable" :class="{'can-scroll-top': canScrollTop, 'can-scroll-bottom': canScrollBottom}" ref="container">
|
|
||||||
<div v-if="!disabled" class="scroll-shadow scroll-shadow-top"></div>
|
|
||||||
<slot></slot>
|
|
||||||
<div v-if="!disabled" class="scroll-shadow scroll-shadow-bottom"></div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
disabled: Boolean,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
scrollTop: 0,
|
|
||||||
scrollHeight: 0,
|
|
||||||
height: 0,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
let { container, content } = this.$refs;
|
|
||||||
container.addEventListener('scroll', this.handleScroll, { passive: true });
|
|
||||||
|
|
||||||
this.scrollHeight = container.scrollHeight;
|
|
||||||
this.height = container.clientHeight;
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
canScrollTop() {
|
|
||||||
return !this.disabled && this.scrollTop > 1;
|
|
||||||
},
|
|
||||||
|
|
||||||
maxScrollTop() {
|
|
||||||
return this.scrollHeight - this.height;
|
|
||||||
},
|
|
||||||
|
|
||||||
canScrollBottom() {
|
|
||||||
return !this.disabled && this.scrollTop < this.maxScrollTop - 1;
|
|
||||||
},
|
|
||||||
|
|
||||||
scrollProgress() {
|
|
||||||
return this.scrollTop / this.maxScrollTop;
|
|
||||||
},
|
|
||||||
|
|
||||||
scrollProgressEnd() {
|
|
||||||
return this.scrollProgress + this.maxScrollTop / this.scrollHeight;
|
|
||||||
},
|
|
||||||
|
|
||||||
scrollBottom() {
|
|
||||||
return this.scrollHeight * this.scrollProgressEnd;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
handleScroll(event) {
|
|
||||||
let { container } = this.$refs;
|
|
||||||
this.scrollTop = container.scrollTop;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
scrollTop(value, oldValue) {
|
|
||||||
let { container } = this.$refs;
|
|
||||||
if (container && oldValue === 0) {
|
|
||||||
this.scrollHeight = container.scrollHeight;
|
|
||||||
this.height = container.clientHeight;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
template,
|
|
||||||
components: {},
|
|
||||||
compilerOptions: {
|
|
||||||
isCustomElement: tag => tag.startsWith('wa-'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
.ui-slider {
|
|
||||||
display: grid;
|
|
||||||
grid-template:
|
|
||||||
'label label label'
|
|
||||||
'min slider max';
|
|
||||||
grid-template-columns: auto 1fr auto;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--wa-space-2xs);
|
|
||||||
|
|
||||||
wa-slider {
|
|
||||||
display: block;
|
|
||||||
grid-area: slider;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(.ui-slider-min) wa-slider {
|
|
||||||
&::part(label) {
|
|
||||||
margin-inline-start: calc(-1 * (var(--wa-space-s) + 1rem + 2 * var(--wa-border-width-m)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear-button {
|
|
||||||
vertical-align: middle;
|
|
||||||
margin-inline-start: var(--wa-space-xs);
|
|
||||||
font-size: var(--wa-font-size-xs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ui-slider-header {
|
|
||||||
grid-area: label;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ui-slider-min,
|
|
||||||
.ui-slider-max {
|
|
||||||
width: min-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ui-slider-min {
|
|
||||||
grid-area: min;
|
|
||||||
margin-inline-start: calc(-1 * var(--wa-space-s));
|
|
||||||
}
|
|
||||||
|
|
||||||
.ui-slider-max {
|
|
||||||
grid-area: max;
|
|
||||||
margin-inline-end: calc(-1 * var(--wa-space-s));
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
import inputMixin from '../mixins/input.js';
|
|
||||||
import InfoTip from './info-tip.js';
|
|
||||||
|
|
||||||
let maxUid = 0;
|
|
||||||
|
|
||||||
const template = `
|
|
||||||
<div class="ui-slider">
|
|
||||||
<div class="ui-slider-header">
|
|
||||||
<label :for="sliderId">{{ label }}</label>
|
|
||||||
<info-tip v-if="clearable && (value !== defaultValue ?? initialValue)" :text="'Reset to ' + valueFormatter(defaultValue ?? initialValue)">
|
|
||||||
<wa-button @click="value = defaultValue ?? initialValue" class="clear-button">
|
|
||||||
<wa-icon name="circle-xmark" library="system" variant="regular" :label="'Reset to ' + tooltipFormatter(defaultValue ?? initialValue)"></wa-icon>
|
|
||||||
</wa-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-'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
// Like v-text, but doesn't complain if the element has content,
|
|
||||||
// making it possible to use in a PE fashion, with the contents being the fallback
|
|
||||||
export default function content(el, { value, arg }) {
|
|
||||||
if (!el.dataset.fallback) {
|
|
||||||
// Store the original content as a fallback the first time
|
|
||||||
el.dataset.fallback = el.textContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value === '') {
|
|
||||||
value = el.dataset.fallback;
|
|
||||||
} else {
|
|
||||||
if (arg === 'number') {
|
|
||||||
value = Number(value).toLocaleString(undefined, { maximumSignificantDigits: 2 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (arg === 'html') {
|
|
||||||
el.innerHTML = value;
|
|
||||||
} else {
|
|
||||||
el.textContent = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
/**
|
|
||||||
* Mixin for components that behave like form controls.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
modelValue: {
|
|
||||||
type: [String, Number, Boolean],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
initialValue: this.modelValue,
|
|
||||||
value: this.modelValue,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
emits: ['update:modelValue', 'input'],
|
|
||||||
methods: {
|
|
||||||
handleInput(e) {
|
|
||||||
this.value = e.target.value;
|
|
||||||
this.$emit('input', this.value);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
value(value) {
|
|
||||||
this.$emit('update:modelValue', value);
|
|
||||||
},
|
|
||||||
modelValue(value) {
|
|
||||||
this.value = value;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import my from '/assets/scripts/my.js';
|
|
||||||
import Permalink from '/assets/scripts/permalink.js';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
uid: undefined,
|
|
||||||
saved: null,
|
|
||||||
unsavedChanges: false,
|
|
||||||
permalink: new Permalink(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
if (this.permalink.has('uid')) {
|
|
||||||
this.uid = Number(this.permalink.get('uid'));
|
|
||||||
this.saved = this.controller.saved.find(p => p.uid === this.uid);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.controller.addEventListener('delete', ({ detail: entity }) => {
|
|
||||||
if (entity.uid === this.saved?.uid) {
|
|
||||||
this.postDelete();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
this.$nextTick().then(() => {
|
|
||||||
if (!location.search || this.saved) {
|
|
||||||
this.unsavedChanges = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
controller() {
|
|
||||||
return my[this.collection];
|
|
||||||
},
|
|
||||||
|
|
||||||
title() {
|
|
||||||
if (this.saved) {
|
|
||||||
return this.saved.title;
|
|
||||||
} else if (this.unsavedChanges) {
|
|
||||||
return this.defaultTitle;
|
|
||||||
} else {
|
|
||||||
return this.originalTitle;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
saved: {
|
|
||||||
deep: true,
|
|
||||||
handler() {
|
|
||||||
this.unsavedChanges = !this.saved;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
async save({ title } = {}) {
|
|
||||||
let uid = this.uid;
|
|
||||||
|
|
||||||
this.saved ??= { uid: this.uid };
|
|
||||||
this.saved.id = this.id;
|
|
||||||
|
|
||||||
if (title) {
|
|
||||||
// Renaming
|
|
||||||
this.saved.title = title;
|
|
||||||
} else {
|
|
||||||
this.saved.title ??= this.defaultTitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.saved.search = location.search;
|
|
||||||
|
|
||||||
this.saved = this.controller.save(this.saved);
|
|
||||||
|
|
||||||
if (uid !== this.saved.uid) {
|
|
||||||
// UID changed (most likely from saving a new entity)
|
|
||||||
this.uid = this.saved.uid;
|
|
||||||
this.permalink.set('uid', this.uid);
|
|
||||||
this.permalink.updateLocation();
|
|
||||||
await this.$nextTick();
|
|
||||||
this.save(); // Save again to update the search param to include the UID
|
|
||||||
}
|
|
||||||
|
|
||||||
this.unsavedChanges = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
rename() {
|
|
||||||
let newTitle = prompt('Title:', this.saved?.title ?? this.defaultTitle);
|
|
||||||
|
|
||||||
if (newTitle && newTitle !== this.saved?.title) {
|
|
||||||
this.save({ title: newTitle });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Cannot name this delete() because Vue complains
|
|
||||||
deleteSaved() {
|
|
||||||
this.controller.delete(this.saved);
|
|
||||||
},
|
|
||||||
|
|
||||||
postDelete() {
|
|
||||||
this.saved = null;
|
|
||||||
this.permalink.delete('uid');
|
|
||||||
this.uid = undefined;
|
|
||||||
this.permalink.updateLocation();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
256
packages/webawesome/docs/docs/color-palettes.njk
Normal file
256
packages/webawesome/docs/docs/color-palettes.njk
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
---
|
||||||
|
title: Color Palettes
|
||||||
|
description: 'Color palettes give you a full spectrum of colors to add life to your project.'
|
||||||
|
layout: page
|
||||||
|
isPro: true
|
||||||
|
---
|
||||||
|
|
||||||
|
<p>Color palettes give you a full spectrum of colors to add life to your project.</p>
|
||||||
|
|
||||||
|
<p>Each palette defines 10 color hues with a scale of 11 tints. While the tints have consistent lightness values across palettes, each palette has unique hue shifts and chroma to give it unique character so you can find just the right vibe to your project.</p>
|
||||||
|
|
||||||
|
{% for palette in themer.palettes %}
|
||||||
|
<link rel="stylesheet" href="/dist/styles/color/palettes/{{palette.filename}}" />
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div id="color-palettes">
|
||||||
|
<wa-radio-group id="palette-picker" label="Color Palette" value="default" orientation="horizontal">
|
||||||
|
{% for palette in themer.palettes %}
|
||||||
|
<wa-radio class="palette-card" value="{{ palette.name | lower }}">{{ palette.name }}</wa-radio>
|
||||||
|
{% endfor %}
|
||||||
|
</wa-radio-group>
|
||||||
|
|
||||||
|
{% for color in themer.colors %}
|
||||||
|
<div class="palette">
|
||||||
|
<div class="palette-label">{{ color }}</div>
|
||||||
|
<div class="palette-swatches">
|
||||||
|
{% for tint in themer.tints %}
|
||||||
|
<wa-copy-button
|
||||||
|
class="palette-swatch"
|
||||||
|
copy-label="{{ color }} {{ tint }}"
|
||||||
|
value="var(--wa-color-{{ color }}-{{ tint }})"
|
||||||
|
style="--color: var(--wa-color-{{ color }}-{{ tint }}); --tint: '{{ tint }}'"
|
||||||
|
>
|
||||||
|
<span class="sr-only">--wa-color-{{ color }}-{{ tint }}</span>
|
||||||
|
</wa-copy-button>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Using This Palette</h2>
|
||||||
|
<div id="import-code">
|
||||||
|
{% for palette in themer.palettes %}
|
||||||
|
<div class="palette-instructions" data-palette="{{ palette.name | lower }}" {% if not loop.first %}hidden{% endif %}>
|
||||||
|
<p>
|
||||||
|
To import this palette, set <code><html class="wa-theme-{{ palette.name | lower }}"></code> and import the following stylesheet:
|
||||||
|
</p>
|
||||||
|
<pre><code class="language-html"><link rel="stylesheet" href="{% cdnUrl %}styles/color/palettes/{{ palette.filename }}" /></code></pre>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
const paletteContainer = document.getElementById('color-palettes');
|
||||||
|
const palettePicker = document.getElementById('palette-picker');
|
||||||
|
|
||||||
|
// Set first radio as checked and add initial theme class
|
||||||
|
const firstRadio = palettePicker.querySelector('wa-radio');
|
||||||
|
if (firstRadio) {
|
||||||
|
firstRadio.checked = true;
|
||||||
|
paletteContainer.classList.add(`wa-palette-${firstRadio.value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for radio changes
|
||||||
|
palettePicker.addEventListener('input', function(event) {
|
||||||
|
const selectedValue = event.target.value;
|
||||||
|
|
||||||
|
// Update palette container class
|
||||||
|
const existingThemeClasses = [...paletteContainer.classList].filter(className => className.startsWith('wa-palette-'));
|
||||||
|
existingThemeClasses.forEach(className => paletteContainer.classList.remove(className));
|
||||||
|
paletteContainer.classList.add(`wa-palette-${selectedValue}`);
|
||||||
|
|
||||||
|
// Show/hide appropriate instructions
|
||||||
|
document.querySelectorAll('.palette-instructions').forEach(instruction => {
|
||||||
|
instruction.hidden = instruction.dataset.palette !== selectedValue;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
wa-page > main {
|
||||||
|
max-width: 120ch;
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#color-palettes {
|
||||||
|
margin-block-end: var(--wa-space-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
#palette-picker {
|
||||||
|
margin-bottom: var(--wa-space-l);
|
||||||
|
|
||||||
|
&::part(form-control-input) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--wa-space-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::part(form-control-label) {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
clip-path: inset(50%);
|
||||||
|
border: none;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-card {
|
||||||
|
--border-radius: var(--wa-border-radius-m);
|
||||||
|
--spacing: var(--wa-space-s) var(--wa-space-m);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: var(--wa-space-3xl);
|
||||||
|
padding: var(--spacing);
|
||||||
|
background-color: var(--wa-color-surface-default);
|
||||||
|
border: 1px solid var(--wa-color-surface-border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: var(--wa-shadow-s);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: var(--wa-font-weight-action);
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-card:state(checked) {
|
||||||
|
border-color: var(--wa-color-brand-border-loud);
|
||||||
|
background-color: var(--wa-color-brand-fill-quiet);
|
||||||
|
color: var(--wa-color-brand-text-loud);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-card::part(control) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-card::part(label) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 5rem 1fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--wa-space-m);
|
||||||
|
margin-bottom: var(--wa-space-s);
|
||||||
|
width: 100%;
|
||||||
|
padding-top: var(--wa-space-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-label {
|
||||||
|
font-weight: var(--wa-font-weight-bold);
|
||||||
|
text-transform: capitalize;
|
||||||
|
font-size: var(--wa-font-size-s);
|
||||||
|
color: var(--wa-color-text-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-swatches {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(11, 1fr);
|
||||||
|
gap: var(--wa-space-3xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-swatch {
|
||||||
|
padding: 0;
|
||||||
|
aspect-ratio: 1.75 / 1; /* Wider than tall on desktop */
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: var(--tint);
|
||||||
|
position: absolute;
|
||||||
|
top: calc(-1 * var(--wa-space-l));
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: var(--wa-font-size-xs);
|
||||||
|
color: var(--wa-color-text-quiet);
|
||||||
|
font-weight: var(--wa-font-weight-action);
|
||||||
|
text-align: center;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::part(button) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--color);
|
||||||
|
border-radius: var(--wa-border-radius-m);
|
||||||
|
transition: transform 0.1s ease, box-shadow 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover::part(button) {
|
||||||
|
transform: scale(1.075);
|
||||||
|
box-shadow: var(--wa-shadow-s);
|
||||||
|
z-index: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::part(copy-icon),
|
||||||
|
&::part(success-icon),
|
||||||
|
&::part(error-icon) {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smaller screens */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.palette {
|
||||||
|
grid-template-columns: 3.75rem 1fr;
|
||||||
|
gap: var(--wa-space-s);
|
||||||
|
margin-bottom: var(--wa-space-m);
|
||||||
|
padding-top: var(--wa-space-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-label {
|
||||||
|
font-size: var(--wa-font-size-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-swatches {
|
||||||
|
grid-template-columns: repeat(6, 1fr); /* Two rows on mobile */
|
||||||
|
gap: var(--wa-space-2xs);
|
||||||
|
row-gap: var(--wa-space-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-swatch {
|
||||||
|
aspect-ratio: 1 / 1; /* Square on mobile */
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
font-size: var(--wa-font-size-2xs);
|
||||||
|
top: calc(-1 * var(--wa-space-m));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: Animated Image
|
title: Animated Image
|
||||||
description: A component for displaying animated GIFs and WEBPs that play and pause on interaction.
|
description: A component for displaying animated GIFs and WEBPs that play and pause on interaction.
|
||||||
tags: [imagery, niche]
|
layout: component
|
||||||
icon: animated-image
|
category: Imagery
|
||||||
---
|
---
|
||||||
|
|
||||||
```html {.example}
|
```html {.example}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: Animation
|
title: Animation
|
||||||
description: Animate elements declaratively with nearly 100 baked-in presets, or roll your own with custom keyframes.
|
description: Animate elements declaratively with nearly 100 baked-in presets, or roll your own with custom keyframes.
|
||||||
tags: [helpers, primitives]
|
layout: component
|
||||||
icon: animation
|
category: Utilities
|
||||||
---
|
---
|
||||||
|
|
||||||
To animate an element, wrap it in `<wa-animation>` and set an animation `name`. The animation will not start until you add the `play` attribute. Refer to the [properties table](#properties) for a list of all animation options.
|
To animate an element, wrap it in `<wa-animation>` and set an animation `name`. The animation will not start until you add the `play` attribute. Refer to the [properties table](#properties) for a list of all animation options.
|
||||||
@@ -45,8 +45,7 @@ This example demonstrates all of the baked-in animations and easings. Animations
|
|||||||
<div class="controls">
|
<div class="controls">
|
||||||
<wa-select label="Animation" value="bounce"></wa-select>
|
<wa-select label="Animation" value="bounce"></wa-select>
|
||||||
<wa-select label="Easing" value="linear"></wa-select>
|
<wa-select label="Easing" value="linear"></wa-select>
|
||||||
<wa-input label="Playback Rate" type="number" min="0" max="2" step=".25" value="1">
|
<wa-input label="Playback Rate" type="number" min="0" max="2" step=".25" value="1"> </wa-input>
|
||||||
</wa-input>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -64,7 +63,7 @@ This example demonstrates all of the baked-in animations and easings. Animations
|
|||||||
animations.map(name => {
|
animations.map(name => {
|
||||||
const option = Object.assign(document.createElement('wa-option'), {
|
const option = Object.assign(document.createElement('wa-option'), {
|
||||||
textContent: name,
|
textContent: name,
|
||||||
value: name
|
value: name,
|
||||||
});
|
});
|
||||||
animationName.appendChild(option);
|
animationName.appendChild(option);
|
||||||
});
|
});
|
||||||
@@ -72,7 +71,7 @@ This example demonstrates all of the baked-in animations and easings. Animations
|
|||||||
easings.map(name => {
|
easings.map(name => {
|
||||||
const option = Object.assign(document.createElement('wa-option'), {
|
const option = Object.assign(document.createElement('wa-option'), {
|
||||||
textContent: name,
|
textContent: name,
|
||||||
value: name
|
value: name,
|
||||||
});
|
});
|
||||||
easingName.appendChild(option);
|
easingName.appendChild(option);
|
||||||
});
|
});
|
||||||
@@ -156,15 +155,15 @@ Supply your own [keyframe formats](https://developer.mozilla.org/en-US/docs/Web/
|
|||||||
easing: 'cubic-bezier(0.250, 0.460, 0.450, 0.940)',
|
easing: 'cubic-bezier(0.250, 0.460, 0.450, 0.940)',
|
||||||
fillMode: 'both',
|
fillMode: 'both',
|
||||||
transformOrigin: 'center center',
|
transformOrigin: 'center center',
|
||||||
transform: 'rotate(0)'
|
transform: 'rotate(0)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
offset: 1,
|
offset: 1,
|
||||||
easing: 'cubic-bezier(0.250, 0.460, 0.450, 0.940)',
|
easing: 'cubic-bezier(0.250, 0.460, 0.450, 0.940)',
|
||||||
fillMode: 'both',
|
fillMode: 'both',
|
||||||
transformOrigin: 'center center',
|
transformOrigin: 'center center',
|
||||||
transform: 'rotate(90deg)'
|
transform: 'rotate(90deg)',
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -197,3 +196,4 @@ Animations won't play until you apply the `play` attribute. You can omit it init
|
|||||||
animation.play = true;
|
animation.play = true;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: Avatar
|
title: Avatar
|
||||||
description: Avatars are used to represent a person or object.
|
description: Avatars are used to represent a person or object.
|
||||||
tags: [imagery, apps, content]
|
layout: component
|
||||||
icon: avatar
|
category: Imagery
|
||||||
---
|
---
|
||||||
|
|
||||||
By default, a generic icon will be shown. You can personalize avatars by adding custom icons, initials, and images. You should always provide a `label` for assistive devices.
|
By default, a generic icon will be shown. You can personalize avatars by adding custom icons, initials, and images. You should always provide a `label` for assistive devices.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: Badge
|
title: Badge
|
||||||
description: Badges are used to draw attention and display statuses or counts.
|
description: Badges are used to draw attention and display statuses or counts.
|
||||||
tags: [feedback, content]
|
layout: component
|
||||||
icon: badge
|
category: Feedback & Status
|
||||||
---
|
---
|
||||||
|
|
||||||
```html {.example}
|
```html {.example}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: Breadcrumb Item
|
title: Breadcrumb Item
|
||||||
description: Breadcrumb Items are used inside breadcrumbs to represent different links.
|
description: Breadcrumb Items are used inside breadcrumbs to represent different links.
|
||||||
tags: component
|
layout: component
|
||||||
parent: breadcrumb
|
category: Navigation
|
||||||
---
|
---
|
||||||
|
|
||||||
This component must be used as a child of `<wa-breadcrumb>`. Please see the [Breadcrumb docs](/docs/components/breadcrumb) to see examples of this component in action.
|
This component must be used as a child of `<wa-breadcrumb>`. Please see the [Breadcrumb docs](/docs/components/breadcrumb) to see examples of this component in action.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: Breadcrumb
|
title: Breadcrumb
|
||||||
description: Breadcrumbs provide a group of links so users can easily navigate a website's hierarchy.
|
description: Breadcrumbs provide a group of links so users can easily navigate a website's hierarchy.
|
||||||
tags: [navigation]
|
layout: component
|
||||||
icon: breadcrumb
|
category: Navigation
|
||||||
---
|
---
|
||||||
|
|
||||||
Breadcrumbs are usually placed before a page's main content with the current page shown last to indicate the user's position in the navigation.
|
Breadcrumbs are usually placed before a page's main content with the current page shown last to indicate the user's position in the navigation.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: Button Group
|
title: Button Group
|
||||||
description: Button groups can be used to group related buttons into sections.
|
description: Button groups can be used to group related buttons into sections.
|
||||||
tags: [actions, forms, apps]
|
layout: component
|
||||||
icon: button-group
|
category: Actions
|
||||||
---
|
---
|
||||||
|
|
||||||
```html {.example}
|
```html {.example}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: Button
|
title: Button
|
||||||
description: Buttons represent actions that are available to the user.
|
description: Buttons represent actions that are available to the user.
|
||||||
tags: [actions, forms]
|
layout: component
|
||||||
native: button
|
category: Actions
|
||||||
icon: button
|
|
||||||
---
|
---
|
||||||
|
|
||||||
```html {.example}
|
```html {.example}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: Callout
|
title: Callout
|
||||||
description: Callouts are used to display important messages inline.
|
description: Callouts are used to display important messages inline.
|
||||||
tags: [feedback, content]
|
layout: component
|
||||||
icon: callout
|
category: Feedback & Status
|
||||||
native: callout
|
|
||||||
---
|
---
|
||||||
|
|
||||||
```html {.example}
|
```html {.example}
|
||||||
@@ -135,5 +134,6 @@ Icons are optional. Simply omit the `icon` slot if you don't want them.
|
|||||||
### Styling
|
### Styling
|
||||||
|
|
||||||
You can customize the callout's appearance mostly by setting regular CSS properties:
|
You can customize the callout's appearance mostly by setting regular CSS properties:
|
||||||
|
|
||||||
- `background`, `border`, `border-radius`, `color`, `padding`, `margin`, etc. work as expected
|
- `background`, `border`, `border-radius`, `color`, `padding`, `margin`, etc. work as expected
|
||||||
- `gap` sets the space between the icon and the content
|
- `gap` sets the space between the icon and the content
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user