mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 20:19:13 +00:00
untying the knots
This commit is contained in:
@@ -8,8 +8,10 @@
|
||||
"APG",
|
||||
"apos",
|
||||
"atrule",
|
||||
"autocapitalize",
|
||||
"autocorrect",
|
||||
"autofix",
|
||||
"autofocus",
|
||||
"autoload",
|
||||
"autoloader",
|
||||
"autoloading",
|
||||
@@ -50,7 +52,9 @@
|
||||
"dropdowns",
|
||||
"easings",
|
||||
"ecommerce",
|
||||
"elif",
|
||||
"endfor",
|
||||
"endmarkdown",
|
||||
"endraw",
|
||||
"endregion",
|
||||
"enterkeyhint",
|
||||
@@ -60,6 +64,7 @@
|
||||
"esbuild",
|
||||
"exportmaps",
|
||||
"exportparts",
|
||||
"fetchpriority",
|
||||
"fieldsets",
|
||||
"focusin",
|
||||
"focusout",
|
||||
@@ -88,6 +93,7 @@
|
||||
"inputmode",
|
||||
"ionicon",
|
||||
"ionicons",
|
||||
"jank",
|
||||
"jsDelivr",
|
||||
"jsfiddle",
|
||||
"keydown",
|
||||
@@ -123,6 +129,7 @@
|
||||
"noindex",
|
||||
"noopener",
|
||||
"noreferrer",
|
||||
"noscript",
|
||||
"novalidate",
|
||||
"Numberish",
|
||||
"nums",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { parse } from 'path';
|
||||
import { anchorHeadingsPlugin } from './_utils/anchor-headings.js';
|
||||
import { codeExamplesPlugin } from './_utils/code-examples.js';
|
||||
import { copyCodePlugin } from './_utils/copy-code.js';
|
||||
@@ -10,8 +11,6 @@ import { markdown } from './_utils/markdown.js';
|
||||
// import litPlugin from '@lit-labs/eleventy-plugin-lit';
|
||||
import { readFile } from 'fs/promises';
|
||||
import nunjucks from 'nunjucks';
|
||||
// import componentList from './_data/componentList.js';
|
||||
import * as filters from './_utils/filters.js';
|
||||
import { outlinePlugin } from './_utils/outline.js';
|
||||
import { replaceTextPlugin } from './_utils/replace-text.js';
|
||||
import { searchPlugin } from './_utils/search.js';
|
||||
@@ -57,10 +56,13 @@ export default async function (eleventyConfig) {
|
||||
// Template filters - {{ content | filter }}
|
||||
eleventyConfig.addFilter('inlineMarkdown', content => markdown.renderInline(content || ''));
|
||||
eleventyConfig.addFilter('markdown', content => markdown.render(content || ''));
|
||||
|
||||
for (let name in filters) {
|
||||
eleventyConfig.addFilter(name, filters[name]);
|
||||
}
|
||||
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;
|
||||
});
|
||||
|
||||
// Shortcodes - {% shortCode arg1, arg2 %}
|
||||
eleventyConfig.addShortcode('cdnUrl', location => {
|
||||
|
||||
@@ -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';
|
||||
@@ -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', '');
|
||||
}
|
||||
@@ -10,7 +10,6 @@
|
||||
<script type="module" src="/assets/scripts/search.js"></script>
|
||||
<script type="module" src="/assets/scripts/outline.js"></script>
|
||||
{% if hasSidebar %}<script type="module" src="/assets/scripts/sidebar.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>
|
||||
|
||||
{# Docs styles #}
|
||||
@@ -94,7 +93,6 @@
|
||||
<div id="flashes">{% server "flashes" %}</div>
|
||||
|
||||
{% block header %}
|
||||
{% include 'breadcrumbs.njk' %}
|
||||
<h1 class="title">{{ title }}</h1>
|
||||
{% 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-icon class="only-light" slot="prefix" name="sun" variant="regular"></wa-icon>
|
||||
<wa-icon class="only-dark" slot="prefix" name="moon" variant="regular"></wa-icon>
|
||||
|
||||
@@ -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>
|
||||
@@ -24,9 +24,6 @@
|
||||
<link rel="stylesheet" href="/assets/styles/hydration-errors.css">
|
||||
<link rel="preconnect" href="https://cdn.jsdelivr.net">
|
||||
|
||||
{# Internal components #}
|
||||
<script type="module" src="/assets/components/scoped.js"></script>
|
||||
|
||||
{# Web Awesome #}
|
||||
<script type="module" src="/dist/webawesome.loader.js"></script>
|
||||
|
||||
@@ -45,22 +42,7 @@
|
||||
}
|
||||
</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/themes/default.css" render="blocking" fetchpriority="high" />
|
||||
|
||||
<link rel="stylesheet" href="/dist/styles/webawesome.css" />
|
||||
|
||||
|
||||
@@ -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 +1,6 @@
|
||||
{# Preset theme selector #}
|
||||
<wa-select appearance="filled" size="small" value="default" pill class="preset-theme-selector">
|
||||
<wa-icon slot="prefix" name="paintbrush" variant="regular"></wa-icon>
|
||||
{% for theme in collections.theme | sort %}
|
||||
<wa-option value="{{ theme.page.fileSlug }}">
|
||||
{{ theme.data.title }}
|
||||
</wa-option>
|
||||
{% endfor %}
|
||||
<wa-option value="default">Default</wa-option>
|
||||
<wa-option value="shoelace">Shoelace</wa-option>
|
||||
<wa-option value="awesome">Awesome</wa-option>
|
||||
</wa-select>
|
||||
|
||||
@@ -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>
|
||||
@@ -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="prefix" name="shopping-bag"></wa-icon>
|
||||
Checkout
|
||||
</wa-button>
|
||||
</div>
|
||||
</wa-card>
|
||||
<wa-card>
|
||||
<wa-avatar shape="rounded" style="--size: 1.9lh; float: left; margin-right: var(--wa-space-m);">
|
||||
<wa-icon slot="icon" name="hat-wizard" 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="prefix" name="envelope" variant="regular"></wa-icon>
|
||||
</wa-input>
|
||||
<wa-input tabindex="-1" label="Password" type="password">
|
||||
<wa-icon slot="prefix" name="lock" variant="regular"></wa-icon>
|
||||
</wa-input>
|
||||
<wa-button tabindex="-1" variant="brand">Sign In</wa-button>
|
||||
<a href="#" tabindex="-1" class="wa-body-s">I forgot my password</a>
|
||||
</div>
|
||||
</wa-card>
|
||||
<wa-card>
|
||||
<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="prefix" name="at"></wa-icon>
|
||||
Email
|
||||
</wa-button>
|
||||
<wa-button appearance="outlined" tabindex="-1">
|
||||
<wa-icon slot="prefix" name="phone"></wa-icon>
|
||||
Phone
|
||||
</wa-button>
|
||||
</div>
|
||||
</wa-card>
|
||||
<wa-card>
|
||||
<div class="wa-flank:end">
|
||||
<a href="" class="wa-flank wa-link-plain" tabindex="-1">
|
||||
<wa-avatar shape="rounded" style="--background-color: var(--wa-color-yellow-90); --text-color: var(--wa-color-yellow-50)">
|
||||
<wa-icon slot="icon" name="egg-fried"></wa-icon>
|
||||
</wa-avatar>
|
||||
<div class="wa-gap-2xs wa-stack">
|
||||
<span class="wa-heading-s">Second Breakfast</span>
|
||||
<span class="wa-caption-m">19 Items</span>
|
||||
</div>
|
||||
</a>
|
||||
<wa-dropdown>
|
||||
<wa-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>
|
||||
@@ -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,4 +1,4 @@
|
||||
{% extends '../_layouts/block.njk' %}
|
||||
{% extends '../_layouts/docs.njk' %}
|
||||
{% set component = components[page.fileSlug] %}
|
||||
|
||||
{# Component API #}
|
||||
|
||||
@@ -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="prefix" 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="prefix" 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="suffix" 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="prefix" variant="regular"></wa-icon>
|
||||
<wa-option v-for="(palette, paletteId) in palettes" :label="palette.title" :value="paletteId === baseTheme.palette ? '' : paletteId">
|
||||
<palette-card :palette="paletteId" size="small">
|
||||
<template #extra>
|
||||
<wa-badge v-if="paletteId === baseTheme.palette" variant="neutral" appearance="outlined">Theme default</wa-badge>
|
||||
</template>
|
||||
</palette-card>
|
||||
</wa-option>
|
||||
</wa-select>
|
||||
|
||||
<color-select :model-value="computed.brand" @update:model-value="value => theme.brand = value" label="Brand color"
|
||||
:values="hues"></color-select>
|
||||
|
||||
<wa-select name="colors" class="theme-colors-select" label="Color contrast from…" value="" with-clear v-model="theme.colors">
|
||||
<wa-icon name="palette" slot="prefix" variant="regular"></wa-icon>
|
||||
<template v-for="(themeMeta, themeId) in themes">
|
||||
<wa-option v-if="themeId !== 'custom'" :label="themeMeta.title" :value="themeId === computed.colors ? '' : themeId">
|
||||
<theme-card :theme="themeId" type="colors" :rest="{base: computed.base, palette: computed.palette, brand: computed.brand}" size="small">
|
||||
<template #extra>
|
||||
<wa-badge v-if="themeId === theme.base" variant="neutral" appearance="outlined">This theme</wa-badge>
|
||||
</template>
|
||||
</theme-card>
|
||||
</wa-option>
|
||||
</template>
|
||||
</wa-select>
|
||||
|
||||
<wa-select name="typography" label="Typography from…" with-clear v-model="theme.typography">
|
||||
<wa-icon name="font-case" slot="prefix"></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);
|
||||
}
|
||||
@@ -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="prefix" 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="prefix" 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="prefix" 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>
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
@@ -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,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();
|
||||
}
|
||||
@@ -7,33 +7,27 @@ if (!initialPageLoadComplete) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* A wrapper around `document.startViewTransition()` that falls back to no transitions in unsupportive browsers.
|
||||
*/
|
||||
export function domChange(fn, { behavior = 'smooth', ignoreInitialLoad = true } = {}) {
|
||||
export function doViewTransition(callback, { 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);
|
||||
callback(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (canUseViewTransitions && behavior === 'smooth') {
|
||||
const transition = document.startViewTransition(() => {
|
||||
fn(true);
|
||||
callback(true);
|
||||
// Wait a brief delay before finishing the transition to prevent jumpiness
|
||||
return new Promise(resolve => setTimeout(resolve, 200));
|
||||
});
|
||||
return transition;
|
||||
} else {
|
||||
fn(false);
|
||||
callback(false);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default domChange;
|
||||
@@ -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();
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Animated Image
|
||||
description: A component for displaying animated GIFs and WEBPs that play and pause on interaction.
|
||||
tags: [imagery, niche]
|
||||
icon: animated-image
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Animation
|
||||
description: Animate elements declaratively with nearly 100 baked-in presets, or roll your own with custom keyframes.
|
||||
tags: [helpers, primitives]
|
||||
icon: animation
|
||||
---
|
||||
|
||||
@@ -45,8 +44,7 @@ This example demonstrates all of the baked-in animations and easings. Animations
|
||||
<div class="controls">
|
||||
<wa-select label="Animation" value="bounce"></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>
|
||||
<wa-input label="Playback Rate" type="number" min="0" max="2" step=".25" value="1"> </wa-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -64,7 +62,7 @@ This example demonstrates all of the baked-in animations and easings. Animations
|
||||
animations.map(name => {
|
||||
const option = Object.assign(document.createElement('wa-option'), {
|
||||
textContent: name,
|
||||
value: name
|
||||
value: name,
|
||||
});
|
||||
animationName.appendChild(option);
|
||||
});
|
||||
@@ -72,7 +70,7 @@ This example demonstrates all of the baked-in animations and easings. Animations
|
||||
easings.map(name => {
|
||||
const option = Object.assign(document.createElement('wa-option'), {
|
||||
textContent: name,
|
||||
value: name
|
||||
value: name,
|
||||
});
|
||||
easingName.appendChild(option);
|
||||
});
|
||||
@@ -156,15 +154,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)',
|
||||
fillMode: 'both',
|
||||
transformOrigin: 'center center',
|
||||
transform: 'rotate(0)'
|
||||
transform: 'rotate(0)',
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
easing: 'cubic-bezier(0.250, 0.460, 0.450, 0.940)',
|
||||
fillMode: 'both',
|
||||
transformOrigin: 'center center',
|
||||
transform: 'rotate(90deg)'
|
||||
}
|
||||
transform: 'rotate(90deg)',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
@@ -197,3 +195,4 @@ Animations won't play until you apply the `play` attribute. You can omit it init
|
||||
animation.play = true;
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Avatar
|
||||
description: Avatars are used to represent a person or object.
|
||||
tags: [imagery, apps, content]
|
||||
icon: avatar
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Badge
|
||||
description: Badges are used to draw attention and display statuses or counts.
|
||||
tags: [feedback, content]
|
||||
icon: badge
|
||||
---
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
---
|
||||
title: Breadcrumb Item
|
||||
description: Breadcrumb Items are used inside breadcrumbs to represent different links.
|
||||
tags: component
|
||||
parent: breadcrumb
|
||||
---
|
||||
|
||||
```html {.example}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Breadcrumb
|
||||
description: Breadcrumbs provide a group of links so users can easily navigate a website's hierarchy.
|
||||
tags: [navigation]
|
||||
icon: breadcrumb
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Button Group
|
||||
description: Button groups can be used to group related buttons into sections.
|
||||
tags: [actions, forms, apps]
|
||||
icon: button-group
|
||||
---
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
---
|
||||
title: Button
|
||||
description: Buttons represent actions that are available to the user.
|
||||
tags: [actions, forms]
|
||||
native: button
|
||||
icon: button
|
||||
---
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
---
|
||||
title: Callout
|
||||
description: Callouts are used to display important messages inline.
|
||||
tags: [feedback, content]
|
||||
icon: callout
|
||||
native: callout
|
||||
---
|
||||
|
||||
```html {.example}
|
||||
@@ -135,5 +133,6 @@ Icons are optional. Simply omit the `icon` slot if you don't want them.
|
||||
### Styling
|
||||
|
||||
You can customize the callout's appearance mostly by setting regular CSS properties:
|
||||
|
||||
- `background`, `border`, `border-radius`, `color`, `padding`, `margin`, etc. work as expected
|
||||
- `gap` sets the space between the icon and the content
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Card
|
||||
description: Cards can be used to group related subjects in a container.
|
||||
tags: [organization, layout]
|
||||
icon: card
|
||||
---
|
||||
|
||||
@@ -115,7 +114,7 @@ If using SSR, you need to also use the `with-media` attribute to add a media sec
|
||||
</wa-card>
|
||||
<wa-card class="card-media">
|
||||
<video slot="media" controls>
|
||||
<source src="https://uploads.webawesome.com/dog-with-glasses.mp4">
|
||||
<source src="https://uploads.webawesome.com/dog-with-glasses.mp4" />
|
||||
<p>Your browser doesn't support HTML video</p>
|
||||
</video>
|
||||
This is a kitten, but not just any kitten. This kitten likes walking along pallets.
|
||||
@@ -135,25 +134,25 @@ Use the `appearance` attribute to change the card's visual appearance.
|
||||
|
||||
```html {.example}
|
||||
<div class="wa-grid">
|
||||
<wa-card>
|
||||
<img
|
||||
slot="media"
|
||||
src="https://images.unsplash.com/photo-1559209172-0ff8f6d49ff7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=80"
|
||||
alt="A kitten sits patiently between a terracotta pot and decorative grasses."
|
||||
/>
|
||||
<div slot="header">Outlined (default)</div>
|
||||
Card content.
|
||||
</wa-card>
|
||||
{% for appearance in ['outlined filled', 'outlined accent', 'plain', 'filled', 'accent'] -%}
|
||||
<wa-card appearance="{{ appearance }}">
|
||||
<img
|
||||
slot="media"
|
||||
src="https://images.unsplash.com/photo-1559209172-0ff8f6d49ff7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=80"
|
||||
alt="A kitten sits patiently between a terracotta pot and decorative grasses."
|
||||
/>
|
||||
<div slot="header">{{ appearance | capitalize }}</div>
|
||||
Card content.
|
||||
</wa-card>
|
||||
{%- endfor %}
|
||||
<wa-card>
|
||||
<img
|
||||
slot="media"
|
||||
src="https://images.unsplash.com/photo-1559209172-0ff8f6d49ff7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=80"
|
||||
alt="A kitten sits patiently between a terracotta pot and decorative grasses."
|
||||
/>
|
||||
<div slot="header">Outlined (default)</div>
|
||||
Card content.
|
||||
</wa-card>
|
||||
{% for appearance in ['outlined filled', 'outlined accent', 'plain', 'filled', 'accent'] -%}
|
||||
<wa-card appearance="{{ appearance }}">
|
||||
<img
|
||||
slot="media"
|
||||
src="https://images.unsplash.com/photo-1559209172-0ff8f6d49ff7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=500&q=80"
|
||||
alt="A kitten sits patiently between a terracotta pot and decorative grasses."
|
||||
/>
|
||||
<div slot="header">{{ appearance | capitalize }}</div>
|
||||
Card content.
|
||||
</wa-card>
|
||||
{%- endfor %}
|
||||
</div>
|
||||
```
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
---
|
||||
title: Carousel Item
|
||||
description: A carousel item represent a slide within a carousel.
|
||||
tags: component
|
||||
parent: carousel
|
||||
icon: carousel-item
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Carousel
|
||||
description: Carousels display an arbitrary number of content slides along a horizontal or vertical axis.
|
||||
tags: [imagery, disclosure]
|
||||
icon: carousel
|
||||
---
|
||||
|
||||
@@ -536,7 +535,11 @@ The carousel has a robust API that makes it possible to extend and customize. Th
|
||||
|
||||
<div class="thumbnails">
|
||||
<div class="scroller">
|
||||
<img alt="Thumbnail by 1" class="image active" src="https://images.unsplash.com/photo-1426604966848-d7adac402bff?q=10" />
|
||||
<img
|
||||
alt="Thumbnail by 1"
|
||||
class="image active"
|
||||
src="https://images.unsplash.com/photo-1426604966848-d7adac402bff?q=10"
|
||||
/>
|
||||
<img alt="Thumbnail by 2" class="image" src="https://images.unsplash.com/photo-1473448912268-2022ce9509d8?q=10" />
|
||||
<img alt="Thumbnail by 3" class="image" src="https://images.unsplash.com/photo-1499002238440-d264edd596ec?q=10" />
|
||||
<img alt="Thumbnail by 4" class="image" src="https://images.unsplash.com/photo-1475113548554-5a36f1f523d6?q=10" />
|
||||
@@ -606,7 +609,7 @@ The carousel has a robust API that makes it possible to extend and customize. Th
|
||||
thumb.classList.toggle('active', i === slideIndex);
|
||||
if (i === slideIndex) {
|
||||
thumb.scrollIntoView({
|
||||
block: 'nearest'
|
||||
block: 'nearest',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
---
|
||||
title: Checkbox
|
||||
description: Checkboxes allow the user to toggle an option on or off.
|
||||
tags: [inputs, forms]
|
||||
native: checkbox
|
||||
icon: checkbox
|
||||
---
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
---
|
||||
title: Color Picker
|
||||
description: Color pickers allow the user to select a color.
|
||||
tags: [inputs, forms]
|
||||
native: input
|
||||
icon: color-picker
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Comparison
|
||||
description: Compare visual differences between similar content with a sliding panel.
|
||||
tags: [imagery, niche]
|
||||
icon: comparison
|
||||
---
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"layout": "component.njk",
|
||||
"tags": ["components"],
|
||||
"eleventyComputed": {
|
||||
"component": "{{ components[page.fileSlug] }}",
|
||||
"description": "{{ components[page.fileSlug].summary }}",
|
||||
"status": "{{ components[page.fileSlug].status }}",
|
||||
"since": "{{ components[page.fileSlug].since }}",
|
||||
"element": "<{{ components[page.fileSlug].tagName }}>"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Copy Button
|
||||
description: Copies data to the clipboard when the user clicks the button.
|
||||
tags: [actions, apps]
|
||||
icon: copy-button
|
||||
---
|
||||
|
||||
@@ -53,13 +52,13 @@ To copy data from an attribute, use `from="id[attr]"` where `id` is the id of th
|
||||
<span id="my-phone">+1 (234) 456-7890</span>
|
||||
<wa-copy-button from="my-phone"></wa-copy-button>
|
||||
|
||||
<br><br>
|
||||
<br /><br />
|
||||
|
||||
<!-- Copies the input's "value" property -->
|
||||
<wa-input id="my-input" type="text" value="User input" style="display: inline-block; max-width: 300px;"></wa-input>
|
||||
<wa-copy-button from="my-input.value"></wa-copy-button>
|
||||
|
||||
<br><br>
|
||||
<br /><br />
|
||||
|
||||
<!-- Copies the link's "href" attribute -->
|
||||
<a id="my-link" href="https://shoelace.style/">Web Awesome Website</a>
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
---
|
||||
title: Details
|
||||
description: Details show a brief summary and expand to show additional content.
|
||||
tags: [organization, apps, content, disclosure]
|
||||
icon: details
|
||||
native: details
|
||||
---
|
||||
|
||||
<!-- cspell:dictionaries lorem-ipsum -->
|
||||
@@ -83,22 +80,22 @@ Use the `appearance` attribute to change the element’s visual appearance.
|
||||
|
||||
```html {.example}
|
||||
<div class="wa-stack">
|
||||
<wa-details summary="Outlined (default)">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</wa-details>
|
||||
<wa-details summary="Filled" appearance="filled">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</wa-details>
|
||||
<wa-details summary="Filled + Outlined" appearance="filled outlined">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</wa-details>
|
||||
<wa-details summary="Plain" appearance="plain">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</wa-details>
|
||||
<wa-details summary="Outlined (default)">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</wa-details>
|
||||
<wa-details summary="Filled" appearance="filled">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</wa-details>
|
||||
<wa-details summary="Filled + Outlined" appearance="filled outlined">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</wa-details>
|
||||
<wa-details summary="Plain" appearance="plain">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</wa-details>
|
||||
</div>
|
||||
```
|
||||
|
||||
@@ -109,18 +106,18 @@ Use the `name` attribute to create accordion-like behavior where only one detail
|
||||
```html {.example}
|
||||
<div class="wa-stack">
|
||||
<wa-details name="group-1" summary="Section 1" open>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</wa-details>
|
||||
|
||||
<wa-details name="group-1" summary="Section 2">
|
||||
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam,
|
||||
eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.
|
||||
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam,
|
||||
eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.
|
||||
</wa-details>
|
||||
|
||||
<wa-details name="group-1" summary="Section 3">
|
||||
At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque
|
||||
corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident.
|
||||
At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque
|
||||
corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident.
|
||||
</wa-details>
|
||||
</div>
|
||||
```
|
||||
```
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
---
|
||||
title: Dialog
|
||||
description: 'Dialogs, sometimes called "modals", appear above the page and require the user''s immediate attention.'
|
||||
tags: [organization, apps, disclosure]
|
||||
icon: dialog
|
||||
native: dialog
|
||||
keywords: modal
|
||||
---
|
||||
|
||||
<!-- cspell:dictionaries lorem-ipsum -->
|
||||
@@ -21,7 +18,7 @@ keywords: modal
|
||||
const dialog = document.querySelector('#dialog-overview');
|
||||
const openButton = dialog.nextElementSibling;
|
||||
|
||||
openButton.addEventListener('click', () => dialog.open = true);
|
||||
openButton.addEventListener('click', () => (dialog.open = true));
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -43,7 +40,7 @@ Headers are enabled by default. To render a dialog without a header, add the `wi
|
||||
const dialog = document.querySelector('.dialog-without-header');
|
||||
const openButton = dialog.nextElementSibling;
|
||||
|
||||
openButton.addEventListener('click', () => dialog.open = true);
|
||||
openButton.addEventListener('click', () => (dialog.open = true));
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -63,7 +60,7 @@ Footers can be used to display titles and more. Use the `footer` slot to add a f
|
||||
const dialog = document.querySelector('.dialog-footer');
|
||||
const openButton = dialog.nextElementSibling;
|
||||
|
||||
openButton.addEventListener('click', () => dialog.open = true);
|
||||
openButton.addEventListener('click', () => (dialog.open = true));
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -107,7 +104,7 @@ Just use the `--width` custom property to set the dialog's width.
|
||||
const dialog = document.querySelector('.dialog-width');
|
||||
const openButton = dialog.nextElementSibling;
|
||||
|
||||
openButton.addEventListener('click', () => dialog.open = true);
|
||||
openButton.addEventListener('click', () => (dialog.open = true));
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -129,7 +126,7 @@ By design, a dialog's height will never exceed that of the viewport. As such, di
|
||||
const dialog = document.querySelector('.dialog-scrolling');
|
||||
const openButton = dialog.nextElementSibling;
|
||||
|
||||
openButton.addEventListener('click', () => dialog.open = true);
|
||||
openButton.addEventListener('click', () => (dialog.open = true));
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -153,7 +150,7 @@ The header shows a functional close button by default. You can use the `header-a
|
||||
const openButton = dialog.nextElementSibling;
|
||||
const newWindowButton = dialog.querySelector('.new-window');
|
||||
|
||||
openButton.addEventListener('click', () => dialog.open = true);
|
||||
openButton.addEventListener('click', () => (dialog.open = true));
|
||||
newWindowButton.addEventListener('click', () => window.open(location.href));
|
||||
</script>
|
||||
```
|
||||
@@ -174,7 +171,7 @@ If you want the dialog to close when the user clicks on the overlay, add the `li
|
||||
const dialog = document.querySelector('.dialog-light-dismiss');
|
||||
const openButton = dialog.nextElementSibling;
|
||||
|
||||
openButton.addEventListener('click', () => dialog.open = true);
|
||||
openButton.addEventListener('click', () => (dialog.open = true));
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -199,7 +196,7 @@ You can use `event.detail.source` to determine which element triggered the reque
|
||||
const openButton = dialog.nextElementSibling;
|
||||
const closeButton = dialog.querySelector('wa-button[slot="footer"]');
|
||||
|
||||
openButton.addEventListener('click', () => dialog.open = true);
|
||||
openButton.addEventListener('click', () => (dialog.open = true));
|
||||
|
||||
// Prevent the dialog from closing unless the close button was clicked
|
||||
dialog.addEventListener('wa-hide', event => {
|
||||
@@ -227,6 +224,6 @@ To give focus to a specific element when the dialog opens, use the `autofocus` a
|
||||
const input = dialog.querySelector('wa-input');
|
||||
const openButton = dialog.nextElementSibling;
|
||||
|
||||
openButton.addEventListener('click', () => dialog.open = true);
|
||||
openButton.addEventListener('click', () => (dialog.open = true));
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Divider
|
||||
description: Dividers are used to visually separate or group elements.
|
||||
tags: [organization, content, forms]
|
||||
icon: divider
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Drawer
|
||||
description: Drawers slide in from a container to expose additional options and information.
|
||||
tags: [organization, disclosure]
|
||||
icon: drawer
|
||||
---
|
||||
|
||||
@@ -19,7 +18,7 @@ icon: drawer
|
||||
const drawer = document.querySelector('#drawer-overview');
|
||||
const openButton = drawer.nextElementSibling;
|
||||
|
||||
openButton.addEventListener('click', () => drawer.open = true);
|
||||
openButton.addEventListener('click', () => (drawer.open = true));
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -41,7 +40,7 @@ Headers are enabled by default. To render a drawer without a header, add the `wi
|
||||
const drawer = document.querySelector('.drawer-without-header');
|
||||
const openButton = drawer.nextElementSibling;
|
||||
|
||||
openButton.addEventListener('click', () => drawer.open = true);
|
||||
openButton.addEventListener('click', () => (drawer.open = true));
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -61,7 +60,7 @@ Footers can be used to display titles and more. Use the `footer` slot to add a f
|
||||
const drawer = document.querySelector('.drawer-footer');
|
||||
const openButton = drawer.nextElementSibling;
|
||||
|
||||
openButton.addEventListener('click', () => drawer.open = true);
|
||||
openButton.addEventListener('click', () => (drawer.open = true));
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -105,7 +104,7 @@ By default, drawers slide in from the end. To make the drawer slide in from the
|
||||
const drawer = document.querySelector('.drawer-placement-start');
|
||||
const openButton = drawer.nextElementSibling;
|
||||
|
||||
openButton.addEventListener('click', () => drawer.open = true);
|
||||
openButton.addEventListener('click', () => (drawer.open = true));
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -125,7 +124,7 @@ To make the drawer slide in from the top, set the `placement` attribute to `top`
|
||||
const drawer = document.querySelector('.drawer-placement-top');
|
||||
const openButton = drawer.nextElementSibling;
|
||||
|
||||
openButton.addEventListener('click', () => drawer.open = true);
|
||||
openButton.addEventListener('click', () => (drawer.open = true));
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -145,7 +144,7 @@ To make the drawer slide in from the bottom, set the `placement` attribute to `b
|
||||
const drawer = document.querySelector('.drawer-placement-bottom');
|
||||
const openButton = drawer.nextElementSibling;
|
||||
|
||||
openButton.addEventListener('click', () => drawer.open = true);
|
||||
openButton.addEventListener('click', () => (drawer.open = true));
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -165,7 +164,7 @@ Use the `--size` custom property to set the drawer's size. This will be applied
|
||||
const drawer = document.querySelector('.drawer-custom-size');
|
||||
const openButton = drawer.nextElementSibling;
|
||||
|
||||
openButton.addEventListener('click', () => drawer.open = true);
|
||||
openButton.addEventListener('click', () => (drawer.open = true));
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -187,7 +186,7 @@ By design, a drawer's height will never exceed 100% of its container. As such, d
|
||||
const drawer = document.querySelector('.drawer-scrolling');
|
||||
const openButton = drawer.nextElementSibling;
|
||||
|
||||
openButton.addEventListener('click', () => drawer.open = true);
|
||||
openButton.addEventListener('click', () => (drawer.open = true));
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -211,7 +210,7 @@ The header shows a functional close button by default. You can use the `header-a
|
||||
const openButton = drawer.nextElementSibling;
|
||||
const newWindowButton = drawer.querySelector('.new-window');
|
||||
|
||||
openButton.addEventListener('click', () => drawer.open = true);
|
||||
openButton.addEventListener('click', () => (drawer.open = true));
|
||||
newWindowButton.addEventListener('click', () => window.open(location.href));
|
||||
</script>
|
||||
```
|
||||
@@ -232,7 +231,7 @@ If you want the drawer to close when the user clicks on the overlay, add the `li
|
||||
const drawer = document.querySelector('.drawer-light-dismiss');
|
||||
const openButton = drawer.nextElementSibling;
|
||||
|
||||
openButton.addEventListener('click', () => drawer.open = true);
|
||||
openButton.addEventListener('click', () => (drawer.open = true));
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -257,7 +256,7 @@ You can use `event.detail.source` to determine what triggered the request to clo
|
||||
const openButton = drawer.nextElementSibling;
|
||||
const closeButton = drawer.querySelector('wa-button[slot="footer"]');
|
||||
|
||||
openButton.addEventListener('click', () => drawer.open = true);
|
||||
openButton.addEventListener('click', () => (drawer.open = true));
|
||||
|
||||
// Prevent the drawer from closing unless the close button is clicked
|
||||
drawer.addEventListener('wa-hide', event => {
|
||||
@@ -285,6 +284,6 @@ To give focus to a specific element when the drawer opens, use the `autofocus` a
|
||||
const input = drawer.querySelector('wa-input');
|
||||
const openButton = drawer.nextElementSibling;
|
||||
|
||||
openButton.addEventListener('click', () => drawer.open = true);
|
||||
openButton.addEventListener('click', () => (drawer.open = true));
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Dropdown
|
||||
description: 'Dropdowns expose additional content that "drops down" in a panel.'
|
||||
tags: [actions, apps]
|
||||
icon: dropdown
|
||||
---
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user