Skip to content

docs(docs-app): wire up build-time .gjs.md docs, start .md migration - #743

Merged
patricklx merged 6 commits into
mainfrom
docs/kolay-wrap-demos-status
Aug 20, 2026
Merged

patricklx merged 6 commits into
mainfrom
docs/kolay-wrap-demos-status

Conversation

@patricklx

@patricklx patricklx commented Aug 5, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to the "check whether kolay's new shadow-DOM support can replace our downstream implementation" investigation, now extended into starting the migration of docs-app's component docs from runtime-compiled .md (kolay's in-browser REPL) to build-time-compiled .gjs.md.

Why move to .gjs.md

.md docs are compiled on-demand in the browser (via repl-sdk); .gjs.md docs are compiled once at build time into real Ember components. Moving reduces runtime REPL/compile cost per page and gets these docs full type-checking via glint.

What this PR adds

kolay's vite plugin (kolay()) already forwards scope/rehypePlugins/remarkPlugins to its build-time .gjs.md compiler (undocumented in kolay's public Options type, but wired in combined.js), so no upstream change was needed. Two real gaps had to close first, both scoped to the build-time path only:

  • rehypeShadowDemo silently produced empty demos. It renames each demo's placeholder <div id="repl_N"> to <carbon-shadow-demo id="repl_N">. Runtime .md grafts the compiled demo into that element by id directly. Build-time .gjs.md (kolay's gjs-md.js) instead finds the node by id and injects the invocation by string-replacing the node's literal </div> — which no longer exists once the div is renamed, so the replace silently no-ops and the demo renders styled but empty. Fixed by giving rehypeShadowDemo an opt-in { forBuildTimeInjection: true } mode that keeps an inner <div> for the injector to target (CarbonShadowDemo re-parents whatever ends up inside it regardless of tag, so the wrapper is otherwise inert). The runtime call site in routes/application.ts is untouched and emits byte-identical output.
  • Two demo-fence import specifiers don't actually resolve. carbon-components-ember/components and carbon-components-ember/helpers only work at runtime because setupKolay's dynamic modules map intercepts them before real module resolution — the package's real exports only has the /index forms. Aliased both (anchored RegExps, so already-correct /index//icon specifiers elsewhere aren't affected) in vite.config.mjs. Also added a docs-support alias (new docs-app/app/docs-support/index.ts barrel) backing the ThemeSupport/didInsert imports demos use, and a scope string so <ThemeSwitcher />/<APIDocs>/<ComponentSignature>/<ModifierSignature>/<Callout> resolve at the top level of a .gjs.md file, mirroring the runtime topLevelScope.

Converted so far

tags.md and list/item.md (one flat page, one nested-folder page, to exercise both URL shapes) — verified in a real browser (not just glint/a green build) that the demo's shadow root contains real rendered component markup, compared against an unconverted .md control page in the same session. An empty demo looks identical to a known docs-app live-preview flake otherwise, so build success alone isn't proof.

notifications.md was converted to .gjs.md by rewriting its demo's plain Context class (which relied on a runtime-only setOwner() shim) as a real Component subclass — Ember auto-injects the owner into components invoked via the template, so @service('carbon.notifications') works at build time with no extra infra changes.

@patricklx
patricklx force-pushed the docs/kolay-wrap-demos-status branch from 2d8fa61 to 17681b4 Compare August 7, 2026 14:57
@patricklx patricklx changed the title docs(docs-app): correct shadow-demo comments; note kolay wrapDemos status docs(docs-app): wire up build-time .gjs.md docs, start .md migration Aug 14, 2026
@patricklx
patricklx force-pushed the docs/kolay-wrap-demos-status branch from 54523d9 to c9f3c2c Compare August 14, 2026 10:51
@github-actions

github-actions Bot commented Aug 14, 2026 •

Copy link
Copy Markdown
Contributor

📖 Docs Preview

Preview has been removed (PR closed or preview label removed).

github-actions Bot pushed a commit that referenced this pull request Aug 14, 2026
github-actions Bot pushed a commit that referenced this pull request Aug 14, 2026
github-actions Bot pushed a commit that referenced this pull request Aug 14, 2026
@patricklx

Copy link
Copy Markdown
Collaborator Author

the code blocks in docs do not have syntax highlighting. it works on main

@patricklx

Copy link
Copy Markdown
Collaborator Author

Fixed the syntax-highlighting regression (17d51acd).

Root cause: vite.config.mjs's kolay() plugin (build-time .gjs.md compiler) only registered rehypeShadowDemo in rehypePlugins. routes/application.ts's runtime setupKolay() call separately registers rehypeShikiFromHighlighter for syntax highlighting — the build-time path never got the equivalent plugin, so converted pages' code fences rendered as plain unhighlighted text.

Fix: added @shikijs/rehype's default export to kolay()'s rehypePlugins, with the same langs/theme config as the runtime setup. Since vite.config.mjs runs in Node at build time, it can use the package's self-contained highlighter creation instead of the runtime path's manual getHighlighterCore/loadWasm (which exists only to work around browser WASM loading).

Verified:

  • DOCS_URL=versions/main pnpm build succeeds; every built .gjs chunk now contains shiki output (grep shiki-light on all dist/assets/*.gjs-*.js)
  • Playwright, served prod build: 2-components/tags (converted, build-time path) and 2-components/notifications (still .md, runtime path — the one intentionally-unconverted control page) render visually identical multi-color token highlighting, same shiki shiki-themes classes
  • glint/root pnpm lint stay at the existing baseline (no new errors/warnings)

🤖 Generated with Claude Code

@patricklx patricklx removed the review label Aug 14, 2026
github-actions Bot pushed a commit that referenced this pull request Aug 14, 2026
patricklx and others added 6 commits August 18, 2026 23:45
…atus

`shadow-demo-element.ts` still claimed it imports the app's stylesheets into
each shadow root "mirroring `<Shadowed includeStyles>`". It stopped doing that
in 1d7bfa3 ("do not add host styles"), which left the `STYLESHEET_SELECTOR`
constant dead and the comment wrong. Drop the constant and describe what
actually supplies the CSS: demos render `<ThemeSupport />` themselves, so the
Carbon stylesheets land inside the shadow root as an inline `<style>`, and
`:root` theme tokens inherit across the boundary as custom properties.

Also record why `rehype-shadow-demo.ts` stays hand-rolled. kolay merged a
generic `wrapDemos` rehype plugin (universal-ember/kolay#361, unreleased as of
kolay 5.4.0 - it ships in the pending 6.0.0), but it wraps the demo placeholder
in a component invocation rather than renaming it. repl-sdk grafts each
compiled demo in afterwards via `element.querySelector('#<placeholderId>')`,
which does not cross a shadow boundary, so a shadow-DOM wrapper would hide the
placeholder and trip repl-sdk's "Could not find placeholder / target element"
assertion. Renaming the placeholder keeps it in the light DOM, which is why the
current implementation works.

Comments only - no behavior change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Patrick Pircher <patrick.pircher@ibm.com>
Start of moving docs-app component docs off runtime-compiled .md (kolay's
in-browser REPL) to build-time-compiled .gjs.md, following up on this PR's
own investigation into kolay's wrapDemos status.

Two infra gaps had to close first, both scoped to build-time docs only:

- rehypeShadowDemo renames each demo placeholder div to a custom element,
  which kolay's build-time compiler can't find (it string-replaces the
  placeholder's literal `</div>`, which no longer exists), silently
  producing empty demos. Give it an opt-in `forBuildTimeInjection` mode
  that keeps an inner `<div>` for the injector to target; the runtime call
  site is untouched and emits byte-identical output.
- Demo fences import `carbon-components-ember/components` and
  `carbon-components-ember/helpers`, which only resolve at runtime via
  setupKolay's dynamic module map (the package's real exports are the
  `/index` forms). Alias them for the build-time path, plus a `docs-support`
  alias backing `ThemeSupport`/`didInsert` and a `scope` string so
  `<ThemeSwitcher />`/`<APIDocs>`/etc. resolve at the top level of a
  `.gjs.md` file, mirroring the runtime `topLevelScope`.

Converts tags.md and list/item.md (one flat, one nested-folder page) as a
verified spike - browser-checked (not just glint/build) against an
unconverted control page, since an empty demo looks identical to a known
docs-app live-preview flake otherwise. notifications.md is intentionally
left as .md: its demo uses setOwner() on a plain class for @service
injection, which has no build-time equivalent without rewriting it as a
real Component.

Remaining ~58 .md docs still need converting; recipe and gotchas recorded
in .pjp-runner/todo.md for follow-up work.

Signed-off-by: Patrick Pircher <patrick.pircher@ibm.com>
Applies the recipe from the Tag/List Item spike (previous commit) to the
remaining 32 flat top-level component docs, moving them off the
runtime-compiled REPL onto kolay's build-time .gjs.md compiler. Each file
browser-verified (Playwright, headless build of dist) to confirm its live
demos actually render component markup inside their shadow root, not just
that glint/build stay green - an empty demo looks identical to a known
docs-app live-preview flake otherwise.

pagination.md's "sizes" demo used `@onPageChanged={{() => {}}}`, an inline
arrow expression inside a mustache that kolay's runtime REPL tolerated but
the real ember-template-compiler used for build-time .gjs.md does not
(`Expecting 'OPEN_SEXPR', ... got 'CLOSE_SEXPR'`). Fixed to use the same
`const noop = () => null` + `{{noop}}` pattern already used by the other two
demos in that file.

Remaining nested-folder docs (form/, list/, select/, etc.) and
1-get-started/index.md are left for a follow-up batch; notifications.md
stays .md pending its Context class becoming a real Component (see
.pjp-runner/todo.md).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Patrick Pircher <patrick.pircher@ibm.com>
…js.md

Continues the flat-file batch: converts the form/, indicator/, layout/,
list/, select/, skeleton/, and text-input/ subfolders (22 files) to
build-time .gjs.md, following the same recipe. Each browser-verified
(Playwright against a built dist) to confirm live demos render real content
in their shadow root.

progress/bar.md is left as .md: invoking bare <ProgressBar /> in a built
.gjs.md page throws "Cannot destructure property 'compilable' of 'component'
as it is null" during template compilation, regardless of args. Bisected via
scratch pages to confirm it's not markdown content, not IconMap/registerIcon
state (nothing in the library ever calls registerIcon, so that registry is
always empty and unrelated), and not the generic <Icon> component in
isolation (a standalone <Icon @ICON='...' /> demo builds and renders fine).
ProgressBar is the only component in the library that invokes the generic
<Icon @ICON='...'> string-keyed component from within a conditional block,
so the bug is scoped to how kolay's build-time per-page compiler resolves
that specific nested-conditional-component pattern from a precompiled
library template. Needs deeper investigation into kolay/embroider's
component resolution for this case; noted in .pjp-runner/todo.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Patrick Pircher <patrick.pircher@ibm.com>
patricklx reported code blocks lost syntax highlighting on pages converted
to build-time .gjs.md (works on main's runtime-compiled .md). vite.config.mjs's
kolay() plugin only wired up rehypeShadowDemo for the build-time compiler;
routes/application.ts's runtime setupKolay() call separately registers
rehypeShikiFromHighlighter, which the build-time path never got.

Added the equivalent rehypeShiki plugin to kolay()'s rehypePlugins, using
@shikijs/rehype's default export (self-contained highlighter creation) since
vite.config.mjs runs in Node at build time and doesn't need the runtime
path's manual getHighlighterCore/loadWasm dance. Verified via Playwright
that a converted page (tags) and an unconverted control (notifications,
still runtime .md) render visually identical multi-color token highlighting,
and that every built .gjs chunk contains shiki output.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Patrick Pircher <patrick.pircher@ibm.com>
Rewrites the demo's plain Context class (which used a downstream
setOwner() shim only available at runtime) as a real Component
subclass, since Ember auto-injects the owner into components invoked
via the template. This unblocks @service('carbon.notifications')
injection at build time with no new infra changes needed.

Verified in a real browser (served production build) that the demo
renders real Notification/Button markup with no page errors, and that
clicking "Notify" exercises the service and renders a toast.

Signed-off-by: Patrick Pircher <patrick.pircher@ibm.com>
@patricklx
patricklx force-pushed the docs/kolay-wrap-demos-status branch from 17d51ac to a01886f Compare August 18, 2026 21:51
github-actions Bot pushed a commit that referenced this pull request Aug 18, 2026
@patricklx
patricklx merged commit 27c9553 into main Aug 20, 2026
8 checks passed
@patricklx
patricklx deleted the docs/kolay-wrap-demos-status branch August 20, 2026 20:54
github-actions Bot pushed a commit that referenced this pull request Aug 20, 2026
This was referenced Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant