From 0b4c1a59346c6d9bc6c162bc202dbcb9095f60ce Mon Sep 17 00:00:00 2001 From: Lea Verou Date: Tue, 20 May 2025 10:16:49 -0400 Subject: [PATCH] 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 `` * 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 * ``: 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 Co-authored-by: Cory LaViska --- docs/_data/themes.js | 61 ++++++++ docs/_includes/sidebar-link.njk | 2 +- .../svgs/{comparer.njk => comparison.njk} | 0 docs/_includes/theme-showcase.njk | 26 +-- docs/_utils/filters.js | 9 ++ docs/assets/components/scoped.js | 25 +-- docs/assets/data/palettes.js.njk | 27 +++- docs/assets/data/themes.js.njk | 17 +- docs/assets/data/theming.js | 38 ++++- docs/assets/scripts/permalink.js | 39 +++-- docs/assets/scripts/tweak/code.js | 6 +- docs/assets/scripts/util/string.js | 18 +++ docs/assets/styles/theme-icons.css | 28 ++++ docs/assets/vue/components/editable-text.js | 28 ++-- docs/assets/vue/components/fonts-card.js | 4 +- docs/assets/vue/components/index.js | 1 + docs/assets/vue/components/info-tip.js | 3 +- docs/assets/vue/components/palette-card.js | 2 +- docs/assets/vue/components/panel.css | 12 +- docs/assets/vue/components/swatch-select.js | 21 +-- docs/assets/vue/components/theme-card.js | 40 +++-- docs/assets/vue/components/ui-slider.css | 46 ++++++ docs/assets/vue/components/ui-slider.js | 86 ++++++++++ docs/assets/vue/mixins/input.js | 32 ++++ docs/docs/components/cheatsheet/index.njk | 2 +- .../components/{comparer.md => comparison.md} | 12 +- docs/docs/palettes/data.js.njk | 32 ---- docs/docs/palettes/data.json.njk | 27 ---- docs/docs/palettes/tweak.js | 15 +- docs/docs/resources/changelog.md | 4 +- docs/docs/themes/active.md | 6 +- docs/docs/themes/awesome.md | 6 +- docs/docs/themes/brutalist.md | 5 - docs/docs/themes/classic.md | 5 - docs/docs/themes/data.js.njk | 22 --- docs/docs/themes/default.md | 5 - docs/docs/themes/demo/index.njk | 8 +- docs/docs/themes/edit/index.js | 148 ++++++++++++++---- docs/docs/themes/edit/index.njk | 76 ++++++++- docs/docs/themes/edit/style.css | 93 ++++++++++- docs/docs/themes/glossy.md | 5 - docs/docs/themes/matter.md | 5 - docs/docs/themes/mellow.md | 5 - docs/docs/themes/playful.md | 6 +- docs/docs/themes/premium.md | 5 - docs/docs/themes/preview/preview.js | 28 +++- docs/docs/themes/tailspin.md | 5 - .../comparison.css} | 17 +- .../comparison.test.ts} | 76 ++++----- .../comparer.ts => comparison/comparison.ts} | 20 ++- src/styles/themes/active/dimension.css | 24 ++- src/styles/themes/awesome.css | 1 + src/styles/themes/awesome/dimension.css | 89 +++++++++++ src/styles/themes/awesome/overrides.css | 62 -------- src/styles/themes/brutalist.css | 2 +- src/styles/themes/brutalist/overrides.css | 2 +- src/styles/themes/glossy/dimension.css | 39 ++++- src/styles/themes/playful/dimension.css | 49 +++--- 58 files changed, 1027 insertions(+), 450 deletions(-) create mode 100644 docs/_data/themes.js rename docs/_includes/svgs/{comparer.njk => comparison.njk} (100%) create mode 100644 docs/assets/vue/components/ui-slider.css create mode 100644 docs/assets/vue/components/ui-slider.js create mode 100644 docs/assets/vue/mixins/input.js rename docs/docs/components/{comparer.md => comparison.md} (93%) delete mode 100644 docs/docs/palettes/data.js.njk delete mode 100644 docs/docs/palettes/data.json.njk delete mode 100644 docs/docs/themes/data.js.njk rename src/components/{comparer/comparer.css => comparison/comparison.css} (92%) rename src/components/{comparer/comparer.test.ts => comparison/comparison.test.ts} (81%) rename src/components/{comparer/comparer.ts => comparison/comparison.ts} (90%) create mode 100644 src/styles/themes/awesome/dimension.css diff --git a/docs/_data/themes.js b/docs/_data/themes.js new file mode 100644 index 000000000..86e505ead --- /dev/null +++ b/docs/_data/themes.js @@ -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-(?[a-z-]+)?:\s*(?.+?)\s*(\/\*.+?\*\/)?\s*;$/gm; +const importRegex = /^\s*@import\s+url\(['"](?.+?)['"]\);$/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', ''); +} diff --git a/docs/_includes/sidebar-link.njk b/docs/_includes/sidebar-link.njk index a7629c1d6..fafaf399d 100644 --- a/docs/_includes/sidebar-link.njk +++ b/docs/_includes/sidebar-link.njk @@ -1,4 +1,4 @@ -{% if page -%} +{% if page and not page.data.unlisted -%}
  • {{ page.data.title }} {% if page.data.status == 'experimental' %}{% endif %} diff --git a/docs/_includes/svgs/comparer.njk b/docs/_includes/svgs/comparison.njk similarity index 100% rename from docs/_includes/svgs/comparer.njk rename to docs/_includes/svgs/comparison.njk diff --git a/docs/_includes/theme-showcase.njk b/docs/_includes/theme-showcase.njk index 64945da36..6a9625547 100644 --- a/docs/_includes/theme-showcase.njk +++ b/docs/_includes/theme-showcase.njk @@ -118,7 +118,7 @@
    - @@ -195,7 +195,7 @@ - Get this Plan + Get this Plan
    -
    diff --git a/docs/_utils/filters.js b/docs/_utils/filters.js index c943c17a8..5b0f5a285 100644 --- a/docs/_utils/filters.js +++ b/docs/_utils/filters.js @@ -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); +} diff --git a/docs/assets/components/scoped.js b/docs/assets/components/scoped.js index 781768ac5..a4c777119 100644 --- a/docs/assets/components/scoped.js +++ b/docs/assets/components/scoped.js @@ -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: diff --git a/docs/docs/themes/edit/index.js b/docs/docs/themes/edit/index.js index 3e5da3039..6837d3587 100644 --- a/docs/docs/themes/edit/index.js +++ b/docs/docs/themes/edit/index.js @@ -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: '', + }; +} diff --git a/docs/docs/themes/edit/index.njk b/docs/docs/themes/edit/index.njk index 7113a2c0e..1a3250cdc 100644 --- a/docs/docs/themes/edit/index.njk +++ b/docs/docs/themes/edit/index.njk @@ -22,7 +22,7 @@ unlisted: true
    -

    +

    @@ -88,6 +88,8 @@ unlisted: true + + @@ -95,7 +97,12 @@ unlisted: true - + + + Editing the starting theme will reset your . + + +