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 - - - ${!cssContent ? '' : html` -

CSS

- - Copy to Clipboard - - - `}${!jsContent ? '' : html` -

JavaScript

- - Copy to Clipboard - - `} - -
-`; - }); - - 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 + + Toggle line wrap + `; + return html` - -
+
+
+ + + value="${this.colorPalette}" + target="${target}" + allow="${this.allow}"> -
-

Example

- -
-
${!hasHtml ? '' : html` -
-

HTML

- -
`}${!hasCss ? '' : html` -
-

CSS

- -
`}${!hasJs ? '' : html` -
-

JS

- -
`} -
- + +
+ + ${this.ssrController.allContent} + + + HTML + + + ${this.ssrController.htmlContent} + ${actionsLabels} + + + CSS + + + ${this.ssrController.cssContent} + ${actionsLabels} + + + JS + + + ${this.ssrController.jsContent} + ${actionsLabels} + + + +
`; } diff --git a/docs/patterns/card/examples.md b/docs/patterns/card/examples.md index 90dac37352..14be71085b 100644 --- a/docs/patterns/card/examples.md +++ b/docs/patterns/card/examples.md @@ -25,129 +25,112 @@ subnav: - - ## Asset cards Use to display that an asset can be downloaded. An icon and label group or text may be used to describe the asset. -### Text and CTA -{% uxdotPattern %}{% include './patterns/asset-text-and-cta.html' %}{% enduxdotPattern %} + +

Text and CTA

+
-### Title and Link -{% uxdotPattern %}{% include './patterns/asset-title-and-link.html' %}{% enduxdotPattern %} + + + -### Title and Link - Top -{% uxdotPattern %}{% include './patterns/asset-title-and-link-top.html' %}{% enduxdotPattern %} + + + -## Avatars card + +

Avatars card

-Use to highlight a group of people who engage in an event. A label -should be included, but including text is optional. + Use to highlight a group of people who engage in an event. A label + should be included, but including text is optional. -Use the [``](/elements/avatar/) element to element to present -the list of users. + Use the [``](/elements/avatar/) element to element to present + the list of users. -{% uxdotPattern %}{% include './patterns/avatars.html' %}{% enduxdotPattern %} +
-## Fast facts card -Use to display quick facts or short data points under a label. A Secondary -call to action may be used or not. + +

Fast facts card

-{% uxdotPattern %}{% include './patterns/fast-facts.html' %}{% enduxdotPattern %} + Use to display quick facts or short data points under a label. A Secondary + call to action may be used or not. -## Icon card +
-Use to add an icon to the basic style above the text. Secondary and Default -calls to action may be used. + +

Icon card

-{% uxdotPattern %}{% include './patterns/icon.html' %}{% enduxdotPattern %} + Use to add an icon to the basic style above the text. Secondary and Default + calls to action may be used. -## Image card +
-Use to add an image to the basic style above the text. -Secondary and Default calls to action may be used. + +

Image card

+ + Use to add an image to the basic style above the text. + Secondary and Default calls to action may be used. -{% uxdotPattern %}{% include './patterns/image.html' %}{% enduxdotPattern %} +
## List cards Use to display a short amount of content using various list styles. Secondary and Default calls to action may be used. -### Flat list -{% uxdotPattern %}{% include './patterns/list-flat.html' %}{% enduxdotPattern %} + +

Flat list

+
-### List with dividers -{% uxdotPattern %}{% include './patterns/list-with-dividers.html' %}{% enduxdotPattern %} + +

List with dividers

+
-### Ordered list + +

Ordered list

+
-{% uxdotPattern %}{% include './patterns/ordered-list.html' %}{% enduxdotPattern %} - -### Unordered list - -{% uxdotPattern %}{% include './patterns/unordered-list.html' %}{% enduxdotPattern %} + +

Unordered list

+
## Logo cards Use to display a customer logo in a variety of arrangements. A call to action is required, otherwise use a logo wall. -### CTA only -{% uxdotPattern %}{% include './patterns/logo-cta.html' %}{% enduxdotPattern %} + +

CTA only

+
### Text and CTA -{% uxdotPattern %}{% include './patterns/logo-text-and-cta.html' %}{% enduxdotPattern %} + + -## Title bar card + +

Title bar card

-Use to add a small icon and a label group to the header section. A larger icon -or a logo may be used. + Use to add a small icon and a label group to the header section. A larger + icon or a logo may be used. -Alternative title bar styles can be achieved by selecting [card's `header` CSS -Shadow Part](/elements/card/code/#parts). + Alternative title bar styles can be achieved by selecting [card's `header` + CSS Shadow Part](/elements/card/code/#parts). -{% uxdotPattern %}{% include './patterns/title-bar.html' %}{% enduxdotPattern %} +
## Quote cards Use to display a short quote with attribution text. Logos, images, and a Secondary call to action may be used or not. -### Basic -{% uxdotPattern %}{% include './patterns/quote.html' %}{% enduxdotPattern %} - -### Logo and quote -{% uxdotPattern %}{% include './patterns/logo-and-quote.html' %}{% enduxdotPattern %} - - - + +

Basic

+
+ +

Logo and quote

+
{% include 'partials/component/feedback.html' %} diff --git a/docs/patterns/card/index.md b/docs/patterns/card/index.md index 2ffbf9eeec..bb22092262 100644 --- a/docs/patterns/card/index.md +++ b/docs/patterns/card/index.md @@ -25,7 +25,6 @@ subnav: - ## Overview A card formats content in a small, contained space. It can be used to display a @@ -59,22 +58,28 @@ Cards will automatically react to the `color-palette` context provided by a parent element, like ``.
-{% uxdotPattern allow="light, lighter, lightest" %}{% include './patterns/themes.html' %}{% enduxdotPattern %} -{% uxdotPattern allow="dark, darker, darkest" %}{% include './patterns/themes.html' %}{% enduxdotPattern %} + +
### Explicit card theming Cards can play an active role in theming by declaring a specific `color-palette`. -{% uxdotPattern stacked=true %}{% include './patterns/explicit-themes.html' %}{% enduxdotPattern %} + + ### Custom theming When using design tokens to apply custom themes, it is important to ensure that the colors used meet [color contrast guidelines][color-contrast]. -{% uxdotPattern stacked=true %}{% include './patterns/custom-themes.html' %}{% enduxdotPattern %} + + {% include 'partials/component/feedback.html' %} diff --git a/docs/patterns/card/patterns/video.html b/docs/patterns/card/patterns/video.html deleted file mode 100644 index f2b323ebb4..0000000000 --- a/docs/patterns/card/patterns/video.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - Call to action - - - Copy to Clipboard - - - - - Copy to Clipboard - - - - \ No newline at end of file diff --git a/docs/patterns/logo-wall/examples.md b/docs/patterns/logo-wall/examples.md index c02d365167..17da0e26a1 100644 --- a/docs/patterns/logo-wall/examples.md +++ b/docs/patterns/logo-wall/examples.md @@ -22,25 +22,29 @@ importElements: -## Within a promo band (bordered) - -{% uxdotPattern stacked=true, css='./logo-wall-lightdom.css', target="example-1x1-grid" %} -{% include './patterns/1x1-grid.html' %} -{% enduxdotPattern %} - -## Within a promo band (borderless) -{% uxdotPattern stacked=true, css='./logo-wall-lightdom.css', target="example-1x1-grid-flat" %} -{% include './patterns/1x1-grid-flat.html' %} -{% enduxdotPattern %} - -## With 2x2 grid -{% uxdotPattern stacked=true, css='./logo-wall-lightdom.css', target="example-2x2-grid" %} -{% include './patterns/2x2-grid.html' %} -{% enduxdotPattern %} - -## With 2x3 grid -{% uxdotPattern stacked=true, css='./logo-wall-lightdom.css', target="example-2x3-grid" %} -{% include './patterns/2x3-grid.html' %} -{% enduxdotPattern %} + +

Within a promo band (bordered)

+
+ + + +

Within a promo band (borderless)

+
+ + +

With 2x2 grid

+
+ + +

With 2x3 grid

+
{% include 'partials/component/feedback.html' %} diff --git a/docs/patterns/tabs/examples.md b/docs/patterns/tabs/examples.md new file mode 100644 index 0000000000..a1e740fae3 --- /dev/null +++ b/docs/patterns/tabs/examples.md @@ -0,0 +1,17 @@ + +

Link to tab

+ + Use this pattern sparingly. If your tabs serve only as page navigation, + use the Subnav element instead. + +

Use to activate a particular tab when the page's URL hash refers to an element + within the tab panel, or to the tab itself.

+
+ + +

Code Tabs

+

Use this pattern to display highlighted code blocks of one or more + languages, for example: the HTML, CSS, and JavaScript needed to implement a + pattern.

+
+ diff --git a/docs/patterns/tabs/index.md b/docs/patterns/tabs/index.md index 34975b967c..5f5e7eb0b3 100644 --- a/docs/patterns/tabs/index.md +++ b/docs/patterns/tabs/index.md @@ -13,41 +13,3 @@ tags: import '@rhds/elements/lib/elements/rh-context-picker/rh-context-picker.js'; -{% set linkToTabPattern %}{% include './patterns/link-to-tab.html' %}{% endset %} - -## Link to tab - -Use this pattern sparingly. If your tabs serve only as - page navigation, use the [Subnav](/elements/subnavigation) element - instead. - -Use to activate a particular tab when the page's URL hash refers to an element -within the tab panel, or to the tab itself. - - - Copy to Clipboard - - Toggle word wrap - - - - - - -{% include './patterns/link-to-tab.html' %} - -[element]: /elements/tabs -[css-props]: /elements/tabs/code/#css-custom-properties - diff --git a/docs/patterns/tabs/patterns/code-tabs.html b/docs/patterns/tabs/patterns/code-tabs.html new file mode 100644 index 0000000000..fc456dfa0f --- /dev/null +++ b/docs/patterns/tabs/patterns/code-tabs.html @@ -0,0 +1,61 @@ + + TypeScript + + + + + + CSS + + + + + + + + diff --git a/docs/patterns/tabs/patterns/link-to-tab.html b/docs/patterns/tabs/patterns/link-to-tab.html index efd164bbd1..cab27c4e24 100644 --- a/docs/patterns/tabs/patterns/link-to-tab.html +++ b/docs/patterns/tabs/patterns/link-to-tab.html @@ -1,3 +1,14 @@ + + North The North diff --git a/docs/patterns/tile/examples.md b/docs/patterns/tile/examples.md index 9e51eddc7a..70c4bbe7c1 100644 --- a/docs/patterns/tile/examples.md +++ b/docs/patterns/tile/examples.md @@ -22,16 +22,19 @@ subnav: import '@rhds/elements/rh-blockquote/rh-blockquote.js'; - - + + -## Accented tile - -The accented tile pattern should be used to draw attention to a specifc tile or -set of tiles. Be careful not to apply the accented tile pattern to all tiles -within a page, otherwise the accent effect will be lost. After all, "if -everything is special, then nothing is special." - -{% uxdotPattern %}{% include './patterns/accented-tile.html' %}{% enduxdotPattern %} + +

Accented tile

+

The accented tile pattern should be used to draw attention to a specific + tile or set of tiles. Be careful not to apply the accented tile pattern to + all tiles within a page, otherwise the accent effect will be lost. After + all, "if everything is special, then nothing is special."

+
View accented tile demo diff --git a/docs/patterns/tile/index.md b/docs/patterns/tile/index.md index a24436419d..48c7e76a18 100644 --- a/docs/patterns/tile/index.md +++ b/docs/patterns/tile/index.md @@ -53,7 +53,10 @@ Examples include: For more information, please see the docs on [theming][theming] and [`` css custom properties][css-props]. -{% uxdotPattern stacked=true, target="custom-tiles" %}{% include './patterns/custom-themes.html' %}{% enduxdotPattern %} + +

Custom tiles

+
[element]: /elements/tile/ [css-props]: /elements/tile/code/#css-custom-properties diff --git a/docs/patterns/video-thumbnail/index.md b/docs/patterns/video-thumbnail/index.md index 16179b49ea..9e11a6a209 100644 --- a/docs/patterns/video-thumbnail/index.md +++ b/docs/patterns/video-thumbnail/index.md @@ -229,10 +229,8 @@ values between elements. - {% spacerTokensTable headline="", - caption='', - headingLevel="4", - tokens="--rh-space-xl" %} + {% spacerTokensTable headingLevel="4", + tokens="--rh-space-xl" %} {% endspacerTokensTable %} diff --git a/docs/theming/color-palettes.css b/docs/theming/color-palettes.css index d45c3d7ff6..d198bd4419 100644 --- a/docs/theming/color-palettes.css +++ b/docs/theming/color-palettes.css @@ -2,20 +2,7 @@ width: 100%; } -#elements-grid { - &::part(example) { - display: block; - width: auto; - } -} - .card-snippet-grid { - &::part(example) { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); - gap: var(--rh-space-lg); - } - & h4[slot='heading'] { margin: 0; } diff --git a/docs/theming/color-palettes.md b/docs/theming/color-palettes.md index 0a9904ca1e..ae79c6aba8 100644 --- a/docs/theming/color-palettes.md +++ b/docs/theming/color-palettes.md @@ -40,9 +40,8 @@ less effort and greater cross-property consistency. RHDS defines six color palettes They range from lightest to darkest, and those are the two palettes you will use the most for the majority of your projects. -{% uxdotPattern id="elements-grid", stacked=true %} -{% include './patterns/collage.html' %} -{% enduxdotPattern %} + + ### How color palettes work @@ -119,9 +118,10 @@ Authors may define the color palette of a container using the `color-palette` HTML attribute. So for example, to create a card with the darkest color palette, use this HTML: -{% uxdotPattern class="card-snippet-grid", fullHeight=true %} -{% include './patterns/card-default-vs-set-palette.html' %} -{% enduxdotPattern %} + + A color palette provider is a **surface** on which a particular color palette is active, as well as a **container** for themeable consumer elements. @@ -164,9 +164,10 @@ Extending our card example from above, if our page author then adds an `` to the card, it will *automatically* adopt the dark color theme. The page author need not and should not customize the CTA. -{% uxdotPattern class="card-snippet-grid", fullHeight=true %} -{% include './patterns/card-child-consumers.html' %} -{% enduxdotPattern %} + + Color palettes lets authors write more HTML, simpler CSS, and @@ -182,9 +183,10 @@ Some elements are both providers and consumers. Card, for example is both a provider and a consumer. It can accept the color theme of its parent context and it can also set its own color palette. -{% uxdotPattern class="card-snippet-grid", fullHeight=true %} -{% include './patterns/card-consumer-provider.html' %} -{% enduxdotPattern %} + + ## Inline color palettes Beta @@ -221,8 +223,8 @@ palette, e.g. from `lightest` to `light`.

More information

High contrast is using bright elements, patterns, or images in dark -environments and vice versa. This is useful to focus attention or create -visual tension.

+ environments and vice versa. This is useful to focus attention or create + visual tension.

{% 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.