Skip to content

Commit ae2304e

Browse files
committed
refactor(json-render-ui): one component per file
Split the grouped component modules (layout/controls/data/typography/icon) into one file per catalog component — Stack, Card, Divider, Text, Badge, CodeBlock, Button, Icon, TextInput, Switch, KeyValueTable, DataTable, Progress, Tree — so each is individually importable and easy to locate. Shared helpers (JrComponent, toNumber, formatValue) live in _shared; the internal error placeholder stays in _error. No behavior change. Created with the help of an agent.
1 parent 9dfaf6a commit ae2304e

20 files changed

Lines changed: 439 additions & 405 deletions
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { JrComponent } from './_shared'
2+
import DisplayBadge from '@antfu/design/components/Display/DisplayBadge.vue'
3+
import { h } from 'vue'
4+
5+
interface BadgeProps {
6+
text?: string
7+
variant?: 'default' | 'success' | 'warning' | 'danger' | 'info'
8+
minWidth?: number
9+
}
10+
11+
// Map the catalog's semantic variants onto `@antfu/design` `DisplayBadge`
12+
// palette color names (`false` = neutral/muted). The resulting
13+
// `badge-color-<name>` classes are safelisted (see the package uno.config and
14+
// the JSON-Render docs) since a spec picks the variant at runtime.
15+
const badgeColor: Record<string, boolean | string> = {
16+
default: false,
17+
success: 'green',
18+
warning: 'amber',
19+
danger: 'red',
20+
info: 'blue',
21+
}
22+
23+
export const Badge: JrComponent<BadgeProps> = ({ props, children }) => {
24+
const style = props.minWidth != null ? { minWidth: `${props.minWidth}px` } : undefined
25+
return h(
26+
DisplayBadge,
27+
{ text: props.text, color: badgeColor[props.variant ?? 'default'], style },
28+
props.text ? undefined : () => children,
29+
)
30+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { JrComponent } from './_shared'
2+
import ActionButton from '@antfu/design/components/Action/ActionButton.vue'
3+
import { h } from 'vue'
4+
import { Icon } from './Icon'
5+
6+
interface ButtonProps {
7+
label?: string
8+
variant?: 'primary' | 'secondary' | 'ghost' | 'danger'
9+
/** Icon name resolved at runtime (e.g. `ph:arrow-clockwise`). */
10+
icon?: string
11+
disabled?: boolean
12+
loading?: boolean
13+
}
14+
15+
// Catalog variant → `@antfu/design` ActionButton variant. `danger` has no
16+
// upstream variant, so it renders as `primary` with a red override.
17+
const buttonVariant: Record<string, string> = {
18+
primary: 'primary',
19+
secondary: 'action',
20+
ghost: 'text',
21+
danger: 'primary',
22+
}
23+
24+
export const Button: JrComponent<ButtonProps> = ({ props, on }) => {
25+
const variant = props.variant ?? 'secondary'
26+
return h(
27+
ActionButton,
28+
{
29+
variant: buttonVariant[variant] ?? 'action',
30+
disabled: props.disabled,
31+
loading: props.loading,
32+
// `danger` isn't an ActionButton variant — override the primary tint.
33+
class: variant === 'danger' ? 'bg-red! color-white! border-red! hover:bg-red/90!' : undefined,
34+
onClick: () => on('press').emit(),
35+
},
36+
// Render the dynamic Icon in the slot (ActionButton's own `icon` prop is a
37+
// static UnoCSS class, unsuited to spec-supplied names). Skip it while
38+
// loading — ActionButton already shows its spinner.
39+
() => [
40+
props.icon && !props.loading ? Icon({ props: { name: props.icon, size: 16 } } as Parameters<typeof Icon>[0]) : null,
41+
props.label ? h('span', props.label) : null,
42+
],
43+
)
44+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { JrComponent } from './_shared'
2+
import LayoutCard from '@antfu/design/components/Layout/LayoutCard.vue'
3+
import { h } from 'vue'
4+
5+
interface CardProps {
6+
title?: string
7+
border?: boolean
8+
collapsible?: boolean
9+
defaultCollapsed?: boolean
10+
loading?: boolean
11+
}
12+
13+
const headerClass = 'flex items-center justify-between px4 py2.5 border-b border-base color-base font-medium text-sm'
14+
15+
// Wraps `@antfu/design` LayoutCard for the bordered surface, composing the
16+
// title/collapsible header on top (LayoutCard is a plain surface).
17+
export const Card: JrComponent<CardProps> = ({ props, children, loading }) => {
18+
const isLoading = loading || props.loading
19+
const body = isLoading
20+
? h('div', { class: 'color-faint text-sm' }, 'Loading…')
21+
: (children as any)
22+
23+
if (props.collapsible) {
24+
return h(LayoutCard, { padding: false }, () => h('details', { open: !props.defaultCollapsed }, [
25+
h('summary', { class: `${headerClass} cursor-pointer select-none list-none` }, [
26+
h('span', props.title ?? ''),
27+
h('span', { class: 'color-faint text-xs' }, '▾'),
28+
]),
29+
h('div', { class: 'p4' }, [body]),
30+
]))
31+
}
32+
33+
return h(LayoutCard, { padding: false }, () => [
34+
props.title ? h('div', { class: headerClass }, props.title) : null,
35+
h('div', { class: 'p4' }, [body]),
36+
])
37+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { JrComponent } from './_shared'
2+
import { h } from 'vue'
3+
4+
interface CodeBlockProps {
5+
code?: string
6+
language?: string
7+
filename?: string
8+
height?: number
9+
}
10+
11+
export const CodeBlock: JrComponent<CodeBlockProps> = ({ props }) => {
12+
const header = props.filename || props.language
13+
? h('div', { class: 'flex items-center justify-between px2 py1 border-b border-base bg-secondary text-xs color-faint' }, [
14+
h('span', props.filename ?? ''),
15+
props.language ? h('span', { class: 'font-mono uppercase' }, props.language) : null,
16+
])
17+
: null
18+
const preStyle = props.height != null ? { maxHeight: `${props.height}px` } : undefined
19+
return h('div', { 'class': 'rounded border border-base overflow-hidden bg-base', 'data-language': props.language }, [
20+
header,
21+
h(
22+
'pre',
23+
{ class: 'p2 text-sm font-mono overflow-auto color-base', style: preStyle },
24+
h('code', props.code ?? ''),
25+
),
26+
])
27+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { JrComponent } from './_shared'
2+
import { useBoundProp } from '@json-render/vue'
3+
import { h } from 'vue'
4+
import { formatValue } from './_shared'
5+
6+
interface DataTableColumn { key: string, label?: string }
7+
interface DataTableProps {
8+
columns?: (string | DataTableColumn)[]
9+
rows?: Record<string, unknown>[]
10+
height?: number
11+
loading?: boolean
12+
}
13+
14+
function normalizeColumns(columns: DataTableProps['columns'], rows: DataTableProps['rows']): DataTableColumn[] {
15+
if (columns?.length)
16+
return columns.map(c => (typeof c === 'string' ? { key: c } : c))
17+
const first = rows?.[0]
18+
return first ? Object.keys(first).map(key => ({ key })) : []
19+
}
20+
21+
// `@antfu/design` LayoutDataTable has no per-row click or loading state, both of
22+
// which the catalog requires (`rowClick`, `loading`), so this stays custom on
23+
// the shared tokens. (A candidate to contribute back upstream.)
24+
export const DataTable: JrComponent<DataTableProps> = ({ props, on, bindings, loading }) => {
25+
if (loading || props.loading)
26+
return h('div', { class: 'color-faint text-sm' }, 'Loading…')
27+
const rows = props.rows ?? []
28+
const columns = normalizeColumns(props.columns, rows)
29+
const [, setSelected] = useBoundProp<unknown>(undefined, bindings?.value)
30+
const wrapperStyle = props.height != null ? { maxHeight: `${props.height}px` } : undefined
31+
32+
return h('div', { class: 'rounded border border-base overflow-auto', style: wrapperStyle }, [
33+
h('table', { class: 'w-full text-sm border-collapse' }, [
34+
h('thead', { class: 'sticky top-0 bg-secondary' }, [
35+
h('tr', columns.map(col =>
36+
h('th', { class: 'text-left px2 py1.5 color-muted font-medium border-b border-base' }, col.label ?? col.key))),
37+
]),
38+
h('tbody', rows.map((row, index) =>
39+
h('tr', {
40+
class: 'border-b border-base hover:bg-secondary cursor-pointer',
41+
onClick: () => {
42+
// Deliver row + index into bound state, then fire the action.
43+
setSelected({ row, index })
44+
on('rowClick').emit()
45+
},
46+
}, columns.map(col =>
47+
h('td', { class: 'px2 py1 color-base font-mono' }, formatValue(row[col.key])))))),
48+
]),
49+
])
50+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { JrComponent } from './_shared'
2+
import LayoutSeparator from '@antfu/design/components/Layout/LayoutSeparator.vue'
3+
import { h } from 'vue'
4+
5+
export const Divider: JrComponent<{ label?: string }> = ({ props }) =>
6+
h(LayoutSeparator, { label: props.label })
File renamed without changes.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { JrComponent } from './_shared'
2+
import DisplayKeyValue from '@antfu/design/components/Display/DisplayKeyValue.vue'
3+
import { h } from 'vue'
4+
import { formatValue } from './_shared'
5+
6+
interface KeyValueTableProps {
7+
data?: Record<string, unknown>
8+
loading?: boolean
9+
}
10+
11+
// Renders each entry as an `@antfu/design` DisplayKeyValue row.
12+
export const KeyValueTable: JrComponent<KeyValueTableProps> = ({ props, loading }) => {
13+
if (loading || props.loading)
14+
return h('div', { class: 'color-faint text-sm' }, 'Loading…')
15+
const entries = Object.entries(props.data ?? {})
16+
return h('div', { class: 'flex flex-col gap-1' }, entries.map(([key, value]) =>
17+
h(DisplayKeyValue, { key, label: key, value: formatValue(value) })))
18+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { JrComponent } from './_shared'
2+
import DisplayProgressBar from '@antfu/design/components/Display/DisplayProgressBar.vue'
3+
import { h } from 'vue'
4+
import { toNumber } from './_shared'
5+
6+
interface ProgressProps {
7+
value?: number
8+
max?: number
9+
label?: string
10+
}
11+
12+
// Wraps `@antfu/design` DisplayProgressBar (which takes a 0–1 ratio), adding the
13+
// catalog's visible label + percentage row.
14+
export const Progress: JrComponent<ProgressProps> = ({ props }) => {
15+
const max = toNumber(props.max, 100)
16+
const value = toNumber(props.value, 0)
17+
const ratio = max > 0 ? Math.min(1, Math.max(0, value / max)) : 0
18+
return h('div', { class: 'flex flex-col gap-1' }, [
19+
props.label
20+
? h('div', { class: 'flex justify-between text-xs color-muted' }, [h('span', props.label), h('span', `${Math.round(ratio * 100)}%`)])
21+
: null,
22+
h(DisplayProgressBar, { value: ratio, label: props.label }),
23+
])
24+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { JrComponent } from './_shared'
2+
import { h } from 'vue'
3+
import { toNumber } from './_shared'
4+
5+
interface StackProps {
6+
direction?: 'row' | 'column'
7+
gap?: number
8+
padding?: number
9+
align?: 'start' | 'center' | 'end' | 'stretch'
10+
justify?: 'start' | 'center' | 'end' | 'between' | 'around'
11+
wrap?: boolean
12+
flex?: number | string
13+
}
14+
15+
const alignMap: Record<string, string> = { start: 'flex-start', center: 'center', end: 'flex-end', stretch: 'stretch' }
16+
const justifyMap: Record<string, string> = {
17+
start: 'flex-start',
18+
center: 'center',
19+
end: 'flex-end',
20+
between: 'space-between',
21+
around: 'space-around',
22+
}
23+
24+
// A flex layout primitive — `@antfu/design` has no generic Stack, so this stays
25+
// a thin custom component built on the shared tokens.
26+
export const Stack: JrComponent<StackProps> = ({ props, children }) => {
27+
const style: Record<string, string> = {
28+
display: 'flex',
29+
flexDirection: props.direction === 'row' ? 'row' : 'column',
30+
gap: `${toNumber(props.gap, 8)}px`,
31+
}
32+
if (props.padding != null)
33+
style.padding = `${toNumber(props.padding, 0)}px`
34+
if (props.align)
35+
style.alignItems = alignMap[props.align] ?? props.align
36+
if (props.justify)
37+
style.justifyContent = justifyMap[props.justify] ?? props.justify
38+
if (props.wrap)
39+
style.flexWrap = 'wrap'
40+
if (props.flex != null)
41+
style.flex = String(props.flex)
42+
return h('div', { style }, children as any)
43+
}

0 commit comments

Comments
 (0)