diff --git a/packages/core/src/client/webcomponents/index.ts b/packages/core/src/client/webcomponents/index.ts index da4da964..6ae4c366 100644 --- a/packages/core/src/client/webcomponents/index.ts +++ b/packages/core/src/client/webcomponents/index.ts @@ -1,3 +1,25 @@ export * from './components/DockEmbedded' export * from './components/DockStandalone' +/** + * Opt-in strict types for the json-render base catalog — most spec authors + * use the fully open `JsonRenderElement` from `@vitejs/devtools-kit` instead; + * import these only to narrow a specific element (`JsonRenderElement<'Badge', BadgeProps>`). + */ +export type { + BadgeProps, + ButtonProps, + CodeBlockProps, + DataTableColumn, + DataTableProps, + DividerProps, + IconProps, + JsonRenderElement, + KeyValueTableProps, + ProgressProps, + SwitchProps, + TextInputProps, + TextProps, + TreeProps, +} from './json-render/registry' + export * from './state/docks' diff --git a/packages/core/src/client/webcomponents/json-render/components/Badge.ts b/packages/core/src/client/webcomponents/json-render/components/Badge.ts index e2bfd0ba..ab358739 100644 --- a/packages/core/src/client/webcomponents/json-render/components/Badge.ts +++ b/packages/core/src/client/webcomponents/json-render/components/Badge.ts @@ -1,14 +1,21 @@ -import type { RegistryComponentProps } from './types' import { defineComponent, h } from 'vue' import { colors } from './tokens' +import { registryProps } from './types' + +export interface BadgeProps { + text?: string + variant?: 'default' | 'info' | 'success' | 'warning' | 'danger' + title?: string + minWidth?: number +} export const Badge = defineComponent({ name: 'JrBadge', - props: ['element', 'emit', 'on', 'bindings', 'loading'], - setup(ctx: RegistryComponentProps) { + props: registryProps<'Badge', BadgeProps>(), + setup(ctx) { return () => { const { text, variant = 'default', title, minWidth } = ctx.element.props - const c = colors[variant as keyof typeof colors] || colors.default + const c = colors[variant] || colors.default // Base catalog `minWidth` is a number of pixels. const minWidthCss = minWidth == null ? undefined : (typeof minWidth === 'number' ? `${minWidth}px` : minWidth) return h('span', { diff --git a/packages/core/src/client/webcomponents/json-render/components/Button.ts b/packages/core/src/client/webcomponents/json-render/components/Button.ts index b005864c..7c397399 100644 --- a/packages/core/src/client/webcomponents/json-render/components/Button.ts +++ b/packages/core/src/client/webcomponents/json-render/components/Button.ts @@ -1,15 +1,22 @@ -import type { RegistryComponentProps } from './types' import { defineComponent, h } from 'vue' import BaseButton from '../../components/display/Button.vue' import DockIcon from '../../components/dock/DockIcon.vue' +import { registryProps } from './types' type BaseVariant = 'primary' | 'secondary' | 'ghost' | 'danger' const VARIANTS = new Set(['primary', 'secondary', 'ghost', 'danger']) +export interface ButtonProps { + label?: string + variant?: BaseVariant + icon?: string + disabled?: boolean +} + export const Button = defineComponent({ name: 'JrButton', - props: ['element', 'emit', 'on', 'bindings', 'loading'], - setup(ctx: RegistryComponentProps) { + props: registryProps<'Button', ButtonProps>(), + setup(ctx) { return () => { const { label, icon, variant = 'secondary', disabled } = ctx.element.props const press = ctx.on('press') diff --git a/packages/core/src/client/webcomponents/json-render/components/CodeBlock.ts b/packages/core/src/client/webcomponents/json-render/components/CodeBlock.ts index 00491b86..04a850da 100644 --- a/packages/core/src/client/webcomponents/json-render/components/CodeBlock.ts +++ b/packages/core/src/client/webcomponents/json-render/components/CodeBlock.ts @@ -1,11 +1,18 @@ -import type { RegistryComponentProps } from './types' import { defineComponent, h } from 'vue' import { borderSolid, surfaceMuted } from './tokens' +import { registryProps } from './types' + +export interface CodeBlockProps { + code?: string + filename?: string + language?: string + height?: number +} export const CodeBlock = defineComponent({ name: 'JrCodeBlock', - props: ['element', 'emit', 'on', 'bindings', 'loading'], - setup(ctx: RegistryComponentProps) { + props: registryProps<'CodeBlock', CodeBlockProps>(), + setup(ctx) { return () => { const { code, filename, language, height } = ctx.element.props const header = filename || language diff --git a/packages/core/src/client/webcomponents/json-render/components/DataTable.ts b/packages/core/src/client/webcomponents/json-render/components/DataTable.ts index b4455a9a..453e1a72 100644 --- a/packages/core/src/client/webcomponents/json-render/components/DataTable.ts +++ b/packages/core/src/client/webcomponents/json-render/components/DataTable.ts @@ -1,18 +1,29 @@ -import type { RegistryComponentProps } from './types' import { defineComponent, h } from 'vue' import { bg, border, borderMuted, borderSolid, hoverOverlay } from './tokens' +import { registryProps } from './types' + +export interface DataTableColumn { + key: string + label?: string + width?: string +} + +export interface DataTableProps { + columns?: (string | DataTableColumn)[] + rows?: Record[] + height?: number +} export const DataTable = defineComponent({ name: 'JrDataTable', - props: ['element', 'emit', 'on', 'bindings', 'loading'], - setup(ctx: RegistryComponentProps) { + props: registryProps<'DataTable', DataTableProps>(), + setup(ctx) { return () => { const { columns: rawColumns = [], rows = [], height } = ctx.element.props const rowClick = ctx.on('rowClick') // Base catalog columns are `string | { key, label? }`; normalize to objects. - const columns = (rawColumns as (string | { key: string, label?: string, width?: string })[]) - .map(col => (typeof col === 'string' ? { key: col } : col)) + const columns = rawColumns.map(col => (typeof col === 'string' ? { key: col } : col)) return h('div', { class: 'jr-data-table', @@ -30,7 +41,7 @@ export const DataTable = defineComponent({ h('thead', [ h('tr', { style: { borderBottom: borderSolid(border) }, - }, columns.map((col: any) => + }, columns.map(col => h('th', { style: { padding: '8px', @@ -47,7 +58,7 @@ export const DataTable = defineComponent({ }, col.label ?? col.key), )), ]), - h('tbody', rows.map((row: any, index: number) => + h('tbody', rows.map((row, index) => h('tr', { key: index, style: { @@ -57,7 +68,7 @@ export const DataTable = defineComponent({ onClick: rowClick.bound ? () => rowClick.emit() : undefined, onMouseenter: (e: MouseEvent) => { (e.currentTarget as HTMLElement).style.backgroundColor = hoverOverlay }, onMouseleave: (e: MouseEvent) => { (e.currentTarget as HTMLElement).style.backgroundColor = '' }, - }, columns.map((col: any) => + }, columns.map(col => h('td', { style: { padding: '6px 8px', fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis' }, }, String(row[col.key] ?? '')), diff --git a/packages/core/src/client/webcomponents/json-render/components/Divider.ts b/packages/core/src/client/webcomponents/json-render/components/Divider.ts index 8c436a0b..4c4253b2 100644 --- a/packages/core/src/client/webcomponents/json-render/components/Divider.ts +++ b/packages/core/src/client/webcomponents/json-render/components/Divider.ts @@ -1,11 +1,15 @@ -import type { RegistryComponentProps } from './types' import { defineComponent, h } from 'vue' import { borderSolid } from './tokens' +import { registryProps } from './types' + +export interface DividerProps { + label?: string +} export const Divider = defineComponent({ name: 'JrDivider', - props: ['element', 'emit', 'on', 'bindings', 'loading'], - setup(ctx: RegistryComponentProps) { + props: registryProps<'Divider', DividerProps>(), + setup(ctx) { return () => { const { label } = ctx.element.props const rule = { flex: 1, border: 'none', borderTop: borderSolid() } diff --git a/packages/core/src/client/webcomponents/json-render/components/Icon.ts b/packages/core/src/client/webcomponents/json-render/components/Icon.ts index 525fc83f..082a7563 100644 --- a/packages/core/src/client/webcomponents/json-render/components/Icon.ts +++ b/packages/core/src/client/webcomponents/json-render/components/Icon.ts @@ -1,11 +1,15 @@ -import type { RegistryComponentProps } from './types' import { defineComponent, h } from 'vue' -import { useIconSvg } from './types' +import { registryProps, useIconSvg } from './types' + +export interface IconProps { + name?: string + size?: number +} export const Icon = defineComponent({ name: 'JrIcon', - props: ['element', 'emit', 'on', 'bindings', 'loading'], - setup(ctx: RegistryComponentProps) { + props: registryProps<'Icon', IconProps>(), + setup(ctx) { const iconSvg = useIconSvg(() => ctx.element.props.name) return () => { diff --git a/packages/core/src/client/webcomponents/json-render/components/KeyValueTable.ts b/packages/core/src/client/webcomponents/json-render/components/KeyValueTable.ts index e4159dba..a31d4a3a 100644 --- a/packages/core/src/client/webcomponents/json-render/components/KeyValueTable.ts +++ b/packages/core/src/client/webcomponents/json-render/components/KeyValueTable.ts @@ -1,14 +1,18 @@ -import type { RegistryComponentProps } from './types' import { defineComponent, h } from 'vue' import { borderSolid, borderSubtle } from './tokens' +import { registryProps } from './types' + +export interface KeyValueTableProps { + data?: Record +} export const KeyValueTable = defineComponent({ name: 'JrKeyValueTable', - props: ['element', 'emit', 'on', 'bindings', 'loading'], - setup(ctx: RegistryComponentProps) { + props: registryProps<'KeyValueTable', KeyValueTableProps>(), + setup(ctx) { return () => { const { data = {} } = ctx.element.props - const entries = Object.entries(data as Record) + const entries = Object.entries(data) return h('div', { class: 'jr-kv-table' }, [ h('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: '12px' }, diff --git a/packages/core/src/client/webcomponents/json-render/components/Progress.ts b/packages/core/src/client/webcomponents/json-render/components/Progress.ts index 25aec930..399204bd 100644 --- a/packages/core/src/client/webcomponents/json-render/components/Progress.ts +++ b/packages/core/src/client/webcomponents/json-render/components/Progress.ts @@ -1,11 +1,17 @@ -import type { RegistryComponentProps } from './types' import { defineComponent, h } from 'vue' import { primary, surfaceBadge } from './tokens' +import { registryProps } from './types' + +export interface ProgressProps { + value: number + max?: number + label?: string +} export const Progress = defineComponent({ name: 'JrProgress', - props: ['element', 'emit', 'on', 'bindings', 'loading'], - setup(ctx: RegistryComponentProps) { + props: registryProps<'Progress', ProgressProps>(), + setup(ctx) { return () => { const { value, max = 100, label } = ctx.element.props const percent = Math.min(100, Math.max(0, (value / max) * 100)) diff --git a/packages/core/src/client/webcomponents/json-render/components/Switch.ts b/packages/core/src/client/webcomponents/json-render/components/Switch.ts index 8a23ed75..177e1ecf 100644 --- a/packages/core/src/client/webcomponents/json-render/components/Switch.ts +++ b/packages/core/src/client/webcomponents/json-render/components/Switch.ts @@ -1,12 +1,19 @@ -import type { RegistryComponentProps } from './types' import { useBoundProp } from '@json-render/vue' import { defineComponent, h } from 'vue' import { primary, surfaceSubtle } from './tokens' +import { registryProps } from './types' + +export interface SwitchProps { + /** Two-way bindable via `{ $bindState: '...' }`. */ + value?: boolean + label?: string + disabled?: boolean +} export const Switch = defineComponent({ name: 'JrSwitch', - props: ['element', 'emit', 'on', 'bindings', 'loading'], - setup(ctx: RegistryComponentProps) { + props: registryProps<'Switch', SwitchProps>(), + setup(ctx) { return () => { const { label, disabled } = ctx.element.props const [value, setValue] = useBoundProp(ctx.element.props.value, ctx.bindings?.value) diff --git a/packages/core/src/client/webcomponents/json-render/components/Text.ts b/packages/core/src/client/webcomponents/json-render/components/Text.ts index e06572c7..d5a58a28 100644 --- a/packages/core/src/client/webcomponents/json-render/components/Text.ts +++ b/packages/core/src/client/webcomponents/json-render/components/Text.ts @@ -1,11 +1,17 @@ -import type { RegistryComponentProps } from './types' import { defineComponent, h } from 'vue' import { surfaceSubtle } from './tokens' +import { registryProps } from './types' + +export interface TextProps { + text?: string + variant?: 'heading' | 'subheading' | 'body' | 'caption' | 'code' + weight?: 'normal' | 'medium' | 'bold' +} export const Text = defineComponent({ name: 'JrText', - props: ['element', 'emit', 'on', 'bindings', 'loading'], - setup(ctx: RegistryComponentProps) { + props: registryProps<'Text', TextProps>(), + setup(ctx) { return () => { const { text, variant = 'body', weight } = ctx.element.props const styles: Record> = { diff --git a/packages/core/src/client/webcomponents/json-render/components/TextInput.ts b/packages/core/src/client/webcomponents/json-render/components/TextInput.ts index 712643a0..7fcf0a3a 100644 --- a/packages/core/src/client/webcomponents/json-render/components/TextInput.ts +++ b/packages/core/src/client/webcomponents/json-render/components/TextInput.ts @@ -1,12 +1,20 @@ -import type { RegistryComponentProps } from './types' import { useBoundProp } from '@json-render/vue' import { defineComponent, h } from 'vue' import { borderInput, borderSolid } from './tokens' +import { registryProps } from './types' + +export interface TextInputProps { + /** Two-way bindable via `{ $bindState: '...' }`. */ + value?: string + placeholder?: string + label?: string + disabled?: boolean +} export const TextInput = defineComponent({ name: 'JrTextInput', - props: ['element', 'emit', 'on', 'bindings', 'loading'], - setup(ctx: RegistryComponentProps) { + props: registryProps<'TextInput', TextInputProps>(), + setup(ctx) { return () => { const { placeholder, label, disabled } = ctx.element.props const [value, setValue] = useBoundProp(ctx.element.props.value, ctx.bindings?.value) diff --git a/packages/core/src/client/webcomponents/json-render/components/Tree.ts b/packages/core/src/client/webcomponents/json-render/components/Tree.ts index 702c166d..d206a617 100644 --- a/packages/core/src/client/webcomponents/json-render/components/Tree.ts +++ b/packages/core/src/client/webcomponents/json-render/components/Tree.ts @@ -1,12 +1,17 @@ import type { VNode } from 'vue' -import type { RegistryComponentProps } from './types' import { defineComponent, h, ref } from 'vue' import { syntaxNumber, syntaxString } from './tokens' +import { registryProps } from './types' + +export interface TreeProps { + data?: unknown + defaultExpanded?: boolean +} export const Tree = defineComponent({ name: 'JrTree', - props: ['element', 'emit', 'on', 'bindings', 'loading'], - setup(ctx: RegistryComponentProps) { + props: registryProps<'Tree', TreeProps>(), + setup(ctx) { function renderNode(value: unknown, key: string, depth: number, expandLevel: number): VNode { const isExpanded = ref(depth < expandLevel) const isObject = value !== null && typeof value === 'object' diff --git a/packages/core/src/client/webcomponents/json-render/components/types.ts b/packages/core/src/client/webcomponents/json-render/components/types.ts index 67906b48..e7bf116f 100644 --- a/packages/core/src/client/webcomponents/json-render/components/types.ts +++ b/packages/core/src/client/webcomponents/json-render/components/types.ts @@ -1,14 +1,32 @@ +import type { UIElement } from '@json-render/core' +import type { PropType } from 'vue' import { ref, watchEffect } from 'vue' import { getIconifySvg } from '../../utils/iconify' -export interface RegistryComponentProps { - element: { type: string, props: Record } +export interface RegistryComponentProps> { + element: UIElement emit: (event: string) => void on: (event: string) => { emit: () => void, shouldPreventDefault: boolean, bound: boolean } bindings?: Record loading?: boolean } +/** + * Vue props for a registry component, typed to its own element shape. + * Replaces the untyped `props: ['element', 'emit', ...]` array form so + * `defineComponent` infers `setup`'s `ctx` as `RegistryComponentProps` + * instead of leaving `element.props` an untyped `Record`. + */ +export function registryProps>() { + return { + element: { type: Object as PropType>, required: true as const }, + emit: { type: Function as PropType<(event: string) => void>, required: true as const }, + on: { type: Function as PropType<(event: string) => { emit: () => void, shouldPreventDefault: boolean, bound: boolean }>, required: true as const }, + bindings: { type: Object as PropType>, required: false as const }, + loading: { type: Boolean, required: false as const }, + } +} + export function useIconSvg(getName: () => string | undefined) { const svg = ref(null) watchEffect(async () => { diff --git a/packages/core/src/client/webcomponents/json-render/registry.ts b/packages/core/src/client/webcomponents/json-render/registry.ts index 06824684..bd21fa75 100644 --- a/packages/core/src/client/webcomponents/json-render/registry.ts +++ b/packages/core/src/client/webcomponents/json-render/registry.ts @@ -22,6 +22,26 @@ import { UnsupportedComponent } from './components/UnsupportedComponent' */ export { UnsupportedComponent } +/** + * Per-component `props` shapes, co-located with each component and + * re-exported here as this catalog's public type surface — the single + * source of truth a spec author can opt into for strict typing, instead of + * each component's props duplicated into a separate types package. + */ +export type { BadgeProps } from './components/Badge' +export type { ButtonProps } from './components/Button' +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 { SwitchProps } from './components/Switch' +export type { TextProps } from './components/Text' +export type { TextInputProps } from './components/TextInput' +export type { TreeProps } from './components/Tree' +export type { UIElement as JsonRenderElement } from '@json-render/core' + export const devtoolsRegistry: Record = { Stack, Card, 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 8e82716b..ed75e479 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.d.ts +++ b/test/__snapshots__/tsnapi/@vitejs/devtools/client/webcomponents.snapshot.d.ts @@ -1,6 +1,84 @@ /** * Generated by tsnapi — public API snapshot of `@vitejs/devtools/client/webcomponents` */ +// #region Interfaces +export interface BadgeProps { + text?: string; + variant?: 'default' | 'info' | 'success' | 'warning' | 'danger'; + title?: string; + minWidth?: number; +} +export interface ButtonProps { + label?: string; + variant?: BaseVariant; + icon?: string; + disabled?: boolean; +} +export interface CodeBlockProps { + code?: string; + filename?: string; + language?: string; + height?: number; +} +export interface DataTableColumn { + key: string; + label?: string; + width?: string; +} +export interface DataTableProps { + columns?: (string | DataTableColumn)[]; + rows?: Record[]; + height?: number; +} +export interface DividerProps { + label?: string; +} +export interface IconProps { + name?: string; + size?: number; +} +export interface JsonRenderElement> { + type: T; + props: P; + children?: string[]; + visible?: VisibilityCondition; + on?: Record; + repeat?: { + statePath: string; + key?: string; + }; + watch?: Record; +} +export interface KeyValueTableProps { + data?: Record; +} +export interface ProgressProps { + value: number; + max?: number; + label?: string; +} +export interface SwitchProps { + value?: boolean; + label?: string; + disabled?: boolean; +} +export interface TextInputProps { + value?: string; + placeholder?: string; + label?: string; + disabled?: boolean; +} +export interface TextProps { + text?: string; + variant?: 'heading' | 'subheading' | 'body' | 'caption' | 'code'; + weight?: 'normal' | 'medium' | 'bold'; +} +export interface TreeProps { + data?: unknown; + defaultExpanded?: boolean; +} +// #endregion + // #region Functions export declare function createDockEntryState(_: DevToolsDockEntry, _: Ref): DockEntryState; export declare function DEFAULT_DOCK_PANEL_STORE(): DockPanelStorage;