Skip to content

Commit c279ef3

Browse files
authored
feat(core): opt-in variant/interactive on json-render Card & Stack, fix dock scrollbar in dark mode (#477)
1 parent 2ee70f6 commit c279ef3

6 files changed

Lines changed: 130 additions & 15 deletions

File tree

packages/core/src/client/webcomponents/.generated/css.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

packages/core/src/client/webcomponents/json-render/JsonRender.stories.ts

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,20 @@ import { devtoolsRegistry, UnsupportedComponent } from './registry'
77
/**
88
* Render a json-render `Spec` with the DevTools registry, the same way
99
* `ViewJsonRender` does at runtime — including the `UnsupportedComponent`
10-
* fallback for any element `type` absent from the registry.
10+
* fallback for any element `type` absent from the registry. Accepts a getter
11+
* instead of a plain `Spec` so stories can rebuild the spec from reactive
12+
* Storybook `args` (e.g. toggling `variant`/`interactive` live via Controls).
1113
*/
12-
function renderSpec(spec: Spec) {
14+
function renderSpec(specOrGetter: Spec | (() => Spec)) {
1315
return defineComponent({
1416
setup() {
15-
const initialState = (spec as any).state ?? {}
17+
const getSpec = typeof specOrGetter === 'function' ? specOrGetter : () => specOrGetter
18+
const initialState = (getSpec() as any).state ?? {}
1619
return () => h(
1720
'div',
1821
{ class: 'max-w-160 p6 bg-base color-base font-sans' },
1922
h(JSONUIProvider, { registry: devtoolsRegistry, handlers: {}, initialState }, {
20-
default: () => h(Renderer, { spec, registry: devtoolsRegistry, fallback: UnsupportedComponent }),
23+
default: () => h(Renderer, { spec: getSpec(), registry: devtoolsRegistry, fallback: UnsupportedComponent }),
2124
}),
2225
)
2326
},
@@ -78,18 +81,39 @@ export const Gallery: Story = {
7881
} as unknown as Spec),
7982
}
8083

81-
/** A `Card` grouping content under a titled, bordered surface. */
82-
export const Card: Story = {
83-
render: () => renderSpec({
84+
/**
85+
* A `Card` grouping content under a titled, bordered surface. Toggle the
86+
* Controls below to compare today's default (`primary`, non-`interactive` —
87+
* unchanged from before this fix) against the new opt-in look: `variant`
88+
* tints the background (`secondary`/`danger`) or leaves it untouched
89+
* (`primary`/`ghost`); `interactive` strengthens the Card's border on hover
90+
* and tints each row's (`Stack`) background on hover.
91+
*/
92+
interface CardArgs {
93+
variant: 'primary' | 'secondary' | 'ghost' | 'danger'
94+
interactive: boolean
95+
}
96+
97+
export const Card: StoryObj<Meta<CardArgs>> = {
98+
argTypes: {
99+
variant: { control: 'select', options: ['primary', 'secondary', 'ghost', 'danger'] },
100+
interactive: { control: 'boolean' },
101+
},
102+
args: { variant: 'primary', interactive: false },
103+
render: args => renderSpec(() => ({
84104
root: 'root',
85105
state: {},
86106
elements: {
87-
root: { type: 'Card', props: { title: 'Plugin', collapsible: false }, children: ['body'] },
88-
body: { type: 'Stack', props: { direction: 'column', gap: 8, padding: 4 }, children: ['t', 'badge'] },
107+
root: { type: 'Card', props: { title: 'Plugin', collapsible: false, variant: args.variant, interactive: args.interactive }, children: ['body'] },
108+
body: { type: 'Stack', props: { direction: 'column', gap: 4, padding: 4 }, children: ['row1', 'row2'] },
109+
row1: { type: 'Stack', props: { direction: 'row', gap: 8, align: 'center', interactive: args.interactive }, children: ['t', 'badge'] },
89110
t: { type: 'Text', props: { text: 'vite-plugin-inspect', variant: 'code' } },
90111
badge: { type: 'Badge', props: { text: 'enabled', variant: 'success' } },
112+
row2: { type: 'Stack', props: { direction: 'row', gap: 8, align: 'center', interactive: args.interactive }, children: ['t2', 'badge2'] },
113+
t2: { type: 'Text', props: { text: 'vite-plugin-vue', variant: 'code' } },
114+
badge2: { type: 'Badge', props: { text: 'enabled', variant: 'success' } },
91115
},
92-
} as unknown as Spec),
116+
} as unknown as Spec)),
93117
}
94118

95119
/**

packages/core/src/client/webcomponents/json-render/components/Card.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,38 @@
11
import type { RegistryComponentProps } from './types'
22
import { defineComponent, h, ref } from 'vue'
3-
import { border, borderSolid } from './tokens'
3+
import { border, borderSolid, borderStrong, colors, surfaceMuted } from './tokens'
4+
5+
// Mirrors `ContainerCard.vue`'s (packages/ui) opt-in `variant` model: `primary`
6+
// (default) keeps today's fully transparent look, so existing specs render
7+
// unchanged; `ghost` is the same no-fill look under a more intentional name.
8+
const variantBackground: Record<string, string | undefined> = {
9+
primary: undefined,
10+
secondary: surfaceMuted,
11+
ghost: undefined,
12+
danger: colors.danger.bg,
13+
}
414

515
export const Card = defineComponent({
616
name: 'JrCard',
717
props: ['element', 'emit', 'on', 'bindings', 'loading'],
818
setup(ctx: RegistryComponentProps, { slots }) {
919
const collapsed = ref(false)
1020
return () => {
11-
const { title, collapsible } = ctx.element.props
21+
const { title, collapsible, variant = 'primary', interactive = false } = ctx.element.props
1222
return h('div', {
1323
class: 'jr-card',
1424
style: {
1525
border: borderSolid(border),
1626
borderRadius: '6px',
1727
overflow: 'hidden',
28+
backgroundColor: variantBackground[variant],
29+
transition: interactive ? 'border-color 0.15s ease' : undefined,
1830
},
31+
// `interactive` strengthens the border on hover rather than tinting
32+
// the background (already set by `variant`) — same transition timing
33+
// as Stack's row hover, just on `border-color` instead of `background`.
34+
onMouseenter: interactive ? (e: MouseEvent) => { (e.currentTarget as HTMLElement).style.borderColor = borderStrong } : undefined,
35+
onMouseleave: interactive ? (e: MouseEvent) => { (e.currentTarget as HTMLElement).style.borderColor = border } : undefined,
1936
}, [
2037
title && h('div', {
2138
class: 'jr-card-header',

packages/core/src/client/webcomponents/json-render/components/Stack.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
11
import type { RegistryComponentProps } from './types'
22
import { defineComponent, h } from 'vue'
3+
import { colors, surfaceMuted, surfaceSubtle } from './tokens'
34

45
// Map the base catalog's `align` / `justify` enums onto CSS flexbox values.
56
const alignMap: Record<string, string> = { start: 'flex-start', center: 'center', end: 'flex-end', stretch: 'stretch' }
67
const justifyMap: Record<string, string> = { start: 'flex-start', center: 'center', end: 'flex-end', between: 'space-between', around: 'space-around' }
78

9+
// Mirrors `ContainerCard.vue`'s (packages/ui) opt-in `variant` model: `primary`
10+
// (default) keeps today's fully transparent look, so existing specs render
11+
// unchanged; `ghost` is the same no-fill look under a more intentional name.
12+
const variantBackground: Record<string, string | undefined> = {
13+
primary: undefined,
14+
secondary: surfaceMuted,
15+
ghost: undefined,
16+
danger: colors.danger.bg,
17+
}
18+
819
export const Stack = defineComponent({
920
name: 'JrStack',
1021
props: ['element', 'emit', 'on', 'bindings', 'loading'],
1122
setup(ctx: RegistryComponentProps, { slots }) {
1223
return () => {
13-
const { direction = 'column', gap = 8, align, justify, padding, wrap, flex } = ctx.element.props
24+
const { direction = 'column', gap = 8, align, justify, padding, wrap, flex, variant = 'primary', interactive = false } = ctx.element.props
1425
const isHorizontal = direction === 'row'
26+
const restingBackground = variantBackground[variant]
1527
return h('div', {
1628
class: 'jr-stack',
1729
style: {
@@ -21,9 +33,21 @@ export const Stack = defineComponent({
2133
alignItems: align ? (alignMap[align] ?? align) : (isHorizontal ? 'center' : 'stretch'),
2234
justifyContent: justify ? (justifyMap[justify] ?? justify) : undefined,
2335
flexWrap: wrap ? 'wrap' : undefined,
24-
padding: padding ? `${padding}px` : undefined,
36+
// `interactive` rows get a small default padding/radius so the
37+
// hover tint reads as a deliberate affordance instead of hugging
38+
// the content edge-to-edge; an explicit `padding` prop still wins.
39+
padding: padding ? `${padding}px` : (interactive ? '4px 6px' : undefined),
40+
// Matches Card's own borderRadius so a row's hover tint reads
41+
// consistently with the card surfaces it sits inside.
42+
borderRadius: interactive ? '6px' : undefined,
43+
transition: interactive ? 'background-color 0.15s ease' : undefined,
2544
flex: flex != null ? String(flex) : undefined,
45+
backgroundColor: restingBackground,
2646
},
47+
// `interactive` only ever tints on hover — Stack has no click/press
48+
// semantics of its own, so rows built from it stay non-clickable.
49+
onMouseenter: interactive ? (e: MouseEvent) => { (e.currentTarget as HTMLElement).style.backgroundColor = surfaceSubtle } : undefined,
50+
onMouseleave: interactive ? (e: MouseEvent) => { (e.currentTarget as HTMLElement).style.backgroundColor = restingBackground ?? '' } : undefined,
2751
}, slots.default?.())
2852
}
2953
},

packages/core/src/client/webcomponents/json-render/components/tokens.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export const border = 'var(--jr-border, rgba(128,128,128,0.2))'
88
export const borderSubtle = 'var(--jr-border, rgba(128,128,128,0.1))'
99
export const borderMuted = 'var(--jr-border, rgba(128,128,128,0.08))'
1010
export const borderInput = 'var(--jr-border, rgba(128,128,128,0.3))'
11+
// Interactive-hover border (Card) — a clearer step up than borderInput, without going full `primary`.
12+
export const borderStrong = 'var(--jr-border, rgba(128,128,128,0.5))'
1113
// Keep in sync with the DevTools theme primary (packages/ui/src/unocss/theme.ts).
1214
export const primary = 'var(--jr-primary, #6b84fd)'
1315
export const bg = 'var(--jr-bg, inherit)'

packages/core/src/client/webcomponents/style.css

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,54 @@
22
--un-text-opacity: 100%;
33
}
44

5+
/*
6+
* Thin/transparent scrollbar, matching packages/ui/src/styles/global.css —
7+
* that stylesheet only reaches the Nuxt plugin apps, not this injected
8+
* webcomponents shell, which otherwise falls back to the browser default
9+
* (thick, opaque-gutter) scrollbar clashing with the translucent dock.
10+
* `:host`, not `:root` — this stylesheet is adopted into the dock's shadow
11+
* root (`defineCustomElement(..., { shadowRoot: true, styles: [css] })`),
12+
* where `:root` only ever matches the top-level document's <html>.
13+
* Rules below are unscoped (not `#vite-devtools-anchor ...`) since the
14+
* shadow boundary already keeps them from leaking onto the host page, and
15+
* scoping under the anchor's id would miss edge-docked mode entirely
16+
* (`DockEmbedded.vue` renders `DockEdge`/`#vite-devtools-edge-panel` and
17+
* `Dock`/`#vite-devtools-anchor` as mutually exclusive siblings).
18+
*/
19+
:host {
20+
--app-scrollbar-size: 6px;
21+
--app-scrollbar-radius: 1px;
22+
--app-scrollbar-thumb: #8884;
23+
--app-scrollbar-thumb-hover: #8885;
24+
}
25+
26+
::-webkit-scrollbar {
27+
width: var(--app-scrollbar-size);
28+
}
29+
30+
::-webkit-scrollbar:horizontal {
31+
height: var(--app-scrollbar-size);
32+
}
33+
34+
::-webkit-scrollbar-corner {
35+
background: transparent;
36+
}
37+
38+
::-webkit-scrollbar-thumb {
39+
background-color: var(--app-scrollbar-thumb);
40+
transition: background 0.2s ease;
41+
border-radius: var(--app-scrollbar-radius);
42+
}
43+
44+
::-webkit-scrollbar-thumb:hover {
45+
background-color: var(--app-scrollbar-thumb-hover);
46+
}
47+
48+
::-webkit-scrollbar-track {
49+
border-radius: var(--app-scrollbar-radius);
50+
background: transparent;
51+
}
52+
553
#vite-devtools-anchor {
654
--uno: w-0 z-floating-anchor fixed origin-center font-sans text-[15px] box-border;
755
transform: translate(-50%, -50%) rotate(0);

0 commit comments

Comments
 (0)