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
5 changes: 5 additions & 0 deletions .changeset/fancy-words-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/devtools-ui': patch
---

new ui components and enhancements for json tree
1 change: 1 addition & 0 deletions packages/devtools-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"build": "vite build"
},
"dependencies": {
"clsx": "^2.1.1",
"goober": "^2.1.16",
"solid-js": "^1.9.7"
},
Expand Down
42 changes: 42 additions & 0 deletions packages/devtools-ui/src/components/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { splitProps } from 'solid-js'
import clsx from 'clsx'
import { useStyles } from '../styles/use-styles'
import type { JSX } from 'solid-js'

export type ButtonVariant =
| 'primary'
| 'secondary'
| 'danger'
| 'success'
| 'info'
| 'warning'
type ButtonProps = JSX.ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: ButtonVariant
outline?: boolean
ghost?: boolean
children?: any
className?: string
}

export function Button(props: ButtonProps) {
const styles = useStyles()
const [local, rest] = splitProps(props, [
'variant',
'outline',
'ghost',
'children',
'className',
])
const variant = local.variant || 'primary'
const classes = clsx(
styles().button.base,
styles().button.variant(variant, local.outline, local.ghost),
local.className,
)

return (
<button {...rest} class={classes}>
{local.children}
</button>
)
}
22 changes: 22 additions & 0 deletions packages/devtools-ui/src/components/tag.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Show } from 'solid-js'
import { useStyles } from '../styles/use-styles'
import type { tokens } from '../styles/tokens'

export const Tag = (props: {
color: keyof typeof tokens.colors
label: string
count?: number
disabled?: boolean
}) => {
const styles = useStyles()
return (
<button disabled={props.disabled} class={styles().tag.base}>
<span class={styles().tag.dot(props.color)} />
<span class={styles().tag.label}>{props.label}</span>

<Show when={props.count && props.count > 0}>
<span class={styles().tag.count}>{props.count}</span>
</Show>
</button>
)
}
217 changes: 125 additions & 92 deletions packages/devtools-ui/src/components/tree.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { For } from 'solid-js'
import { For, Show, createSignal } from 'solid-js'
import clsx from 'clsx'
import { useStyles } from '../styles/use-styles'

export function JsonTree(props: { value: any }) {
Expand All @@ -16,119 +17,151 @@ function JsonValue(props: {

return (
<span class={styles().tree.valueContainer(isRoot)}>
{keyName && typeof value !== 'object' && !Array.isArray(value) && (
<span class={styles().tree.valueKey}>&quot;{keyName}&quot;: </span>
)}
{(() => {
if (typeof value === 'string') {
return (
<span>
{keyName && (
<span class={styles().tree.valueKey}>
&quot;{keyName}&quot;:{' '}
</span>
)}
<span class={styles().tree.valueString}>&quot;{value}&quot;</span>
</span>
<span class={styles().tree.valueString}>&quot;{value}&quot;</span>
)
}
if (typeof value === 'number') {
return (
<span>
{keyName && (
<span class={styles().tree.valueKey}>
&quot;{keyName}&quot;:{' '}
</span>
)}
<span class={styles().tree.valueNumber}>{value}</span>
</span>
)
return <span class={styles().tree.valueNumber}>{value}</span>
}
if (typeof value === 'boolean') {
return (
<span>
{keyName && (
<span class={styles().tree.valueKey}>
&quot;{keyName}&quot;:{' '}
</span>
)}
<span class={styles().tree.valueBoolean}>{String(value)}</span>
</span>
)
return <span class={styles().tree.valueBoolean}>{String(value)}</span>
}
if (value === null) {
return (
<span>
{keyName && (
<span class={styles().tree.valueKey}>
&quot;{keyName}&quot;:{' '}
</span>
)}
<span class={styles().tree.valueNull}>null</span>
</span>
)
return <span class={styles().tree.valueNull}>null</span>
}
if (value === undefined) {
return <span class={styles().tree.valueNull}>undefined</span>
}
if (typeof value === 'function') {
return (
<span>
{keyName && (
<span class={styles().tree.valueKey}>
&quot;{keyName}&quot;:{' '}
</span>
)}
<span class={styles().tree.valueNull}>undefined</span>
</span>
<span class={styles().tree.valueFunction}>{String(value)}</span>
)
}
if (Array.isArray(value)) {
return (
<span>
{keyName && (
<span class={styles().tree.valueKey}>
&quot;{keyName}&quot;:{' '}
</span>
)}
<span class={styles().tree.valueBraces}>[</span>
<For each={value}>
{(item, i) => {
const isLastKey = i() === value.length - 1
return (
<>
<JsonValue value={item} isLastKey={isLastKey} />
</>
)
}}
</For>
<span class={styles().tree.valueBraces}>]</span>
</span>
)
return <ArrayValue keyName={keyName} value={value} />
}
if (typeof value === 'object') {
const keys = Object.keys(value)
const lastKeyName = keys[keys.length - 1]
return (
<span>
{keyName && (
<span class={styles().tree.valueKey}>
&quot;{keyName}&quot;:{' '}
</span>
)}
<span class={styles().tree.valueBraces}>{'{'}</span>
<For each={keys}>
{(k) => (
<>
<JsonValue
value={value[k]}
keyName={k}
isLastKey={lastKeyName === k}
/>
</>
)}
</For>
<span class={styles().tree.valueBraces}>{'}'}</span>
</span>
)
return <ObjectValue keyName={keyName} value={value} />
}
return <span />
})()}
{isLastKey || isRoot ? '' : <span>,</span>}
</span>
)
}

const ArrayValue = ({
value,
keyName,
}: {
value: Array<any>
keyName?: string
}) => {
const styles = useStyles()
const [expanded, setExpanded] = createSignal(true)
return (
<span>
{keyName && (
<span
onclick={(e) => {
e.stopPropagation()
e.stopImmediatePropagation()
setExpanded(!expanded())
}}
class={clsx(styles().tree.valueKey, styles().tree.collapsible)}
>
&quot;{keyName}&quot;:{' '}
</span>
)}
<span class={styles().tree.valueBraces}>[</span>
<Show when={expanded()}>
<For each={value}>
{(item, i) => {
const isLastKey = i() === value.length - 1
return (
<>
<JsonValue value={item} isLastKey={isLastKey} />
</>
)
}}
</For>
</Show>
<Show when={!expanded()}>
<span
onClick={(e) => {
e.stopPropagation()
e.stopImmediatePropagation()
setExpanded(!expanded())
}}
class={clsx(styles().tree.valueKey, styles().tree.collapsible)}
>
{`... ${value.length} more`}
</span>
</Show>
<span class={styles().tree.valueBraces}>]</span>
</span>
)
}

const ObjectValue = ({
value,
keyName,
}: {
value: Record<string, any>
keyName?: string
}) => {
const styles = useStyles()
const [expanded, setExpanded] = createSignal(true)
const keys = Object.keys(value)
const lastKeyName = keys[keys.length - 1]

return (
<span>
{keyName && (
<span
onClick={(e) => {
e.stopPropagation()
e.stopImmediatePropagation()
setExpanded(!expanded())
}}
class={clsx(styles().tree.valueKey, styles().tree.collapsible)}
>
&quot;{keyName}&quot;:{' '}
</span>
)}
<span class={styles().tree.valueBraces}>{'{'}</span>
<Show when={expanded()}>
<For each={keys}>
{(k) => (
<>
<JsonValue
value={value[k]}
keyName={k}
isLastKey={lastKeyName === k}
/>
</>
)}
</For>
</Show>
<Show when={!expanded()}>
<span
onClick={(e) => {
e.stopPropagation()
e.stopImmediatePropagation()
setExpanded(!expanded())
}}
class={clsx(styles().tree.valueKey, styles().tree.collapsible)}
>
{`... ${keys.length} more`}
</span>
</Show>
<span class={styles().tree.valueBraces}>{'}'}</span>
</span>
)
}
2 changes: 2 additions & 0 deletions packages/devtools-ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export { Input } from './components/input'
export { Select } from './components/select'
export { TanStackLogo } from './components/logo'
export { JsonTree } from './components/tree'
export { Button } from './components/button'
export { Tag } from './components/tag'
Loading