diff --git a/docs/_includes/base.njk b/docs/_includes/base.njk index 3742a94db..d0d8cafd4 100644 --- a/docs/_includes/base.njk +++ b/docs/_includes/base.njk @@ -15,6 +15,8 @@ {# Docs styles #} + + {% block head %}{% endblock %} diff --git a/docs/_includes/page-card.njk b/docs/_includes/page-card.njk index 6afb3fb16..f63d813ed 100644 --- a/docs/_includes/page-card.njk +++ b/docs/_includes/page-card.njk @@ -5,6 +5,9 @@ {% include "svgs/" + (page.data.icon or "thumbnail-placeholder") + ".njk" %} {{ page.data.title }} + {% if pageSubtitle -%} +
{{ pageSubtitle }}
+ {%- endif %} {% endif %} diff --git a/docs/_includes/svgs/palette.njk b/docs/_includes/svgs/palette.njk index dbe9f4208..1faa7f837 100644 --- a/docs/_includes/svgs/palette.njk +++ b/docs/_includes/svgs/palette.njk @@ -1,4 +1,4 @@ -{% set paletteId = page.fileSlug %} +{% set paletteId = palette.fileSlug or page.fileSlug %} {% set tints = [80, 60, 40, 20] %} {% set width = 20 %} {% set height = 13 %} diff --git a/docs/_includes/svgs/theme-color.njk b/docs/_includes/svgs/theme-color.njk new file mode 100644 index 000000000..0c41f7f5d --- /dev/null +++ b/docs/_includes/svgs/theme-color.njk @@ -0,0 +1,24 @@ +{% set themeId = theme.fileSlug %} + +
+ +
diff --git a/docs/_includes/svgs/theme-typography.njk b/docs/_includes/svgs/theme-typography.njk new file mode 100644 index 000000000..06ba0212a --- /dev/null +++ b/docs/_includes/svgs/theme-typography.njk @@ -0,0 +1,16 @@ +{% set themeId = theme.fileSlug %} + +
+ +
diff --git a/docs/_layouts/theme.njk b/docs/_layouts/theme.njk index 03be52f24..6dbfd782e 100644 --- a/docs/_layouts/theme.njk +++ b/docs/_layouts/theme.njk @@ -4,72 +4,131 @@ {% extends '../_includes/base.njk' %} +{% block head %} + + + +{% endblock %} + {% block header %} + -

- - +

+ Remix - - - + + Customize this theme by changing its colors and/or remixing it with design elements from other themes! +

+ + - (Theme default) - {% for theme in collections.theme | sort %} - {% if theme.fileSlug !== page.fileSlug %} - {{ theme.data.title }} - {% endif %} + {% set currentTheme = theme.fileSlug == page.fileSlug %} + + +
+ {% include "svgs/theme-color.njk" %} +
+ + {{ theme.data.title }} + {% if theme.data.isPro %}PRO{% endif %} + {% if currentTheme %}This theme{% endif %} + +
+
{% endfor %}
- + - (Theme default) - - {% for p in collections.palette | sort %} - {% if p.fileSlug !== palette %} - {{ p.data.title }} - {% endif %} + {% set defaultPalette = palette %} + {% for palette in collections.palette | sort %} + {% set currentPalette = palette.fileSlug == defaultPalette %} + + +
+ {% include "svgs/" + (palette.data.icon or "thumbnail-placeholder") + ".njk" %} +
+ + {{ palette.data.title }} + {% if palette.data.isPro %}PRO{% endif %} + {% if currentPalette %}Theme default{% endif %} + +
+
+ {% endfor %} + {% set palette = defaultPalette %} +
+ + +
+ {% for hue in hues %} + {% set currentBrand = hue == brand %} + + {{ hue | capitalize }} + {% if currentBrand %}Theme default{% endif %} + {% endfor %}
- + - (Theme default) - {% for theme in collections.theme | sort %} - {% if theme.fileSlug !== page.fileSlug %} - {{ theme.data.title }} - {% endif %} + {% set currentTheme = theme.fileSlug == page.fileSlug %} + + +
+ {% include "svgs/theme-typography.njk" %} +
+ + {{ theme.data.title }} + {% if theme.data.isPro %}PRO{% endif %} + {% if currentTheme %}This theme{% endif %} + +
+
{% endfor %}
+ - -

- - -

Default Color Palette

+

Color

{% set paletteURL = '/docs/palettes/' + palette + '/' %} -{% set themePage = page %} -{% set page = paletteURL | getCollectionItemFromUrl %} -
-{% include 'page-card.njk' %} -
-{% set page = themePage %} + +
+ {% set themePage = page %} + {% set page = paletteURL | getCollectionItemFromUrl %} + {% set pageSubtitle = "Default color palette" %} + {% include 'page-card.njk' %} + {% set page = themePage %} + + +
{% endblock %} {% block afterContent %} diff --git a/docs/assets/styles/docs.css b/docs/assets/styles/docs.css index 37ddd9fa3..7491fc306 100644 --- a/docs/assets/styles/docs.css +++ b/docs/assets/styles/docs.css @@ -360,7 +360,7 @@ wa-page > main:has(> .index-grid) { } &::part(header) { - background-color: var(--wa-color-neutral-fill-quiet); + background-color: var(--header-background, var(--wa-color-neutral-fill-quiet)); border-bottom: none; display: flex; align-items: center; @@ -543,23 +543,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); - } - } } diff --git a/docs/assets/styles/theme-icons.css b/docs/assets/styles/theme-icons.css new file mode 100644 index 000000000..573e2aee1 --- /dev/null +++ b/docs/assets/styles/theme-icons.css @@ -0,0 +1,34 @@ +.theme-color-icon { + display: grid; + gap: var(--wa-space-xs); + grid-template-columns: repeat(4, auto); + + div { + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + border-radius: var(--wa-border-radius-m); + background-color: var(--background-color); + border: var(--wa-border-width-s) var(--wa-border-style) var(--border-color); + padding: var(--wa-space-2xs) var(--wa-space-xs); + color: var(--text-color); + font-weight: var(--wa-font-weight-semibold); + + &.plain { + font-weight: var(--wa-font-weight-bold); + } + } +} + +.theme-typography-icon { + display: flex; + flex-direction: column; + gap: var(--wa-space-xs); + + h3, + p { + margin-block: 0; + padding: 0; + } +} diff --git a/docs/docs/palettes/palettes.json b/docs/docs/palettes/palettes.json index 1e08935c4..3c8d82240 100644 --- a/docs/docs/palettes/palettes.json +++ b/docs/docs/palettes/palettes.json @@ -3,6 +3,7 @@ "tags": ["palettes", "palette"], "eleventyComputed": { "snippet": ".wa-palette-{{ page.fileSlug }}", - "icon": "palette" + "icon": "palette", + "file": "styles/color/{{ page.fileSlug }}.css" } } diff --git a/docs/docs/themes/active.md b/docs/docs/themes/active.md index 8b2f44b54..85dd20e01 100644 --- a/docs/docs/themes/active.md +++ b/docs/docs/themes/active.md @@ -4,4 +4,5 @@ description: Energetic and tactile, always in motion. isPro: true tags: pro palette: rudimentary +brand: green --- diff --git a/docs/docs/themes/awesome.md b/docs/docs/themes/awesome.md index 5a9e63e92..3954e27a1 100644 --- a/docs/docs/themes/awesome.md +++ b/docs/docs/themes/awesome.md @@ -3,4 +3,5 @@ title: Awesome description: Punchy and vibrant, the rockstar of themes. order: 0.2 palette: bright +brand: blue --- diff --git a/docs/docs/themes/brutalist.md b/docs/docs/themes/brutalist.md index 497c0a6ff..35cda07dd 100644 --- a/docs/docs/themes/brutalist.md +++ b/docs/docs/themes/brutalist.md @@ -4,4 +4,5 @@ description: Sharp, square, and unapologetically bold. isPro: true tags: pro palette: default +brand: blue --- diff --git a/docs/docs/themes/default.md b/docs/docs/themes/default.md index 7aa4f6802..e2da0baaf 100644 --- a/docs/docs/themes/default.md +++ b/docs/docs/themes/default.md @@ -3,4 +3,5 @@ title: Default description: Your trusty companion, like a perfectly broken-in pair of jeans. order: 0 palette: default +brand: blue --- diff --git a/docs/docs/themes/demo.njk b/docs/docs/themes/demo.njk index c511f639b..634fb20ad 100644 --- a/docs/docs/themes/demo.njk +++ b/docs/docs/themes/demo.njk @@ -41,11 +41,13 @@ function updateTheme() { const stylesheetURLs = { colors: id => `/dist/styles/themes/${ id }/color.css`, palette: id => `/dist/styles/color/${ id }.css`, + brand: id => `/dist/styles/brand/${ id }.css`, typography: id => `/dist/styles/themes/${ id }/typography.css` }; const icons = { colors: 'palette', palette: 'swatchbook', + brand: 'droplet', typography: 'font-case' } @@ -58,7 +60,7 @@ function updateTheme() { for (let name in stylesheetURLs) { if (params.get(name)) { let url = stylesheetURLs[name](params.get(name)); - script.insertAdjacentHTML('afterend', ``); + script.insertAdjacentHTML('beforebegin', ``); let docsURL = (name === 'palette' ? '/docs/palettes/' : '/docs/themes/') + params.get(name) + '/'; let title = params.get(name).replace(/^[a-z]/g, c => c.toUpperCase()); let icon = icons[name]; @@ -70,7 +72,7 @@ function updateTheme() { p.hidden = msgs.length === 0; if (msgs.length) { let icon = - p.innerHTML = ` Remixed ` + msgs.map(msg => ` + p.innerHTML = ` Remixed ` + msgs.map(msg => ` ${ msg }`).join(' '); } } diff --git a/docs/docs/themes/glossy.md b/docs/docs/themes/glossy.md index da84f5a86..60f8f8d66 100644 --- a/docs/docs/themes/glossy.md +++ b/docs/docs/themes/glossy.md @@ -4,4 +4,5 @@ description: Bustling with plenty of luster and shine. isPro: true tags: pro palette: elegant +brand: indigo --- diff --git a/docs/docs/themes/matter.md b/docs/docs/themes/matter.md index 5b45c14dc..5dfb418c5 100644 --- a/docs/docs/themes/matter.md +++ b/docs/docs/themes/matter.md @@ -4,6 +4,7 @@ description: Digital design inspired by the real world. isPro: true tags: pro palette: mild +brand: indigo --- Set the page theme to "{{ title }}" from the top right to preview the following examples. diff --git a/docs/docs/themes/playful.md b/docs/docs/themes/playful.md index 8cc08e1ec..514343eb1 100644 --- a/docs/docs/themes/playful.md +++ b/docs/docs/themes/playful.md @@ -4,4 +4,5 @@ description: Cheerful and engaging, like a playground on screen. isPro: true tags: pro palette: rudimentary +brand: purple --- diff --git a/docs/docs/themes/premium.md b/docs/docs/themes/premium.md index 40f38b00e..919d8847a 100644 --- a/docs/docs/themes/premium.md +++ b/docs/docs/themes/premium.md @@ -4,4 +4,5 @@ description: The ultimate in sophistication and style. isPro: true tags: pro palette: anodized +brand: cyan --- diff --git a/docs/docs/themes/remix.css b/docs/docs/themes/remix.css new file mode 100644 index 000000000..e9472e600 --- /dev/null +++ b/docs/docs/themes/remix.css @@ -0,0 +1,127 @@ +#mix_and_match { + margin-block-end: var(--wa-space-2xl); + + &::part(content) { + display: flex; + gap: var(--wa-space-xl); + padding-block-start: var(--wa-space-m); + } + + > [slot='summary'] { + margin: 0; + + wa-icon:first-of-type { + vertical-align: -0.15em; + color: var(--wa-color-text-quiet); + } + + wa-icon#what-is-remixing { + vertical-align: -0.1em; + font-size: var(--wa-font-size-s); + color: var(--wa-color-text-quiet); + } + } + + wa-select { + &::part(label) { + margin-block-end: 0; + } + } + + wa-select:has(> wa-option > wa-card) { + &::part(listbox) { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + width: min(90vw, 800px); + } + + &:state(blank)::part(display-input) { + font-style: italic; + color: var(--wa-color-text-quiet); + } + } + + wa-option:has(> wa-card) { + position: relative; + padding: var(--wa-space-smaller); + + &::part(checked-icon) { + position: absolute; + inset-block-start: calc(var(--wa-space-smaller) - 0.5em); + inset-inline-end: calc(var(--wa-space-smaller) - 0.5em); + width: 1em; + height: 1em; + line-height: 1em; + padding: 0.4em; + border-radius: var(--wa-border-radius-circle); + text-align: center; + background: var(--wa-color-brand-fill-loud); + color: var(--wa-color-brand-on-loud); + font-size: var(--wa-font-size-xs); + } + + wa-card { + --spacing: var(--wa-space-smaller); + + &::part(body) { + background: var(--wa-color-neutral-fill-quiet); + padding-block: var(--wa-space-s); + } + } + + &:state(current), + &:state(hover), + &:state(selected), + &:hover { + background: transparent; + } + + &:hover { + wa-card { + border-color: var(--wa-color-brand-border-loud); + box-shadow: 0 0 0 var(--wa-border-width-m) var(--wa-color-brand-border-normal); + } + } + + &[aria-selected='true'] { + wa-card { + --border-color: var(--wa-color-brand-border-loud); + box-shadow: 0 0 0 var(--wa-border-width-m) var(--wa-color-brand-border-loud); + + &::part(body) { + background: var(--wa-color-brand-fill-quiet); + } + } + } + } + + .selected-swatch, + wa-select[name='brand'] wa-option::before { + content: ''; + display: inline-block; + width: 1.2em; + aspect-ratio: 1; + flex: none; + border-radius: var(--wa-border-radius-m); + background: var(--color); + border: 1px solid var(--wa-color-surface-default); + } + + wa-select[name='brand'] wa-option { + white-space: nowrap; + + &::before { + width: 1em; + margin-inline: var(--wa-space-xs); + } + + &::part(checked-icon) { + order: 2; + } + } +} + +#test_select wa-option:state(selected) { + outline: 2px solid red; + background: yellow; +} diff --git a/docs/docs/themes/remix.js b/docs/docs/themes/remix.js new file mode 100644 index 000000000..e5e3add7e --- /dev/null +++ b/docs/docs/themes/remix.js @@ -0,0 +1,146 @@ +await Promise.all(['wa-select', 'wa-option', 'wa-details'].map(tag => customElements.whenDefined(tag))); +const domChange = document.startViewTransition ? document.startViewTransition.bind(document) : fn => fn(); + +let selects, data; + +let computed = { + get isRemixed() { + return Object.values(data.params).filter(Boolean).length > 0; + }, + get palette() { + return data.params.palette || data.defaultParams.palette; + }, + get brand() { + return data.params.brand || data.defaultParams.brand; + }, +}; + +function selectsChanged(event) { + data.params[event.target.name] = event.target.value; + render(event.target.name); +} + +function init() { + selects = Object.fromEntries( + [...document.querySelectorAll('#mix_and_match wa-select')].map(select => [select.getAttribute('name'), select]), + ); + + data = { + baseTheme: wa_data.baseTheme, + themes: wa_data.themes, + palettes: wa_data.palettes, + defaultParams: { + colors: '', + get palette() { + let colors = data.params.colors || data.baseTheme; + return data.themes[colors].palette; + }, + get brand() { + let colors = data.params.colors || data.baseTheme; + return data.themes[colors].brand; + }, + typography: '', + }, + params: { colors: '', palette: '', brand: '', typography: '' }, + urlParams: new URLSearchParams(location.search), + }; + + // Read URL params and apply them. This facilitates permalinks. + if (location.search) { + for (let aspect in data.params) { + if (data.urlParams.has(aspect)) { + data.params[aspect] = data.urlParams.get(aspect); + } + } + } + + if (computed.isRemixed) { + // Start with the remixing UI open if the theme has been remixed + mix_and_match.setAttribute('open', ''); + mix_and_match.open = true; + } + + for (let name in selects) { + selects[name].addEventListener('change', selectsChanged); + } + + Promise.all(Object.values(selects).map(select => select.updateComplete)).then(() => render()); + + return { selects, data, computed, render }; +} + +globalThis.remixApp = init(); + +// Async load CSS for other themes *before* current theme stylesheet +let themeStylesheet = document.querySelector('#theme-stylesheet'); + +for (const theme in data.themes) { + themeStylesheet.insertAdjacentHTML( + 'beforebegin', + ` + `, + ); +} + +for (const palette in data.palettes) { + themeStylesheet.insertAdjacentHTML( + 'beforebegin', + ``, + ); +} + +function setDefault(select, value) { + let oldDefaultOption = select.querySelector(`wa-option[value=""]:not([data-id="${value}"])`); + let newDefaultOption = select.querySelector(`wa-option[value="${value}"]`); + + if (oldDefaultOption) { + oldDefaultOption.value = oldDefaultOption.dataset.id; + } + + if (newDefaultOption) { + newDefaultOption.dataset.id ??= newDefaultOption.value; + newDefaultOption.value = ''; + } +} + +function render(changedAspect) { + let url = new URL(demo.src); + + if (!changedAspect || changedAspect === 'colors') { + // Update the default palette when the theme colors change to the default palette of that theme + setDefault(selects.palette, data.defaultParams.palette); + setDefault(selects.brand, data.defaultParams.brand); + } + + let brand = data.params.brand || data.defaultParams.brand; + selects.brand.style.setProperty('--color', `var(--wa-color-${brand})`); + selects.brand.className = `wa-palette-${computed.palette}`; + + for (let aspect in data.params) { + let value = data.params[aspect]; + + if (value) { + data.urlParams.set(aspect, value); + } else { + data.urlParams.delete(aspect); + } + + selects[aspect].value = value; + } + + // Update demo URL + domChange(() => { + url.search = data.urlParams; + demo.src = url; + return new Promise(resolve => (demo.onload = resolve)); + }); + + // Update page URL. If there’s already a search, replace it. + // We don’t want to clog the user’s history while they iterate + let historyAction = location.search ? 'replaceState' : 'pushState'; + history[historyAction](null, '', `?${data.urlParams}`); +} + +addEventListener('turbo:render', event => { + remixApp = init(); +}); diff --git a/docs/docs/themes/showcase.css b/docs/docs/themes/showcase.css index d073cf0f8..b4693e0fe 100644 --- a/docs/docs/themes/showcase.css +++ b/docs/docs/themes/showcase.css @@ -14,7 +14,18 @@ body, color: var(--wa-color-text-quiet); wa-icon { - vertical-align: middle; + vertical-align: -0.15em; + } + + > strong { + margin-inline-end: var(--wa-space-2xs); + } + + wa-badge { + > a { + text-decoration: none; + color: inherit; + } } } diff --git a/docs/docs/themes/tailspin.md b/docs/docs/themes/tailspin.md index 0623424ce..212376fe7 100644 --- a/docs/docs/themes/tailspin.md +++ b/docs/docs/themes/tailspin.md @@ -4,4 +4,5 @@ description: Like a bird in flight, guiding you from there to here. isPro: true tags: pro palette: vogue +brand: indigo --- diff --git a/docs/docs/themes/themes.json b/docs/docs/themes/themes.json index 11dfb3eb0..3af2e88e3 100644 --- a/docs/docs/themes/themes.json +++ b/docs/docs/themes/themes.json @@ -1,5 +1,9 @@ { "layout": "theme.njk", "wide": true, - "tags": ["themes", "theme"] + "tags": ["themes", "theme"], + "brand": "blue", + "eleventyComputed": { + "file": "styles/themes/{{ page.fileSlug }}.css" + } } diff --git a/src/components/option/option.ts b/src/components/option/option.ts index cc67a0831..d2b3e78c2 100644 --- a/src/components/option/option.ts +++ b/src/components/option/option.ts @@ -155,6 +155,8 @@ export default class WaOption extends WebAwesomeElement { console.error(`Option values cannot include a space. All spaces have been replaced with underscores.`, this); this.value = this.value.replace(/ /g, '_'); } + + this.handleDefaultSlotChange(); } if (changedProperties.has('current')) {