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 . + + +