mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 04:09:12 +00:00
Themer 2nd slice: Look & Feel (#920)
* Exclude Create link from sidebar, for reals this time * Fix bug * Very rough prototype of look & feel * a11y * Clean up data files * Automatically generate theme metadata * Read look & feel params straight from theme * First stab at dimensionality icons * Fix rounding 0 bug * Add border width slider * [Image-comparer] Expose wrapper as part * [Comparer] `pointer-events: none` while dragging * Dark mode slider * Adjust increments and ranges for look + feel sliders * Fix preview * Fix bug where dark mode was not inverted * Ability to select panel from URL * Create mixin for Vue form controls and use it in `<swatch-select>` * Prototype of slider min/max icon buttons * Nx tooltip * Icons * Prevent failed request * info-tip: Support passing text as prop * Clearable * [Brutalist] Match `--wa-shadow-offset-x-scale` to `--wa-shadow-offset-y-scale` * Add 'Blocky' dimension (derived from Awesome theme) * Only show Reset button when `clearable` is set * Remove `clearable` from Look & Feel sliders * Add tooltips to min/max buttons * Remove superfluous `aria-label` * Do not assume that all hyphens in URLs mean nesting, make it explicit * Formatting * Fix bug where styles were not applied on page load * Update Subtle dimension to maximize compatibility * `<wa-scoped>`: Do not allow non-template children * Workaround for card not updating * Update Glossy dimension to maximize compatibility * Sync scrolling between regular and inverted preview * Fix bug * Make changing the base theme reset customizations * Fix palette page * Remove cancel button from editable text * Don't error in theme pages * Update Playful dimension to maximize compatibility * Rename 'Look and Feel' to 'Elements' for better parallel structure * Hide dimensionality controls * Make back icon motion more subtle * Expand spacing slider bounds * Add `tabindex="-1"` where missing in theme showcase * Remove extraneous gap from theme headers * fix edit button bug * rename comparer => comparison; fix aria-controls * Always save theme name on blur * Add changelog for themer and new patterns category --------- Co-authored-by: lindsaym-fa <dev@lindsaym.design> Co-authored-by: Cory LaViska <cory@abeautifulsite.net>
This commit is contained in:
61
docs/_data/themes.js
Normal file
61
docs/_data/themes.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
// import { inlined } from '../../dist/components/icon/library.wa.js';
|
||||
const __dirname = path.resolve();
|
||||
const THEME_DIR = path.join(__dirname, 'dist/styles/themes/');
|
||||
|
||||
const themeFiles = fs.readdirSync(THEME_DIR).filter(file => file.endsWith('.css') && !file.endsWith('base.css'));
|
||||
|
||||
const declarationRegex = /^\s*--wa-(?<property>[a-z-]+)?:\s*(?<value>.+?)\s*(\/\*.+?\*\/)?\s*;$/gm;
|
||||
const importRegex = /^\s*@import\s+url\(['"](?<path>.+?)['"]\);$/gm;
|
||||
const themes = {};
|
||||
|
||||
for (const file of themeFiles) {
|
||||
const id = file.replace('.css', '');
|
||||
const { imports, declarations } = readCSSFile(file);
|
||||
let theme = { palette: 'default', declarations, imports };
|
||||
|
||||
for (const url of imports) {
|
||||
if (url.endsWith('/color.css')) {
|
||||
// Color settings
|
||||
const color = readCSSFile(url);
|
||||
for (const colorUrl of color.imports) {
|
||||
if (colorUrl.startsWith('../../color/')) {
|
||||
// Color palette
|
||||
theme.palette = getFileSlug(colorUrl);
|
||||
} else if (colorUrl.startsWith('../../brand/')) {
|
||||
// Brand color
|
||||
theme.brand = getFileSlug(colorUrl);
|
||||
}
|
||||
}
|
||||
} else if (url.endsWith('/dimension.css')) {
|
||||
theme.dimension = true;
|
||||
}
|
||||
}
|
||||
|
||||
let icon = {};
|
||||
icon.family = theme.declarations['icon-family'] ?? theme.default?.iconFamily ?? 'classic';
|
||||
icon.variant = theme.declarations['icon-variant'] ?? theme.default?.iconVariant ?? 'solid';
|
||||
theme.icons = icon;
|
||||
|
||||
theme.rounding = Number(theme.declarations['border-radius-scale'] ?? theme.default?.rounding ?? 1);
|
||||
theme.spacing = Number(theme.declarations['space-scale'] ?? theme.default?.spacing ?? 1);
|
||||
theme.borderWidth = Number(theme.declarations['border-width-scale'] ?? theme.default?.borderWidth ?? 1);
|
||||
|
||||
themes[id] = theme;
|
||||
}
|
||||
|
||||
export default themes;
|
||||
|
||||
function readCSSFile(url) {
|
||||
const contents = fs.readFileSync(path.join(THEME_DIR, url), 'utf8');
|
||||
const imports = [...contents.matchAll(importRegex)].map(match => match.groups.path);
|
||||
const declarations = Object.fromEntries(
|
||||
[...contents.matchAll(declarationRegex)].map(match => [match.groups.property, match.groups.value]),
|
||||
);
|
||||
return { imports, declarations };
|
||||
}
|
||||
|
||||
function getFileSlug(url) {
|
||||
return url.split('/').pop().replace('.css', '');
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
{% if page -%}
|
||||
{% if page and not page.data.unlisted -%}
|
||||
<li>
|
||||
<a href="{{ page.url }}">{{ page.data.title }}</a>
|
||||
{% if page.data.status == 'experimental' %}<wa-icon name="flask"></wa-icon>{% endif %}
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
@@ -118,7 +118,7 @@
|
||||
<div class="wa-stack">
|
||||
<h3 class="wa-heading-m">Chalmun's Spaceport Cantina</h3>
|
||||
<div class="wa-cluster wa-gap-xs">
|
||||
<wa-rating value="4.6" read-only></wa-rating>
|
||||
<wa-rating value="4.6" readonly tabindex="-1"></wa-rating>
|
||||
<strong>4.6</strong>
|
||||
<span>(419 reviews)</span>
|
||||
</div>
|
||||
@@ -144,7 +144,7 @@
|
||||
<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"></wa-switch>
|
||||
<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>
|
||||
@@ -175,7 +175,7 @@
|
||||
</dl>
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<a href="" class="wa-cluster wa-gap-2xs">
|
||||
<a href="" class="wa-cluster wa-gap-2xs" tabindex="-1">
|
||||
<span>Download Receipt</span>
|
||||
<wa-icon name="arrow-right"></wa-icon>
|
||||
</a>
|
||||
@@ -195,7 +195,7 @@
|
||||
<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">Get this Plan</wa-button>
|
||||
<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>
|
||||
@@ -231,11 +231,11 @@
|
||||
<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">
|
||||
<wa-button appearance="outlined" tabindex="-1">
|
||||
<wa-icon slot="prefix" name="at"></wa-icon>
|
||||
Email
|
||||
</wa-button>
|
||||
<wa-button appearance="outlined">
|
||||
<wa-button appearance="outlined" tabindex="-1">
|
||||
<wa-icon slot="prefix" name="phone"></wa-icon>
|
||||
Phone
|
||||
</wa-button>
|
||||
@@ -243,7 +243,7 @@
|
||||
</wa-card>
|
||||
<wa-card>
|
||||
<div class="wa-flank:end">
|
||||
<a href="" class="wa-flank wa-link-plain">
|
||||
<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>
|
||||
@@ -253,7 +253,7 @@
|
||||
</div>
|
||||
</a>
|
||||
<wa-dropdown>
|
||||
<wa-icon-button id="more-actions-2" slot="trigger" name="ellipsis-vertical" label="View menu"></wa-icon-button>
|
||||
<wa-icon-button id="more-actions-2" slot="trigger" name="ellipsis-vertical" label="View menu" tabindex="-1"></wa-icon-button>
|
||||
<wa-menu>
|
||||
<wa-menu-item>Copy link</wa-menu-item>
|
||||
<wa-menu-item>Rename</wa-menu-item>
|
||||
@@ -270,7 +270,7 @@
|
||||
<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">
|
||||
<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>
|
||||
@@ -283,7 +283,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="" class="wa-flank wa-align-items-start wa-link-plain">
|
||||
<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>
|
||||
@@ -296,7 +296,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href=""class="wa-flank wa-align-items-start wa-link-plain">
|
||||
<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>
|
||||
@@ -309,7 +309,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="" class="wa-flank wa-align-items-start wa-link-plain">
|
||||
<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>
|
||||
@@ -325,7 +325,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<a href="" class="wa-cluster wa-gap-xs">
|
||||
<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>
|
||||
|
||||
@@ -399,3 +399,12 @@ export function attr(value, name) {
|
||||
|
||||
return safe(ret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format an object as JSON, with formatting & indentation (unlike the default `dump` filter)
|
||||
* @param {*} value
|
||||
* @returns {string}
|
||||
*/
|
||||
export function json(value) {
|
||||
return JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export default class WaScoped extends HTMLElement {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
|
||||
this.observer = new MutationObserver(() => this.render());
|
||||
this.observer = new MutationObserver(records => this.render(records));
|
||||
this.observer.observe(this, { childList: true, subtree: true, characterData: true });
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export default class WaScoped extends HTMLElement {
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
render(records) {
|
||||
this.observer.takeRecords();
|
||||
this.observer.disconnect();
|
||||
|
||||
@@ -33,17 +33,18 @@ export default class WaScoped extends HTMLElement {
|
||||
let nodes = [];
|
||||
|
||||
for (let template of this.childNodes) {
|
||||
// Other solutions we can try if needed: <script type="text/html">, or comment nodes
|
||||
if (template instanceof HTMLTemplateElement) {
|
||||
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);
|
||||
if (!(template instanceof HTMLTemplateElement)) {
|
||||
if (template.nodeType === Node.ELEMENT_NODE) {
|
||||
console.warn('<wa-scoped> can only contain <template> elements');
|
||||
}
|
||||
} else {
|
||||
// Regular child, suck it out of the light DOM
|
||||
nodes.push(template);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ layout: null
|
||||
permalink: '/assets/data/palettes.js'
|
||||
eleventyExcludeFromCollections: true
|
||||
---
|
||||
export default {
|
||||
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 -%}
|
||||
@@ -30,3 +32,26 @@ export default {
|
||||
{%- 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;
|
||||
|
||||
@@ -3,19 +3,24 @@ layout: null
|
||||
permalink: '/assets/data/themes.js'
|
||||
eleventyExcludeFromCollections: true
|
||||
---
|
||||
|
||||
export default {
|
||||
{%- for theme in collections.theme | sort %}
|
||||
{%- if not theme.data.unlisted %}
|
||||
{%- if not theme.data.unlisted and theme.fileSlug !== 'edit' and theme.fileSlug !== 'custom' %}
|
||||
{% set themeId = theme.fileSlug -%}
|
||||
{%- set colors = themes[themeId] -%}
|
||||
{%- set themeMeta = themes[themeId] -%}
|
||||
'{{ themeId }}': {
|
||||
id: '{{ themeId }}',
|
||||
title: '{{ theme.data.title }}',
|
||||
palette: '{{ theme.data.palette }}',
|
||||
brand: '{{ theme.data.brand }}',
|
||||
palette: '{{ themeMeta.palette }}',
|
||||
brand: '{{ themeMeta.brand }}',
|
||||
isPro: {{ theme.data.isPro or 'pro' in theme.data.tags }},
|
||||
fonts: {{ (theme.data.fonts | dump or 'null') | safe }},
|
||||
icons: {{ (theme.data.icons | dump or 'null') | safe }},
|
||||
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 %}
|
||||
|
||||
@@ -41,7 +41,10 @@ export const themeConfig = {
|
||||
},
|
||||
},
|
||||
icon: {
|
||||
library: { cssProperty: '--wa-icon-library', default: 'default' },
|
||||
library: {
|
||||
cssProperty: '--wa-icon-library',
|
||||
default: 'default',
|
||||
},
|
||||
family: {
|
||||
cssProperty: '--wa-icon-family',
|
||||
default(baseTheme) {
|
||||
@@ -55,8 +58,41 @@ export const themeConfig = {
|
||||
},
|
||||
},
|
||||
},
|
||||
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);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { deepEach, deepGet, deepSet } from './util/deep.js';
|
||||
import { camelCase, kebabCase } from './util/string.js';
|
||||
|
||||
export default class Permalink extends URLSearchParams {
|
||||
/** Params changed since last URL I/O */
|
||||
@@ -22,15 +23,16 @@ export default class Permalink extends URLSearchParams {
|
||||
setAll(values, defaults) {
|
||||
deepEach(values, (value, key, parent, path) => {
|
||||
let fullPath = [...path, key];
|
||||
let param = fullPath.join('-');
|
||||
let defaultValue = deepGet(defaults, fullPath);
|
||||
let param = fullPath.map(kebabCase).join('-');
|
||||
|
||||
if (typeof value === 'object') {
|
||||
// We'll handle this when we descend into it
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value || value === defaultValue) {
|
||||
let defaultValue = deepGet(defaults, fullPath);
|
||||
|
||||
if (equals(value, defaultValue)) {
|
||||
// Remove the param from the URL
|
||||
this.delete(param);
|
||||
return;
|
||||
@@ -40,17 +42,36 @@ export default class Permalink extends URLSearchParams {
|
||||
});
|
||||
}
|
||||
|
||||
getAll(...args) {
|
||||
if (args.length > 0) {
|
||||
return super.getAll(...args);
|
||||
}
|
||||
/**
|
||||
* Convert the URL params to a (potentially nested) object.
|
||||
* @param {object} options - Options object.
|
||||
* @param {(key: string, value: string) => string[]} options.getPath - Function to get the path of a param.
|
||||
* @returns {object} The nested object.
|
||||
*/
|
||||
toObject(options = {}) {
|
||||
// Default getPath() assumes hyphens always mean nesting
|
||||
let { ignoreKeys = [], getPath = param => param.split('-') } = options;
|
||||
|
||||
// Get all values as a nested object
|
||||
// Assumes that hyphens always mean nesting
|
||||
|
||||
let obj = {};
|
||||
|
||||
for (let [key, value] of this.entries()) {
|
||||
let path = key.split('-');
|
||||
let path = getPath(key, value);
|
||||
|
||||
if (path === null || ignoreKeys.includes(key)) {
|
||||
// Skip this param
|
||||
continue;
|
||||
}
|
||||
|
||||
// Default to key if `getPath()` returns undefined
|
||||
path ??= key;
|
||||
|
||||
path = Array.isArray(path) ? path : [path];
|
||||
|
||||
// Camel case any remaining hyphens
|
||||
path = path.map(camelCase);
|
||||
|
||||
deepSet(obj, path, value);
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ export function getThemeCode(theme, options = {}) {
|
||||
|
||||
let value = deepGet(theme, [...path, aspect]);
|
||||
|
||||
if (!value) {
|
||||
if (!value && value !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -64,8 +64,8 @@ export function getThemeCode(theme, options = {}) {
|
||||
if (declarations.length > 0) {
|
||||
let cssCode = cssRule(selectors.theme(id), declarations, options);
|
||||
|
||||
let faKitAttribute = ` data-fa-kit-code="${theme.icon.kit}"`;
|
||||
if (theme.icon.kit) {
|
||||
if (theme.icon?.kit) {
|
||||
let faKitAttribute = ` data-fa-kit-code="${theme.icon.kit}"`;
|
||||
options.attributes ??= '';
|
||||
options.attributes += faKitAttribute;
|
||||
cssCode =
|
||||
|
||||
@@ -22,3 +22,21 @@ export function slugify(str) {
|
||||
.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();
|
||||
}
|
||||
|
||||
@@ -141,6 +141,34 @@ wa-card:has(
|
||||
}
|
||||
}
|
||||
|
||||
.theme-icon.theme-dimensionality-icon {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: var(--wa-space-2xs);
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-height: 6.75rem;
|
||||
box-sizing: border-box;
|
||||
|
||||
wa-card {
|
||||
display: block;
|
||||
|
||||
&::part(body) {
|
||||
display: flex;
|
||||
gap: var(--wa-space-xs);
|
||||
}
|
||||
|
||||
wa-input {
|
||||
flex: 4;
|
||||
min-width: 1em;
|
||||
}
|
||||
|
||||
wa-button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fonts-icon {
|
||||
font-family: var(--wa-font-family-body);
|
||||
padding-block: var(--wa-space-s);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
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" />
|
||||
<wa-icon-button name="check" label="Done editing" @click="done"></wa-icon-button>
|
||||
<wa-icon-button name="xmark" label="Cancel" @click="cancel"></wa-icon-button>
|
||||
<input ref="input" class="wa-size-s" :aria-label="label" :value="value" @input="handleInput" @keydown.enter="done" @keydown.esc="cancel" @blur="handleBlur" />
|
||||
<wa-icon-button v-if="blur !== 'done'" name="check" label="Done editing" @click="done"></wa-icon-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="text" ref="wrapper" @focus="edit" @click="edit" tabindex="0">{{ value }}</span>
|
||||
@@ -13,17 +14,22 @@ const template = `
|
||||
`;
|
||||
|
||||
export default {
|
||||
mixins: [inputMixin],
|
||||
props: {
|
||||
modelValue: String,
|
||||
label: {
|
||||
type: String,
|
||||
default: 'Rename',
|
||||
},
|
||||
blur: {
|
||||
type: String,
|
||||
validator(value) {
|
||||
return ['', 'done', 'cancel'].includes(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue', 'submit'],
|
||||
data() {
|
||||
return {
|
||||
value: this.modelValue,
|
||||
previousValue: undefined,
|
||||
isEditing: false,
|
||||
};
|
||||
@@ -69,14 +75,12 @@ export default {
|
||||
this.isEditing = false;
|
||||
this.value = this.previousValue;
|
||||
},
|
||||
handleInput(event) {
|
||||
this.value = event.target.value;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value(newValue) {
|
||||
this.$emit('update:modelValue', newValue);
|
||||
handleBlur(event) {
|
||||
this.done(event);
|
||||
},
|
||||
},
|
||||
template,
|
||||
compilerOptions: {
|
||||
isCustomElement: tag => tag.startsWith('wa-'),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
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';
|
||||
import themes from '/docs/themes/data.js';
|
||||
|
||||
const template = `
|
||||
<page-card class="fonts-card" :info="computedPairing">
|
||||
<template #icon>
|
||||
<wa-scoped slot="header" class="fonts-icon-host" inert>
|
||||
<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">
|
||||
|
||||
@@ -10,3 +10,4 @@ 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';
|
||||
|
||||
@@ -2,7 +2,7 @@ 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"></slot></wa-tooltip>
|
||||
<wa-tooltip :slot :for="id" ref="tooltip"><slot name="content">{{ text }}</slot></wa-tooltip>
|
||||
`;
|
||||
|
||||
let maxUid = 0;
|
||||
@@ -10,6 +10,7 @@ let maxUid = 0;
|
||||
export default {
|
||||
props: {
|
||||
slot: String,
|
||||
text: String,
|
||||
},
|
||||
data() {
|
||||
let uid = ++maxUid;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import palettes from '../../data/palettes.js';
|
||||
import PageCard from './page-card.js';
|
||||
import { hues } from '/assets/data/index.js';
|
||||
import palettes from '/docs/palettes/data.js';
|
||||
|
||||
// TODO import from data.js once available
|
||||
const allHues = [...hues, 'gray'];
|
||||
|
||||
@@ -9,12 +9,6 @@
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
@keyframes back-icon-hover {
|
||||
to {
|
||||
transform: translateX(-0.2em);
|
||||
}
|
||||
}
|
||||
|
||||
.panel {
|
||||
/* Remove the uniform spacing used in wa-details */
|
||||
--spacing: 0;
|
||||
@@ -31,7 +25,7 @@
|
||||
border: none;
|
||||
transition:
|
||||
translate var(--wa-transition-slow) allow-discrete,
|
||||
opacity var(--wa-transition-slow) 25ms allow-discrete;
|
||||
opacity var(--wa-transition-slow) 50ms allow-discrete;
|
||||
/* Ensure horizontal scrollbar isn't visible when translate takes effect */
|
||||
overflow-x: hidden !important;
|
||||
|
||||
@@ -62,11 +56,11 @@
|
||||
vertical-align: -0.15em;
|
||||
margin-inline-end: var(--wa-space-xs);
|
||||
font-size: var(--wa-font-size-m);
|
||||
transition: transform var(--wa-transition-normal);
|
||||
transition: transform var(--wa-transition-normal) var(--wa-transition-easing);
|
||||
}
|
||||
|
||||
&:hover .back-icon {
|
||||
animation: back-icon-hover var(--wa-transition-slow) alternate infinite;
|
||||
transform: translateX(-0.25em);
|
||||
}
|
||||
|
||||
label {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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="modelValue" @input="handleInput">
|
||||
<wa-radio-group :label class="swatch-select" :class="'swatch-shape-' + shape" orientation="horizontal" :value @input="handleInput">
|
||||
<info-tip v-for="value in values">
|
||||
<wa-radio-button :value :label="getLabel(value)" :style="{'--color': getColor(value)}"></wa-radio-button>
|
||||
<template #content>
|
||||
@@ -13,6 +14,7 @@ const template = `
|
||||
`;
|
||||
|
||||
export default {
|
||||
mixins: [inputMixin],
|
||||
props: {
|
||||
modelValue: String,
|
||||
name: String,
|
||||
@@ -35,32 +37,17 @@ export default {
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue', 'input'],
|
||||
data() {
|
||||
return {
|
||||
value: this.modelValue,
|
||||
};
|
||||
},
|
||||
computed: {},
|
||||
|
||||
methods: {
|
||||
capitalize,
|
||||
handleInput(e) {
|
||||
this.value = e.target.value;
|
||||
this.$emit('input', this.value);
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
value() {
|
||||
this.$emit('update:modelValue', this.value);
|
||||
},
|
||||
},
|
||||
|
||||
template,
|
||||
components: {
|
||||
InfoTip,
|
||||
},
|
||||
|
||||
compilerOptions: {
|
||||
isCustomElement: tag => tag.startsWith('wa-'),
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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';
|
||||
import themes from '/docs/themes/data.js';
|
||||
|
||||
const iconTemplates = {
|
||||
colors: `
|
||||
@@ -15,7 +16,13 @@ const iconTemplates = {
|
||||
<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>`,
|
||||
theme: `
|
||||
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">
|
||||
@@ -32,21 +39,22 @@ const iconTemplates = {
|
||||
};
|
||||
|
||||
const template = `
|
||||
<page-card class="theme-card" :class="type + '-card'" :info="themeMeta">
|
||||
<page-card class="theme-card" :class="type + '-card'" :info="themeMeta" :data-theme="theme">
|
||||
<template #icon>
|
||||
<wa-scoped slot="header" class="theme-icon-host" inert>
|
||||
<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'" >
|
||||
<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.theme}
|
||||
${iconTemplates.overall}
|
||||
</div>
|
||||
</template>
|
||||
</wa-scoped>
|
||||
@@ -64,7 +72,7 @@ export default {
|
||||
type: {
|
||||
type: String,
|
||||
validator(value) {
|
||||
return !value || ['colors'].includes(value);
|
||||
return !value || value in iconTemplates;
|
||||
},
|
||||
},
|
||||
rest: Object,
|
||||
@@ -74,15 +82,29 @@ export default {
|
||||
return {};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.iconTemplates = iconTemplates;
|
||||
},
|
||||
|
||||
computed: {
|
||||
themeMeta() {
|
||||
return themes[this.theme] ?? {};
|
||||
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/' });
|
||||
},
|
||||
},
|
||||
|
||||
46
docs/assets/vue/components/ui-slider.css
Normal file
46
docs/assets/vue/components/ui-slider.css
Normal file
@@ -0,0 +1,46 @@
|
||||
.ui-slider {
|
||||
display: grid;
|
||||
grid-template:
|
||||
'label label label'
|
||||
'min slider max';
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: var(--wa-space-2xs);
|
||||
|
||||
wa-slider {
|
||||
display: block;
|
||||
grid-area: slider;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&:has(.ui-slider-min) wa-slider {
|
||||
&::part(label) {
|
||||
margin-inline-start: calc(-1 * (var(--wa-space-s) + 1rem + 2 * var(--wa-border-width-m)));
|
||||
}
|
||||
}
|
||||
|
||||
.clear-button {
|
||||
vertical-align: middle;
|
||||
margin-inline-start: var(--wa-space-xs);
|
||||
font-size: var(--wa-font-size-xs);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-slider-header {
|
||||
grid-area: label;
|
||||
}
|
||||
|
||||
.ui-slider-min,
|
||||
.ui-slider-max {
|
||||
width: min-content;
|
||||
}
|
||||
|
||||
.ui-slider-min {
|
||||
grid-area: min;
|
||||
margin-inline-start: calc(-1 * var(--wa-space-s));
|
||||
}
|
||||
|
||||
.ui-slider-max {
|
||||
grid-area: max;
|
||||
margin-inline-end: calc(-1 * var(--wa-space-s));
|
||||
}
|
||||
86
docs/assets/vue/components/ui-slider.js
Normal file
86
docs/assets/vue/components/ui-slider.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import inputMixin from '../mixins/input.js';
|
||||
import InfoTip from './info-tip.js';
|
||||
|
||||
let maxUid = 0;
|
||||
|
||||
const template = `
|
||||
<div class="ui-slider">
|
||||
<div class="ui-slider-header">
|
||||
<label :for="sliderId">{{ label }}</label>
|
||||
<info-tip v-if="clearable && (value !== defaultValue ?? initialValue)" :text="'Reset to ' + valueFormatter(defaultValue ?? initialValue)">
|
||||
<wa-icon-button @click="value = defaultValue ?? initialValue" class="clear-button" name="circle-xmark" library="system" variant="regular" :label="'Reset to ' + tooltipFormatter(defaultValue ?? initialValue)"></wa-icon-button>
|
||||
</info-tip>
|
||||
</div>
|
||||
<info-tip v-if="$slots.min" :text="'Set to min (' + valueFormatter(min) + ')'">
|
||||
<wa-button class="ui-slider-min" appearance="plain" size="small" @click="value = min"><slot name="min"></slot></wa-button>
|
||||
</info-tip>
|
||||
<wa-slider ref="slider" :id="sliderId" class="ui-slider" :value @input="handleInput"
|
||||
:min="min" :max="max" :step="step">
|
||||
</wa-slider>
|
||||
<info-tip v-if="$slots.max" :text="'Set to max (' + valueFormatter(max) + ')'">
|
||||
<wa-button class="ui-slider-max" appearance="plain" size="small" @click="value = max"><slot name="max"></slot></wa-button>
|
||||
</info-tip>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default {
|
||||
mixins: [inputMixin],
|
||||
props: {
|
||||
label: String,
|
||||
id: String,
|
||||
defaultValue: Number,
|
||||
min: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
default(rawProps) {
|
||||
return (rawProps.max - rawProps.min) / 100;
|
||||
},
|
||||
},
|
||||
format: [Function, String],
|
||||
clearable: Boolean,
|
||||
},
|
||||
data() {
|
||||
let uid = ++maxUid;
|
||||
return { uid, value: this.modelValue };
|
||||
},
|
||||
mounted() {
|
||||
if (this.format) {
|
||||
this.$refs.slider.tooltipFormatter = this.valueFormatter;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sliderId() {
|
||||
return this.id || `ui-slider-${this.uid}`;
|
||||
},
|
||||
valueFormatter() {
|
||||
if (typeof this.format === 'string') {
|
||||
return v => this.format.replaceAll('{value}', v);
|
||||
}
|
||||
|
||||
return this.format;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
tooltip() {
|
||||
if (this.$refs.slider) {
|
||||
this.$refs.slider.tooltipFormatter = this.tooltipFormatter;
|
||||
}
|
||||
},
|
||||
},
|
||||
template,
|
||||
|
||||
components: {
|
||||
InfoTip,
|
||||
},
|
||||
compilerOptions: {
|
||||
isCustomElement: tag => tag.startsWith('wa-'),
|
||||
},
|
||||
};
|
||||
32
docs/assets/vue/mixins/input.js
Normal file
32
docs/assets/vue/mixins/input.js
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Mixin for components that behave like form controls.
|
||||
*/
|
||||
|
||||
export default {
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [String, Number, Boolean],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
initialValue: this.modelValue,
|
||||
value: this.modelValue,
|
||||
};
|
||||
},
|
||||
emits: ['update:modelValue', 'input'],
|
||||
methods: {
|
||||
handleInput(e) {
|
||||
this.value = e.target.value;
|
||||
this.$emit('input', this.value);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value(value) {
|
||||
this.$emit('update:modelValue', value);
|
||||
},
|
||||
modelValue(value) {
|
||||
this.value = value;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Component Cheatsheet
|
||||
layout: docs
|
||||
unpublished: true
|
||||
unlisted: true
|
||||
---
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
title: Comparer
|
||||
title: Comparison
|
||||
description: Compare visual differences between similar content with a sliding panel.
|
||||
tags: [imagery, niche]
|
||||
icon: comparer
|
||||
icon: comparison
|
||||
---
|
||||
|
||||
This is especially useful for comparing images, but can be used for comparing any type of content (for an example of using it to compare entire UIs, check out our [theme pages](/docs/themes/default/)).
|
||||
@@ -10,7 +10,7 @@ For best results, use content that shares the same dimensions.
|
||||
The slider can be controlled by dragging or pressing the left and right arrow keys. (Tip: press shift + arrows to move the slider in larger intervals, or home + end to jump to the beginning or end.)
|
||||
|
||||
```html {.example}
|
||||
<wa-comparer>
|
||||
<wa-comparison>
|
||||
<img
|
||||
slot="before"
|
||||
src="https://images.unsplash.com/photo-1517331156700-3c241d2b4d83?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=800&q=80&sat=-100&bri=-5"
|
||||
@@ -21,7 +21,7 @@ The slider can be controlled by dragging or pressing the left and right arrow ke
|
||||
src="https://images.unsplash.com/photo-1517331156700-3c241d2b4d83?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=800&q=80"
|
||||
alt="Color version of kittens in a basket looking around."
|
||||
/>
|
||||
</wa-comparer>
|
||||
</wa-comparison>
|
||||
```
|
||||
|
||||
## Examples
|
||||
@@ -31,7 +31,7 @@ The slider can be controlled by dragging or pressing the left and right arrow ke
|
||||
Use the `position` attribute to set the initial position of the slider. This is a percentage from `0` to `100`.
|
||||
|
||||
```html {.example}
|
||||
<wa-comparer position="25">
|
||||
<wa-comparison position="25">
|
||||
<img
|
||||
slot="before"
|
||||
src="https://images.unsplash.com/photo-1520903074185-8eca362b3dce?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1200&q=80"
|
||||
@@ -42,5 +42,5 @@ Use the `position` attribute to set the initial position of the slider. This is
|
||||
src="https://images.unsplash.com/photo-1520640023173-50a135e35804?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2250&q=80"
|
||||
alt="A person sitting on a yellow curb tying shoelaces on a boot."
|
||||
/>
|
||||
</wa-comparer>
|
||||
</wa-comparison>
|
||||
```
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
layout: null
|
||||
permalink: '/docs/palettes/data.js'
|
||||
eleventyExcludeFromCollections: true
|
||||
---
|
||||
export default {
|
||||
{%- 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 %}
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
layout: null
|
||||
permalink: "/docs/palettes/data.json"
|
||||
eleventyExcludeFromCollections: true
|
||||
---
|
||||
{
|
||||
{% for palette in collections.palettes %}
|
||||
{% set paletteId = palette.fileSlug -%}
|
||||
{% set colors = palettes[paletteId] -%}
|
||||
"{{ paletteId }}": {
|
||||
"title": "{{ palette.data.title }}",
|
||||
"colors": {
|
||||
{% for hue, tints in colors -%}
|
||||
"{{ hue }}": {
|
||||
{% for tint, value in tints -%}
|
||||
{% if tint != "05" -%}
|
||||
{% set value = value.coords or value -%}
|
||||
{% set key = "05" if tint == "5" else tint -%}
|
||||
"{{ key }}": {{ value | dump | safe }}{{ ', ' if not loop.last }}
|
||||
{%- endif %}
|
||||
{% endfor -%}
|
||||
}{{ ', ' if not loop.last }}
|
||||
{% endfor %} {# end of hue #}
|
||||
}
|
||||
}{{ ', ' if not loop.last }} {# end of palette #}
|
||||
{% endfor %}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { maxGrayChroma, moreHue, selectors, themeConfig } from '../../assets/dat
|
||||
import { cdnUrl, hueRanges, hues, tints } from '../../assets/scripts/tweak.js';
|
||||
import { cssImport, cssLiteral, cssRule } from '../../assets/scripts/tweak/code.js';
|
||||
import { subtractAngles } from '../../assets/scripts/tweak/util.js';
|
||||
import allPalettes from '/assets/data/palettes.js';
|
||||
import Prism from '/assets/scripts/prism.js';
|
||||
import { SwatchSelect } from '/assets/vue/components/index.js';
|
||||
import content from '/assets/vue/directives/content.js';
|
||||
@@ -22,22 +23,8 @@ await Promise.all(['wa-slider'].map(tag => customElements.whenDefined(tag)));
|
||||
// return computedColor.endsWith(' 0)');
|
||||
// })();
|
||||
|
||||
let allPalettes = await fetch('/docs/palettes/data.json').then(r => r.json());
|
||||
globalThis.allPalettes = allPalettes;
|
||||
|
||||
for (let palette in allPalettes) {
|
||||
for (let hue in allPalettes[palette].colors) {
|
||||
let scale = allPalettes[palette].colors[hue];
|
||||
for (let tint of tints) {
|
||||
let color = scale[tint];
|
||||
|
||||
if (Array.isArray(color)) {
|
||||
scale[tint] = new Color('oklch', color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const percentFormatter = value => value.toLocaleString(undefined, { style: 'percent' });
|
||||
|
||||
let paletteAppSpec = {
|
||||
|
||||
@@ -14,8 +14,10 @@ During the alpha period, things might break! We take breaking changes very serio
|
||||
|
||||
## Next
|
||||
|
||||
- 🚨 BREAKING: Renamed `<image-comparer>` to `<wa-comparer>` and improved compatibility for non-image content
|
||||
- 🚨 BREAKING: Renamed `<image-comparer>` to `<wa-comparison>` and improved compatibility for non-image content
|
||||
- 🚨 BREAKING: Added slot detection to `<wa-dialog>` and `<wa-drawer>` so you don't need to specify `with-header` and `with-footer`; headers are on by default now, but you can use the `without-header` attribute to turn them off
|
||||
- Added [a theme builder](/docs/themes/edit/) to create your own themes
|
||||
- Added a new Blog & News pattern category
|
||||
- Added a new free component: `<wa-scroller>` (#1 of 14 per stretch goals)
|
||||
- Added support for Duotone Thin, Light, and Regular styles and the Sharp Duotone family of styles to `<wa-icon>`
|
||||
- Fixed a bug that caused `<wa-radio-group>` to have an undesired margin below it
|
||||
|
||||
6
docs/docs/themes/active.md
vendored
6
docs/docs/themes/active.md
vendored
@@ -3,11 +3,7 @@ title: Active
|
||||
description: Energetic and tactile, always in motion.
|
||||
isPro: true
|
||||
tags: pro
|
||||
palette: rudimentary
|
||||
brand: green
|
||||
fonts:
|
||||
body: Inter
|
||||
icons:
|
||||
family: classic
|
||||
style: solid
|
||||
dimension: Subtle
|
||||
---
|
||||
|
||||
6
docs/docs/themes/awesome.md
vendored
6
docs/docs/themes/awesome.md
vendored
@@ -2,11 +2,7 @@
|
||||
title: Awesome
|
||||
description: Punchy and vibrant, the rockstar of themes.
|
||||
order: 0.2
|
||||
palette: bright
|
||||
brand: blue
|
||||
fonts:
|
||||
body: Quicksand
|
||||
icons:
|
||||
family: classic
|
||||
style: solid
|
||||
dimension: Blocky
|
||||
---
|
||||
|
||||
5
docs/docs/themes/brutalist.md
vendored
5
docs/docs/themes/brutalist.md
vendored
@@ -3,13 +3,8 @@ title: Brutalist
|
||||
description: Sharp, square, and unapologetically bold.
|
||||
isPro: true
|
||||
tags: pro
|
||||
palette: default
|
||||
brand: blue
|
||||
fonts:
|
||||
body: Space Grotesk
|
||||
heading: IBM Plex Sans Condensed
|
||||
code: Space Mono
|
||||
icons:
|
||||
family: sharp
|
||||
style: solid
|
||||
---
|
||||
|
||||
5
docs/docs/themes/classic.md
vendored
5
docs/docs/themes/classic.md
vendored
@@ -2,9 +2,4 @@
|
||||
title: Classic
|
||||
description: Timeless elegance that never goes out of style.
|
||||
order: 0.1
|
||||
palette: classic
|
||||
brand: blue
|
||||
icons:
|
||||
family: classic
|
||||
style: light
|
||||
---
|
||||
|
||||
22
docs/docs/themes/data.js.njk
vendored
22
docs/docs/themes/data.js.njk
vendored
@@ -1,22 +0,0 @@
|
||||
---
|
||||
layout: null
|
||||
permalink: '/docs/themes/data.js'
|
||||
eleventyExcludeFromCollections: true
|
||||
---
|
||||
export default {
|
||||
{%- for theme in collections.theme | sort %}
|
||||
{%- if not theme.data.unlisted %}
|
||||
{% set themeId = theme.fileSlug -%}
|
||||
{%- set colors = themes[themeId] -%}
|
||||
'{{ themeId }}': {
|
||||
id: '{{ themeId }}',
|
||||
title: '{{ theme.data.title }}',
|
||||
palette: '{{ theme.data.palette }}',
|
||||
brand: '{{ theme.data.brand }}',
|
||||
isPro: {{ theme.data.isPro or 'pro' in theme.data.tags }},
|
||||
fonts: {{ (theme.data.fonts | dump or 'null') | safe }},
|
||||
icon: {{ (theme.data.icons | dump or 'null') | safe }},
|
||||
},
|
||||
{%- endif %}
|
||||
{% endfor %}
|
||||
};
|
||||
5
docs/docs/themes/default.md
vendored
5
docs/docs/themes/default.md
vendored
@@ -2,13 +2,8 @@
|
||||
title: Default
|
||||
description: Your trusty companion, like a perfectly broken-in pair of jeans.
|
||||
order: 0
|
||||
palette: default
|
||||
brand: blue
|
||||
fonts:
|
||||
body: ui-sans-serif
|
||||
code: ui-monospace
|
||||
longform: ui-serif
|
||||
icons:
|
||||
family: classic
|
||||
style: solid
|
||||
---
|
||||
|
||||
8
docs/docs/themes/demo/index.njk
vendored
8
docs/docs/themes/demo/index.njk
vendored
@@ -35,13 +35,13 @@ unlisted: true
|
||||
{% include 'theme-showcase.njk' %}
|
||||
{% endset %}
|
||||
|
||||
<wa-comparer style="width: 100%" position="90">
|
||||
<div slot="after" class="theme-showcase wa-gap-xl">
|
||||
<wa-comparison style="width: 100%" position="90">
|
||||
<div slot="after" class="theme-showcase wa-gap-0">
|
||||
{{ content | safe }}
|
||||
</div>
|
||||
<div slot="before" class="theme-showcase wa-gap-xl wa-invert">
|
||||
<div slot="before" class="theme-showcase wa-gap-0 wa-invert">
|
||||
{{ content | safe }}
|
||||
</div>
|
||||
</wa-comparer>
|
||||
</wa-comparison >
|
||||
|
||||
<script type="module" src="../demo/index.js"></script>
|
||||
|
||||
148
docs/docs/themes/edit/index.js
vendored
148
docs/docs/themes/edit/index.js
vendored
@@ -2,11 +2,13 @@
|
||||
import { createApp } from 'https://cdn.jsdelivr.net/npm/vue@3/dist/vue.esm-browser.js';
|
||||
import { pairingsList, sameAs } from '/assets/data/fonts.js';
|
||||
import { allHues, cdnUrl, iconLibraries } from '/assets/data/index.js';
|
||||
import { themeDefaults } from '/assets/data/theming.js';
|
||||
import palettes from '/assets/data/palettes.js';
|
||||
import themes from '/assets/data/themes.js';
|
||||
import { getPath, themeDefaults } from '/assets/data/theming.js';
|
||||
import Prism from '/assets/scripts/prism.js';
|
||||
import { getThemeCode } from '/assets/scripts/tweak/code.js';
|
||||
import { deepClone, deepEach, deepMerge } from '/assets/scripts/util/deep.js';
|
||||
import { capitalize, slugify } from '/assets/scripts/util/string.js';
|
||||
import { deepClone, deepEach, deepEntries, deepGet, deepMerge } from '/assets/scripts/util/deep.js';
|
||||
import { camelCase, capitalize, slugify } from '/assets/scripts/util/string.js';
|
||||
import {
|
||||
ColorSelect,
|
||||
EditableText,
|
||||
@@ -19,11 +21,10 @@ import {
|
||||
ThemeCard,
|
||||
UiPanel,
|
||||
UiPanelContainer,
|
||||
UiSlider,
|
||||
} from '/assets/vue/components/index.js';
|
||||
import content from '/assets/vue/directives/content.js';
|
||||
import savedMixin from '/assets/vue/mixins/saved.js';
|
||||
import palettes from '/docs/palettes/data.js';
|
||||
import themes from '/docs/themes/data.js';
|
||||
|
||||
let appSpec = {
|
||||
mixins: [savedMixin],
|
||||
@@ -44,19 +45,7 @@ let appSpec = {
|
||||
id: id === 'edit' ? 'custom' : id,
|
||||
isCustom,
|
||||
urlParams: location.search,
|
||||
theme: {
|
||||
base: isCustom ? '' : id,
|
||||
palette: '',
|
||||
typography: '',
|
||||
colors: '',
|
||||
brand: '',
|
||||
icon: {
|
||||
kit: '',
|
||||
library: '',
|
||||
family: '',
|
||||
style: '',
|
||||
},
|
||||
},
|
||||
theme: getBlankTheme(isCustom ? '' : id),
|
||||
ui: {
|
||||
panel: 'styles',
|
||||
showCode: false,
|
||||
@@ -82,13 +71,60 @@ let appSpec = {
|
||||
});
|
||||
|
||||
if (location.search) {
|
||||
let urlTheme = this.permalink.getAll();
|
||||
let urlTheme = this.permalink.toObject({
|
||||
ignoreKeys: ['panel', 'color-scheme'],
|
||||
getPath,
|
||||
});
|
||||
deepMerge(this.theme, urlTheme, { emptyValues: [undefined, ''] });
|
||||
|
||||
if (this.permalink.has('panel')) {
|
||||
this.ui.panel = this.permalink.get('panel');
|
||||
}
|
||||
}
|
||||
|
||||
this.isCreated = true;
|
||||
},
|
||||
|
||||
mounted() {
|
||||
let { preview, previewInvert } = this.$refs;
|
||||
|
||||
if (!preview || !previewInvert) {
|
||||
return;
|
||||
}
|
||||
|
||||
let contentWindow, contentWindowInvert;
|
||||
|
||||
preview.addEventListener('load', () => {
|
||||
try {
|
||||
contentWindow = preview.contentWindow;
|
||||
} catch (e) {}
|
||||
|
||||
if (contentWindow) {
|
||||
contentWindow.addEventListener('scroll', e => {
|
||||
let { scrollX, scrollY } = contentWindow;
|
||||
if (contentWindowInvert) {
|
||||
contentWindowInvert.scrollTo(scrollX, scrollY);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
previewInvert.addEventListener('load', () => {
|
||||
try {
|
||||
contentWindowInvert = previewInvert.contentWindow;
|
||||
} catch (e) {}
|
||||
|
||||
if (contentWindowInvert) {
|
||||
contentWindowInvert.addEventListener('scroll', e => {
|
||||
let { scrollX, scrollY } = contentWindowInvert;
|
||||
if (contentWindow) {
|
||||
contentWindow.scrollTo(scrollX, scrollY);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
computed: {
|
||||
originalTitle() {
|
||||
if (this.isCustom) {
|
||||
@@ -139,6 +175,16 @@ let appSpec = {
|
||||
return ret;
|
||||
},
|
||||
|
||||
customizations() {
|
||||
return deepEntries(this.theme, {
|
||||
filter: (value, key, parent, path) => {
|
||||
let fullPath = [...path, key];
|
||||
let defaultValue = deepGet(this.defaults, fullPath);
|
||||
return key !== 'base' && typeof value !== 'object' && value !== '' && value !== defaultValue;
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
computed() {
|
||||
let ret = deepClone(themeDefaults);
|
||||
|
||||
@@ -185,22 +231,23 @@ let appSpec = {
|
||||
tweaked() {
|
||||
return Object.values(this.theme).filter(Boolean).length > 0;
|
||||
},
|
||||
|
||||
urlParamsInvert() {
|
||||
let invert = 'color-scheme=invert';
|
||||
return (this.urlParams ? this.urlParams + '&' : '?') + invert;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
theme: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.permalink.setAll(this.theme, this.defaults);
|
||||
async handler() {
|
||||
await this.$nextTick(); // give defaults a chance to update
|
||||
|
||||
// Update page URL
|
||||
this.permalink.setAll(this.theme, this.defaults);
|
||||
this.permalink.updateLocation();
|
||||
let theme = JSON.parse(JSON.stringify(this.theme));
|
||||
this.$refs.preview?.contentWindow.postMessage({
|
||||
type: 'updatePreview',
|
||||
theme,
|
||||
id: this.slug,
|
||||
});
|
||||
|
||||
this.updatePreview();
|
||||
|
||||
this.unsavedChanges = true;
|
||||
},
|
||||
@@ -231,6 +278,30 @@ let appSpec = {
|
||||
console.log(...args);
|
||||
return args[0];
|
||||
},
|
||||
|
||||
updatePreview() {
|
||||
// Update page URL
|
||||
let theme = JSON.parse(JSON.stringify(this.theme));
|
||||
let message = {
|
||||
type: 'updatePreview',
|
||||
theme,
|
||||
id: this.slug,
|
||||
};
|
||||
|
||||
this.$refs.preview?.contentWindow.postMessage(message);
|
||||
this.$refs.previewInvert?.contentWindow.postMessage(message);
|
||||
},
|
||||
|
||||
resetTo(base) {
|
||||
let kit = this.theme.icon.kit;
|
||||
let theme = getBlankTheme(base);
|
||||
|
||||
if (kit) {
|
||||
theme.icon.kit = kit;
|
||||
}
|
||||
|
||||
return (this.theme = theme);
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
@@ -245,6 +316,7 @@ let appSpec = {
|
||||
UiPanel,
|
||||
UiPanelContainer,
|
||||
SwatchSelect,
|
||||
UiSlider,
|
||||
},
|
||||
|
||||
directives: { content },
|
||||
@@ -267,3 +339,23 @@ function init() {
|
||||
|
||||
init();
|
||||
addEventListener('turbo:render', init);
|
||||
|
||||
function getBlankTheme(base) {
|
||||
return {
|
||||
base,
|
||||
palette: '',
|
||||
typography: '',
|
||||
colors: '',
|
||||
brand: '',
|
||||
icon: {
|
||||
kit: '',
|
||||
library: '',
|
||||
family: '',
|
||||
style: '',
|
||||
},
|
||||
rounding: '',
|
||||
spacing: '',
|
||||
borderWidth: '',
|
||||
dimensionality: '',
|
||||
};
|
||||
}
|
||||
|
||||
76
docs/docs/themes/edit/index.njk
vendored
76
docs/docs/themes/edit/index.njk
vendored
@@ -22,7 +22,7 @@ unlisted: true
|
||||
</a>
|
||||
|
||||
<div class="title">
|
||||
<h1><editable-text :model-value="title" label="theme name" @submit="newTitle => save({title: newTitle})"></editable-text></h1>
|
||||
<h1><editable-text :model-value="title" label="theme name" @submit="newTitle => save({title: newTitle})" blur="cancel"></editable-text></h1>
|
||||
<wa-icon-button v-if="saved" class="delete" name="trash" label="Delete theme" @click="deleteSaved"></wa-icon-button>
|
||||
</div>
|
||||
|
||||
@@ -88,6 +88,8 @@ unlisted: true
|
||||
<icons-card size="small" :action="e => ui.panel = 'icons'" :family="computed.icon.family" :style="computed.icon.style"
|
||||
title="Icons" :subtitle="`${ capitalize(computed.icon.family) } • ${ capitalize(computed.icon.style) }`">
|
||||
</icons-card>
|
||||
|
||||
<theme-card type="dimensionality" :theme="computed.dimensionality" title="Elements" size="small" :action="e => ui.panel = 'elements'"></theme-card>
|
||||
</ui-panel>
|
||||
|
||||
<ui-panel v-model="ui.panel" value="theme" :step="1">
|
||||
@@ -95,7 +97,12 @@ unlisted: true
|
||||
<label for="starting-theme">Starting Theme</label>
|
||||
</template>
|
||||
|
||||
<wa-radio-group id="starting-theme" class="card-picker" v-model="theme.base">
|
||||
<wa-callout v-if="customizations.length > 0" variant="danger" size="small">
|
||||
<wa-icon slot="icon" name="circle-exclamation" variant="regular"></wa-icon>
|
||||
Editing the starting theme will reset your <span v-text="customizations.length + ' customization' + (customizations.length > 1 ? 's' : '')"></span>.
|
||||
</wa-callout>
|
||||
|
||||
<wa-radio-group id="starting-theme" class="card-picker" :value="theme.base" @input="e => resetTo(e.target.value)">
|
||||
<template v-for="(themeMeta, themeId) in themes">
|
||||
<wa-radio v-if="themeId !== 'custom'" :value="themeId" size="small" :aria-selected="!theme.base && themeId === 'default' ? 'true' : null">
|
||||
<theme-card :theme="themeId"></theme-card>
|
||||
@@ -156,8 +163,61 @@ unlisted: true
|
||||
</wa-input>
|
||||
</ui-panel>
|
||||
|
||||
<ui-panel v-model="ui.panel" value="elements" title="Elements" :step="1">
|
||||
<ui-slider label="Rounding" :default-value="1" :min="0" :max="2" :step="0.125" format="{value}x" :model-value="computed.rounding" @update:model-value="value => theme.rounding = value">
|
||||
<template #min>
|
||||
<div class="corner" style="--border-radius: 0;"></div>
|
||||
</template>
|
||||
<template #max>
|
||||
<div class="corner" style="--border-radius: var(--wa-border-radius-l);"></div>
|
||||
</template>
|
||||
</ui-slider>
|
||||
<ui-slider label="Spacing" :default-value="1":min="0.5" :max="2" :step="0.125" format="{value}x" :model-value="computed.spacing" @update:model-value="value => theme.spacing = value">
|
||||
<template #min>
|
||||
<div class="spacing" style="--spacing: 0.5;"><div></div><div></div><div></div></div>
|
||||
</template>
|
||||
<template #max>
|
||||
<div class="spacing" style="--spacing: 1.5;"><div></div><div></div><div></div></div>
|
||||
</template>
|
||||
</ui-slider>
|
||||
|
||||
<ui-slider label="Border thickness" :default-value="1" :min="0.5" :max="3" :step="0.5" format="{value}x" :model-value="computed.borderWidth" @update:model-value="value => theme.borderWidth = value">
|
||||
<template #min>
|
||||
<div class="borders" style="--border-width: 1px;"></div>
|
||||
</template>
|
||||
<template #max>
|
||||
<div class="borders" style="--border-width: var(--wa-border-width-l);"></div>
|
||||
</template>
|
||||
</ui-slider>
|
||||
|
||||
<!--
|
||||
<theme-card type="dimensionality" :theme="computed.dimensionality" title="Dimensionality" :subtitle="computed.dimensionality ? undefined : 'Flat'" size="small" :action="e => ui.panel = 'dimensionality'"></theme-card>
|
||||
-->
|
||||
</ui-panel>
|
||||
|
||||
<!--
|
||||
<ui-panel v-model="ui.panel" value="dimensionality" title="Dimensionality" :step="2">
|
||||
<template #title>
|
||||
<label for="dimensionality-radio-group">Dimensionality</label>
|
||||
</template>
|
||||
<wa-radio-group id="dimensionality-radio-group" class="card-picker" v-model="theme.dimensionality">
|
||||
<wa-radio value="" size="small" :aria-selected="!theme.dimensionality || theme.dimensionality === computed.base ? 'true' : null">
|
||||
<theme-card type="dimensionality" :theme="computed.base"></theme-card>
|
||||
</wa-radio>
|
||||
<template v-for="(themeMeta, themeId) in themes">
|
||||
<wa-radio v-if="themeId !== computed.base && themeMeta.dimension" :value="themeId" size="small" :aria-selected="theme.dimensionality === themeId ? 'true' : null">
|
||||
<theme-card type="dimensionality" :theme="themeId"></theme-card>
|
||||
</wa-radio>
|
||||
</template>
|
||||
</wa-radio-group>
|
||||
</ui-panel>
|
||||
-->
|
||||
|
||||
<ui-panel v-model="ui.panel" value="icon-family" title="Icon Family" :step="2">
|
||||
<wa-radio-group id="starting-theme" class="card-picker" v-model="theme.icon.family">
|
||||
<template #title>
|
||||
<label for="icon-family-radio-group">Icon Family</label>
|
||||
</template>
|
||||
<wa-radio-group id="icon-family-radio-group" class="card-picker" v-model="theme.icon.family">
|
||||
<wa-radio v-for="family of iconLibraries.default.family" :value="family" size="small" :aria-selected="computed.icon.family === family ? 'true' : null">
|
||||
<icons-card type="family" vary="style" :family="family" :library="computed.icon.library"></icons-card>
|
||||
</wa-radio>
|
||||
@@ -165,7 +225,10 @@ unlisted: true
|
||||
</ui-panel>
|
||||
|
||||
<ui-panel v-model="ui.panel" value="icon-style" title="Icon Style" :step="2">
|
||||
<wa-radio-group id="starting-theme" class="card-picker" v-model="theme.icon.style">
|
||||
<template #title>
|
||||
<label for="icon-style-radio-group">Icon Style</label>
|
||||
</template>
|
||||
<wa-radio-group id="icon-style-radio-group" class="card-picker" v-model="theme.icon.style">
|
||||
<wa-radio v-for="style of iconLibraries.default.style" :value="style" size="small" :aria-selected="computed.icon.style === style ? 'true' : null">
|
||||
<icons-card type="style" :style="style" :defaults="computed.icon"></icons-card>
|
||||
</wa-radio>
|
||||
@@ -174,6 +237,9 @@ unlisted: true
|
||||
</ui-panel-container>
|
||||
|
||||
<div class="preview-container">
|
||||
<iframe class="preview" ref="preview" :src="`{{ page.url }}../preview/${ui.preview}/${urlParams}`"></iframe>
|
||||
<wa-comparison position="80">
|
||||
<iframe slot="before" class="preview" ref="previewInvert" :src="`{{ page.url }}../preview/${ui.preview}/${urlParamsInvert}`"></iframe>
|
||||
<iframe slot="after" class="preview" ref="preview" :src="`{{ page.url }}../preview/${ui.preview}/${urlParams}`"></iframe>
|
||||
</wa-comparison>
|
||||
</div>
|
||||
</wa-page>
|
||||
|
||||
93
docs/docs/themes/edit/style.css
vendored
93
docs/docs/themes/edit/style.css
vendored
@@ -1,7 +1,7 @@
|
||||
@import url('/assets/styles/theme-icons.css');
|
||||
@import url('/assets/vue/components/panel.css');
|
||||
@import url('/assets/vue/components/scrollable.css');
|
||||
|
||||
@import url('/assets/vue/components/ui-slider.css');
|
||||
:root {
|
||||
--ui-border: var(--wa-border-style) var(--wa-panel-border-width) var(--wa-color-surface-border);
|
||||
}
|
||||
@@ -80,6 +80,16 @@ wa-page > header {
|
||||
max-inline-size: 140ch;
|
||||
margin: auto;
|
||||
|
||||
wa-comparison {
|
||||
height: 100%;
|
||||
|
||||
&::part(base),
|
||||
&::part(before),
|
||||
&::part(after) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
iframe.preview {
|
||||
border-radius: var(--wa-border-radius-m);
|
||||
border: var(--ui-border);
|
||||
@@ -118,6 +128,14 @@ wa-radio-group.card-picker {
|
||||
--spacing: 0;
|
||||
}
|
||||
|
||||
> wa-callout {
|
||||
margin-bottom: 0;
|
||||
|
||||
&[size='small'][variant='danger'] {
|
||||
font-weight: var(--wa-font-weight-semibold);
|
||||
}
|
||||
}
|
||||
|
||||
> footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -147,3 +165,76 @@ wa-card wa-icon.angle-right {
|
||||
font-size: var(--wa-font-size-l);
|
||||
color: var(--wa-color-text-quiet);
|
||||
}
|
||||
|
||||
.dimensionality-card {
|
||||
.page-name .wa-caption-m {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-slider {
|
||||
.corner,
|
||||
.borders,
|
||||
.spacing {
|
||||
aspect-ratio: 1;
|
||||
width: max(var(--wa-font-size-l), var(--wa-border-radius-l) * 1.25);
|
||||
}
|
||||
|
||||
.corner,
|
||||
.borders {
|
||||
--border-color: var(--wa-color-border-normal);
|
||||
background-color: var(--wa-color-fill-quiet);
|
||||
border-color: var(--border-color, var(--wa-color-border-normal));
|
||||
border-style: solid;
|
||||
border-width: var(--border-width, var(--wa-border-width-m));
|
||||
|
||||
&:is(wa-button:hover *) {
|
||||
background-color: var(--wa-color-fill-normal);
|
||||
}
|
||||
}
|
||||
|
||||
.corner {
|
||||
border-bottom: none;
|
||||
border-left: none;
|
||||
border-top-right-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.spacing {
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: 3fr 2fr;
|
||||
gap: calc(2px + var(--spacing) * 2px);
|
||||
border-radius: 2px;
|
||||
|
||||
div {
|
||||
padding: calc(2px + var(--spacing) * 1px);
|
||||
border-radius: inherit;
|
||||
background-color: var(--wa-color-border-quiet);
|
||||
}
|
||||
|
||||
div:nth-child(3) {
|
||||
grid-column: 2;
|
||||
grid-row: 1 / 3;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
.corner,
|
||||
.borders {
|
||||
--border-color: var(--wa-color-brand);
|
||||
}
|
||||
|
||||
.spacing {
|
||||
background-color: lab(from var(--wa-color-brand) l a b / 10%);
|
||||
|
||||
&:is(wa-button:hover *) {
|
||||
background-color: lab(from var(--wa-color-brand) l a b / 15%);
|
||||
|
||||
div {
|
||||
background-color: var(--wa-color-border-normal);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
5
docs/docs/themes/glossy.md
vendored
5
docs/docs/themes/glossy.md
vendored
@@ -3,11 +3,6 @@ title: Glossy
|
||||
description: Bustling with plenty of luster and shine.
|
||||
isPro: true
|
||||
tags: pro
|
||||
palette: elegant
|
||||
brand: indigo
|
||||
fonts:
|
||||
body: Figtree
|
||||
icons:
|
||||
family: classic
|
||||
style: solid
|
||||
---
|
||||
|
||||
5
docs/docs/themes/matter.md
vendored
5
docs/docs/themes/matter.md
vendored
@@ -3,15 +3,10 @@ title: Matter
|
||||
description: Digital design inspired by the real world.
|
||||
isPro: true
|
||||
tags: pro
|
||||
palette: mild
|
||||
brand: indigo
|
||||
fonts:
|
||||
body: 'Wix Madefor Text'
|
||||
code: Roboto Mono
|
||||
longform: Roboto Serif
|
||||
icons:
|
||||
family: classic
|
||||
style: regular
|
||||
---
|
||||
|
||||
Set the page theme to "{{ title }}" from the top right to preview the following examples.
|
||||
|
||||
5
docs/docs/themes/mellow.md
vendored
5
docs/docs/themes/mellow.md
vendored
@@ -2,13 +2,8 @@
|
||||
title: Mellow
|
||||
description: Soft and soothing, like a lazy Sunday morning.
|
||||
isPro: true
|
||||
tags: pro
|
||||
palette: natural
|
||||
fonts:
|
||||
body: Mulish
|
||||
heading: Lora
|
||||
longform: Lora
|
||||
icons:
|
||||
family: classic
|
||||
style: solid
|
||||
---
|
||||
|
||||
6
docs/docs/themes/playful.md
vendored
6
docs/docs/themes/playful.md
vendored
@@ -3,13 +3,9 @@ title: Playful
|
||||
description: Cheerful and engaging, like a playground on screen.
|
||||
isPro: true
|
||||
tags: pro
|
||||
palette: rudimentary
|
||||
brand: purple
|
||||
dimension: Smooth
|
||||
fonts:
|
||||
body: Nunito
|
||||
heading: Fredoka
|
||||
code: Azeret Mono
|
||||
icons:
|
||||
family: classic
|
||||
style: solid
|
||||
---
|
||||
|
||||
5
docs/docs/themes/premium.md
vendored
5
docs/docs/themes/premium.md
vendored
@@ -3,13 +3,8 @@ title: Premium
|
||||
description: The ultimate in sophistication and style.
|
||||
isPro: true
|
||||
tags: pro
|
||||
palette: anodized
|
||||
brand: cyan
|
||||
fonts:
|
||||
body: DM Sans
|
||||
heading: Playfair Display
|
||||
longform: Playfair
|
||||
icons:
|
||||
family: sharp
|
||||
style: regular
|
||||
---
|
||||
|
||||
28
docs/docs/themes/preview/preview.js
vendored
28
docs/docs/themes/preview/preview.js
vendored
@@ -1,7 +1,7 @@
|
||||
import palettes from '../../palettes/data.js';
|
||||
import themes from '../data.js';
|
||||
import { allHues } from '/assets/data/index.js';
|
||||
import { themeConfig, themeDefaults, themeParams } from '/assets/data/theming.js';
|
||||
import palettes from '/assets/data/palettes.js';
|
||||
import themes from '/assets/data/themes.js';
|
||||
import { getPath, themeConfig, themeDefaults, themeParams } from '/assets/data/theming.js';
|
||||
import Permalink from '/assets/scripts/permalink.js';
|
||||
import { getThemeCode } from '/assets/scripts/tweak/code.js';
|
||||
import { deepClone, deepEach, deepGet, deepMerge } from '/assets/scripts/util/deep.js';
|
||||
@@ -58,9 +58,17 @@ export const documentTheme = { ...theme };
|
||||
if (location.search) {
|
||||
let permalink = new Permalink();
|
||||
// Apply any overrides from URL
|
||||
let urlOverrides = permalink.getAll();
|
||||
let urlOverrides = permalink.toObject({
|
||||
ignoreKeys: ['color-scheme'],
|
||||
getPath,
|
||||
});
|
||||
|
||||
updateTheme(urlOverrides, { silent: true });
|
||||
|
||||
let colorScheme = permalink.get('color-scheme');
|
||||
if (colorScheme) {
|
||||
document.body.classList.add('wa-' + colorScheme);
|
||||
}
|
||||
}
|
||||
|
||||
theme.base ??= 'default';
|
||||
@@ -168,6 +176,8 @@ export async function updatePreview(options = {}) {
|
||||
let changeDom = false;
|
||||
|
||||
// DOM diffing of old and new <link> elements
|
||||
// We want to keep any <link> elements that have not changed,
|
||||
// and add any new ones near the old ones, in the right order
|
||||
for (let aspect of themeParams) {
|
||||
allStylesheets[aspect] ??= {};
|
||||
let stylesheets = allStylesheets[aspect];
|
||||
@@ -238,7 +248,15 @@ export async function updatePreview(options = {}) {
|
||||
first.before(link);
|
||||
} else {
|
||||
// If no first, it means we didn't find any theme stylesheets
|
||||
document.head.append(link);
|
||||
// We may still have <style> elements though
|
||||
let firstStyleElement = document.querySelector(
|
||||
'style:is(.wa-themer, .wa-palette, [class^="wa-theme-"], [class*=" wa-theme-"])',
|
||||
);
|
||||
if (firstStyleElement) {
|
||||
firstStyleElement.before(link);
|
||||
} else {
|
||||
document.head.append(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
5
docs/docs/themes/tailspin.md
vendored
5
docs/docs/themes/tailspin.md
vendored
@@ -3,11 +3,6 @@ title: Tailspin
|
||||
description: Like a bird in flight, guiding you from there to here.
|
||||
isPro: true
|
||||
tags: pro
|
||||
palette: vogue
|
||||
brand: indigo
|
||||
fonts:
|
||||
body: Inter
|
||||
icons:
|
||||
family: classic
|
||||
style: solid
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user