Since the json-render frontend has only just been ported into @devframes/json-render-ui, I assume some of this is known or already being worked on. I'm happy to open PRs for any of it — I have most of the fixes scoped already — but I'd rather check first than duplicate work in progress. If you can mark which items are already handled (or are intentional), I'll pick up the rest.
Summary
Since the port (components re-based on @antfu/design), the prebuilt renderer module renders incorrectly inside the hub dock. Reproduced with Vite DevTools (vitejs/devtools playgrounds/core, @devframes/* 0.9.1, examples/plugin-git-ui dock), and reproducible in devframe's own examples/json-render.
The headline cause is a single selector bug: the renderer puts the .dark class on the very element it styles, so no dark variant applies inside the renderer's shadow root. Most of the "unreadable in dark mode" symptoms follow from that, plus an opaque background that hides the dock's glass.
Environment: @devframes/hub-ui / @devframes/json-render-ui 0.9.1, Chrome 1xx, macOS, dark scheme.
1. .dark is applied to the styled element itself, so every dark variant is dead
packages/json-render-ui/src/renderer-module/index.ts#L52-L58 creates the scroll root with the surface classes and toggles the scheme class on that same node:
const root = document.createElement('div')
root.className = 'w-full h-full of-auto p4 bg-base color-base font-sans text-sm'
const syncScheme = (): void => {
const dark = isDarkFor(container)
root.classList.toggle('dark', dark) // ← same element as `bg-base`/`color-base`
root.classList.toggle('light', !dark)
root.style.colorScheme = dark ? 'dark' : 'light'
}
UnoCSS compiles the dark variant as a descendant selector. From the generated stylesheet (packages/json-render-ui/src/.generated/css.ts, build output):
.bg-base{--un-jr-bg-opacity:1;background-color:rgb(255 255 255/var(--un-jr-bg-opacity))}
.dark .bg-base,…{--un-jr-bg-opacity:1;background-color:rgb(17 17 17/var(--un-jr-bg-opacity))}
.dark .bg-base cannot match an element that is .dark. Consequences in dark mode:
- the panel paints
bg-white — the opaque white block over the dock;
- text stays light-mode
color-base (neutral-800), i.e. dark text on it;
- the same holds for every
dark: rule in the sheet, so the whole subtree is themed light while the dock around it is dark.
hub-ui gets this right with a display: contents wrapper — ColorSchemeRoot.vue — so the renderer module needs the same shape: scheme class on an ancestor, styles on the child. (:host(.dark) isn't an option: the compiled utilities are descendant selectors that can't cross the shadow boundary, and changing the dark-variant selector would break the SPA and Storybook builds, where :host never matches.)
2. The renderer paints an opaque background over the dock's glass
The dock panel is deliberately translucent — bg-glass:80 (bg-white/104 dark:bg-#111/80 backdrop-blur-7):
The renderer's own bg-base (index.ts#L53) punches a solid rectangle through it, so a json-render dock is the only view that loses the glass. Before the port the equivalent wrapper had no background at all and its component tokens defaulted to var(--jr-bg, inherit) plus hue-neutral rgba(128,128,128,…) overlays — see ViewJsonRender.vue and json-render/components/tokens.ts at the commit before 352456fb removed them.
3. @antfu/design surfaces are opaque, so nested layers don't read as layers
The ported components inherit opaque surfaces from the design package (@antfu/design@0.3.4, not editable from here):
components/Layout/LayoutCard.vue → border border-base rounded-xl bg-base
components/Form/FormTextInput.vue root <label> → px-2 border rounded bg-base
components/Form/FormSelect.vue trigger and SelectContent → bg-base
Tabs active state → data-[state=active]:bg-base
- plus
bg-secondary at components/Text.ts#L16 (inline code) and renderer.ts#L106 (warning strip)
Only bg-base/bg-secondary are opaque in that preset; bg-hover (#8882), bg-ambient, bg-code (gray-500/5), bg-active and border-base (#8882) are already alpha-based. So a card-on-glass and an input-on-card collapse into the same flat tone — and, combined with issue 1, into dark-on-dark. What seems missing is a small set of panel-relative translucent layering tokens (raised / sunken / veil) that composite over whatever is behind them, replacing the baked bg-base on the components whose root is a real element.
FormSelect/FormCombobox can't be fixed that way: their root is reka's renderless SelectRoot, so a class passed from Select.ts#L91 is dropped entirely — they'd need a shadow-scoped rule, or a class hook upstream.
4. Scrollbar (and animation / popper) styling never reaches the renderer's shadow root
packages/json-render-ui/scripts/build-css.ts#L16-L22 calls buildShadowCss without userStylePath, so the injected sheet is UnoCSS output only:
$ grep -c scrollbar packages/json-render-ui/src/.generated/css.ts
0
The scroll root is of-auto, so it falls back to the browser's thick opaque-gutter scrollbar, which clashes with the translucent dock — the exact problem hub-ui/src/client/style.css#L1-L51 documents and solves for the dock (:host custom properties + unscoped ::-webkit-scrollbar*, wired via hub-ui/scripts/build-css.ts#L20). A global stylesheet can't pierce the boundary, so every shadow-isolated surface needs its own copy. scrollbar-gutter: stable, present pre-port, was also dropped.
Same gap for two design stylesheets that are never loaded for this surface: @antfu/design/styles/animations.css (af-fade keyframes → 0 occurrences in the generated CSS, so data-af-animate presence transitions don't run) and styles/reka-ui.css ([data-reka-popper-content-wrapper]{z-index:70}).
5. FormSelect / FormCombobox portal out of the shadow root
reka-ui's SelectPortal/ComboboxPortal resolve their target as props.to ?? configContext.teleportTo ?? 'body', and nothing configures it — dock-renderer.ts#L56-L64 mounts the app with no ConfigProvider. The dropdown is therefore appended to document.body, outside the shadow root that owns the stylesheet:
- no
bg-base, border-base, z-dropdown, no i-ph:* icons, none of the --colors-primary-* / --un-jr-* variables → unstyled popup;
- position is computed against a trigger in a different tree, with
z-dropdown (z-[40]) undefined on the host page versus the dock anchor's z-[2147483644] → the floating panel lands in the wrong place, or behind the dock.
A ConfigProvider teleportTo pointing at a container inside the renderer's shadow root fixes both. It should be a layer outside the of-auto scroll root (and inside the scheme wrapper from issue 1), otherwise the popper is clipped and scrolls with the content. I have this part working locally. The existing native: true escape hatch is the current workaround.
6. ActionButton variants disagree on padding
components/Button.ts#L17-L22 maps primary→primary, secondary→action, ghost→text, danger→primary+red override, and never passes size. The upstream variant classes carry three different paddings (@antfu/design components/Action/ActionButton.vue + unocss/shortcuts.ts):
| catalog variant |
design variant |
padding |
primary / danger |
btn-primary |
px3 py1.5 |
secondary |
btn-action |
px2 py1 |
ghost |
text |
none at all |
So a [Refresh][Commit][Stage All] row from one spec renders with mismatched box sizes, and a ghost button gets no hit padding.
7. Collapsible Card toggle is a tiny text glyph
components/Card.ts#L25-L28:
h('summary', { class: `${headerClass} cursor-pointer select-none list-none` }, [
h('span', props.title ?? ''),
h('span', { class: 'color-faint text-xs' }, '▾'),
])
A literal ▾ at text-xs/color-faint: hard to see, no open/closed state, and no ::-webkit-details-marker { display: none } for Safari (only list-none). A sized i-ph:caret-down rotating on details[open] would match the rest of the dock chrome.
8. Remote icons render with an odd background / sizing
components/Icon.ts#L20-L28 → @antfu/design's DisplayIconifyRemoteIcon.vue, which renders <div v-html="svg"> with no size, while the API markup is width="100%" height="100%":
$ curl -s 'https://api.iconify.design/ph/plus-circle.svg?color=currentColor&width=100%'
<svg xmlns="…" width="100%" height="100%" viewBox="0 0 256 256"><path fill="currentColor" …
The markup itself is clean (no background, fill="currentColor" — checked for ph:plus-circle, ph:check, ph:git-branch-duotone), so the visible square is either the unsized v-html wrapper resolving 100% against an auto-width box, or a presetIcons mask / background-color: currentColor fallback in the shadow sheet. This is the one item I haven't pinned down — screenshot below shows it on a filled (primary) button and on the dark surface.
Reproduction
vitejs/devtools @ 9230d057 (@devframes/* 0.9.1), pnpm -C playgrounds/core dev — the plugin-git-ui example registers a json-render dock.
- Open the dock, switch the dock's colour scheme to dark.
- Observed: the json-render view is an opaque white panel with dark text over the dark glass dock; the input on the card is unreadable; the scrollbar is the browser default; opening a
Select shows an unstyled dropdown at the wrong position; button paddings differ; the collapse caret is a tiny ▾.
Also reproducible entirely inside devframe: pnpm play → examples/json-render, which mounts the renderer module through the hub dock (same nested shadow root).
Since the json-render frontend has only just been ported into
@devframes/json-render-ui, I assume some of this is known or already being worked on. I'm happy to open PRs for any of it — I have most of the fixes scoped already — but I'd rather check first than duplicate work in progress. If you can mark which items are already handled (or are intentional), I'll pick up the rest.Summary
Since the port (components re-based on
@antfu/design), the prebuilt renderer module renders incorrectly inside the hub dock. Reproduced with Vite DevTools (vitejs/devtoolsplaygrounds/core,@devframes/*0.9.1,examples/plugin-git-uidock), and reproducible in devframe's ownexamples/json-render.The headline cause is a single selector bug: the renderer puts the
.darkclass on the very element it styles, so no dark variant applies inside the renderer's shadow root. Most of the "unreadable in dark mode" symptoms follow from that, plus an opaque background that hides the dock's glass.Environment:
@devframes/hub-ui/@devframes/json-render-ui0.9.1, Chrome 1xx, macOS, dark scheme.1.
.darkis applied to the styled element itself, so every dark variant is deadpackages/json-render-ui/src/renderer-module/index.ts#L52-L58creates the scroll root with the surface classes and toggles the scheme class on that same node:UnoCSS compiles the dark variant as a descendant selector. From the generated stylesheet (
packages/json-render-ui/src/.generated/css.ts, build output):.dark .bg-basecannot match an element that is.dark. Consequences in dark mode:bg-white— the opaque white block over the dock;color-base(neutral-800), i.e. dark text on it;dark:rule in the sheet, so the whole subtree is themed light while the dock around it is dark.hub-uigets this right with adisplay: contentswrapper —ColorSchemeRoot.vue— so the renderer module needs the same shape: scheme class on an ancestor, styles on the child. (:host(.dark)isn't an option: the compiled utilities are descendant selectors that can't cross the shadow boundary, and changing the dark-variant selector would break the SPA and Storybook builds, where:hostnever matches.)2. The renderer paints an opaque background over the dock's glass
The dock panel is deliberately translucent —
bg-glass:80(bg-white/104 dark:bg-#111/80 backdrop-blur-7):DockPanel.vue#L178DockEdge.vue#L410hub-ui/src/client/style.css#L72(the icon dock itself)The renderer's own
bg-base(index.ts#L53) punches a solid rectangle through it, so a json-render dock is the only view that loses the glass. Before the port the equivalent wrapper had no background at all and its component tokens defaulted tovar(--jr-bg, inherit)plus hue-neutralrgba(128,128,128,…)overlays — seeViewJsonRender.vueandjson-render/components/tokens.tsat the commit before352456fbremoved them.3.
@antfu/designsurfaces are opaque, so nested layers don't read as layersThe ported components inherit opaque surfaces from the design package (
@antfu/design@0.3.4, not editable from here):components/Layout/LayoutCard.vue→border border-base rounded-xl bg-basecomponents/Form/FormTextInput.vueroot<label>→px-2 border rounded bg-basecomponents/Form/FormSelect.vuetrigger andSelectContent→bg-baseTabsactive state →data-[state=active]:bg-basebg-secondaryatcomponents/Text.ts#L16(inline code) andrenderer.ts#L106(warning strip)Only
bg-base/bg-secondaryare opaque in that preset;bg-hover(#8882),bg-ambient,bg-code(gray-500/5),bg-activeandborder-base(#8882) are already alpha-based. So a card-on-glass and an input-on-card collapse into the same flat tone — and, combined with issue 1, into dark-on-dark. What seems missing is a small set of panel-relative translucent layering tokens (raised / sunken / veil) that composite over whatever is behind them, replacing the bakedbg-baseon the components whose root is a real element.FormSelect/FormComboboxcan't be fixed that way: their root is reka's renderlessSelectRoot, so aclasspassed fromSelect.ts#L91is dropped entirely — they'd need a shadow-scoped rule, or a class hook upstream.4. Scrollbar (and animation / popper) styling never reaches the renderer's shadow root
packages/json-render-ui/scripts/build-css.ts#L16-L22callsbuildShadowCsswithoutuserStylePath, so the injected sheet is UnoCSS output only:The scroll root is
of-auto, so it falls back to the browser's thick opaque-gutter scrollbar, which clashes with the translucent dock — the exact problemhub-ui/src/client/style.css#L1-L51documents and solves for the dock (:hostcustom properties + unscoped::-webkit-scrollbar*, wired viahub-ui/scripts/build-css.ts#L20). A global stylesheet can't pierce the boundary, so every shadow-isolated surface needs its own copy.scrollbar-gutter: stable, present pre-port, was also dropped.Same gap for two design stylesheets that are never loaded for this surface:
@antfu/design/styles/animations.css(af-fadekeyframes → 0 occurrences in the generated CSS, sodata-af-animatepresence transitions don't run) andstyles/reka-ui.css([data-reka-popper-content-wrapper]{z-index:70}).5.
FormSelect/FormComboboxportal out of the shadow rootreka-ui's
SelectPortal/ComboboxPortalresolve their target asprops.to ?? configContext.teleportTo ?? 'body', and nothing configures it —dock-renderer.ts#L56-L64mounts the app with noConfigProvider. The dropdown is therefore appended todocument.body, outside the shadow root that owns the stylesheet:bg-base,border-base,z-dropdown, noi-ph:*icons, none of the--colors-primary-*/--un-jr-*variables → unstyled popup;z-dropdown(z-[40]) undefined on the host page versus the dock anchor'sz-[2147483644]→ the floating panel lands in the wrong place, or behind the dock.A
ConfigProvider teleportTopointing at a container inside the renderer's shadow root fixes both. It should be a layer outside theof-autoscroll root (and inside the scheme wrapper from issue 1), otherwise the popper is clipped and scrolls with the content. I have this part working locally. The existingnative: trueescape hatch is the current workaround.6.
ActionButtonvariants disagree on paddingcomponents/Button.ts#L17-L22mapsprimary→primary,secondary→action,ghost→text,danger→primary+red override, and never passessize. The upstream variant classes carry three different paddings (@antfu/designcomponents/Action/ActionButton.vue+unocss/shortcuts.ts):primary/dangerbtn-primarypx3 py1.5secondarybtn-actionpx2 py1ghosttextSo a
[Refresh][Commit][Stage All]row from one spec renders with mismatched box sizes, and a ghost button gets no hit padding.7. Collapsible
Cardtoggle is a tiny text glyphcomponents/Card.ts#L25-L28:A literal
▾attext-xs/color-faint: hard to see, no open/closed state, and no::-webkit-details-marker { display: none }for Safari (onlylist-none). A sizedi-ph:caret-downrotating ondetails[open]would match the rest of the dock chrome.8. Remote icons render with an odd background / sizing
components/Icon.ts#L20-L28→@antfu/design'sDisplayIconifyRemoteIcon.vue, which renders<div v-html="svg">with no size, while the API markup iswidth="100%" height="100%":The markup itself is clean (no background,
fill="currentColor"— checked forph:plus-circle,ph:check,ph:git-branch-duotone), so the visible square is either the unsizedv-htmlwrapper resolving100%against an auto-width box, or apresetIconsmask /background-color: currentColorfallback in the shadow sheet. This is the one item I haven't pinned down — screenshot below shows it on a filled (primary) button and on the dark surface.Reproduction
vitejs/devtools@9230d057(@devframes/*0.9.1),pnpm -C playgrounds/core dev— theplugin-git-uiexample registers ajson-renderdock.Selectshows an unstyled dropdown at the wrong position; button paddings differ; the collapse caret is a tiny▾.Also reproducible entirely inside devframe:
pnpm play→examples/json-render, which mounts the renderer module through the hub dock (same nested shadow root).