Skip to content

Bug: several UI regression following json-renderer refactor #267

Description

@dvcolomban

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

Image

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

Image

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.vueborder border-base rounded-xl bg-base
  • components/Form/FormTextInput.vue root <label>px-2 border rounded bg-base
  • components/Form/FormSelect.vue trigger and SelectContentbg-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

  1. vitejs/devtools @ 9230d057 (@devframes/* 0.9.1), pnpm -C playgrounds/core dev — the plugin-git-ui example registers a json-render dock.
  2. Open the dock, switch the dock's colour scheme to dark.
  3. 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 playexamples/json-render, which mounts the renderer module through the hub dock (same nested shadow root).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions