diff --git a/.changeset/fancy-words-unite.md b/.changeset/fancy-words-unite.md new file mode 100644 index 00000000..1278624b --- /dev/null +++ b/.changeset/fancy-words-unite.md @@ -0,0 +1,5 @@ +--- +'@tanstack/devtools-ui': patch +--- + +new ui components and enhancements for json tree diff --git a/packages/devtools-ui/package.json b/packages/devtools-ui/package.json index ea6318d9..983a1a7b 100644 --- a/packages/devtools-ui/package.json +++ b/packages/devtools-ui/package.json @@ -53,6 +53,7 @@ "build": "vite build" }, "dependencies": { + "clsx": "^2.1.1", "goober": "^2.1.16", "solid-js": "^1.9.7" }, diff --git a/packages/devtools-ui/src/components/button.tsx b/packages/devtools-ui/src/components/button.tsx new file mode 100644 index 00000000..e064455c --- /dev/null +++ b/packages/devtools-ui/src/components/button.tsx @@ -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 & { + 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 ( + + ) +} diff --git a/packages/devtools-ui/src/components/tag.tsx b/packages/devtools-ui/src/components/tag.tsx new file mode 100644 index 00000000..72b87de7 --- /dev/null +++ b/packages/devtools-ui/src/components/tag.tsx @@ -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 ( + + ) +} diff --git a/packages/devtools-ui/src/components/tree.tsx b/packages/devtools-ui/src/components/tree.tsx index ba048d36..fca8f494 100644 --- a/packages/devtools-ui/src/components/tree.tsx +++ b/packages/devtools-ui/src/components/tree.tsx @@ -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 }) { @@ -16,115 +17,37 @@ function JsonValue(props: { return ( + {keyName && typeof value !== 'object' && !Array.isArray(value) && ( + "{keyName}": + )} {(() => { if (typeof value === 'string') { return ( - - {keyName && ( - - "{keyName}":{' '} - - )} - "{value}" - + "{value}" ) } if (typeof value === 'number') { - return ( - - {keyName && ( - - "{keyName}":{' '} - - )} - {value} - - ) + return {value} } if (typeof value === 'boolean') { - return ( - - {keyName && ( - - "{keyName}":{' '} - - )} - {String(value)} - - ) + return {String(value)} } if (value === null) { - return ( - - {keyName && ( - - "{keyName}":{' '} - - )} - null - - ) + return null } if (value === undefined) { + return undefined + } + if (typeof value === 'function') { return ( - - {keyName && ( - - "{keyName}":{' '} - - )} - undefined - + {String(value)} ) } if (Array.isArray(value)) { - return ( - - {keyName && ( - - "{keyName}":{' '} - - )} - [ - - {(item, i) => { - const isLastKey = i() === value.length - 1 - return ( - <> - - - ) - }} - - ] - - ) + return } if (typeof value === 'object') { - const keys = Object.keys(value) - const lastKeyName = keys[keys.length - 1] - return ( - - {keyName && ( - - "{keyName}":{' '} - - )} - {'{'} - - {(k) => ( - <> - - - )} - - {'}'} - - ) + return } return })()} @@ -132,3 +55,113 @@ function JsonValue(props: { ) } + +const ArrayValue = ({ + value, + keyName, +}: { + value: Array + keyName?: string +}) => { + const styles = useStyles() + const [expanded, setExpanded] = createSignal(true) + return ( + + {keyName && ( + { + e.stopPropagation() + e.stopImmediatePropagation() + setExpanded(!expanded()) + }} + class={clsx(styles().tree.valueKey, styles().tree.collapsible)} + > + "{keyName}":{' '} + + )} + [ + + + {(item, i) => { + const isLastKey = i() === value.length - 1 + return ( + <> + + + ) + }} + + + + { + e.stopPropagation() + e.stopImmediatePropagation() + setExpanded(!expanded()) + }} + class={clsx(styles().tree.valueKey, styles().tree.collapsible)} + > + {`... ${value.length} more`} + + + ] + + ) +} + +const ObjectValue = ({ + value, + keyName, +}: { + value: Record + keyName?: string +}) => { + const styles = useStyles() + const [expanded, setExpanded] = createSignal(true) + const keys = Object.keys(value) + const lastKeyName = keys[keys.length - 1] + + return ( + + {keyName && ( + { + e.stopPropagation() + e.stopImmediatePropagation() + setExpanded(!expanded()) + }} + class={clsx(styles().tree.valueKey, styles().tree.collapsible)} + > + "{keyName}":{' '} + + )} + {'{'} + + + {(k) => ( + <> + + + )} + + + + { + e.stopPropagation() + e.stopImmediatePropagation() + setExpanded(!expanded()) + }} + class={clsx(styles().tree.valueKey, styles().tree.collapsible)} + > + {`... ${keys.length} more`} + + + {'}'} + + ) +} diff --git a/packages/devtools-ui/src/index.ts b/packages/devtools-ui/src/index.ts index ccbd745c..b7044322 100644 --- a/packages/devtools-ui/src/index.ts +++ b/packages/devtools-ui/src/index.ts @@ -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' diff --git a/packages/devtools-ui/src/styles/use-styles.ts b/packages/devtools-ui/src/styles/use-styles.ts index be7c4acd..c0620f60 100644 --- a/packages/devtools-ui/src/styles/use-styles.ts +++ b/packages/devtools-ui/src/styles/use-styles.ts @@ -1,11 +1,60 @@ import * as goober from 'goober' import { createSignal } from 'solid-js' import { tokens } from './tokens' +import type { ButtonVariant } from '../components/button' -const stylesFactory = () => { +const buttonVariantColors: Record< + ButtonVariant, + { bg: string; hover: string; active: string; text: string; border: string } +> = { + primary: { + bg: tokens.colors.purple[500], + hover: tokens.colors.purple[600], + active: tokens.colors.purple[700], + text: '#fff', + border: tokens.colors.purple[500], + }, + secondary: { + bg: tokens.colors.gray[800], + hover: tokens.colors.gray[700], + active: tokens.colors.gray[600], + text: tokens.colors.gray[100], + border: tokens.colors.gray[700], + }, + info: { + bg: tokens.colors.blue[500], + hover: tokens.colors.blue[600], + active: tokens.colors.blue[700], + text: '#fff', + border: tokens.colors.blue[500], + }, + warning: { + bg: tokens.colors.yellow[500], + hover: tokens.colors.yellow[600], + active: tokens.colors.yellow[700], + text: '#fff', + border: tokens.colors.yellow[500], + }, + danger: { + bg: tokens.colors.red[500], + hover: tokens.colors.red[600], + active: tokens.colors.red[700], + text: '#fff', + border: tokens.colors.red[500], + }, + success: { + bg: tokens.colors.green[500], + hover: tokens.colors.green[600], + active: tokens.colors.green[700], + text: '#fff', + border: tokens.colors.green[500], + }, +} +const stylesFactory = (theme: 'light' | 'dark' = 'dark') => { const { colors, font, size, alpha } = tokens const { fontFamily } = font const css = goober.css + const t = (light: string, dark: string) => (theme === 'light' ? light : dark) return { logo: css` @@ -192,7 +241,132 @@ const stylesFactory = () => { line-height: 1.3; text-align: left; `, + button: { + base: css` + display: inline-flex; + align-items: center; + justify-content: center; + font-family: ${tokens.font.fontFamily.sans}; + font-size: 0.8rem; + font-weight: 500; + border-radius: 0.2rem; + padding: 0.2rem 0.6rem; + cursor: pointer; + transition: + background 0.2s, + color 0.2s, + border 0.2s, + box-shadow 0.2s; + outline: none; + border-width: 1px; + border-style: solid; + `, + variant(variant: ButtonVariant, outline?: boolean, ghost?: boolean) { + const v = buttonVariantColors[variant] + if (ghost) { + return css` + background: transparent; + color: ${v.bg}; + border-color: transparent; + &:hover { + background: ${tokens.colors.purple[100]}; + } + &:active { + background: ${tokens.colors.purple[200]}; + } + ` + } + if (outline) { + return css` + background: transparent; + color: ${v.bg}; + border-color: ${v.bg}; + &:hover { + background: ${tokens.colors.purple[100]}; + border-color: ${v.hover}; + } + &:active { + background: ${tokens.colors.purple[200]}; + border-color: ${v.active}; + } + ` + } + // Default solid button + return css` + background: ${v.bg}; + color: ${v.text}; + border-color: ${v.border}; + &:hover { + background: ${v.hover}; + border-color: ${v.hover}; + } + &:active { + background: ${v.active}; + border-color: ${v.active}; + } + ` + }, + }, + tag: { + dot: (color: keyof typeof tokens.colors) => css` + width: ${tokens.size[1.5]}; + height: ${tokens.size[1.5]}; + border-radius: ${tokens.border.radius.full}; + background-color: ${tokens.colors[color][500]}; + `, + base: css` + display: flex; + gap: ${tokens.size[1.5]}; + box-sizing: border-box; + height: ${tokens.size[6.5]}; + background: ${t(colors.gray[50], colors.darkGray[500])}; + color: ${t(colors.gray[700], colors.gray[300])}; + border-radius: ${tokens.border.radius.sm}; + font-size: ${font.size.sm}; + padding: ${tokens.size[1]}; + padding-left: ${tokens.size[1.5]}; + align-items: center; + font-weight: ${font.weight.medium}; + border: ${t('1px solid ' + colors.gray[300], '1px solid transparent')}; + user-select: none; + position: relative; + &:focus-visible { + outline-offset: 2px; + outline: 2px solid ${colors.blue[800]}; + } + `, + label: css` + font-size: ${font.size.xs}; + `, + count: css` + font-size: ${font.size.xs}; + padding: 0 5px; + display: flex; + align-items: center; + justify-content: center; + color: ${t(colors.gray[500], colors.gray[400])}; + background-color: ${t(colors.gray[200], colors.darkGray[300])}; + border-radius: 2px; + font-variant-numeric: tabular-nums; + height: ${tokens.size[4.5]}; + `, + }, tree: { + collapsible: css` + cursor: pointer; + transition: all 0.2s ease; + &:hover { + background-color: ${colors.darkGray[700]}; + border-radius: ${tokens.border.radius.sm}; + padding: 0 ${tokens.size[1]}; + } + `, + valueCollapsed: css` + color: ${colors.gray[400]}; + `, + valueFunction: css` + color: ${colors.cyan[400]}; + `, valueString: css` color: ${colors.green[400]}; `, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c89016d..dfda6f3c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -237,6 +237,8 @@ importers: specifier: ^4.2.4 version: 4.2.4 + examples/react/start/generated/prisma: {} + examples/react/time-travel: dependencies: '@tanstack/devtools-event-client': @@ -353,6 +355,9 @@ importers: packages/devtools-ui: dependencies: + clsx: + specifier: ^2.1.1 + version: 2.1.1 goober: specifier: ^2.1.16 version: 2.1.16(csstype@3.1.3)