diff --git a/declaration.d.ts b/declaration.d.ts
index f8afc5e132..a202e6ff21 100644
--- a/declaration.d.ts
+++ b/declaration.d.ts
@@ -7,3 +7,12 @@ declare module 'prism-esm/plugins/line-numbers/prism-line-numbers.js' {
import type { Prism } from "prism-esm";
export function Plugin(prism: Prism): void
}
+
+declare module '@11ty/eleventy-plugin-syntaxhighlight/src/HighlightPairedShortcode.js' {
+ export default function HighlightPairedShortcode(content: string, language: string, highlightLines: string, options: object): any
+}
+
+declare module '@11ty/eleventy-plugin-syntaxhighlight/src/getAttributes.js' {
+ export default function getAttributes(...args: any[]): string
+}
+
diff --git a/docs/_plugins/lit-ssr/lit.cjs b/docs/_plugins/lit-ssr/lit.cjs
new file mode 100644
index 0000000000..9d00feedc5
--- /dev/null
+++ b/docs/_plugins/lit-ssr/lit.cjs
@@ -0,0 +1,110 @@
+/**
+ * @license based on code from eleventy-plugin-lit
+ * Copyright 2021 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+const path = require('node:path');
+const { pathToFileURL } = require('node:url');
+// eslint-disable-next-line no-redeclare
+const { Worker } = require('node:worker_threads');
+
+// Lit SSR includes comment markers to track the outer template from
+// the template we've generated here, but it's not possible for this
+// outer template to be hydrated, so they serve no purpose.
+function trimOuterMarkers(renderedContent) {
+ return renderedContent
+ .replace(/^(()|(<\?>)|\s)+/, '')
+ .replace(/(()|(<\?>)|\s)+$/, '');
+}
+
+/**
+ * @param {import('@11ty/eleventy').UserConfig} eleventyConfig
+ * @param {{componentModules: string[]}} resolvedComponentModules
+ */
+module.exports = function(eleventyConfig, { componentModules } = {}) {
+ if (componentModules === undefined || componentModules.length === 0) {
+ // If there are no component modules, we could never have anything to
+ // render.
+ return;
+ }
+
+ const resolvedComponentModules = componentModules.map(module =>
+ pathToFileURL(path.resolve(process.cwd(), module)).href);
+
+ let worker;
+
+ const requestIdResolveMap = new Map();
+ let requestId = 0;
+
+ eleventyConfig.on('eleventy.before', async function() {
+ worker = new Worker(path.resolve(__dirname, './worker/worker.js'));
+
+ worker.on('error', err => {
+ // eslint-disable-next-line no-console
+ console.error('Unexpected error while rendering lit component in worker thread', err);
+ throw err;
+ });
+
+ let requestResolve;
+ const requestPromise = new Promise(resolve => {
+ requestResolve = resolve;
+ });
+
+ worker.on('message', message => {
+ switch (message.type) {
+ case 'initialize-response': {
+ requestResolve();
+ break;
+ }
+
+ case 'render-response': {
+ const { id, rendered } = message;
+ const resolve = requestIdResolveMap.get(id);
+ if (resolve === undefined) {
+ throw new Error(
+ '@lit-labs/eleventy-plugin-lit received invalid render-response message'
+ );
+ }
+ resolve(rendered);
+ requestIdResolveMap.delete(id);
+ break;
+ }
+ }
+ });
+
+ const message = {
+ type: 'initialize-request',
+ imports: resolvedComponentModules,
+ };
+
+ worker.postMessage(message);
+ await requestPromise;
+ });
+
+ eleventyConfig.on('eleventy.after', async () => {
+ await worker.terminate();
+ });
+
+ eleventyConfig.addTransform('render-lit', async function(content) {
+ if (!this.page.outputPath.endsWith('.html')) {
+ return content;
+ }
+
+ const renderedContent = await new Promise(resolve => {
+ requestIdResolveMap.set(requestId, resolve);
+ const message = {
+ type: 'render-request',
+ id: requestId++,
+ content,
+ page: JSON.parse(JSON.stringify(this.page)),
+ };
+ worker.postMessage(message);
+ });
+
+ const outerMarkersTrimmed = trimOuterMarkers(renderedContent);
+ return outerMarkersTrimmed;
+ }
+ );
+};
+
diff --git a/docs/_plugins/lit-ssr/worker/worker.js b/docs/_plugins/lit-ssr/worker/worker.js
new file mode 100644
index 0000000000..f7992027fa
--- /dev/null
+++ b/docs/_plugins/lit-ssr/worker/worker.js
@@ -0,0 +1,73 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+/** @import { RHDSSSRController } from '@rhds/elements/lib/ssr-controller.js' */
+/** @import { ReactiveController } from 'lit' */
+
+import { parentPort } from 'worker_threads';
+import { render } from '@lit-labs/ssr';
+import { unsafeHTML } from 'lit/directives/unsafe-html.js';
+import { collectResult } from '@lit-labs/ssr/lib/render-result.js';
+
+import { LitElementRenderer } from '@lit-labs/ssr/lib/lit-element-renderer.js';
+
+import { ssrControllerMap } from '@rhds/elements/lib/ssr-controller.js';
+
+if (parentPort === null) {
+ throw new Error('worker.js must only be run in a worker thread');
+}
+
+let initialized = false;
+
+/**
+ * @param {ReactiveController} controller
+ * @returns {controller is RHDSSSRController}
+ */
+function isRHDSSSRController(controller) {
+ return !!controller.isRHDSSSRController;
+}
+
+parentPort.on('message', async message => {
+ switch (message.type) {
+ case 'initialize-request': {
+ if (!initialized) {
+ await Promise.all(message.imports.map(module => import(module)));
+ parentPort.postMessage({ type: 'initialize-response' });
+ }
+ initialized = true;
+ break;
+ }
+
+ case 'render-request': {
+ const { id, content, page } = message;
+ const result = render(unsafeHTML(content), {
+ elementRenderers: [
+ class RHDSSSRableRenderer extends LitElementRenderer {
+ * renderShadow(renderInfo) {
+ const controllers = ssrControllerMap.get(this.element);
+ yield controllers?.map(async x => {
+ if (isRHDSSSRController(x)) {
+ x.page = page;
+ await x.ssrSetup();
+ return [];
+ }
+ }) ?? [];
+ yield* super.renderShadow(renderInfo);
+ }
+ },
+ ],
+ });
+ const rendered = await collectResult(result);
+ parentPort.postMessage({
+ type: 'render-response',
+ id,
+ rendered,
+ });
+ break;
+ }
+ }
+});
+
diff --git a/docs/_plugins/rhds.cjs b/docs/_plugins/rhds.cjs
index 1b384b2bad..d5c8223ef2 100644
--- a/docs/_plugins/rhds.cjs
+++ b/docs/_plugins/rhds.cjs
@@ -368,9 +368,6 @@ module.exports = function(eleventyConfig, { tagsToAlphabetize }) {
}
});
- eleventyConfig.addWatchTarget('docs/patterns/**/patterns/*.html');
- eleventyConfig.addWatchTarget('docs/theming/**/patterns/*.html');
-
for (const tagName of fs.readdirSync(path.join(process.cwd(), './elements/'))) {
const dir = path.join(process.cwd(), './elements/', tagName, 'docs/');
eleventyConfig.addWatchTarget(dir);
diff --git a/docs/_plugins/shortcodes.cjs b/docs/_plugins/shortcodes.cjs
index 539d15628d..85c70347a2 100644
--- a/docs/_plugins/shortcodes.cjs
+++ b/docs/_plugins/shortcodes.cjs
@@ -7,7 +7,6 @@ const RenderInstallation = require('./shortcodes/renderInstallation.cjs');
const RenderLightDom = require('./shortcodes/renderLightDom.cjs');
const RenderCodeDocs = require('./shortcodes/renderCodeDocs.cjs');
const SpacerTokensTable = require('./shortcodes/spacerTokensTable.cjs');
-const UxdotPattern = require('./shortcodes/uxdotPattern.cjs');
module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(RepoStatusList);
@@ -17,5 +16,4 @@ module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(RenderLightDom);
eleventyConfig.addPlugin(SpacerTokensTable);
eleventyConfig.addPlugin(RenderCodeDocs);
- eleventyConfig.addPlugin(UxdotPattern);
};
diff --git a/docs/_plugins/shortcodes/uxdotPattern.cjs b/docs/_plugins/shortcodes/uxdotPattern.cjs
deleted file mode 100644
index 0f5b537741..0000000000
--- a/docs/_plugins/shortcodes/uxdotPattern.cjs
+++ /dev/null
@@ -1,113 +0,0 @@
-const { readFile } = require('node:fs/promises');
-const { join } = require('node:path');
-const { pathToFileURL } = require('node:url');
-
-// for editor highlighting
-const html = String.raw;
-
-// because markdown, we have to replace newlines with a cookie crumb
-const COMMENT = '';
-
-/**
- * Returns a string with common indent stripped from each line. Useful for templating HTML
- * @param {string} str
- */
-function dedent(str) {
- const stripped = str.replace(/^\n/, '');
- const match = stripped.match(/^\s+/);
- return match ? stripped.replace(new RegExp(`^${match[0]}`, 'gm'), '') : str;
-}
-
-async function getCss(partial, kwargs) {
- const Tools = await import('@parse5/tools');
- let cssContent = kwargs.css ? await readFile(new URL(kwargs.css, kwargs.baseUrl), 'utf-8') : '';
- for (const styleTag of Tools.queryAll(partial, node =>
- Tools.isElementNode(node) && node.tagName === 'style')) {
- cssContent += `\n${dedent(Tools.getTextContent(styleTag))}`;
- Tools.removeNode(styleTag);
- }
- return cssContent.trim();
-}
-
-/**
- * **LF**
- * **LF**
- */
-const NEWLINE_RE = /\n\n/gm;
-
-const dash = s => s.replace(/([A-Z])/g, '-$1');
-
-const attrMap = attrs => Object.entries(attrs)
- .map(([name, value]) => `${value ? dash(name) : ''}${value === true || value === false ? '' : `="${value}"`}`)
- .join(' ');
-
-/** @param {import('@11ty/eleventy').UserConfig} eleventyConfig */
-module.exports = function(eleventyConfig) {
- eleventyConfig.addPairedShortcode('uxdotPattern', async function(_content, kwargs = {}) {
- const { __keywords, src, js, fullHeight, ...patternArgs } = kwargs;
- if ('allow' in patternArgs) {
- const allowed = patternArgs.allow.split(',').map(x => x.trim());
- patternArgs.colorPalette ??= 'lightest';
- while (allowed.length && !allowed.includes(patternArgs.colorPalette)) {
- patternArgs.colorPalette = allowed.shift();
- }
- }
- const { parseFragment, serialize } = await import('parse5');
- const content = !src ? _content : await readFile(join(process.cwd(), src), 'utf8');
- const partial = parseFragment(content);
- const baseUrl = pathToFileURL(this.page.inputPath);
- const cssContent = await getCss(partial, { ...kwargs, baseUrl });
- const jsContent = js && await readFile(new URL(js, baseUrl), 'utf-8');
- return html`
-
- Example
- ${content}
- HTML
-
- Copy to Clipboard
- Copied!
-
- ${!cssContent ? '' : html`
- CSS
-
- Copy to Clipboard
- Copied!
-
- `}${!jsContent ? '' : html`
- JavaScript
-
- Copy to Clipboard
- Copied!
- `}
-
-
-`;
- });
-
- eleventyConfig.addTransform('uxdot-pattern-restore-newlines', async function(content) {
- const { parse, serialize } = await import('parse5');
- const {
- queryAll,
- isElementNode,
- isTextNode,
- getTextContent,
- setTextContent,
- } = await import('@parse5/tools');
- const document = parse(content);
- const isUxDotPattern = node =>
- isElementNode(node)
- && node.tagName === 'uxdot-pattern';
- for (const pattern of queryAll(document, isUxDotPattern)) {
- for (const node of queryAll(pattern, isTextNode)) {
- setTextContent(node, getTextContent(node).replaceAll(COMMENT, '\n\n'));
- }
- }
- return serialize(document);
- });
-};
diff --git a/docs/assets/javascript/elements/uxdot-pattern-ssr-controller-client.ts b/docs/assets/javascript/elements/uxdot-pattern-ssr-controller-client.ts
new file mode 100644
index 0000000000..14b6045421
--- /dev/null
+++ b/docs/assets/javascript/elements/uxdot-pattern-ssr-controller-client.ts
@@ -0,0 +1,26 @@
+import type { UxdotPattern } from './uxdot-pattern.js';
+import { isServer } from 'lit';
+import { RHDSSSRController } from '@rhds/elements/lib/ssr-controller.js';
+
+/** Hydrate the results of SSR on the client */
+export class UxdotPatternSSRControllerClient extends RHDSSSRController {
+ allContent?: Node;
+ htmlContent?: Node;
+ jsContent?: Node;
+ cssContent?: Node;
+ hasCss = false;
+ hasJs = false;
+ constructor(host: UxdotPattern) {
+ super(host);
+ const { shadowRoot, hasUpdated } = this.host;
+ if (!isServer && shadowRoot && !hasUpdated) {
+ this.allContent ||= shadowRoot.getElementById('content')!;
+ this.htmlContent ||= shadowRoot.querySelector('.language-html')!;
+ this.jsContent ||= shadowRoot.querySelector('.language-js')!;
+ this.cssContent ||= shadowRoot.querySelector('.language-css')!;
+ this.hasCss = !this.cssContent?.textContent?.trim();
+ this.hasJs = !this.jsContent?.textContent?.trim();
+ }
+ }
+}
+
diff --git a/docs/assets/javascript/elements/uxdot-pattern-ssr-controller-server.ts b/docs/assets/javascript/elements/uxdot-pattern-ssr-controller-server.ts
new file mode 100644
index 0000000000..6a6bc730fa
--- /dev/null
+++ b/docs/assets/javascript/elements/uxdot-pattern-ssr-controller-server.ts
@@ -0,0 +1,119 @@
+import type { DirectiveResult } from 'lit/directive.js';
+
+import { readFile } from 'node:fs/promises';
+import { join, dirname } from 'node:path';
+import { pathToFileURL } from 'node:url';
+import { parseFragment, serialize } from 'parse5';
+import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
+import { RHDSSSRController } from '@rhds/elements/lib/ssr-controller.js';
+
+import * as Tools from '@parse5/tools';
+import type { UxdotPattern } from './uxdot-pattern.js';
+
+let HighlightPairedShortcode: (
+ content: string,
+ language: string,
+ highlights: '',
+ options: {
+ lineSeparator: string;
+ errorOnInvalidLanguage: boolean;
+ alwaysWrapLineHighlights: boolean;
+ preAttributes: Record;
+ codeAttributes: Record;
+ },
+) => string;
+
+function dedent(str: string) {
+ const stripped = str.replace(/^\n/, '');
+ const match = stripped.match(/^\s+/);
+ return match ? stripped.replace(new RegExp(`^${match[0]}`, 'gm'), '') : str;
+}
+
+interface EleventyPageData {
+ inputPath: string;
+ outputPath: string;
+}
+
+export class UxdotPatternSSRControllerServer extends RHDSSSRController {
+ declare host: UxdotPattern;
+ allContent?: DirectiveResult;
+ htmlContent?: DirectiveResult;
+ cssContent?: DirectiveResult;
+ jsContent?: DirectiveResult;
+ hasCss = false;
+ hasJs = false;
+ // this is set in the worker
+ page!: EleventyPageData;
+
+
+ async #extractInlineContent(kind: 'js' | 'css', partial: Tools.Node) {
+ const prop = kind === 'js' ? 'jsSrc' as const : 'cssSrc' as const;
+ const nodePred = kind === 'js' ? (node: Tools.Element) => node.tagName === 'script'
+ : kind === 'css' ? (node: Tools.Element) => node.tagName === 'style'
+ : () => false;
+ const baseUrl = pathToFileURL(this.page.inputPath);
+ let content = !this.host[prop] ? ''
+ : await readFile(new URL(this.host[prop], baseUrl.href), 'utf-8');
+ for (const scriptTag of Tools.queryAll(partial, node =>
+ Tools.isElementNode(node) && nodePred(node))) {
+ content += `\n${dedent(Tools.getTextContent(scriptTag))}`;
+ Tools.removeNode(scriptTag);
+ }
+ return content.trim();
+ }
+
+ async #getPatternContent() {
+ const { src } = this.host;
+ if (!src) {
+ return '';
+ } else {
+ return readFile(join(dirname(this.page.inputPath), src), 'utf8');
+ }
+ }
+
+ #highlight(language: string, content: string) {
+ const result = HighlightPairedShortcode(content, language, '', {
+ lineSeparator: '\n',
+ errorOnInvalidLanguage: false,
+ alwaysWrapLineHighlights: false,
+ preAttributes: {},
+ codeAttributes: {},
+ }) ?? '';
+ return result;
+ }
+
+ async #loadHighlighter() {
+ // START workaround for ssr: prism will try to use the DOM is `document` is available
+ // but lit-ssr needs the shims, so delete it before highlighting and restore it after
+ const shim = globalThis.document;
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ delete globalThis.document;
+ const hl = await import('@11ty/eleventy-plugin-syntaxhighlight/src/HighlightPairedShortcode.js')
+ .then(m => m.default);
+ // END workaround
+ globalThis.document = shim;
+ return hl;
+ }
+
+ async ssrSetup() {
+ HighlightPairedShortcode ||= await this.#loadHighlighter();
+ const allContent = await this.#getPatternContent();
+ const partial = parseFragment(allContent);
+
+ // NB: the css and js content functions *mutate* the partial,
+ // so it's important that the HTML content is serialized last, and that
+ // the entire content is printed as the runtime portion of the pattern.
+ const cssContent = await this.#extractInlineContent('css', partial);
+ const jsContent = await this.#extractInlineContent('js', partial);
+ const htmlContent = serialize(partial).trim();
+
+ this.hasCss = !!cssContent.length;
+ this.hasJs = !!jsContent.length;
+ this.allContent = unsafeHTML(allContent);
+ this.cssContent = unsafeHTML(this.#highlight('css', cssContent));
+ this.jsContent = unsafeHTML(this.#highlight('js', jsContent));
+ this.htmlContent = unsafeHTML(this.#highlight('html', htmlContent));
+ }
+}
+
diff --git a/docs/assets/javascript/elements/uxdot-pattern-ssr-controller.ts b/docs/assets/javascript/elements/uxdot-pattern-ssr-controller.ts
new file mode 100644
index 0000000000..70c26ac4f5
--- /dev/null
+++ b/docs/assets/javascript/elements/uxdot-pattern-ssr-controller.ts
@@ -0,0 +1,8 @@
+/* eslint-disable @stylistic/max-len */
+import { isServer } from 'lit';
+import type { UxdotPatternSSRControllerServer } from './uxdot-pattern-ssr-controller-server.js';
+import type { UxdotPatternSSRControllerClient } from './uxdot-pattern-ssr-controller-client.js';
+
+export const UxdotPatternSSRController: typeof UxdotPatternSSRControllerClient | typeof UxdotPatternSSRControllerServer =
+ isServer ? await import('./uxdot-pattern-ssr-controller-server.js').then(m => m.UxdotPatternSSRControllerServer)
+ : await import('./uxdot-pattern-ssr-controller-client.js').then(m => m.UxdotPatternSSRControllerClient);
diff --git a/docs/assets/javascript/elements/uxdot-pattern.css b/docs/assets/javascript/elements/uxdot-pattern.css
index e4f6456298..88b2c24662 100644
--- a/docs/assets/javascript/elements/uxdot-pattern.css
+++ b/docs/assets/javascript/elements/uxdot-pattern.css
@@ -1,89 +1,97 @@
+@import url('/styles/rh-code-block-lightdom.css');
+
:host {
+ container: host / inline-size;
display: block;
- container-name: host;
- container-type: inline-size;
- margin-block-end: var(--rh-space-2xl, 32px);
+ margin-block-start: var(--rh-space-2xl);
+ max-width: 56rem; /* warning: magic number */
}
#container {
display: grid;
- grid-template-columns: 1fr;
- grid-template-areas: 'controls' 'example' 'code';
- gap: var(--rh-space-2xl, 32px) var(--rh-space-4xl, 64px);
- padding: var(--rh-space-2xl, 32px);
- border: var(--rh-border-width-sm, 1px) solid var(--rh-color-border-subtle);
- border-radius: var(--rh-border-radius-default, 3px);
- background-color: var(--rh-color-surface);
- color: var(--rh-color-text-primary);
- border-color: var(--rh-color-border-subtle);
+ grid-template-columns: auto;
+ grid-template-areas: 'heading' 'controls' 'content' 'code';
+ gap: var(--rh-space-2xl) var(--rh-space-4xl);
- --rh-color-surface: inherit;
-}
+ @container host (width >= 530px) {
+ grid-template-columns: auto auto;
+ grid-template-areas:
+ 'heading controls'
+ 'content content'
+ 'code code';
+ }
-#container h3,
-#container ::slotted(h2),
-#container ::slotted(h3),
-#container ::slotted(h4),
-#container ::slotted(h5),
-#container ::slotted(h6) {
- margin-block: 0 var(--rh-space-lg, 16px) !important;
- font-size: var(--rh-font-size-body-text-lg, 1.125rem) !important;
+ @container host (width >= 992px) {
+ grid-template-areas:
+ 'heading heading'
+ 'controls controls'
+ 'content content'
+ 'code code';
+ }
}
-#color-picker {
- grid-area: controls;
- justify-self: flex-end;
+#heading {
+ grid-area: heading;
display: flex;
align-items: center;
- gap: var(--rh-space-lg, 16px);
}
-#example {
- grid-area: example;
-}
+#description { grid-area: content; }
-:host([stacked]) #example ::slotted(*) {
- max-width: 100%;
+h3,
+::slotted(h2),
+::slotted(h3),
+::slotted(h4),
+::slotted(h5),
+::slotted(h6) {
+ margin-block: 0 !important;
+ font-size: var(--rh-font-size-body-text-lg) !important;
}
-#code {
- grid-area: code;
- display: flex;
- flex-direction: column;
- gap: var(--rh-space-lg, 16px);
-}
+#content {
+ display: block;
+ resize: vertical;
+ overflow: auto;
+ grid-area: content;
+ padding: var(--rh-space-lg);
+ border: var(--rh-border-width-sm) solid var(--rh-color-border-subtle);
+ border-radius: var(--rh-border-radius-default);
+ background-color: var(--rh-color-surface);
+ color: var(--rh-color-text-primary);
+ border-color: var(--rh-color-border-subtle);
-#container.noColorPicker {
- grid-template-areas: 'example' 'code';
+ @container host (width > 300px) { padding: var(--rh-space-xl); }
+
+ @container host (width > 540px) { padding: var(--rh-space-4xl); }
}
-#color-picker.noColorPicker {
- display: none !important;
+#content > * {
+ margin-inline: auto;
}
-#example slot[name='heading'] {
- display: block;
- grid-column: -1/1;
+#color-picker {
+ grid-area: controls;
+ justify-self: flex-end;
+ display: flex;
+ align-items: center;
+ gap: var(--rh-space-lg, 16px);
}
-@container host (min-width: 992px) {
- #container {
- grid-template-columns: max-content auto;
- grid-template-areas:
- 'controls controls'
- 'example code';
- }
+rh-tabs {
+ border: var(--rh-border-width-sm) solid var(--rh-color-border-subtle);
+ border-radius: var(--rh-border-radius-default);
+ overflow: hidden;
+ grid-area: code;
- #container.noColorPicker {
- grid-template-areas: 'example code';
+ & rh-tab-panel {
+ padding: 0;
+ border-radius: 0;
}
- #container.stacked {
- grid-template-columns: 1fr;
- grid-template-areas: 'controls' 'example' 'code';
- }
+ & rh-code-block {
+ --rh-border-radius-default: 0;
+ --rh-border-width-sm: 0px;
- #container.stacked.noColorPicker {
- grid-template-areas: 'example' 'code';
+ border-width: 0;
}
}
diff --git a/docs/assets/javascript/elements/uxdot-pattern.ts b/docs/assets/javascript/elements/uxdot-pattern.ts
index 62444968ae..a44db089a3 100644
--- a/docs/assets/javascript/elements/uxdot-pattern.ts
+++ b/docs/assets/javascript/elements/uxdot-pattern.ts
@@ -1,14 +1,10 @@
+import type { ColorPalette } from '@rhds/elements/lib/context/color/provider.js';
+
import { LitElement, html, isServer } from 'lit';
-import { SlotController } from '@patternfly/pfe-core/controllers/slot-controller.js';
import { customElement } from 'lit/decorators/custom-element.js';
import { property } from 'lit/decorators/property.js';
-import { classMap } from 'lit/directives/class-map.js';
-import {
- colorContextProvider,
- type ColorPalette,
-} from '@rhds/elements/lib/context/color/provider.js';
import {
colorContextConsumer,
type ColorTheme,
@@ -20,9 +16,12 @@ import {
paletteNames,
} from '@rhds/elements/lib/elements/rh-context-picker/rh-context-picker.js';
+import '@rhds/elements/lib/elements/rh-context-picker/rh-context-picker.js';
import '@rhds/elements/rh-surface/rh-surface.js';
import '@rhds/elements/rh-code-block/rh-code-block.js';
-import '@rhds/elements/lib/elements/rh-context-picker/rh-context-picker.js';
+import '@rhds/elements/rh-tabs/rh-tabs.js';
+
+import { UxdotPatternSSRController } from './uxdot-pattern-ssr-controller.js';
import styles from './uxdot-pattern.css';
@@ -30,65 +29,96 @@ import styles from './uxdot-pattern.css';
export class UxdotPattern extends LitElement {
static styles = [styles];
- @colorContextProvider()
+ /** Which color palette to apply to the demo surface */
@property({ reflect: true, attribute: 'color-palette' })
colorPalette: ColorPalette = 'lightest';
- @colorContextConsumer() private on?: ColorTheme;
+ /** The id of the element in shadow DOM which the color picker targets */
+ @property({ reflect: true }) target = 'content';
- @property({ reflect: true })
- target = 'container';
+ /** Path to the pattern source file, relative to the input file */
+ @property({ reflect: true }) src?: string;
- @property({ type: Boolean })
- noColorPicker = false;
+ /** Path to additional CSS file to include */
+ @property({ reflect: true, attribute: 'css-src' }) cssSrc?: string;
- @property({ type: Boolean })
- stacked = false;
+ /** Path to additional JS file to include */
+ @property({ reflect: true, attribute: 'js-src' }) jsSrc?: string;
- @property({ converter: ColorPaletteListConverter })
- allow = paletteNames;
+ /** Should the color picker be hidden? */
+ @property({ type: Boolean, attribute: 'no-color-picker' }) noColorPicker = false;
- #slots = new SlotController(this, 'html', 'css', 'js');
+ /** Should the code blocks be expanded? */
+ @property({ type: Boolean, attribute: 'full-height' }) fullHeight = false;
- render() {
- const { noColorPicker, stacked, on = 'light' } = this;
- const hasHtml = this.#slots.hasSlotted('html');
- const hasCss = this.#slots.hasSlotted('css');
- const hasJs = this.#slots.hasSlotted('js');
+ /** Which colour palettes should be allowed in the picker? (default: all) */
+ @property({ converter: ColorPaletteListConverter }) allow = paletteNames;
+
+ @colorContextConsumer() private on?: ColorTheme;
+ ssrController = new UxdotPatternSSRController(this);
+
+ render() {
const target = isServer || (this.target === 'container') ? this.target
: (this.getRootNode() as Document).getElementById(this.target);
+
+ const actionsLabels = html`
+ Copy to Clipboard
+ Copied!
+ Toggle line wrap
+ `;
+
return html`
-
-
{% include 'partials/component/feedback.html' %}
diff --git a/docs/theming/customizing.md b/docs/theming/customizing.md
index 95b5c0b004..6e352bb002 100644
--- a/docs/theming/customizing.md
+++ b/docs/theming/customizing.md
@@ -40,9 +40,9 @@ To create a custom theme, designers and developers need only set the values for
the relevant color properties in the design system. For example, a _Bordeaux_
theme might look like this:
-{% uxdotPattern class="card-snippet-grid", stacked=true %}
-{% include './patterns/card-bordeaux.html' %}
-{% enduxdotPattern %}
+
+
When writing themes, use the semantic, themeable tokens such as
`--rh-color-interactive-primary-default-on-light` rather than the crayon tokens
@@ -54,7 +54,7 @@ e.g. `--rh-color-purple-10`.
Do not set the "computed" theme tokens, e.g.
`--rh-color-interactive-primary-default`, those will always be calculated for
you from their `-on-light` and `-on-dark` versions.
-ke m
+
diff --git a/docs/theming/developers.css b/docs/theming/developers.css
index 799d1c81c7..645f84e150 100644
--- a/docs/theming/developers.css
+++ b/docs/theming/developers.css
@@ -1,11 +1,3 @@
-.band-example {
- &::part(example) {
- display: grid;
- grid-template-columns: 1fr;
- gap: var(--rh-space-lg);
- }
-}
-
.code-tabs {
border: var(--rh-border-width-sm) solid var(--rh-color-border-subtle);
border-radius: var(--rh-border-radius-default);
diff --git a/docs/theming/developers.md b/docs/theming/developers.md
index 2a23e92a0a..f66ded0e7c 100644
--- a/docs/theming/developers.md
+++ b/docs/theming/developers.md
@@ -56,10 +56,10 @@ A common pattern for a themeable container is the full-width band. For example,
a `` may be used as a full-width container and provide the
*Bordeaux* theme values to a set of 3 cards in a grid:
-{% uxdotPattern src="./docs/theming/patterns/band.html",
- class="band-example",
- target="band",
- stacked=true %}{% enduxdotPattern %}
+
+
### The `color-palette` attribute
diff --git a/docs/theming/index.md b/docs/theming/index.md
index f47816dbb0..37917de307 100644
--- a/docs/theming/index.md
+++ b/docs/theming/index.md
@@ -18,17 +18,25 @@ hasToc: false
import '@rhds/elements/rh-tile/rh-tile.js';
-The Red Hat Design System features a powerful, flexible, and easy-to-use theming system.
+The Red Hat Design System features a powerful, flexible, and easy-to-use theming
+system.
## What is theming?
-We use theming to modify our elements and patterns so they fit a specific visual style. Themes can be applied to an element, a page, or a UI. A common theming use case is dark theme or dark mode.
+We use theming to modify our elements and patterns so they fit a specific visual
+style. Themes can be applied to an element, a page, or a UI. A common theming
+use case is dark theme or dark mode.
## How does it work?
-Our design system includes built-in branded and accessible defaults, so all you need to do if you want to create digital experiences that feel like Red Hat is to write a few lines of HTML.
+Our design system includes built-in branded and accessible defaults, so all you
+need to do if you want to create digital experiences that feel like Red Hat is
+to write a few lines of HTML.
-When we want to flex our design muscles, our elements include powerful theming primitives in the form of **slots**, **design tokens**, and **CSS shadow parts**. These primitives enable you to theme a single element, section, page, or entire app UI.
+When we want to flex our design muscles, our elements include powerful theming
+primitives in the form of **slots**, **design tokens**, and **CSS shadow
+parts**. These primitives enable you to theme a single element, section, page,
+or entire app UI.
diff --git a/eleventy.config.cjs b/eleventy.config.cjs
index cb04446a57..a56a6b68c2 100644
--- a/eleventy.config.cjs
+++ b/eleventy.config.cjs
@@ -11,7 +11,7 @@ const RHDSPlugin = require('./docs/_plugins/rhds.cjs');
const DesignTokensPlugin = require('./docs/_plugins/tokens.cjs');
const RHDSMarkdownItPlugin = require('./docs/_plugins/markdown-it.cjs');
const ImportMapPlugin = require('./docs/_plugins/importMap.cjs');
-const LitPlugin = require('@lit-labs/eleventy-plugin-lit');
+const LitPlugin = require('./docs/_plugins/lit-ssr/lit.cjs');
const HelmetPlugin = require('eleventy-plugin-helmet');
const util = require('node:util');
@@ -34,7 +34,6 @@ module.exports = function(eleventyConfig) {
await exec('npx tspc');
});
- eleventyConfig.addWatchTarget('docs/patterns/**/*.html');
eleventyConfig.watchIgnores?.add('docs/assets/redhat/');
eleventyConfig.watchIgnores?.add('**/*.spec.ts');
eleventyConfig.watchIgnores?.add('**/*.d.ts');
@@ -48,8 +47,6 @@ module.exports = function(eleventyConfig) {
eleventyConfig.addPassthroughCopy('docs/robots.txt');
eleventyConfig.addPassthroughCopy('docs/assets/**/*');
eleventyConfig.addPassthroughCopy('docs/styles/**/*');
- eleventyConfig.addPassthroughCopy('docs/patterns/**/*.css');
- eleventyConfig.addPassthroughCopy('docs/theming/**/*.css');
eleventyConfig.addPassthroughCopy('docs/foundations/**/*.{css,js}');
@@ -59,8 +56,8 @@ module.exports = function(eleventyConfig) {
});
}
- eleventyConfig.addWatchTarget('docs/patterns/**/*.(html|md)');
eleventyConfig.addWatchTarget('docs/styles/');
+ eleventyConfig.addWatchTarget('docs/patterns/**/*.md');
eleventyConfig.addGlobalData('isLocal', isLocal);
@@ -197,7 +194,6 @@ module.exports = function(eleventyConfig) {
});
eleventyConfig.addPlugin(LitPlugin, {
- mode: 'worker',
componentModules: [
'docs/assets/javascript/elements/uxdot-masthead.js',
'docs/assets/javascript/elements/uxdot-header.js',
@@ -210,7 +206,7 @@ module.exports = function(eleventyConfig) {
'docs/assets/javascript/elements/uxdot-best-practice.js',
'docs/assets/javascript/elements/uxdot-search.js',
'docs/assets/javascript/elements/uxdot-toc.js',
- // 'docs/assets/javascript/elements/uxdot-pattern.js',
+ 'docs/assets/javascript/elements/uxdot-pattern.js',
// Uses context API need to work around issues
// 'docs/assets/javascript/elements/uxdot-example.js',
// extends RhTabs so cant DSD yet
diff --git a/lib/ssr-controller.ts b/lib/ssr-controller.ts
new file mode 100644
index 0000000000..1d50a9202b
--- /dev/null
+++ b/lib/ssr-controller.ts
@@ -0,0 +1,17 @@
+import type { ReactiveController, ReactiveElement } from 'lit';
+
+
+export const ssrControllerMap = new WeakMap;
+
+/** This is experimental and may change at any time without warning */
+export class RHDSSSRController implements ReactiveController {
+ isRHDSSSRController = true;
+ /** @internal This is experimental and may change at any time without warning */
+ ssrSetup?(): Promise;
+ hostUpdate?(): void;
+ hostUpdate?(): void;
+ constructor(public host: ReactiveElement) {
+ host.addController(this);
+ ssrControllerMap.set(host, [...ssrControllerMap.get(host) ?? [], this]);
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index e655ee08c7..612c24ae5d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,16 +1,16 @@
{
"name": "@rhds/elements",
- "version": "2.0.1",
+ "version": "2.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@rhds/elements",
- "version": "2.0.1",
+ "version": "2.1.0",
"license": "MIT",
"dependencies": {
"@lit/context": "^1.1.2",
- "@patternfly/pfe-core": "^4.0.2",
+ "@patternfly/pfe-core": "^4.0.4",
"@rhds/icons": "^1.1.2",
"@rhds/tokens": "^2.1.0",
"lit": "^3.2.0",
@@ -32,7 +32,7 @@
"@patternfly/eslint-config-elements": "^4.0.0",
"@patternfly/eslint-plugin-elements": "^2.0.0",
"@patternfly/icons": "^1.0.3",
- "@patternfly/pfe-tools": "^3.0.1",
+ "@patternfly/pfe-tools": "^3.0.2",
"@playwright/test": "^1.47.2",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-typescript": "^11.1.6",
@@ -4043,7 +4043,9 @@
"license": "MIT"
},
"node_modules/@patternfly/pfe-core": {
- "version": "4.0.2",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@patternfly/pfe-core/-/pfe-core-4.0.4.tgz",
+ "integrity": "sha512-KsU3J2/65U+tRMqOfRaJL5BbLLR9rBT+O1fSKp7k27tKfRb/3eXs0CGi5VJr3IdO5/CvIYUXm2paEUXAdqbf3w==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.6.10",
@@ -4052,7 +4054,9 @@
}
},
"node_modules/@patternfly/pfe-tools": {
- "version": "3.0.1",
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@patternfly/pfe-tools/-/pfe-tools-3.0.2.tgz",
+ "integrity": "sha512-XTNMIDDF7+S5GiEKvY86im/bq7wii7FHctWpn6oza0c5pQYK2sXu6akSax+ykmmxa98xIXVobBTU02O5V/ogzg==",
"dev": true,
"license": "MIT",
"engines": {
diff --git a/package.json b/package.json
index 0c4e6aecee..8388e0dc20 100644
--- a/package.json
+++ b/package.json
@@ -289,7 +289,7 @@
},
"dependencies": {
"@lit/context": "^1.1.2",
- "@patternfly/pfe-core": "^4.0.2",
+ "@patternfly/pfe-core": "^4.0.4",
"@rhds/icons": "^1.1.2",
"@rhds/tokens": "^2.1.0",
"lit": "^3.2.0",
@@ -311,7 +311,7 @@
"@patternfly/eslint-config-elements": "^4.0.0",
"@patternfly/eslint-plugin-elements": "^2.0.0",
"@patternfly/icons": "^1.0.3",
- "@patternfly/pfe-tools": "^3.0.1",
+ "@patternfly/pfe-tools": "^3.0.2",
"@playwright/test": "^1.47.2",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-typescript": "^11.1.6",
diff --git a/patches/@lit+context+1.1.2.patch b/patches/@lit+context+1.1.2.patch
new file mode 100644
index 0000000000..4db5348a54
--- /dev/null
+++ b/patches/@lit+context+1.1.2.patch
@@ -0,0 +1,11 @@
+diff --git a/node_modules/@lit/context/lib/controllers/context-provider.js b/node_modules/@lit/context/lib/controllers/context-provider.js
+index 4a2c228..bab057f 100644
+--- a/node_modules/@lit/context/lib/controllers/context-provider.js
++++ b/node_modules/@lit/context/lib/controllers/context-provider.js
+@@ -3,5 +3,5 @@ import{ContextRequestEvent as t}from"../context-request-event.js";import{ValueNo
+ * @license
+ * Copyright 2021 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+- */class e extends Event{constructor(t){super("context-provider",{bubbles:!0,composed:!0}),this.context=t}}class i extends s{constructor(s,e,i){super(void 0!==e.context?e.initialValue:i),this.onContextRequest=t=>{const s=t.composedPath()[0];t.context===this.context&&s!==this.host&&(t.stopPropagation(),this.addCallback(t.callback,s,t.subscribe))},this.onProviderRequest=s=>{const e=s.composedPath()[0];if(s.context!==this.context||e===this.host)return;const i=new Set;for(const[s,{consumerHost:e}]of this.subscriptions)i.has(s)||(i.add(s),e.dispatchEvent(new t(this.context,s,!0)));s.stopPropagation()},this.host=s,void 0!==e.context?this.context=e.context:this.context=e,this.attachListeners(),this.host.addController?.(this)}attachListeners(){this.host.addEventListener("context-request",this.onContextRequest),this.host.addEventListener("context-provider",this.onProviderRequest)}hostConnected(){this.host.dispatchEvent(new e(this.context))}}export{i as ContextProvider,e as ContextProviderEvent};
++ */class e extends Event{constructor(t){super("context-provider",{bubbles:!0,composed:!0}),this.context=t}}class i extends s{constructor(s,e,i){super(void 0!==e.context?e.initialValue:i),this.onContextRequest=t=>{const s=t.composedPath()[0];t.context===this.context&&s!==this.host&&(t.stopPropagation(),this.addCallback(t.callback,s,t.subscribe))},this.onProviderRequest=s=>{const e=s.composedPath()[0];if(s.context!==this.context||e===this.host)return;const i=new Set;for(const[s,{consumerHost:e}]of this.subscriptions)i.has(s)||(i.add(s),e.dispatchEvent(new t(this.context,s,!0)));s.stopPropagation()},this.host=s,void 0!==e.context?this.context=e.context:this.context=e,this.attachListeners(),this.host.addController?.(this)}attachListeners(){this.host.addEventListener?.("context-request",this.onContextRequest),this.host.addEventListener?.("context-provider",this.onProviderRequest)}hostConnected(){this.host.dispatchEvent(new e(this.context))}}export{i as ContextProvider,e as ContextProviderEvent};
+ //# sourceMappingURL=context-provider.js.map
diff --git a/patches/@patternfly+pfe-core+4.0.2.patch b/patches/@patternfly+pfe-core+4.0.2.patch
deleted file mode 100644
index 0d147a66fc..0000000000
--- a/patches/@patternfly+pfe-core+4.0.2.patch
+++ /dev/null
@@ -1,13 +0,0 @@
-diff --git a/node_modules/@patternfly/pfe-core/ssr-shims.js b/node_modules/@patternfly/pfe-core/ssr-shims.js
-index d432769..c7a30fb 100644
---- a/node_modules/@patternfly/pfe-core/ssr-shims.js
-+++ b/node_modules/@patternfly/pfe-core/ssr-shims.js
-@@ -42,6 +42,8 @@ globalThis.IntersectionObserver ?? (globalThis.IntersectionObserver = ObserverSh
- // @ts-expect-error: this runs in node
- globalThis.MutationObserver ?? (globalThis.MutationObserver = ObserverShim);
- // @ts-expect-error: this runs in node
-+globalThis.ResizeObserver ?? (globalThis.ResizeObserver = ObserverShim);
-+// @ts-expect-error: this runs in node
- globalThis.getComputedStyle ?? (globalThis.getComputedStyle = function () {
- return {
- getPropertyPriority() {