Improve theme remixing UI

This commit is contained in:
Lea Verou
2025-01-24 14:03:19 -05:00
parent 71c054d6e4
commit 4316c2670a
8 changed files with 282 additions and 99 deletions

View File

@@ -1,3 +1,5 @@
{%- if not stylesheets %}{% set stylesheets = [stylesheet] %}{% endif -%}
<wa-tab-group>
<wa-tab panel="html">In HTML</wa-tab>
<wa-tab panel="css">In CSS</wa-tab>
@@ -5,14 +7,18 @@
Simply add the following code to the `<head>` of your page:
```html
<link rel="stylesheet" href="{% cdnUrl stylesheet %}" />
{% for stylesheet in stylesheets -%}
<link rel="stylesheet" href="{% cdnUrl stylesheet %}" />{% if not loop.last %}
{% endif %}{% endfor %}
```
</wa-tab-panel>
<wa-tab-panel name="css">
Simply add the following code at the top of your CSS file:
```css
@import url('{% cdnUrl stylesheet %}');
{% for stylesheet in stylesheets -%}
@import url('{% cdnUrl stylesheet %}');{% if not loop.last %}
{% endif %}{% endfor %}
```
</wa-tab-panel>
</wa-tab-group>

View File

@@ -3,64 +3,15 @@
{# {% set forceTheme = page.fileSlug %} #}
{% extends '../_includes/base.njk' %}
{% set themeId = page.fileSlug %}
{% if themeId == 'remixed' -%}
{% set themeId = 'default' %}
{% endif -%}
{% block header %}
<iframe src='{{ page.url }}demo.html' id="demo"></iframe>
<p id="mix_and_match" class="wa-gap-m">
<strong>
<wa-icon name="merge" slot="prefix"></wa-icon>
Remix
<wa-icon-button href="#remixing" name="circle-question" slot="suffix" variant="regular" label="How to use?"></wa-icon-button>
</strong>
<wa-select name="colors" label="Colors from…" size="small">
<wa-icon name="palette" slot="prefix" variant="regular"></wa-icon>
<wa-option value="">(Theme default)</wa-option>
<wa-divider></wa-divider>
{% for theme in collections.theme | sort %}
{% if theme.fileSlug !== page.fileSlug %}
<wa-option value="{{ theme.fileSlug }}">{{ theme.data.title }}</wa-option>
{% endif %}
{% endfor %}
</wa-select>
<wa-select name="palette" label="Palette" size="small">
<wa-icon name="swatchbook" slot="prefix" variant="regular"></wa-icon>
<wa-option value="">(Theme default)</wa-option>
<wa-divider></wa-divider>
{% for p in collections.palette | sort %}
{% if p.fileSlug !== palette %}
<wa-option value="{{ p.fileSlug }}">{{ p.data.title }}</wa-option>
{% endif %}
{% endfor %}
</wa-select>
<wa-select name="typography" label="Typography from…" size="small">
<wa-icon name="font-case" slot="prefix"></wa-icon>
<wa-option value="">(Theme default)</wa-option>
<wa-divider></wa-divider>
{% for theme in collections.theme | sort %}
{% if theme.fileSlug !== page.fileSlug %}
<wa-option value="{{ theme.fileSlug }}">{{ theme.data.title }}</wa-option>
{% endif %}
{% endfor %}
</wa-select>
</p>
<script>
document.querySelector('#mix_and_match').addEventListener('change', function(event) {
let selects = document.querySelectorAll('#mix_and_match wa-select');
let url = new URL(demo.src);
for (let select of selects) {
url.searchParams.set(select.name, select.value);
}
demo.src = url;
});
</script>
<iframe src='/docs/themes/{{ themeId }}/demo.html' id="demo"></iframe>
{% if palette -%}
<h2>Default Color Palette</h2>
{% set paletteURL = '/docs/palettes/' + palette + '/' %}
{% set themePage = page %}
@@ -69,34 +20,20 @@
{% include 'page-card.njk' %}
</div>
{% set page = themePage %}
{% endif %}
{% endblock %}
{% block afterContent %}
{% markdown %}
{%- if page.fileSlug != 'remixed' %}
## How to use this theme
You can import this theme from the Web Awesome CDN.
{% set stylesheet = 'styles/themes/' + page.fileSlug + '.css' %}
{% set stylesheet = 'styles/themes/' + themeId + '.css' %}
{% include 'import-stylesheet-code.md.njk' %}
### Remixing { #remixing }
If you want to combine the **colors** from this theme with another theme, you can import this CSS file *after* the other themes CSS file:
{% set stylesheet = 'styles/themes/' + page.fileSlug + '/color.css' %}
{% include 'import-stylesheet-code.md.njk' %}
To use the **typography** from this theme with another theme, you can import this CSS file *after* the other themes CSS file:
{% set stylesheet = 'styles/themes/' + page.fileSlug + '/typography.css' %}
{% include 'import-stylesheet-code.md.njk' %}
<wa-callout variant="warning">
<wa-icon slot="icon" name="triangle-exclamation" variant="regular"></wa-icon>
Please note that not all combinations will look good — once youre mixing and matching, youre on your own!
</wa-callout>
{% endif %}
## Dark mode

View File

@@ -538,23 +538,4 @@ table.colors {
height: 65vh;
max-height: 21lh;
}
#mix_and_match {
strong {
display: flex;
align-items: center;
gap: var(--wa-space-2xs);
margin-top: 1.2em;
}
wa-select::part(label) {
margin-block-end: 0;
}
wa-select[value='']::part(display-input),
wa-option[value=''] {
font-style: italic;
color: var(--wa-color-text-quiet);
}
}
}

View File

@@ -19,6 +19,10 @@ eleventyComputed:
{% include 'breadcrumbs.njk' %}
<h1 class="title">{{ theme.data.title }}</h1>
<p id="mix_and_match" hidden class="wa-size-s"></p>
<wa-button href="/docs/themes/remixed/?base={{ theme.fileSlug }}" class="remix-link" size="small" variant="brand" target="_parent">
<wa-icon name="merge"></wa-icon>
Remix
</wa-button>
<p>{% include 'status.njk' %}</p>
<p id="theme-showcase-description">{{ theme.data.description | inlineMarkdown | safe }}</p>
</header>
@@ -39,12 +43,12 @@ function updateTheme() {
let params = new URLSearchParams(window.location.search);
let script = document.currentScript;
const stylesheetURLs = {
colors: id => `/dist/styles/themes/${ id }/color.css`,
color: id => `/dist/styles/themes/${ id }/color.css`,
palette: id => `/dist/styles/color/${ id }.css`,
typography: id => `/dist/styles/themes/${ id }/typography.css`
};
const icons = {
colors: 'palette',
color: 'palette',
palette: 'swatchbook',
typography: 'font-case'
}
@@ -66,11 +70,14 @@ function updateTheme() {
}
}
let isRemixed = msgs.length > 0;
document.documentElement.classList.toggle("remixed", isRemixed);
for (let p of mix_and_match) {
p.hidden = msgs.length === 0;
if (msgs.length) {
p.hidden = !isRemixed;
if (isRemixed) {
let icon =
p.innerHTML = `<strong><wa-icon name="merge"></wa-icon> Remixed</strong> ` + msgs.map(msg => `<wa-badge appearance=outlined>
p.innerHTML = `<strong><wa-icon name="merge"></wa-icon> Remixed</strong><br> ` + msgs.map(msg => `<wa-badge appearance=outlined>
${ msg }</wa-badge>`).join(' ');
}
}

138
docs/docs/themes/remixed/index.js vendored Normal file
View File

@@ -0,0 +1,138 @@
let params = { base: 'default', palette: '', color: '', typography: '' };
// Find usage code snippet and prepare it
let snippets = document.querySelectorAll('#remixed-usage ~ wa-tab-group pre > code');
let copyButtons = document.querySelectorAll('#remixed-usage ~ wa-tab-group pre > wa-copy-button');
let codeExamples = [];
for (let snippet of snippets) {
let tokens = [...snippet.children];
let base = tokens.shift();
let [palette, color, typography] = tokens;
codeExamples.push({ snippet, base, palette, color, typography });
// Remove non-base tokens
for (let token of tokens) {
let whitespace = token.previousSibling;
if (whitespace.nodeType === Node.TEXT_NODE) {
// Move whitespace to beginning of node
token.prepend(token.previousSibling);
}
}
}
// Read URL params and apply them. This facilitates permalinks.
if (location.search) {
let urlParams = new URLSearchParams(location.search);
for (let aspect in params) {
if (urlParams.has(aspect)) {
params[aspect] = urlParams.get(aspect);
}
}
}
const selects = Object.fromEntries(
[...document.querySelectorAll('#mix_and_match wa-select')].map(select => [select.getAttribute('name'), select]),
);
document.querySelector('#mix_and_match').addEventListener(
'change',
function (event) {
for (let name in selects) {
params[name] = selects[name].value;
}
render();
},
{ capture: true },
);
function hasOverride(name) {
if (!params[name]) {
return false;
}
if (name === 'palette') {
return params[name] !== defaultPalettes[params.base];
}
if (name !== 'base') {
return params[name] !== params.base;
}
return true;
}
function render() {
let demoUrl = new URL(`/docs/themes/${params.base}/demo.html`, location);
let pageParams = new URLSearchParams(params);
for (let aspect in params) {
if (aspect !== 'base' && params[aspect]) {
demoUrl.searchParams.set(aspect, params[aspect]);
}
if (!params[aspect] || (aspect === 'base' && params[aspect] === 'default') || !hasOverride(aspect)) {
pageParams.delete(aspect);
}
// Output code snippet
if (aspect !== 'base') {
for (let codeExample of codeExamples) {
let token = codeExample[aspect];
let value = params[aspect];
if (hasOverride(aspect)) {
// Update code example
let valueToken = [...token.querySelectorAll('.code-attr-value, .code-url')].pop();
valueToken.textContent = replaceStyleSheetURL(valueToken.textContent, aspect, value);
// Add code example to <pre>
codeExample.snippet.append(token);
} else {
token.remove();
}
}
}
// Update selects
if (selects[aspect].value === undefined) {
selects[aspect].setAttribute('value', params[aspect]);
} else {
selects[aspect].value = params[aspect];
}
}
// Update code snippet copy buttons
for (let copyButton of copyButtons) {
copyButton.value = copyButton.nextElementSibling.textContent;
}
// Update demo URL
demo.src = demoUrl;
// Update page URL. If theres already a search, replace it.
// We dont want to clog the users history while they iterate
let historyAction = location.search ? 'replaceState' : 'pushState';
history[historyAction](null, '', `?${pageParams}`);
}
const regexes = {
base: /\/themes\/([a-z-]+)\.css/,
palette: /\/color\/([a-z-]+)\.css/,
color: /\/themes\/([a-z-]+)\/color\.css/,
typography: /\/themes\/([a-z-]+)\/typography\.css/,
};
function replaceStyleSheetURL(url, name, value) {
let regex = regexes[name];
return url.replace(regex, (match, oldValue) => {
return match.replace(oldValue, value);
});
}
globalThis.params = params;
render();

84
docs/docs/themes/remixed/index.njk vendored Normal file
View File

@@ -0,0 +1,84 @@
---
title: Remixed
description: TODO
isPro: true
tags: pro
---
<link rel="stylesheet" href="{{ page.url }}/style.css">
{% block header %}
<script>
globalThis.defaultPalettes = {
{% for theme in collections.theme | sort -%}
"{{ theme.fileSlug }}": "{{ theme.data.palette }}",
{%- endfor %}
};
</script>
<p id="mix_and_match" class="wa-gap-m">
{# <strong>
<wa-icon name="merge" slot="prefix"></wa-icon>
Remix
<wa-icon-button href="#remixing" name="circle-question" slot="suffix" variant="regular" label="How to use?"></wa-icon-button>
</strong> #}
<wa-select name="base" label="Base:" size="small" value="default">
<wa-icon name="brush" slot="prefix" variant="regular"></wa-icon>
{% for theme in collections.theme | sort %}
{% if theme.fileSlug !== page.fileSlug %}
<wa-option value="{{ theme.fileSlug }}" data-palette="{{ theme.data.palette }}">{{ theme.data.title }}</wa-option>
{% endif %}
{% endfor %}
</wa-select>
<wa-select name="palette" label="Palette" size="small">
<wa-icon name="swatchbook" slot="prefix" variant="regular"></wa-icon>
<wa-option value="">(Theme default)</wa-option>
<wa-divider></wa-divider>
{% for p in collections.palette | sort %}
<wa-option value="{{ p.fileSlug }}">{{ p.data.title }}</wa-option>
{% endfor %}
</wa-select>
<wa-select name="color" label="Colors from…" size="small" value="">
<wa-icon name="palette" slot="prefix" variant="regular"></wa-icon>
<wa-option value="">(Theme default)</wa-option>
<wa-divider></wa-divider>
{% for theme in collections.theme | sort %}
<wa-option value="{{ theme.fileSlug }}">{{ theme.data.title }}</wa-option>
{% endfor %}
</wa-select>
<wa-select name="typography" label="Typography from…" size="small" value="">
<wa-icon name="font-case" slot="prefix"></wa-icon>
<wa-option value="">(Theme default)</wa-option>
<wa-divider></wa-divider>
{% for theme in collections.theme | sort %}
<wa-option value="{{ theme.fileSlug }}">{{ theme.data.title }}</wa-option>
{% endfor %}
</wa-select>
</p>
<script src="{{ page.url }}index.js" type="module"></script>
<wa-callout variant="warning">
<wa-icon slot="icon" name="triangle-exclamation" variant="regular"></wa-icon>
Please note that not all combinations will look good — once youre mixing and matching, youre on your own!
</wa-callout>
{% endblock %}
{% block afterContent %}
{% markdown %}
## How to use your remixed theme { #remixed-usage }
You can import your remixed theme by importing the individual theme files from the Web Awesome CDN.
{# Note: If you change the order here, you need to update index.js too #}
{% set stylesheets = [
'styles/themes/default.css',
'styles/color/default.css',
'styles/themes/default/color.css',
'styles/themes/default/typography.css'
] %}
{% set stylesheet = 'styles/themes/default/color.css' %}
{% include 'import-stylesheet-code.md.njk' %}
{% endmarkdown %}
{% endblock %}

18
docs/docs/themes/remixed/style.css vendored Normal file
View File

@@ -0,0 +1,18 @@
#mix_and_match {
strong {
display: flex;
align-items: center;
gap: var(--wa-space-2xs);
margin-top: 1.2em;
}
wa-select::part(label) {
margin-block-end: 0;
}
wa-select[value='']::part(display-input),
wa-option[value=''] {
font-style: italic;
color: var(--wa-color-text-quiet);
}
}

View File

@@ -9,6 +9,18 @@ body,
overflow: hidden;
}
html.remixed {
.remix-link {
display: none;
}
}
html:not(.remixed) {
#mix_and_match {
display: none;
}
}
#mix_and_match {
font-weight: var(--wa-font-weight-semibold);
color: var(--wa-color-text-quiet);