Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/kit/json-render.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ Flex layout container. Arranges children vertically or horizontally.
| `wrap` | `boolean` | `false` | Allow children to wrap onto multiple lines |
| `flex` | `number \| string` | — | `flex` shorthand for the container |
| `padding` | `number` | — | Padding in pixels |
| `variant` | `'primary' \| 'secondary' \| 'ghost' \| 'danger' \| 'info' \| 'success' \| 'warning'` | `'primary'` | Background tint (`danger`/`info`/`success`/`warning` also draw a matching border) |
| `interactive` | `boolean` | `false` | Tint the background on hover — useful for clickable-looking rows |

<!-- eslint-skip -->
```ts
Expand Down Expand Up @@ -262,6 +264,8 @@ Container with an optional title and collapsible behavior.
| `title` | `string` | — | Header title |
| `collapsible` | `boolean` | `false` | Whether the card can be collapsed |
| `defaultCollapsed` | `boolean` | `false` | Start collapsed (when `collapsible`) |
| `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`) |
| `interactive` | `boolean` | `false` | Brighten the border on hover |
| `loading` | `boolean` | `false` | Show a loading state |

<!-- eslint-skip -->
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/client/webcomponents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from './components/DockStandalone'
export type {
BadgeProps,
ButtonProps,
CardProps,
CodeBlockProps,
DataTableColumn,
DataTableProps,
Expand All @@ -16,6 +17,7 @@ export type {
JsonRenderElement,
KeyValueTableProps,
ProgressProps,
StackProps,
SwitchProps,
TextInputProps,
TextProps,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,34 +82,36 @@ export const Gallery: Story = {
}

/**
* A `Card` grouping content under a titled, bordered surface. Toggle the
* Controls below to compare today's default (`primary`, non-`interactive` —
* unchanged from before this fix) against the new opt-in look: `variant`
* tints the background (`secondary`/`danger`) or leaves it untouched
* (`primary`/`ghost`); `interactive` strengthens the Card's border on hover
* and tints each row's (`Stack`) background on hover.
* A `Card` grouping content under a titled, bordered surface. `secondary`
* tints the background with no border; `ghost` and `primary` are untinted.
* `info`/`success`/`warning`/`danger` each draw a light body tint, a
* stronger same-hue header bar, and a matching border. `interactive`
* strengthens the border on hover (same hue for the four semantic variants)
* and tints each row's (`Stack`) background.
*/
interface CardArgs {
variant: 'primary' | 'secondary' | 'ghost' | 'danger'
variant: 'primary' | 'secondary' | 'ghost' | 'danger' | 'info' | 'success' | 'warning'
rowVariant: 'primary' | 'secondary' | 'ghost' | 'danger' | 'info' | 'success' | 'warning'
interactive: boolean
}

export const Card: StoryObj<Meta<CardArgs>> = {
argTypes: {
variant: { control: 'select', options: ['primary', 'secondary', 'ghost', 'danger'] },
variant: { control: 'select', options: ['primary', 'secondary', 'ghost', 'danger', 'info', 'success', 'warning'] },
rowVariant: { control: 'select', options: ['primary', 'secondary', 'ghost', 'danger', 'info', 'success', 'warning'] },
interactive: { control: 'boolean' },
},
args: { variant: 'primary', interactive: false },
args: { variant: 'primary', rowVariant: 'ghost', interactive: false },
render: args => renderSpec(() => ({
root: 'root',
state: {},
elements: {
root: { type: 'Card', props: { title: 'Plugin', collapsible: false, variant: args.variant, interactive: args.interactive }, children: ['body'] },
body: { type: 'Stack', props: { direction: 'column', gap: 4, padding: 4 }, children: ['row1', 'row2'] },
row1: { type: 'Stack', props: { direction: 'row', gap: 8, align: 'center', interactive: args.interactive }, children: ['t', 'badge'] },
row1: { type: 'Stack', props: { direction: 'row', gap: 8, align: 'center', variant: args.rowVariant, interactive: args.interactive }, children: ['t', 'badge'] },
t: { type: 'Text', props: { text: 'vite-plugin-inspect', variant: 'code' } },
badge: { type: 'Badge', props: { text: 'enabled', variant: 'success' } },
row2: { type: 'Stack', props: { direction: 'row', gap: 8, align: 'center', interactive: args.interactive }, children: ['t2', 'badge2'] },
row2: { type: 'Stack', props: { direction: 'row', gap: 8, align: 'center', variant: args.rowVariant, interactive: args.interactive }, children: ['t2', 'badge2'] },
t2: { type: 'Text', props: { text: 'vite-plugin-vue', variant: 'code' } },
badge2: { type: 'Badge', props: { text: 'enabled', variant: 'success' } },
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,38 @@
import type { RegistryComponentProps } from './types'
import { defineComponent, h, ref } from 'vue'
import { border, borderSolid, borderStrong, colors, surfaceMuted } from './tokens'
import { border, borderSolid, variantSurface } from './tokens'
import { registryProps } from './types'

// Mirrors `ContainerCard.vue`'s (packages/ui) opt-in `variant` model: `primary`
// (default) keeps today's fully transparent look, so existing specs render
// unchanged; `ghost` is the same no-fill look under a more intentional name.
const variantBackground: Record<string, string | undefined> = {
primary: undefined,
secondary: surfaceMuted,
ghost: undefined,
danger: colors.danger.bg,
export interface CardProps {
title?: string
collapsible?: boolean
defaultCollapsed?: boolean
variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'info' | 'success' | 'warning'
interactive?: boolean
}

export const Card = defineComponent({
name: 'JrCard',
props: ['element', 'emit', 'on', 'bindings', 'loading'],
setup(ctx: RegistryComponentProps, { slots }) {
props: registryProps<'Card', CardProps>(),
setup(ctx, { slots }) {
const collapsed = ref(false)
return () => {
const { title, collapsible, variant = 'primary', interactive = false } = ctx.element.props
const surface = variantSurface(variant)
const borderColor = surface.border ?? border
return h('div', {
class: 'jr-card',
style: {
border: borderSolid(border),
border: borderSolid(borderColor),
borderRadius: '6px',
overflow: 'hidden',
backgroundColor: variantBackground[variant],
backgroundColor: surface.background,
transition: interactive ? 'border-color 0.15s ease' : undefined,
},
// `interactive` strengthens the border on hover rather than tinting
// the background (already set by `variant`) — same transition timing
// as Stack's row hover, just on `border-color` instead of `background`.
onMouseenter: interactive ? (e: MouseEvent) => { (e.currentTarget as HTMLElement).style.borderColor = borderStrong } : undefined,
onMouseleave: interactive ? (e: MouseEvent) => { (e.currentTarget as HTMLElement).style.borderColor = border } : undefined,
onMouseenter: interactive ? (e: MouseEvent) => { (e.currentTarget as HTMLElement).style.borderColor = surface.hoverBorder ?? borderColor } : undefined,
onMouseleave: interactive ? (e: MouseEvent) => { (e.currentTarget as HTMLElement).style.borderColor = borderColor } : undefined,
}, [
title && h('div', {
class: 'jr-card-header',
Expand All @@ -44,7 +44,8 @@ export const Card = defineComponent({
alignItems: 'center',
justifyContent: 'space-between',
cursor: collapsible ? 'pointer' : undefined,
borderBottom: collapsed.value ? 'none' : borderSolid(border),
borderBottom: collapsed.value ? 'none' : borderSolid(borderColor),
backgroundColor: surface.headerBackground,
userSelect: 'none',
},
onClick: collapsible ? () => { collapsed.value = !collapsed.value } : undefined,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
import type { RegistryComponentProps } from './types'
import { defineComponent, h } from 'vue'
import { colors, surfaceMuted, surfaceSubtle } from './tokens'
import { borderSolid, variantSurface } from './tokens'
import { registryProps } from './types'

export interface StackProps {
direction?: 'row' | 'column'
gap?: number
align?: 'start' | 'center' | 'end' | 'stretch'
justify?: 'start' | 'center' | 'end' | 'between' | 'around'
padding?: number
wrap?: boolean
flex?: number | string
variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'info' | 'success' | 'warning'
interactive?: boolean
}

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

// Mirrors `ContainerCard.vue`'s (packages/ui) opt-in `variant` model: `primary`
// (default) keeps today's fully transparent look, so existing specs render
// unchanged; `ghost` is the same no-fill look under a more intentional name.
const variantBackground: Record<string, string | undefined> = {
primary: undefined,
secondary: surfaceMuted,
ghost: undefined,
danger: colors.danger.bg,
}

export const Stack = defineComponent({
name: 'JrStack',
props: ['element', 'emit', 'on', 'bindings', 'loading'],
setup(ctx: RegistryComponentProps, { slots }) {
props: registryProps<'Stack', StackProps>(),
setup(ctx, { slots }) {
return () => {
const { direction = 'column', gap = 8, align, justify, padding, wrap, flex, variant = 'primary', interactive = false } = ctx.element.props
const isHorizontal = direction === 'row'
const restingBackground = variantBackground[variant]
const surface = variantSurface(variant)
const restingBackground = surface.background
return h('div', {
class: 'jr-stack',
style: {
Expand All @@ -40,13 +43,15 @@ export const Stack = defineComponent({
// Matches Card's own borderRadius so a row's hover tint reads
// consistently with the card surfaces it sits inside.
borderRadius: interactive ? '6px' : undefined,
transition: interactive ? 'background-color 0.15s ease' : undefined,
// Only the four semantic variants draw a border; primary/secondary/ghost stay borderless.
border: surface.border ? borderSolid(surface.border) : undefined,
transition: interactive ? 'background-color 0.15s ease, border-color 0.15s ease' : undefined,
flex: flex != null ? String(flex) : undefined,
backgroundColor: restingBackground,
},
// `interactive` only ever tints on hover — Stack has no click/press
// semantics of its own, so rows built from it stay non-clickable.
onMouseenter: interactive ? (e: MouseEvent) => { (e.currentTarget as HTMLElement).style.backgroundColor = surfaceSubtle } : undefined,
onMouseenter: interactive ? (e: MouseEvent) => { (e.currentTarget as HTMLElement).style.backgroundColor = surface.hoverBackground ?? '' } : undefined,
onMouseleave: interactive ? (e: MouseEvent) => { (e.currentTarget as HTMLElement).style.backgroundColor = restingBackground ?? '' } : undefined,
}, slots.default?.())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const bg = 'var(--jr-bg, inherit)'

// --- Semantic palette ---

/** 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. */
export const colors = {
info: { bg: 'rgba(59,130,246,0.15)', fg: 'rgb(59,130,246)' },
success: { bg: 'rgba(34,197,94,0.15)', fg: 'rgb(34,197,94)' },
Expand All @@ -41,3 +42,36 @@ export const syntaxNumber = '#dcdcaa'
export function borderSolid(token = border) {
return `1px solid ${token}`
}

/** Resting/hover background + border a `variant` resolves to on Card/Stack. */
export interface VariantSurface {
background?: string
/** 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. */
headerBackground?: string
border?: string
hoverBackground?: string
hoverBorder?: string
}

/** No semantic hue — border stays unset, hover stays a neutral grey. */
const neutralSurfaces: Record<'primary' | 'secondary' | 'ghost', VariantSurface> = {
primary: { hoverBackground: surfaceSubtle, hoverBorder: borderStrong },
secondary: { background: surfaceMuted, hoverBackground: surfaceSubtle, hoverBorder: borderStrong },
ghost: { hoverBackground: surfaceSubtle, hoverBorder: borderStrong },
}

const semanticSurfaces: Record<'info' | 'success' | 'warning' | 'danger', Required<Pick<VariantSurface, 'background' | 'headerBackground' | 'border' | 'hoverBackground' | 'hoverBorder'>>> = {
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)' },
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)' },
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)' },
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)' },
}

/** Resolves a `variant` prop to the surface Card and Stack render at rest and on hover. */
export function variantSurface(variant: string): VariantSurface {
if (variant in neutralSurfaces)
return neutralSurfaces[variant as keyof typeof neutralSurfaces]
if (variant in semanticSurfaces)
return semanticSurfaces[variant as keyof typeof semanticSurfaces]
return neutralSurfaces.primary
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ export { UnsupportedComponent }
*/
export type { BadgeProps } from './components/Badge'
export type { ButtonProps } from './components/Button'
export type { CardProps } from './components/Card'
export type { CodeBlockProps } from './components/CodeBlock'
export type { DataTableColumn, DataTableProps } from './components/DataTable'
export type { DividerProps } from './components/Divider'
export type { IconProps } from './components/Icon'
export type { KeyValueTableProps } from './components/KeyValueTable'
export type { ProgressProps } from './components/Progress'
export type { StackProps } from './components/Stack'
export type { SwitchProps } from './components/Switch'
export type { TextProps } from './components/Text'
export type { TextInputProps } from './components/TextInput'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ export interface ButtonProps {
icon?: string;
disabled?: boolean;
}
export interface CardProps {
title?: string;
collapsible?: boolean;
defaultCollapsed?: boolean;
variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'info' | 'success' | 'warning';
interactive?: boolean;
}
export interface CodeBlockProps {
code?: string;
filename?: string;
Expand Down Expand Up @@ -57,6 +64,17 @@ export interface ProgressProps {
max?: number;
label?: string;
}
export interface StackProps {
direction?: 'row' | 'column';
gap?: number;
align?: 'start' | 'center' | 'end' | 'stretch';
justify?: 'start' | 'center' | 'end' | 'between' | 'around';
padding?: number;
wrap?: boolean;
flex?: number | string;
variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'info' | 'success' | 'warning';
interactive?: boolean;
}
export interface SwitchProps {
value?: boolean;
label?: string;
Expand Down
Loading