Skip to content

POC docs: <uxdot-pattern> support media query patterns with an iframe viewport#3005

Open
zeroedin wants to merge 21 commits into
mainfrom
docs/uxdot-pattern/iframe-viewport
Open

POC docs: <uxdot-pattern> support media query patterns with an iframe viewport#3005
zeroedin wants to merge 21 commits into
mainfrom
docs/uxdot-pattern/iframe-viewport

Conversation

@zeroedin

@zeroedin zeroedin commented May 1, 2026

Copy link
Copy Markdown
Collaborator

THIS IS A Proof of Concept

This is meant for initial discussion, likely needing design input and future direction

What I did

  1. Added viewport attribute to uxdot-pattern which encapsulates the pattern in an iframe
    • WHY?: Because in order to do patterns like stacked-navigation and in the future rh-drawer flow, we need @media query support where running inside a small contained div won't cut it.
  2. Added uxdot-context-select, heavily borrowing from @adamjohnson work on the rh-scheme-dropdown.
    • WHY? This replaces the clunky rh-context-picker component making the UI more mobile friendly. I expect if we like this we can release it as rh-context-select but those are exported in the package so kept this named uxdot-* for the POC.
  3. Added stacked navigation pattern.
    • WHY? This is a pattern we've been meaning to share for awhile, used actively on RHDC blogs. I improved it a bit by adding some progressive enhancement through container scroll-state() but falls back to the intersection observer if not supported. I also needed a pattern to build to prove the concept. rh-drawer not being available yet will also need this style of pattern viewport in order to properly render.

Testing Instructions

  1. View the Stacked Navigation Pattern

Notes to Reviewers

This exposes rh-navigation-secondary inability to respond to system light | dark mode. Known issue we should likely fix.

Adds a `viewport` boolean attribute to uxdot-pattern that renders the
pattern in a standalone iframe instead of inline shadow DOM. This lets
patterns that rely on @media queries respond to their own resizable
container rather than the parent page viewport.

- SSR controller generates a full HTML page with importmap and global
  styles, written to a _viewport/ subdirectory
- Client controller hydrates the iframe src from SSR'd markup
- Iframe is resizable via CSS resize: both
- Empty-description grid rules scoped to non-viewport mode to prevent
  code-tabs overlapping the iframe

Made-with: Cursor
@netlify

netlify Bot commented May 1, 2026

Copy link
Copy Markdown

Deploy Preview for red-hat-design-system ready!

Name Link
🔨 Latest commit 1567dda
🔍 Latest deploy log https://app.netlify.com/projects/red-hat-design-system/deploys/6a4e4a060f43580008eea31e
😎 Deploy Preview https://deploy-preview-3005--red-hat-design-system.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@changeset-bot

changeset-bot Bot commented May 1, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 1567dda

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@qodo-code-review

Copy link
Copy Markdown

Review Summary by Qodo

Add iframe viewport mode and stacked navigation pattern to uxdot-pattern

✨ Enhancement 📝 Documentation

Grey Divider

Walkthroughs

Description
• Add viewport attribute to uxdot-pattern for iframe-based pattern rendering with @media query
  support
• Create uxdot-context-select component replacing rh-context-picker with improved mobile UX
• Implement stacked navigation pattern with sticky scroll and z-index coordination
• Add viewport size controls (mobile/tablet/desktop) for responsive pattern preview
Diagram
flowchart LR
  A["uxdot-pattern<br/>viewport attribute"] -->|"generates"| B["SSR Controller<br/>writes iframe HTML"]
  B -->|"creates"| C["_iframe/<br/>pattern.html"]
  C -->|"renders in"| D["iframe element<br/>with resize"]
  A -->|"uses"| E["uxdot-context-select<br/>color picker"]
  E -->|"posts message"| D
  F["Stacked Navigation<br/>Pattern"] -->|"uses"| A
  F -->|"demonstrates"| G["scroll-state()<br/>+ IntersectionObserver"]
Loading

Grey Divider

File Changes

1. uxdot/uxdot-pattern.ts ✨ Enhancement +80/-17

Add viewport mode and refactor controls layout

uxdot/uxdot-pattern.ts


2. uxdot/uxdot-pattern.css ✨ Enhancement +31/-23

Restructure grid layout and add iframe styling

uxdot/uxdot-pattern.css


3. uxdot/uxdot-context-select.ts ✨ Enhancement +169/-0

New context select component with base-select support

uxdot/uxdot-context-select.ts


View more (7)
4. uxdot/uxdot-context-select.css ✨ Enhancement +144/-0

Style native select with color palette swatches

uxdot/uxdot-context-select.css


5. uxdot/uxdot-pattern-ssr-controller-server.ts ✨ Enhancement +98/-2

Generate standalone HTML pages for iframe viewports

uxdot/uxdot-pattern-ssr-controller-server.ts


6. uxdot/uxdot-pattern-ssr-controller-client.ts ✨ Enhancement +2/-0

Hydrate iframe src from SSR-generated markup

uxdot/uxdot-pattern-ssr-controller-client.ts


7. docs/patterns/stacked-navigation/patterns/stacked-navigation.html 📝 Documentation +473/-0

Complete stacked navigation pattern with primary and secondary nav

docs/patterns/stacked-navigation/patterns/stacked-navigation.html


8. docs/patterns/stacked-navigation/index.md 📝 Documentation +55/-0

Document stacked navigation pattern overview and usage

docs/patterns/stacked-navigation/index.md


9. docs/patterns/stacked-navigation/examples.md 📝 Documentation +27/-0

Add stacked navigation pattern example with viewport preview

docs/patterns/stacked-navigation/examples.md


10. docs/patterns/index.md 📝 Documentation +1/-0

Add stacked navigation to pattern summaries

docs/patterns/index.md


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented May 1, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Scroll-state path broken ✓ Resolved 🐞 Bug ≡ Correctness
Description
The stacked-navigation pattern defines a scroll-state container query on .nav-wrapper but the
markup never renders a .nav-wrapper container, so in browsers that support `container-type:
scroll-state` the z-index coordination never activates and the IntersectionObserver fallback is
skipped.
Code

docs/patterns/stacked-navigation/patterns/stacked-navigation.html[R241-472]

+</rh-navigation-primary>
+
+<div id="sentinel" aria-hidden="true"></div>
+
+<rh-navigation-secondary>
+  <a href="#" slot="logo" id="logo-id">Red Hat Enterprise Linux</a>
+  <ul slot="nav" aria-labelledby="logo-id">
+    <li>
+      <rh-navigation-secondary-dropdown>
+        <a href="#" slot="link">Features</a>
+        <rh-navigation-secondary-menu slot="menu">
+          <rh-navigation-secondary-menu-section>
+            <h3 slot="header">Platform</h3>
+            <ul slot="links">
+              <li><a href="#">Security</a></li>
+              <li><a href="#">Automation</a></li>
+              <li><a href="#">Edge</a></li>
+            </ul>
+          </rh-navigation-secondary-menu-section>
+          <rh-navigation-secondary-menu-section>
+            <h3 slot="header">Ecosystem</h3>
+            <ul slot="links">
+              <li><a href="#">Partners</a></li>
+              <li><a href="#">Integrations</a></li>
+              <li><a href="#">Marketplace</a></li>
+            </ul>
+          </rh-navigation-secondary-menu-section>
+        </rh-navigation-secondary-menu>
+      </rh-navigation-secondary-dropdown>
+    </li>
+    <li>
+      <rh-navigation-secondary-dropdown>
+        <a href="#" slot="link">Resources</a>
+        <rh-navigation-secondary-menu slot="menu">
+          <ul>
+            <li><a href="#">Documentation</a></li>
+            <li><a href="#">Blog</a></li>
+            <li><a href="#">Training</a></li>
+            <li><a href="#">Case studies</a></li>
+            <li><a href="#">Webinars</a></li>
+          </ul>
+        </rh-navigation-secondary-menu>
+      </rh-navigation-secondary-dropdown>
+    </li>
+    <li><a href="#">Pricing</a></li>
+    <li><a href="#">Support</a></li>
+  </ul>
+  <rh-cta slot="cta" href="#">Try it free</rh-cta>
+</rh-navigation-secondary>
+
+<main>
+  <section class="page-section">
+    <h2>The foundation for your hybrid cloud</h2>
+    <p>Red Hat Enterprise Linux provides a consistent foundation across
+    environments&#8212;from bare metal to cloud to edge. Automate lifecycle
+    management, strengthen security posture, and accelerate workload
+    delivery with a trusted, stable platform.</p>
+    <p>Built on open source innovation, RHEL delivers the reliability and
+    performance that enterprises demand, backed by Red Hat's industry-leading
+    support and a vibrant ecosystem of certified partners and ISVs.</p>
+  </section>
+
+  <section class="page-section">
+    <h2>Key capabilities</h2>
+    <div class="grid">
+      <div class="column">
+        <h3>Security &amp; compliance</h3>
+        <p>Integrated security features including SELinux, system-wide
+        cryptographic policies, and automated compliance scanning help
+        reduce risk across your infrastructure.</p>
+      </div>
+      <div class="column">
+        <h3>Automation</h3>
+        <p>System Roles powered by Red Hat Ansible Automation Platform
+        enable consistent configuration across hundreds of systems with
+        minimal manual effort.</p>
+      </div>
+      <div class="column">
+        <h3>Edge computing</h3>
+        <p>Purpose-built for edge deployments with image-based updates,
+        minimal footprints, and zero-touch provisioning for remote
+        locations.</p>
+      </div>
+    </div>
+  </section>
+
+  <section class="page-section">
+    <h2>Trusted by enterprises worldwide</h2>
+    <p>Thousands of organizations depend on Red Hat Enterprise Linux to
+    power mission-critical workloads. From financial services to
+    healthcare to telecommunications, RHEL provides the stability,
+    security, and support that regulated industries require.</p>
+    <p>With predictable release cycles, extended lifecycle support, and
+    backward compatibility guarantees, you can plan your infrastructure
+    strategy with confidence.</p>
+  </section>
+
+  <section class="page-section">
+    <h2>Get started</h2>
+    <p>Evaluate Red Hat Enterprise Linux with a free developer
+    subscription or contact sales to discuss enterprise licensing options
+    for your organization.</p>
+  </section>
+</main>
+
+<link rel="stylesheet" href="/assets/packages/@rhds/elements/elements/rh-navigation-primary/rh-navigation-primary-lightdom.css">
+<link rel="stylesheet" href="/assets/packages/@rhds/elements/elements/rh-navigation-secondary/rh-navigation-secondary-lightdom.css">
+
+<style>
+  #sentinel {
+    block-size: 0;
+    overflow: hidden;
+  }
+
+  .nav-wrapper {
+    container-type: scroll-state;
+  }
+
+  rh-navigation-primary {
+    --rh-navigation-primary-z-index: 103;
+  }
+
+  @container scroll-state(stuck: top) {
+    rh-navigation-primary {
+      --rh-navigation-primary-z-index: 1;
+    }
+  }
+
+  main {
+    max-inline-size: 80ch;
+    margin-inline: auto;
+    padding: var(--rh-space-3xl, 48px) var(--rh-space-xl, 24px);
+    font-family: var(--rh-font-family-body-text, RedHatText, 'Red Hat Text', 'Noto Sans Arabic', 'Noto Sans Hebrew', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans Malayalam', 'Noto Sans SC', 'Noto Sans TC', 'Noto Sans Thai', Overpass, Helvetica, Arial, sans-serif);
+    line-height: var(--rh-line-height-body-text, 1.5);
+  }
+
+  .page-section {
+    margin-block-end: var(--rh-space-4xl, 64px);
+  }
+
+  .page-section h2 {
+    font-size: var(--rh-font-size-heading-md, 1.75rem);
+    margin-block: 0 var(--rh-space-lg, 16px);
+  }
+
+  .page-section h3 {
+    font-size: var(--rh-font-size-body-text-lg, 1.125rem);
+    margin-block: 0 var(--rh-space-md, 8px);
+  }
+
+  .page-section p {
+    margin-block: 0 var(--rh-space-lg, 16px);
+  }
+
+  .grid {
+    display: grid;
+    grid-template-columns: 1fr;
+    gap: var(--rh-space-2xl, 32px);
+
+    @container (min-width: 768px) {
+      grid-template-columns: 1fr 1fr 1fr;
+    }
+
+    & ul {
+      list-style: none;
+      margin: 0;
+      padding: 0;
+      display: flex;
+      flex-direction: column;
+      gap: var(--rh-space-lg, 16px);
+    }
+
+    #profile {
+      margin-inline-start: var(--rh-space-4xl, 64px);
+      display: grid;
+      grid-template-columns: max-content 1fr;
+      grid-template-areas: 'avatar content';
+      grid-template-rows: fit-content;
+      column-gap: var(--rh-space-lg, 16px);
+
+      & rh-avatar {
+        --rh-size-icon-06: 72px;
+        grid-area: avatar;
+        display: block;
+      }
+
+      & .lockup {
+        display: contents;
+
+        & .account {
+          grid-area: content;
+
+          & .name {
+            font-family: var(--rh-font-family-heading, RedHatDisplay, 'Red Hat Display', Helvetica, Arial, sans-serif);
+            font-size: var(--rh-font-size-heading-sm, 1.5rem);
+          }
+        }
+
+        & .info { grid-area: content; grid-row: 2; }
+        & .language { grid-area: content; grid-row: 3; }
+        & .logout { grid-area: content; grid-row: 4; }
+      }
+    }
+
+    .content-column {
+      max-width: 320px;
+      margin-block: var(--rh-space-md, 8px);
+      line-height: var(--rh-line-height-body-text, 1.5);
+    }
+
+    & select { width: 100%; }
+  }
+</style>
+
+<script type="module">
+  import '@rhds/elements/rh-navigation-primary/rh-navigation-primary.js';
+  import '@rhds/elements/rh-navigation-secondary/rh-navigation-secondary.js';
+  import '@rhds/elements/rh-cta/rh-cta.js';
+  import '@rhds/elements/rh-avatar/rh-avatar.js';
+  import '@rhds/elements/rh-button/rh-button.js';
+
+  if (!CSS.supports('container-type', 'scroll-state')) {
+    const sentinel = document.getElementById('sentinel');
+    const primary = document.querySelector('rh-navigation-primary');
+    const observer = new IntersectionObserver(([entry]) => {
+      primary.style.setProperty(
+        '--rh-navigation-primary-z-index',
+        entry.isIntersecting ? '102' : '1'
+      );
+    }, { threshold: 0 });
+    observer.observe(sentinel);
+  }
Evidence
The CSS relies on a .nav-wrapper scroll-state container, but the HTML has no element with that
class (the navs are direct siblings). The JS fallback only runs when
CSS.supports('container-type','scroll-state') is false, so it will not run in browsers that
support the feature—leaving no mechanism to adjust z-index.

docs/patterns/stacked-navigation/patterns/stacked-navigation.html[241-289]
docs/patterns/stacked-navigation/patterns/stacked-navigation.html[355-367]
docs/patterns/stacked-navigation/patterns/stacked-navigation.html[462-472]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The stacked-navigation example intends to lower `rh-navigation-primary`’s z-index when the secondary nav becomes sticky. The CSS uses a scroll-state container query on `.nav-wrapper`, but no `.nav-wrapper` exists in the markup. Because the IntersectionObserver fallback is gated behind `!CSS.supports('container-type','scroll-state')`, browsers that *do* support scroll-state will skip the fallback and still not run the container query.
### Issue Context
This makes the pattern fail specifically on browsers that support `container-type: scroll-state`.
### Fix Focus Areas
- docs/patterns/stacked-navigation/patterns/stacked-navigation.html[241-289]
- docs/patterns/stacked-navigation/patterns/stacked-navigation.html[355-367]
- docs/patterns/stacked-navigation/patterns/stacked-navigation.html[462-472]
### Suggested fix
- Wrap the stacked navs in an element with `class="nav-wrapper"` (and ensure it is the relevant scroll container), **or**
- Remove the unused `.nav-wrapper` container-query path and always use the IntersectionObserver approach (or use a stronger runtime check than `CSS.supports(...)` to decide which mechanism is actually active).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Invalid palette event value ✓ Resolved 🐞 Bug ≡ Correctness
Description
uxdot-context-select dispatches ContextChangeEvent with '' for the “System” option, but
ContextChangeEvent is defined to always carry a valid ColorPalette, so listeners may break when
they receive an unexpected empty string.
Code

uxdot/uxdot-context-select.ts[R35-149]

+  @property() value: ColorPalette | '' = '';
+
+  @property({ converter: ColorPaletteListConverter })
+  allow = paletteNames;
+
+  /** Accessible label for the select */
+  @property({ attribute: 'accessible-label' }) accessibleLabel?: string;
+
+  #internals = InternalsController.of(this);
+
+  #target: HTMLElement | null = null;
+
+  /**
+   * Renders a native `<select>` enhanced with `appearance: base-select`.
+   * During SSR the `<select>` is rendered without the base-select children
+   * (`<button>`, `<selectedcontent>`, swatch `<span>`s) because the browser
+   * HTML parser strips non-conforming `<select>` children from SSR markup,
+   * which would cause a Lit hydration mismatch. On first client update the
+   * base-select elements are injected into the live DOM.
+   */
+  render() {
+    const { allow, value } = this;
+    const label = this.accessibleLabel
+                || this.#internals.computedLabelText
+                || 'Color palette';
+    return html`
+      <select aria-label="${label}"
+              @change="${this.#onChange}">
+        <option value="" ?selected="${value === ''}">System</option>
+        ${allow.map(palette => html`
+        <option value="${palette}" ?selected="${value === palette}">${palette}</option>`)}
+      </select>
+    `;
+  }
+
+  protected override firstUpdated() {
+    for (const label of this.#internals.labels) {
+      label.addEventListener('click', () => this.focus());
+    }
+    if (this.target instanceof HTMLElement) {
+      this.#target = this.target;
+      this.sync();
+    } else if (this.target) {
+      const root = this.getRootNode() as Document | ShadowRoot;
+      if (root instanceof Document || root instanceof ShadowRoot) {
+        this.#target = root.getElementById(this.target);
+        this.sync();
+      }
+    } else {
+      this.#target = this.closest('rh-surface');
+    }
+  }
+
+  protected override updated() {
+    this.#ensureBaseSelectDOM();
+  }
+
+  /** Inject base-select children (`<button>`, `<selectedcontent>`, swatches) into the live DOM */
+  #ensureBaseSelectDOM() {
+    if (isServer) {
+      return;
+    }
+    const select = this.shadowRoot.querySelector('select');
+    if (!select) {
+      return;
+    }
+    if (!select.querySelector(':scope > button')) {
+      const btn = document.createElement('button');
+      btn.setAttribute('type', 'button');
+      const sc = document.createElement('selectedcontent');
+      const caret = document.createElement('rh-icon');
+      caret.setAttribute('set', 'microns');
+      caret.setAttribute('icon', 'caret-down-fill');
+      caret.classList.add('caret');
+      btn.appendChild(sc);
+      btn.appendChild(caret);
+      select.prepend(btn);
+    }
+    for (const option of select.options) {
+      if (!option.querySelector('.swatch') && !option.querySelector('rh-icon')) {
+        if (option.value === '') {
+          const icon = document.createElement('rh-icon');
+          icon.setAttribute('set', 'ui');
+          icon.setAttribute('icon', 'auto-light-dark-mode');
+          icon.classList.add('system-icon');
+          option.prepend(icon);
+        } else {
+          const swatch = document.createElement('span');
+          swatch.className = `swatch ${option.value}`;
+          option.prepend(swatch);
+        }
+      }
+    }
+  }
+
+  formStateRestoreCallback(state: string) {
+    if (state) {
+      this.#setValue(state as this['value']);
+    }
+  }
+
+  #onChange(event: Event) {
+    if (event.target instanceof HTMLSelectElement) {
+      event.stopPropagation();
+      this.#setValue(event.target.value as this['value']);
+    }
+  }
+
+  #setValue(value: this['value']) {
+    this.#internals.setFormValue(value);
+    if (value !== this.value
+        && this.dispatchEvent(new ContextChangeEvent(value as ColorPalette, this.#target))) {
+      this.value = value;
+      this.sync();
+    }
Evidence
The component’s value type includes '' and it has an System, then it casts that value to
ColorPalette when constructing ContextChangeEvent. The event class itself is defined with
public colorPalette: ColorPalette, so '' violates the event’s contract at runtime.

uxdot/uxdot-context-select.ts[35-66]
uxdot/uxdot-context-select.ts[143-149]
lib/elements/rh-context-picker/rh-context-picker.ts[26-34]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`uxdot-context-select` emits `ContextChangeEvent` with an empty string when the user selects the “System” option. `ContextChangeEvent`’s `colorPalette` field is typed/implemented as a real `ColorPalette`, so `''` is an invalid value and can break consumers that don’t defensively treat it as “unset”.
### Issue Context
`ContextChangeEvent` is imported from `rh-context-picker` and is defined as `constructor(public colorPalette: ColorPalette, public provider: HTMLElement | null)`.
### Fix Focus Areas
- uxdot/uxdot-context-select.ts[35-66]
- uxdot/uxdot-context-select.ts[143-149]
- lib/elements/rh-context-picker/rh-context-picker.ts[26-34]
### Suggested fix
Pick one:
1. Define and dispatch a new event type for `uxdot-context-select` where `colorPalette` is `ColorPalette | null` (or `undefined`) to represent “System”, and update listeners accordingly.
2. Keep `ContextChangeEvent`, but do **not** dispatch it for the System option; instead dispatch a separate `change` event or a dedicated `uxdot-context-select:system` event.
3. If you must keep a single event, change the payload to use `null`/`undefined` rather than `''` and avoid `as ColorPalette` casts (this likely requires a different event class than `ContextChangeEvent`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Unvalidated iframe postMessage ✓ Resolved 🐞 Bug ⛨ Security
Description
The parent posts color-palette updates with targetOrigin='*' and the generated iframe page accepts
any message event with the matching type without validating event.origin/event.source,
allowing cross-origin message spoofing to mutate iframe state.
Code

uxdot/uxdot-pattern.ts[R195-215]

#onChange(event: Event) {
if (event instanceof ContextChangeEvent) {
-      this.colorPalette = event.colorPalette;
+      const codeTabs = this.shadowRoot?.querySelector('#code-tabs');
+      if (event.colorPalette) {
+        codeTabs?.setAttribute('color-palette', event.colorPalette);
+      } else {
+        codeTabs?.removeAttribute('color-palette');
+      }
+      if (this.viewport) {
+        this.#postColorPalette(event.colorPalette);
+      }
}
}
+
+  #postColorPalette(colorPalette: ColorPalette) {
+    const iframe = this.shadowRoot?.querySelector<HTMLIFrameElement>('#viewport-frame');
+    iframe?.contentWindow?.postMessage({
+      type: 'uxdot-pattern:color-palette',
+      colorPalette,
+    }, '*');
+  }
Evidence
The parent uses postMessage(..., '*'), and the iframe’s message handler performs no
origin/source checks before applying the palette. This is the standard insecure postMessage pattern:
any embedding page (or any window with a reference) can send a crafted message.

uxdot/uxdot-pattern.ts[195-215]
uxdot/uxdot-pattern-ssr-controller-server.ts[223-237]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The iframe palette sync uses `postMessage` with a wildcard origin and a receiver that does not validate `event.origin` or `event.source`. This allows unintended cross-origin messages to be processed.
### Issue Context
- Sender: `uxdot-pattern` posts `{ type: 'uxdot-pattern:color-palette', ... }` with `'*'`.
- Receiver: iframe page listens for `message` and checks only `event.data?.type`.
### Fix Focus Areas
- uxdot/uxdot-pattern.ts[209-215]
- uxdot/uxdot-pattern-ssr-controller-server.ts[225-236]
### Suggested fix
- Sender: use a concrete `targetOrigin` (e.g., `new URL(iframe.src).origin` once available).
- Receiver: validate `event.origin` matches the expected parent origin, and optionally ensure `event.source === window.parent`.
- Consider adding a per-instance nonce/token in the message payload that the iframe page validates before applying changes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Iframe omits module shims 🐞 Bug ☼ Reliability
Description
The generated iframe HTML emits an import map but does not include the es-module-shims script that
the main docs layout loads, so the iframe preview may fail in environments relying on shims for
importmap/bare-specifier support.
Code

uxdot/uxdot-pattern-ssr-controller-server.ts[R199-207]

+    const page = `<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <script type="importmap">${JSON.stringify(importMap)}</script>
+  <script type="module">import '@lit-labs/ssr-client/lit-element-hydrate-support.js';</script>
+  <link rel="stylesheet" href="/styles/fonts.css">
+  <style>
Evidence
The main docs layout includes es-module-shims after the importmap, but the iframe page only emits
the importmap and then module scripts. If the site depends on shims for consistent importmap
behavior across browsers, the iframe preview is missing the same compatibility layer.

uxdot/uxdot-pattern-ssr-controller-server.ts[199-207]
docs/_includes/layouts/base.njk[18-42]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The iframe preview page includes an import map but does not load `es-module-shims`, while the main docs pages do. This can cause module resolution failures in browsers/environments where importmap support is shimmed.
### Issue Context
The base docs layout emits:
- `<script type="importmap">...`
- `es-module-shims.js`
The iframe HTML currently emits only the importmap and module scripts.
### Fix Focus Areas
- uxdot/uxdot-pattern-ssr-controller-server.ts[199-207]
- docs/_includes/layouts/base.njk[18-42]
### Suggested fix
Add the same `es-module-shims` loader to the iframe page (ideally in the same order as the base layout) if the docs site expects shimmed importmaps for compatibility.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@github-actions

github-actions Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Size Change: +2.86 kB (+0.91%)

Total Size: 317 kB

📦 View Changed
Filename Size Change
elements.js 832 B -2 B (-0.24%)
lib/elements/rh-context-picker/rh-context-picker.js 3.19 kB +1.01 kB (+46.28%) 🚨
uxdot/uxdot-pattern-ssr-controller-client.js 408 B +22 B (+5.7%) 🔍
uxdot/uxdot-pattern-ssr-controller-server.js 2.89 kB +1.18 kB (+68.88%) 🆘
uxdot/uxdot-pattern.js 3.02 kB +648 B (+27.33%) 🚨
ℹ️ View Unchanged
Filename Size
elements/rh-accordion/context.js 162 B
elements/rh-accordion/rh-accordion-header.js 3.33 kB
elements/rh-accordion/rh-accordion-panel.js 2.08 kB
elements/rh-accordion/rh-accordion.js 3.67 kB
elements/rh-alert/rh-alert.js 5.52 kB
elements/rh-announcement/rh-announcement.js 2.84 kB
elements/rh-audio-player/rh-audio-player-about.js 2.28 kB
elements/rh-audio-player/rh-audio-player-rate-stepper.js 2.07 kB
elements/rh-audio-player/rh-audio-player-scrolling-text-overflow.js 1.78 kB
elements/rh-audio-player/rh-audio-player-subscribe.js 1.9 kB
elements/rh-audio-player/rh-audio-player.js 13.2 kB
elements/rh-audio-player/rh-cue.js 2.32 kB
elements/rh-audio-player/rh-transcript.js 3.38 kB
elements/rh-avatar/random-pattern-controller.js 2.72 kB
elements/rh-avatar/rh-avatar.js 3.45 kB
elements/rh-back-to-top/rh-back-to-top.js 2.79 kB
elements/rh-badge/rh-badge.js 2.05 kB
elements/rh-blockquote/rh-blockquote.js 1.93 kB
elements/rh-breadcrumb/rh-breadcrumb.js 2.34 kB
elements/rh-button-group/rh-button-group.js 937 B
elements/rh-button/rh-button.js 4.1 kB
elements/rh-card/rh-card.js 3.46 kB
elements/rh-chip/context.js 165 B
elements/rh-chip/rh-chip-group.js 1.98 kB
elements/rh-chip/rh-chip.js 2.45 kB
elements/rh-code-block/prism.css.js 667 B
elements/rh-code-block/prism.js 572 B
elements/rh-code-block/rh-code-block.js 9.21 kB
elements/rh-cta/rh-cta.js 3.9 kB
elements/rh-dialog/rh-dialog.js 5.04 kB
elements/rh-dialog/yt-api.js 617 B
elements/rh-disclosure/rh-disclosure.js 3.41 kB
elements/rh-footer/rh-footer-block.js 1.07 kB
elements/rh-footer/rh-footer-copyright.js 649 B
elements/rh-footer/rh-footer-links.js 1.65 kB
elements/rh-footer/rh-footer-social-link.js 1.24 kB
elements/rh-footer/rh-footer-universal.js 5.37 kB
elements/rh-footer/rh-footer.js 5.85 kB
elements/rh-health-index/rh-health-index.js 2.48 kB
elements/rh-icon/rh-icon.js 2.98 kB
elements/rh-icon/ssr.js 181 B
elements/rh-jump-links/context.js 179 B
elements/rh-jump-links/rh-jump-link.js 1.89 kB
elements/rh-jump-links/rh-jump-links-list.js 1.67 kB
elements/rh-jump-links/rh-jump-links.js 3.08 kB
elements/rh-menu-dropdown/rh-menu-dropdown.js 4.43 kB
elements/rh-menu/rh-menu-item-group.js 941 B
elements/rh-menu/rh-menu-item.js 2.55 kB
elements/rh-menu/rh-menu.js 2.04 kB
elements/rh-navigation-link/rh-navigation-link.js 2.13 kB
elements/rh-navigation-primary/context.js 176 B
elements/rh-navigation-primary/rh-navigation-primary-item-menu.js 1.45 kB
elements/rh-navigation-primary/rh-navigation-primary-item.js 4.22 kB
elements/rh-navigation-primary/rh-navigation-primary.js 10.1 kB
elements/rh-navigation-secondary/rh-navigation-secondary-dropdown.js 3.15 kB
elements/rh-navigation-secondary/rh-navigation-secondary-menu-section.js 2.02 kB
elements/rh-navigation-secondary/rh-navigation-secondary-menu.js 2.01 kB
elements/rh-navigation-secondary/rh-navigation-secondary-overlay.js 828 B
elements/rh-navigation-secondary/rh-navigation-secondary.js 6.32 kB
elements/rh-navigation-secondary/test/fixtures.js 769 B
elements/rh-navigation-vertical/rh-navigation-vertical-list.js 2.56 kB
elements/rh-navigation-vertical/rh-navigation-vertical.js 1.65 kB
elements/rh-pagination/rh-pagination.js 6.51 kB
elements/rh-progress-stepper/context.js 187 B
elements/rh-progress-stepper/rh-progress-step.js 3.02 kB
elements/rh-progress-stepper/rh-progress-stepper.js 4.78 kB
elements/rh-readtime/rh-readtime.js 2.93 kB
elements/rh-scheme-toggle/rh-scheme-toggle.js 2.36 kB
elements/rh-select/rh-option-group.js 1.6 kB
elements/rh-select/rh-option.js 2.39 kB
elements/rh-select/rh-select.js 9.46 kB
elements/rh-site-status/rh-site-status.js 2.53 kB
elements/rh-skeleton/rh-skeleton.js 742 B
elements/rh-skip-link/rh-skip-link.js 1.45 kB
elements/rh-spinner/rh-spinner.js 1.51 kB
elements/rh-stat/rh-stat.js 2.71 kB
elements/rh-subnav/rh-subnav.js 3.18 kB
elements/rh-surface/rh-surface.js 865 B
elements/rh-surface/test/elements.js 763 B
elements/rh-switch/rh-switch.js 3.22 kB
elements/rh-table/rh-sort-button.js 1.88 kB
elements/rh-table/rh-table.js 3.27 kB
elements/rh-tabs/context.js 223 B
elements/rh-tabs/rh-tab-panel.js 1.19 kB
elements/rh-tabs/rh-tab.js 3.64 kB
elements/rh-tabs/rh-tabs.js 4.35 kB
elements/rh-tag/rh-tag.js 3.32 kB
elements/rh-tile/rh-tile-group.js 1.97 kB
elements/rh-tile/rh-tile.js 5.09 kB
elements/rh-timestamp/rh-timestamp.js 1.33 kB
elements/rh-tooltip/rh-tooltip.js 3.4 kB
elements/rh-video-embed/rh-video-embed.js 5.45 kB
lib/color-palettes.js 851 B
lib/context/headings/consumer.js 591 B
lib/context/headings/provider.js 1.2 kB
lib/elements/rh-context-demo/rh-context-demo.js 1.16 kB
lib/environment.js 194 B
lib/functions.js 175 B
lib/I18nController.js 1.37 kB
lib/ScreenSizeController.js 876 B
lib/ssr-controller.js 201 B
lib/themable.js 549 B
react/lib/color-palettes.js 97 B
react/lib/context/headings/consumer.js 103 B
react/lib/context/headings/provider.js 105 B
react/lib/elements/rh-context-demo/rh-context-demo.js 186 B
react/lib/elements/rh-context-picker/rh-context-picker.js 189 B
react/lib/functions.js 92 B
react/lib/I18nController.js 97 B
react/lib/ScreenSizeController.js 102 B
react/lib/ssr-controller.js 97 B
react/lib/themable.js 91 B
react/rh-accordion/rh-accordion-header.js 199 B
react/rh-accordion/rh-accordion-panel.js 185 B
react/rh-accordion/rh-accordion.js 202 B
react/rh-alert/rh-alert.js 184 B
react/rh-announcement/rh-announcement.js 189 B
react/rh-audio-player/rh-audio-player-about.js 191 B
react/rh-audio-player/rh-audio-player-rate-stepper.js 223 B
react/rh-audio-player/rh-audio-player-scrolling-text-overflow.js 214 B
react/rh-audio-player/rh-audio-player-subscribe.js 196 B
react/rh-audio-player/rh-audio-player.js 183 B
react/rh-audio-player/rh-cue.js 195 B
react/rh-audio-player/rh-transcript.js 207 B
react/rh-avatar/rh-avatar.js 173 B
react/rh-back-to-top/rh-back-to-top.js 183 B
react/rh-badge/rh-badge.js 174 B
react/rh-blockquote/rh-blockquote.js 179 B
react/rh-breadcrumb/rh-breadcrumb.js 179 B
react/rh-button-group/rh-button-group.js 184 B
react/rh-button/rh-button.js 174 B
react/rh-card/rh-card.js 172 B
react/rh-chip/rh-chip-group.js 182 B
react/rh-chip/rh-chip.js 180 B
react/rh-code-block/rh-code-block.js 193 B
react/rh-cta/rh-cta.js 170 B
react/rh-dialog/rh-dialog.js 203 B
react/rh-disclosure/rh-disclosure.js 192 B
react/rh-footer/rh-footer-block.js 184 B
react/rh-footer/rh-footer-copyright.js 187 B
react/rh-footer/rh-footer-links.js 185 B
react/rh-footer/rh-footer-social-link.js 193 B
react/rh-footer/rh-footer-universal.js 188 B
react/rh-footer/rh-footer.js 174 B
react/rh-health-index/rh-health-index.js 184 B
react/rh-icon/rh-icon.js 195 B
react/rh-jump-links/rh-jump-link.js 183 B
react/rh-jump-links/rh-jump-links-list.js 189 B
react/rh-jump-links/rh-jump-links.js 195 B
react/rh-menu-dropdown/rh-menu-dropdown.js 198 B
react/rh-menu/rh-menu-item-group.js 190 B
react/rh-menu/rh-menu-item.js 181 B
react/rh-menu/rh-menu.js 182 B
react/rh-navigation-link/rh-navigation-link.js 186 B
react/rh-navigation-primary/rh-navigation-primary-item-menu.js 205 B
react/rh-navigation-primary/rh-navigation-primary-item.js 210 B
react/rh-navigation-primary/rh-navigation-primary.js 189 B
react/rh-navigation-secondary/rh-navigation-secondary-dropdown.js 227 B
react/rh-navigation-secondary/rh-navigation-secondary-menu-section.js 205 B
react/rh-navigation-secondary/rh-navigation-secondary-menu.js 199 B
react/rh-navigation-secondary/rh-navigation-secondary-overlay.js 201 B
react/rh-navigation-secondary/rh-navigation-secondary.js 213 B
react/rh-navigation-vertical/rh-navigation-vertical-list.js 209 B
react/rh-navigation-vertical/rh-navigation-vertical.js 189 B
react/rh-pagination/rh-pagination.js 178 B
react/rh-progress-stepper/rh-progress-step.js 196 B
react/rh-progress-stepper/rh-progress-stepper.js 186 B
react/rh-readtime/rh-readtime.js 175 B
react/rh-scheme-toggle/rh-scheme-toggle.js 183 B
react/rh-select/rh-option-group.js 187 B
react/rh-select/rh-option.js 177 B
react/rh-select/rh-select.js 204 B
react/rh-site-status/rh-site-status.js 181 B
react/rh-skeleton/rh-skeleton.js 176 B
react/rh-skip-link/rh-skip-link.js 181 B
react/rh-spinner/rh-spinner.js 175 B
react/rh-stat/rh-stat.js 171 B
react/rh-subnav/rh-subnav.js 175 B
react/rh-surface/rh-surface.js 175 B
react/rh-switch/rh-switch.js 185 B
react/rh-table/rh-sort-button.js 200 B
react/rh-table/rh-table.js 174 B
react/rh-tabs/rh-tab-panel.js 181 B
react/rh-tabs/rh-tab.js 187 B
react/rh-tabs/rh-tabs.js 184 B
react/rh-tag/rh-tag.js 171 B
react/rh-tile/rh-tile-group.js 183 B
react/rh-tile/rh-tile.js 181 B
react/rh-timestamp/rh-timestamp.js 176 B
react/rh-tooltip/rh-tooltip.js 175 B
react/rh-video-embed/rh-video-embed.js 227 B
uxdot/ssr-adopt-directive.js 2.02 kB
uxdot/uxdot-best-practice.js 812 B
uxdot/uxdot-copy-button.js 1.24 kB
uxdot/uxdot-copy-permalink.js 1.14 kB
uxdot/uxdot-demo.js 2.89 kB
uxdot/uxdot-example.js 1.14 kB
uxdot/uxdot-feedback.js 983 B
uxdot/uxdot-header.js 886 B
uxdot/uxdot-knob-attribute.js 3.73 kB
uxdot/uxdot-masthead.js 1.45 kB
uxdot/uxdot-pattern-ssr-controller.js 213 B
uxdot/uxdot-repo-status-checklist.js 1.39 kB
uxdot/uxdot-repo-status-list.js 1.24 kB
uxdot/uxdot-repo.js 867 B
uxdot/uxdot-sidenav.js 2.04 kB
uxdot/uxdot-spacer-tokens-table.js 2.45 kB
uxdot/uxdot-toc.js 1.89 kB

compressed-size-action

@zeroedin zeroedin changed the title Docs/uxdot pattern/iframe viewport docs: uxdot pattern support media query patterns with an iframe viewport May 1, 2026
@zeroedin zeroedin changed the title docs: uxdot pattern support media query patterns with an iframe viewport docs: <uxdot-pattern> support media query patterns with an iframe viewport May 1, 2026
@github-actions

github-actions Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Documentation Health

Module Score
lib/elements/rh-context-demo/rh-context-demo.js 22/100 22%
lib/elements/rh-context-picker/rh-context-picker.js 312/460 ⚠️ 67%
Overall 334/560 ⚠️ 59%
rh-context-demo (RhContextDemo) — 22/100 ❌
Category Score
Element description 0/25
Attribute documentation 5/20
Slot documentation 0/15
CSS documentation 2/15
Event documentation 15/15
Demos 0/10
ColorPaletteListConverter — 65/90 ⚠️
Category Score
Element description 0/25
Attribute documentation 20/20
Slot documentation 15/15
CSS documentation 15/15
Event documentation 15/15
Demos 0/0
paletteMap — 65/90 ⚠️
Category Score
Element description 0/25
Attribute documentation 20/20
Slot documentation 15/15
CSS documentation 15/15
Event documentation 15/15
Demos 0/0
paletteNames — 65/90 ⚠️
Category Score
Element description 0/25
Attribute documentation 20/20
Slot documentation 15/15
CSS documentation 15/15
Event documentation 15/15
Demos 0/0
ContextChangeEvent — 65/90 ⚠️
Category Score
Element description 0/25
Attribute documentation 20/20
Slot documentation 15/15
CSS documentation 15/15
Event documentation 15/15
Demos 0/0
rh-context-picker (RhContextPicker) — 52/100 ⚠️
Category Score
Element description 0/25
Attribute documentation 12/20 ⚠️
Slot documentation 15/15
CSS documentation 10/15 ⚠️
Event documentation 15/15
Demos 0/10

Recommendations:

  1. rh-context-demo: add descriptions to all attributes (Attribute documentation, +10 pts)
  2. rh-context-demo: add at least one demo with a URL (Demos, +5 pts)
  3. rh-context-picker: add a description to explain what this declaration does (Element description, +5 pts)
  4. rh-context-demo: use RFC 2119 keywords (MUST, SHOULD, AVOID) to clarify requirements (Element description, +5 pts)
  5. rh-context-picker: add at least one demo with a URL (Demos, +5 pts)

Comment thread docs/patterns/stacked-navigation/patterns/stacked-navigation.html
Comment thread uxdot/uxdot-context-select.ts Outdated
Comment thread uxdot/uxdot-pattern.ts

@adamjohnson adamjohnson left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a pretty neat idea. There's still work to do around design refinement and fallback browser support, but cool to see.

Comment thread docs/patterns/index.md

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will need an accompanying image in docs/assets/patterns/ for the Stacked Navigation thumbnail on the All Patterns page.

Comment thread uxdot/uxdot-context-select.css Outdated
}

select {
appearance: none;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot of select in safari missing a caret icon

This is what this looks like in Safari (browser without base-select support yet. We definitely wanna add a caret icon for the non-custom select supporting browsers. See #2959 for and example of how to do that.

Comment thread uxdot/uxdot-pattern.ts
<rh-surface id="content">${allContent}</rh-surface>
<div id="controls-row">
${this.viewport ? html`
<rh-button-group id="viewport-controls"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like these buttons could use some further design refinement. Perhaps some styling to button group to help unify them, which may include equalizing their height to the neighboring select.

Might add a title attr/tooltip too.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

Comment thread uxdot/uxdot-context-select.ts Outdated
Comment thread uxdot/uxdot-pattern.ts Outdated
Comment thread uxdot/uxdot-pattern.ts
<rh-surface id="content">${allContent}</rh-surface>
<div id="controls-row">
${this.viewport ? html`
<rh-button-group id="viewport-controls"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

really like this idea

Comment thread uxdot/uxdot-context-select.ts Outdated
const label = this.accessibleLabel
|| this.#internals.computedLabelText
|| 'Color palette';
return this.hasUpdated ? html`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bennypowers What do you think about this pattern? This is essentially a workaround to SSR a custom select.

Currently, Lit SSR and <selectedcontent> don't play nice together.

If you deem this acceptable, I can apply this pattern to <rh-scheme-dropdown>.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i need to understand more what the issues are with litssr and selectedcontent. we'll need to have a show and tell i think

generally though, if there's state affecting render which changes on the clientside after SSR, that render has to happen only after hasUpdated

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a little backstory:

1bee358

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for bringing this up

lit/lit#5349

go with the multiple render paths solution for now, gated on hasUpdated

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with a refactor in 42158ab this is now in rh-context-picker.ts

@zeroedin

Copy link
Copy Markdown
Collaborator Author

@coreyvickery and @marionnegp I'd love your feedback on this proof of concept. Not necessarily for a critique of what I've done but to get your wheels spinning for how you can take this idea and improve upon it.

@hellogreg hellogreg added this to the 2026/Q3 — Horsea release milestone Jun 11, 2026
@hellogreg hellogreg changed the title docs: <uxdot-pattern> support media query patterns with an iframe viewport POC docs: <uxdot-pattern> support media query patterns with an iframe viewport Jun 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Review 🔍

Development

Successfully merging this pull request may close these issues.

4 participants