Skip to content

Commit 0c87159

Browse files
authored
feat(core): info/success/warning/danger variants on json-render Card & Stack (#491)
1 parent 3d23cf2 commit 0c87159

8 files changed

Lines changed: 97 additions & 46 deletions

File tree

docs/kit/json-render.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@ Flex layout container. Arranges children vertically or horizontally.
232232
| `wrap` | `boolean` | `false` | Allow children to wrap onto multiple lines |
233233
| `flex` | `number \| string` || `flex` shorthand for the container |
234234
| `padding` | `number` || Padding in pixels |
235+
| `variant` | `'primary' \| 'secondary' \| 'ghost' \| 'danger' \| 'info' \| 'success' \| 'warning'` | `'primary'` | Background tint (`danger`/`info`/`success`/`warning` also draw a matching border) |
236+
| `interactive` | `boolean` | `false` | Tint the background on hover — useful for clickable-looking rows |
235237

236238
<!-- eslint-skip -->
237239
```ts
@@ -262,6 +264,8 @@ Container with an optional title and collapsible behavior.
262264
| `title` | `string` || Header title. A collapsible card still shows its (empty) header without one — otherwise there'd be nothing to click |
263265
| `collapsible` | `boolean` | `false` | Whether the card can be collapsed |
264266
| `defaultCollapsed` | `boolean` | `false` | Start collapsed (when `collapsible`) |
267+
| `variant` | `'primary' \| 'secondary' \| 'ghost' \| 'danger' \| 'info' \| 'success' \| 'warning'` | `'primary'` | A light background tint on the body, a stronger same-hue tint on the header bar, and a matching border (`danger`/`info`/`success`/`warning`) |
268+
| `interactive` | `boolean` | `false` | Brighten the border on hover |
265269
| `loading` | `boolean` | `false` | Show a loading state |
266270

267271
<!-- eslint-skip -->

packages/core/src/client/webcomponents/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export type {
1717
JsonRenderElement,
1818
KeyValueTableProps,
1919
ProgressProps,
20+
StackProps,
2021
SwitchProps,
2122
TabDescriptor,
2223
TabsProps,

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

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,18 @@ export const Gallery: Story = {
8484
}
8585

8686
/**
87-
* A `Card` grouping content under a titled, bordered surface. `variant`
88-
* tints the background (`secondary`/`danger`) or leaves it untouched
89-
* (`primary`/`ghost`); `interactive` strengthens the border on hover and
90-
* tints each row's (`Stack`) background. `collapsible` + `defaultCollapsed`
87+
* A `Card` grouping content under a titled, bordered surface. `secondary`
88+
* tints the background with no border; `ghost` and `primary` are untinted.
89+
* `info`/`success`/`warning`/`danger` each draw a light body tint, a
90+
* stronger same-hue header bar, and a matching border. `interactive`
91+
* strengthens the border on hover (same hue for the four semantic variants)
92+
* and tints each row's (`Stack`) background. `collapsible` + `defaultCollapsed`
9193
* control the initial collapse state; clear `title` to confirm the header
9294
* (and its chevron) still renders without one.
9395
*/
9496
interface CardArgs {
95-
variant: 'primary' | 'secondary' | 'ghost' | 'danger'
97+
variant: 'primary' | 'secondary' | 'ghost' | 'danger' | 'info' | 'success' | 'warning'
98+
rowVariant: 'primary' | 'secondary' | 'ghost' | 'danger' | 'info' | 'success' | 'warning'
9699
interactive: boolean
97100
title: string
98101
collapsible: boolean
@@ -101,23 +104,24 @@ interface CardArgs {
101104

102105
export const Card: StoryObj<Meta<CardArgs>> = {
103106
argTypes: {
104-
variant: { control: 'select', options: ['primary', 'secondary', 'ghost', 'danger'] },
107+
variant: { control: 'select', options: ['primary', 'secondary', 'ghost', 'danger', 'info', 'success', 'warning'] },
108+
rowVariant: { control: 'select', options: ['primary', 'secondary', 'ghost', 'danger', 'info', 'success', 'warning'] },
105109
interactive: { control: 'boolean' },
106110
title: { control: 'text' },
107111
collapsible: { control: 'boolean' },
108112
defaultCollapsed: { control: 'boolean' },
109113
},
110-
args: { variant: 'primary', interactive: false, title: 'Plugin', collapsible: true, defaultCollapsed: false },
114+
args: { variant: 'primary', rowVariant: 'ghost', interactive: false, title: 'Plugin', collapsible: true, defaultCollapsed: false },
111115
render: args => renderSpec(() => ({
112116
root: 'root',
113117
state: {},
114118
elements: {
115119
root: { type: 'Card', props: { title: args.title, collapsible: args.collapsible, defaultCollapsed: args.defaultCollapsed, variant: args.variant, interactive: args.interactive }, children: ['body'] },
116120
body: { type: 'Stack', props: { direction: 'column', gap: 4, padding: 4 }, children: ['row1', 'row2'] },
117-
row1: { type: 'Stack', props: { direction: 'row', gap: 8, align: 'center', interactive: args.interactive }, children: ['t', 'badge'] },
121+
row1: { type: 'Stack', props: { direction: 'row', gap: 8, align: 'center', variant: args.rowVariant, interactive: args.interactive }, children: ['t', 'badge'] },
118122
t: { type: 'Text', props: { text: 'vite-plugin-inspect', variant: 'code' } },
119123
badge: { type: 'Badge', props: { text: 'enabled', variant: 'success' } },
120-
row2: { type: 'Stack', props: { direction: 'row', gap: 8, align: 'center', interactive: args.interactive }, children: ['t2', 'badge2'] },
124+
row2: { type: 'Stack', props: { direction: 'row', gap: 8, align: 'center', variant: args.rowVariant, interactive: args.interactive }, children: ['t2', 'badge2'] },
121125
t2: { type: 'Text', props: { text: 'vite-plugin-vue', variant: 'code' } },
122126
badge2: { type: 'Badge', props: { text: 'enabled', variant: 'success' } },
123127
},

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

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,15 @@
11
import { defineComponent, h, ref } from 'vue'
2-
import { border, borderSolid, borderStrong, colors, surfaceMuted } from './tokens'
2+
import { border, borderSolid, variantSurface } from './tokens'
33
import { registryProps } from './types'
44

5-
export type CardVariant = 'primary' | 'secondary' | 'ghost' | 'danger'
6-
75
export interface CardProps {
86
title?: string
97
collapsible?: boolean
108
defaultCollapsed?: boolean
11-
variant?: CardVariant
9+
variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'info' | 'success' | 'warning'
1210
interactive?: boolean
1311
}
1412

15-
// Mirrors `ContainerCard.vue`'s (packages/ui) opt-in `variant` model: `primary`
16-
// (default) keeps today's fully transparent look, so existing specs render
17-
// unchanged; `ghost` is the same no-fill look under a more intentional name.
18-
const variantBackground: Record<CardVariant, string | undefined> = {
19-
primary: undefined,
20-
secondary: surfaceMuted,
21-
ghost: undefined,
22-
danger: colors.danger.bg,
23-
}
24-
2513
export const Card = defineComponent({
2614
name: 'JrCard',
2715
props: registryProps<'Card', CardProps>(),
@@ -34,20 +22,22 @@ export const Card = defineComponent({
3422
collapsed.value = next
3523
toggle.emit()
3624
}
25+
const surface = variantSurface(variant)
26+
const borderColor = surface.border ?? border
3727
return h('div', {
3828
class: 'jr-card',
3929
style: {
40-
border: borderSolid(border),
30+
border: borderSolid(borderColor),
4131
borderRadius: '6px',
4232
overflow: 'hidden',
43-
backgroundColor: variantBackground[variant],
33+
backgroundColor: surface.background,
4434
transition: interactive ? 'border-color 0.15s ease' : undefined,
4535
},
4636
// `interactive` strengthens the border on hover rather than tinting
4737
// the background (already set by `variant`) — same transition timing
4838
// as Stack's row hover, just on `border-color` instead of `background`.
49-
onMouseenter: interactive ? (e: MouseEvent) => { (e.currentTarget as HTMLElement).style.borderColor = borderStrong } : undefined,
50-
onMouseleave: interactive ? (e: MouseEvent) => { (e.currentTarget as HTMLElement).style.borderColor = border } : undefined,
39+
onMouseenter: interactive ? (e: MouseEvent) => { (e.currentTarget as HTMLElement).style.borderColor = surface.hoverBorder ?? borderColor } : undefined,
40+
onMouseleave: interactive ? (e: MouseEvent) => { (e.currentTarget as HTMLElement).style.borderColor = borderColor } : undefined,
5141
}, [
5242
// Renders without a `title` too — a collapsible card needs this as its click target.
5343
(title || collapsible) && h('div', {
@@ -60,7 +50,8 @@ export const Card = defineComponent({
6050
alignItems: 'center',
6151
justifyContent: 'space-between',
6252
cursor: collapsible ? 'pointer' : undefined,
63-
borderBottom: collapsed.value ? 'none' : borderSolid(border),
53+
borderBottom: collapsed.value ? 'none' : borderSolid(borderColor),
54+
backgroundColor: surface.headerBackground,
6455
userSelect: 'none',
6556
},
6657
onClick: collapsible ? () => setCollapsed(!collapsed.value) : undefined,

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

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,32 @@
1-
import type { RegistryComponentProps } from './types'
21
import { defineComponent, h } from 'vue'
3-
import { colors, surfaceMuted, surfaceSubtle } from './tokens'
2+
import { borderSolid, variantSurface } from './tokens'
3+
import { registryProps } from './types'
4+
5+
export interface StackProps {
6+
direction?: 'row' | 'column'
7+
gap?: number
8+
align?: 'start' | 'center' | 'end' | 'stretch'
9+
justify?: 'start' | 'center' | 'end' | 'between' | 'around'
10+
padding?: number
11+
wrap?: boolean
12+
flex?: number | string
13+
variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'info' | 'success' | 'warning'
14+
interactive?: boolean
15+
}
416

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

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-
1921
export const Stack = defineComponent({
2022
name: 'JrStack',
21-
props: ['element', 'emit', 'on', 'bindings', 'loading'],
22-
setup(ctx: RegistryComponentProps, { slots }) {
23+
props: registryProps<'Stack', StackProps>(),
24+
setup(ctx, { slots }) {
2325
return () => {
2426
const { direction = 'column', gap = 8, align, justify, padding, wrap, flex, variant = 'primary', interactive = false } = ctx.element.props
2527
const isHorizontal = direction === 'row'
26-
const restingBackground = variantBackground[variant]
28+
const surface = variantSurface(variant)
29+
const restingBackground = surface.background
2730
return h('div', {
2831
class: 'jr-stack',
2932
style: {
@@ -40,13 +43,15 @@ export const Stack = defineComponent({
4043
// Matches Card's own borderRadius so a row's hover tint reads
4144
// consistently with the card surfaces it sits inside.
4245
borderRadius: interactive ? '6px' : undefined,
43-
transition: interactive ? 'background-color 0.15s ease' : undefined,
46+
// Only the four semantic variants draw a border; primary/secondary/ghost stay borderless.
47+
border: surface.border ? borderSolid(surface.border) : undefined,
48+
transition: interactive ? 'background-color 0.15s ease, border-color 0.15s ease' : undefined,
4449
flex: flex != null ? String(flex) : undefined,
4550
backgroundColor: restingBackground,
4651
},
4752
// `interactive` only ever tints on hover — Stack has no click/press
4853
// 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,
54+
onMouseenter: interactive ? (e: MouseEvent) => { (e.currentTarget as HTMLElement).style.backgroundColor = surface.hoverBackground ?? '' } : undefined,
5055
onMouseleave: interactive ? (e: MouseEvent) => { (e.currentTarget as HTMLElement).style.backgroundColor = restingBackground ?? '' } : undefined,
5156
}, slots.default?.())
5257
}

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const bg = 'var(--jr-bg, inherit)'
1616

1717
// --- Semantic palette ---
1818

19+
/** Badge's pill fill. Card/Stack use their own lighter tints below — a badge-strength fill would read as a heavy block over a whole card. */
1920
export const colors = {
2021
info: { bg: 'rgba(59,130,246,0.15)', fg: 'rgb(59,130,246)' },
2122
success: { bg: 'rgba(34,197,94,0.15)', fg: 'rgb(34,197,94)' },
@@ -41,3 +42,36 @@ export const syntaxNumber = '#dcdcaa'
4142
export function borderSolid(token = border) {
4243
return `1px solid ${token}`
4344
}
45+
46+
/** Resting/hover background + border a `variant` resolves to on Card/Stack. */
47+
export interface VariantSurface {
48+
background?: string
49+
/** Card-only — its header bar reads a step stronger than the body it sits above, so a titled card is a two-tone surface rather than one flat tint. */
50+
headerBackground?: string
51+
border?: string
52+
hoverBackground?: string
53+
hoverBorder?: string
54+
}
55+
56+
/** No semantic hue — border stays unset, hover stays a neutral grey. */
57+
const neutralSurfaces: Record<'primary' | 'secondary' | 'ghost', VariantSurface> = {
58+
primary: { hoverBackground: surfaceSubtle, hoverBorder: borderStrong },
59+
secondary: { background: surfaceMuted, hoverBackground: surfaceSubtle, hoverBorder: borderStrong },
60+
ghost: { hoverBackground: surfaceSubtle, hoverBorder: borderStrong },
61+
}
62+
63+
const semanticSurfaces: Record<'info' | 'success' | 'warning' | 'danger', Required<Pick<VariantSurface, 'background' | 'headerBackground' | 'border' | 'hoverBackground' | 'hoverBorder'>>> = {
64+
info: { background: 'rgba(59,130,246,0.08)', headerBackground: 'rgba(59,130,246,0.18)', border: 'rgba(59,130,246,0.25)', hoverBackground: 'rgba(59,130,246,0.25)', hoverBorder: 'rgba(59,130,246,0.6)' },
65+
success: { background: 'rgba(34,197,94,0.08)', headerBackground: 'rgba(34,197,94,0.18)', border: 'rgba(34,197,94,0.25)', hoverBackground: 'rgba(34,197,94,0.25)', hoverBorder: 'rgba(34,197,94,0.6)' },
66+
warning: { background: 'rgba(234,179,8,0.08)', headerBackground: 'rgba(234,179,8,0.18)', border: 'rgba(234,179,8,0.25)', hoverBackground: 'rgba(234,179,8,0.25)', hoverBorder: 'rgba(234,179,8,0.6)' },
67+
danger: { background: 'rgba(239,68,68,0.08)', headerBackground: 'rgba(239,68,68,0.18)', border: 'rgba(239,68,68,0.25)', hoverBackground: 'rgba(239,68,68,0.25)', hoverBorder: 'rgba(239,68,68,0.6)' },
68+
}
69+
70+
/** Resolves a `variant` prop to the surface Card and Stack render at rest and on hover. */
71+
export function variantSurface(variant: string): VariantSurface {
72+
if (variant in neutralSurfaces)
73+
return neutralSurfaces[variant as keyof typeof neutralSurfaces]
74+
if (variant in semanticSurfaces)
75+
return semanticSurfaces[variant as keyof typeof semanticSurfaces]
76+
return neutralSurfaces.primary
77+
}

packages/core/src/client/webcomponents/json-render/registry.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export type { DividerProps } from './components/Divider'
3838
export type { IconProps } from './components/Icon'
3939
export type { KeyValueTableProps } from './components/KeyValueTable'
4040
export type { ProgressProps } from './components/Progress'
41+
export type { StackProps } from './components/Stack'
4142
export type { SwitchProps } from './components/Switch'
4243
export type { TabDescriptor, TabsProps } from './components/Tabs'
4344
export type { TextProps } from './components/Text'

test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.d.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export interface CardProps {
1919
title?: string;
2020
collapsible?: boolean;
2121
defaultCollapsed?: boolean;
22-
variant?: CardVariant;
22+
variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'info' | 'success' | 'warning';
2323
interactive?: boolean;
2424
}
2525
export interface CodeBlockProps {
@@ -65,6 +65,17 @@ export interface ProgressProps {
6565
max?: number;
6666
label?: string;
6767
}
68+
export interface StackProps {
69+
direction?: 'row' | 'column';
70+
gap?: number;
71+
align?: 'start' | 'center' | 'end' | 'stretch';
72+
justify?: 'start' | 'center' | 'end' | 'between' | 'around';
73+
padding?: number;
74+
wrap?: boolean;
75+
flex?: number | string;
76+
variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'info' | 'success' | 'warning';
77+
interactive?: boolean;
78+
}
6879
export interface SwitchProps {
6980
value?: boolean;
7081
label?: string;

0 commit comments

Comments
 (0)