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;