diff --git a/docs/kit/json-render.md b/docs/kit/json-render.md
index 1c546c5c..ab2ef345 100644
--- a/docs/kit/json-render.md
+++ b/docs/kit/json-render.md
@@ -259,7 +259,7 @@ Container with an optional title and collapsible behavior.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
-| `title` | `string` | — | Header title |
+| `title` | `string` | — | Header title. A collapsible card still shows its (empty) header without one — otherwise there'd be nothing to click |
| `collapsible` | `boolean` | `false` | Whether the card can be collapsed |
| `defaultCollapsed` | `boolean` | `false` | Start collapsed (when `collapsible`) |
| `loading` | `boolean` | `false` | Show a loading state |
diff --git a/packages/core/src/client/webcomponents/index.ts b/packages/core/src/client/webcomponents/index.ts
index 6ae4c366..93c81230 100644
--- a/packages/core/src/client/webcomponents/index.ts
+++ b/packages/core/src/client/webcomponents/index.ts
@@ -8,6 +8,7 @@ export * from './components/DockStandalone'
export type {
BadgeProps,
ButtonProps,
+ CardProps,
CodeBlockProps,
DataTableColumn,
DataTableProps,
diff --git a/packages/core/src/client/webcomponents/json-render/JsonRender.stories.ts b/packages/core/src/client/webcomponents/json-render/JsonRender.stories.ts
index 8e6a4eb7..f75e618c 100644
--- a/packages/core/src/client/webcomponents/json-render/JsonRender.stories.ts
+++ b/packages/core/src/client/webcomponents/json-render/JsonRender.stories.ts
@@ -82,29 +82,35 @@ 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`
+ * A `Card` grouping content under a titled, bordered surface. `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.
+ * (`primary`/`ghost`); `interactive` strengthens the border on hover and
+ * tints each row's (`Stack`) background. `collapsible` + `defaultCollapsed`
+ * control the initial collapse state; clear `title` to confirm the header
+ * (and its chevron) still renders without one.
*/
interface CardArgs {
variant: 'primary' | 'secondary' | 'ghost' | 'danger'
interactive: boolean
+ title: string
+ collapsible: boolean
+ defaultCollapsed: boolean
}
export const Card: StoryObj> = {
argTypes: {
variant: { control: 'select', options: ['primary', 'secondary', 'ghost', 'danger'] },
interactive: { control: 'boolean' },
+ title: { control: 'text' },
+ collapsible: { control: 'boolean' },
+ defaultCollapsed: { control: 'boolean' },
},
- args: { variant: 'primary', interactive: false },
+ args: { variant: 'primary', interactive: false, title: 'Plugin', collapsible: true, defaultCollapsed: false },
render: args => renderSpec(() => ({
root: 'root',
state: {},
elements: {
- root: { type: 'Card', props: { title: 'Plugin', collapsible: false, variant: args.variant, interactive: args.interactive }, children: ['body'] },
+ root: { type: 'Card', props: { title: args.title, collapsible: args.collapsible, defaultCollapsed: args.defaultCollapsed, 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'] },
t: { type: 'Text', props: { text: 'vite-plugin-inspect', variant: 'code' } },
diff --git a/packages/core/src/client/webcomponents/json-render/components/Card.ts b/packages/core/src/client/webcomponents/json-render/components/Card.ts
index aa6a092d..45b5b0f1 100644
--- a/packages/core/src/client/webcomponents/json-render/components/Card.ts
+++ b/packages/core/src/client/webcomponents/json-render/components/Card.ts
@@ -1,11 +1,21 @@
-import type { RegistryComponentProps } from './types'
import { defineComponent, h, ref } from 'vue'
import { border, borderSolid, borderStrong, colors, surfaceMuted } from './tokens'
+import { registryProps } from './types'
+
+export type CardVariant = 'primary' | 'secondary' | 'ghost' | 'danger'
+
+export interface CardProps {
+ title?: string
+ collapsible?: boolean
+ defaultCollapsed?: boolean
+ variant?: CardVariant
+ interactive?: boolean
+}
// 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 = {
+const variantBackground: Record = {
primary: undefined,
secondary: surfaceMuted,
ghost: undefined,
@@ -14,11 +24,16 @@ const variantBackground: Record = {
export const Card = defineComponent({
name: 'JrCard',
- props: ['element', 'emit', 'on', 'bindings', 'loading'],
- setup(ctx: RegistryComponentProps, { slots }) {
- const collapsed = ref(false)
+ props: registryProps<'Card', CardProps>(),
+ setup(ctx, { slots }) {
+ const collapsed = ref(!!ctx.element.props.defaultCollapsed)
return () => {
const { title, collapsible, variant = 'primary', interactive = false } = ctx.element.props
+ const toggle = ctx.on('toggle')
+ const setCollapsed = (next: boolean) => {
+ collapsed.value = next
+ toggle.emit()
+ }
return h('div', {
class: 'jr-card',
style: {
@@ -34,7 +49,8 @@ export const Card = defineComponent({
onMouseenter: interactive ? (e: MouseEvent) => { (e.currentTarget as HTMLElement).style.borderColor = borderStrong } : undefined,
onMouseleave: interactive ? (e: MouseEvent) => { (e.currentTarget as HTMLElement).style.borderColor = border } : undefined,
}, [
- title && h('div', {
+ // Renders without a `title` too — a collapsible card needs this as its click target.
+ (title || collapsible) && h('div', {
class: 'jr-card-header',
style: {
padding: '8px 12px',
@@ -47,7 +63,7 @@ export const Card = defineComponent({
borderBottom: collapsed.value ? 'none' : borderSolid(border),
userSelect: 'none',
},
- onClick: collapsible ? () => { collapsed.value = !collapsed.value } : undefined,
+ onClick: collapsible ? () => setCollapsed(!collapsed.value) : undefined,
}, [
h('span', title),
collapsible && h('span', {
diff --git a/packages/core/src/client/webcomponents/json-render/registry.ts b/packages/core/src/client/webcomponents/json-render/registry.ts
index bd21fa75..71350645 100644
--- a/packages/core/src/client/webcomponents/json-render/registry.ts
+++ b/packages/core/src/client/webcomponents/json-render/registry.ts
@@ -30,6 +30,7 @@ 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'
diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.d.ts b/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.d.ts
index ed75e479..31763207 100644
--- a/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.d.ts
+++ b/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.d.ts
@@ -14,6 +14,13 @@ export interface ButtonProps {
icon?: string;
disabled?: boolean;
}
+export interface CardProps {
+ title?: string;
+ collapsible?: boolean;
+ defaultCollapsed?: boolean;
+ variant?: CardVariant;
+ interactive?: boolean;
+}
export interface CodeBlockProps {
code?: string;
filename?: string;