Skip to content
Merged
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
22 changes: 22 additions & 0 deletions packages/core/src/client/webcomponents/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Original file line number Diff line number Diff line change
@@ -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', {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<BaseVariant>(['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')
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, unknown>[]
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',
Expand All @@ -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',
Expand All @@ -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: {
Expand All @@ -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] ?? '')),
Expand Down
Original file line number Diff line number Diff line change
@@ -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() }
Expand Down
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, unknown>
}

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<string, unknown>)
const entries = Object.entries(data)
return h('div', { class: 'jr-kv-table' }, [
h('table', {
style: { width: '100%', borderCollapse: 'collapse', fontSize: '12px' },
Expand Down
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
Original file line number Diff line number Diff line change
@@ -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<boolean>(ctx.element.props.value, ctx.bindings?.value)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, Record<string, string>> = {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string>(ctx.element.props.value, ctx.bindings?.value)
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, any> }
export interface RegistryComponentProps<Type extends string = string, Props = Record<string, any>> {
element: UIElement<Type, Props>
emit: (event: string) => void
on: (event: string) => { emit: () => void, shouldPreventDefault: boolean, bound: boolean }
bindings?: Record<string, string>
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<Type, Props>`
* instead of leaving `element.props` an untyped `Record<string, any>`.
*/
export function registryProps<Type extends string = string, Props = Record<string, any>>() {
return {
element: { type: Object as PropType<UIElement<Type, Props>>, 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<Record<string, string>>, required: false as const },
loading: { type: Boolean, required: false as const },
}
}

export function useIconSvg(getName: () => string | undefined) {
const svg = ref<string | null>(null)
watchEffect(async () => {
Expand Down
20 changes: 20 additions & 0 deletions packages/core/src/client/webcomponents/json-render/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Component> = {
Stack,
Card,
Expand Down
Loading
Loading