diff --git a/packages/web-shared/package.json b/packages/web-shared/package.json index 3fd087f395..26e30360e1 100644 --- a/packages/web-shared/package.json +++ b/packages/web-shared/package.json @@ -58,7 +58,7 @@ "lucide-react": "0.575.0", "react": "19.1.0", "react-dom": "19.1.0", - "react-inspector": "9.0.0", + "react-json-view-lite": "2.5.0", "react-virtuoso": "4.18.1", "shiki": "4.0.0", "sonner": "2.0.7", diff --git a/packages/web-shared/src/components/sidebar/copyable-data-block.tsx b/packages/web-shared/src/components/sidebar/copyable-data-block.tsx index d77f4376f9..bc2aa2a9f6 100644 --- a/packages/web-shared/src/components/sidebar/copyable-data-block.tsx +++ b/packages/web-shared/src/components/sidebar/copyable-data-block.tsx @@ -31,7 +31,7 @@ export function CopyableDataBlock({ data }: { data: unknown }) { type="button" aria-label="Copy data" title="Copy" - className="!absolute !right-2 !top-2 !flex !h-6 !w-6 !items-center !justify-center !rounded-md !border !bg-[var(--ds-background-100)] !text-[var(--ds-gray-800)] transition-transform transition-colors duration-100 hover:!bg-[var(--ds-gray-alpha-200)] active:!scale-95 active:!bg-[var(--ds-gray-alpha-300)]" + className="!absolute !right-2 !top-2 !flex !h-6 !w-6 !items-center !justify-center !rounded-md !border !bg-[var(--ds-background-100)] !text-[var(--ds-gray-800)] transition duration-100 hover:!bg-[var(--ds-gray-alpha-200)] active:!scale-95 active:!bg-[var(--ds-gray-alpha-300)]" style={{ borderColor: 'var(--ds-gray-300)' }} onClick={() => { navigator.clipboard diff --git a/packages/web-shared/src/components/ui/data-inspector-tree.tsx b/packages/web-shared/src/components/ui/data-inspector-tree.tsx new file mode 100644 index 0000000000..1f08f8d209 --- /dev/null +++ b/packages/web-shared/src/components/ui/data-inspector-tree.tsx @@ -0,0 +1,1162 @@ +'use client'; + +/** + * Reusable data inspector component with a compact JSON tree renderer. + * + * The tree presentation is intentionally closer to Vercel's newer JSON + * rendering pattern: tighter spacing, click-to-expand labels, explicit + * collapsed previews, and syntax-colored keys/values. Workflow-specific + * badges (stream refs, run refs, decrypt actions) are preserved. + */ + +import { Lock } from 'lucide-react'; +import { + type CSSProperties, + createContext, + type ReactNode, + useContext, + useMemo, + useRef, + useState, +} from 'react'; +import { useDarkMode } from '../../hooks/use-dark-mode'; +import { ENCRYPTED_DISPLAY_NAME } from '../../lib/hydration'; +import { Spinner } from './spinner'; + +// --------------------------------------------------------------------------- +// StreamRef / ClassInstanceRef type detection +// (inline to avoid circular deps with hydration module) +// --------------------------------------------------------------------------- + +const STREAM_REF_TYPE = '__workflow_stream_ref__'; +const CLASS_INSTANCE_REF_TYPE = '__workflow_class_instance_ref__'; +const RUN_REF_TYPE = '__workflow_run_ref__'; + +interface StreamRef { + __type: typeof STREAM_REF_TYPE; + streamId: string; +} + +interface RunRef { + __type: typeof RUN_REF_TYPE; + runId: string; +} + +type TypedArrayValue = + | BigInt64Array + | BigUint64Array + | Float32Array + | Float64Array + | Int8Array + | Int16Array + | Int32Array + | Uint8Array + | Uint8ClampedArray + | Uint16Array + | Uint32Array; + +function isStreamRef(value: unknown): value is StreamRef { + if (value === null || typeof value !== 'object') return false; + // Check both enumerable and non-enumerable __type (opaque refs use non-enumerable) + const desc = Object.getOwnPropertyDescriptor(value, '__type'); + return desc?.value === STREAM_REF_TYPE; +} + +function isRunRef(value: unknown): value is RunRef { + if (value === null || typeof value !== 'object') return false; + const desc = Object.getOwnPropertyDescriptor(value, '__type'); + return desc?.value === RUN_REF_TYPE; +} + +function isClassInstanceRef(value: unknown): value is { + __type: string; + className: string; + classId: string; + data: unknown; +} { + return ( + value !== null && + typeof value === 'object' && + '__type' in value && + (value as Record).__type === CLASS_INSTANCE_REF_TYPE + ); +} + +function isTypedArray(value: unknown): value is TypedArrayValue { + return ArrayBuffer.isView(value) && !(value instanceof DataView); +} + +function isEncryptedDisplay(value: unknown): boolean { + return ( + value !== null && + typeof value === 'object' && + value.constructor?.name === ENCRYPTED_DISPLAY_NAME + ); +} + +// --------------------------------------------------------------------------- +// Stream / decrypt contexts +// --------------------------------------------------------------------------- + +/** + * Context for passing stream click handlers down to DataInspector instances. + * Exported so that parent components (e.g., AttributePanel) can provide the handler. + */ +export const StreamClickContext = createContext< + ((streamId: string) => void) | undefined +>(undefined); + +/** + * Context for passing a decrypt handler down to DataInspector instances. + * When provided, encrypted markers become clickable buttons that trigger decryption. + */ +export type DecryptClickContextValue = { + onDecrypt: () => void; + isDecrypting: boolean; +}; + +export const DecryptClickContext = createContext< + DecryptClickContextValue | undefined +>(undefined); + +export const RunClickContext = createContext< + ((runId: string) => void) | undefined +>(undefined); + +function EncryptedInlineLabel() { + const ctx = useContext(DecryptClickContext); + if (ctx) { + return ( + + ); + } + + return ( + + + Encrypted + + ); +} + +function StreamRefInline({ streamRef }: { streamRef: StreamRef }) { + const onStreamClick = useContext(StreamClickContext); + const [hovered, setHovered] = useState(false); + + return ( + + ); +} + +function RunRefInline({ runRef }: { runRef: RunRef }) { + const onRunClick = useContext(RunClickContext); + const [hovered, setHovered] = useState(false); + + return ( + + ); +} + +// --------------------------------------------------------------------------- +// JSON tree rendering +// --------------------------------------------------------------------------- + +type JsonPathSegment = number | string; + +interface TreeEntry { + key: string; + pathSegment: JsonPathSegment; + value: unknown; +} + +interface TreePrefix { + text: string; + italic?: boolean; +} + +interface CompositeDescriptor { + close: ']' | '}'; + entries: TreeEntry[]; + open: '[' | '{'; + prefix?: TreePrefix; +} + +interface JsonTreeStyles { + boolean: CSSProperties; + childContainer: CSSProperties; + circular: CSSProperties; + clickableKey: CSSProperties; + collapsed: CSSProperties; + container: CSSProperties; + date: CSSProperties; + iconButton: CSSProperties; + key: CSSProperties; + meta: CSSProperties; + nullish: CSSProperties; + number: CSSProperties; + punctuation: CSSProperties; + row: CSSProperties; + string: CSSProperties; +} + +interface JsonNodeProps { + ancestors: object[]; + data: unknown; + depth: number; + expandLevel: number; + expandedPaths: Record; + isLast: boolean; + name?: string; + onToggle: (pathKey: string, defaultExpanded: boolean) => void; + path: JsonPathSegment[]; + styles: JsonTreeStyles; +} + +interface JsonLeafRowProps { + isLast: boolean; + name?: string; + prefix?: TreePrefix; + styles: JsonTreeStyles; + value: ReactNode; +} + +interface JsonCompositeRowProps { + ancestors: object[]; + depth: number; + descriptor: CompositeDescriptor; + expandLevel: number; + expandedPaths: Record; + isLast: boolean; + name?: string; + onToggle: (pathKey: string, defaultExpanded: boolean) => void; + path: JsonPathSegment[]; + styles: JsonTreeStyles; +} + +function createTreeStyles(isDark: boolean): JsonTreeStyles { + return { + container: { + color: 'var(--ds-gray-1000)', + fontFamily: 'var(--font-mono)', + fontSize: '11px', + lineHeight: '20px', + overflowWrap: 'anywhere', + whiteSpace: 'pre-wrap', + wordBreak: 'break-word', + }, + row: { + display: 'block', + margin: 0, + padding: '0 0 0 18px', + }, + childContainer: { + margin: 0, + padding: 0, + paddingLeft: '2ch', + }, + key: { + color: isDark ? 'var(--ds-pink-700)' : 'var(--ds-pink-900)', + fontWeight: 400, + }, + clickableKey: { + background: 'transparent', + border: 0, + color: isDark ? 'var(--ds-pink-700)' : 'var(--ds-pink-900)', + cursor: 'pointer', + font: 'inherit', + fontWeight: 400, + lineHeight: 'inherit', + margin: 0, + padding: 0, + textAlign: 'left', + }, + punctuation: { + color: 'var(--ds-gray-1000)', + }, + string: { + color: isDark ? 'var(--ds-blue-700)' : 'var(--ds-green-900)', + }, + number: { + color: isDark ? 'var(--ds-blue-700)' : 'var(--ds-blue-900)', + }, + boolean: { + color: isDark ? 'var(--ds-amber-700)' : 'var(--ds-amber-900)', + }, + nullish: { + color: isDark ? 'var(--ds-gray-700)' : 'var(--ds-gray-900)', + }, + date: { + color: isDark ? 'var(--ds-pink-700)' : 'var(--ds-pink-900)', + }, + meta: { + color: isDark ? 'var(--ds-gray-700)' : 'var(--ds-gray-900)', + }, + iconButton: { + alignItems: 'center', + background: 'transparent', + border: 0, + borderRadius: '4px', + boxSizing: 'border-box', + color: isDark ? '#fff' : 'var(--ds-gray-900)', + cursor: 'pointer', + display: 'inline-flex', + fontFamily: 'inherit', + fontSize: '14px', + fontWeight: 600, + justifyContent: 'center', + lineHeight: 1, + margin: 0, + marginLeft: '-18px', + minHeight: '18px', + minWidth: '18px', + padding: 0, + userSelect: 'none', + verticalAlign: 'middle', + }, + collapsed: { + color: isDark ? 'var(--ds-gray-700)' : 'var(--ds-gray-900)', + }, + circular: { + color: isDark ? 'var(--ds-gray-700)' : 'var(--ds-gray-900)', + fontStyle: 'italic', + }, + }; +} + +function DisclosureChevron({ expanded }: { expanded: boolean }) { + return ( + + ); +} + +function JsonTree({ + data, + expandLevel, + name, +}: { + data: unknown; + expandLevel: number; + name?: string; +}) { + const [expandedPaths, setExpandedPaths] = useState>( + {} + ); + const isDark = useDarkMode(); + const styles = useMemo(() => createTreeStyles(isDark), [isDark]); + + return ( +
+ { + setExpandedPaths((current) => ({ + ...current, + [pathKey]: !(current[pathKey] ?? defaultExpanded), + })); + }} + path={[]} + styles={styles} + /> +
+ ); +} + +function JsonNode({ + ancestors, + data, + depth, + expandLevel, + expandedPaths, + isLast, + name, + onToggle, + path, + styles, +}: JsonNodeProps) { + if (isEncryptedDisplay(data)) { + return ( + } + /> + ); + } + + if (isStreamRef(data)) { + return ( + } + /> + ); + } + + if (isRunRef(data)) { + return ( + } + /> + ); + } + + if (isClassInstanceRef(data)) { + const prefix = { italic: true, text: data.className } satisfies TreePrefix; + const innerValue = data.data; + + if (isObjectLike(innerValue) && ancestors.includes(innerValue)) { + return ( + [Circular]} + /> + ); + } + + const descriptor = getCompositeDescriptor(innerValue); + if (descriptor) { + return ( + + ); + } + + return ( + + ); + } + + if (isObjectLike(data) && ancestors.includes(data)) { + return ( + [Circular]} + /> + ); + } + + const descriptor = getCompositeDescriptor(data); + if (descriptor) { + return ( + + ); + } + + return ( + + ); +} + +function JsonLeafRow({ + isLast, + name, + prefix, + styles, + value, +}: JsonLeafRowProps) { + return ( +
+ {name != null ? {name} : null} + {name != null ? : : null} + {prefix ? ( + {prefix.text} + ) : null} + {value} + {!isLast ? , : null} +
+ ); +} + +function JsonCompositeRow({ + ancestors, + depth, + descriptor, + expandLevel, + expandedPaths, + isLast, + name, + onToggle, + path, + styles, +}: JsonCompositeRowProps) { + if (descriptor.entries.length === 0) { + return ( + + {descriptor.open} + {descriptor.close} + + } + /> + ); + } + + const pathKey = JSON.stringify(path); + const defaultExpanded = depth < expandLevel; + const isExpanded = expandedPaths[pathKey] ?? defaultExpanded; + + const toggle = () => { + onToggle(pathKey, defaultExpanded); + }; + + return ( + <> +
+ + {name != null ? ( + <> + + : + + ) : null} + {descriptor.prefix ? ( + + {descriptor.prefix.text}{' '} + + ) : null} + {descriptor.open} + {!isExpanded ? ( + <> + ... + {descriptor.close} + {!isLast ? , : null} + + ) : null} +
+ {isExpanded ? ( + <> +
+ {descriptor.entries.map((entry, index) => ( + + ))} +
+
+ {descriptor.close} + {!isLast ? , : null} +
+ + ) : null} + + ); +} + +function getCompositeDescriptor(value: unknown): CompositeDescriptor | null { + if (Array.isArray(value)) { + return { + close: ']', + entries: Array.from({ length: value.length }, (_, index) => ({ + key: String(index), + pathSegment: index, + value: value[index], + })), + open: '[', + }; + } + + if (value instanceof Map) { + return { + close: '}', + entries: Array.from(value.entries(), ([entryKey, entryValue], index) => ({ + key: formatMapKey(entryKey, index), + pathSegment: index, + value: entryValue, + })), + open: '{', + prefix: { text: `Map(${value.size})` }, + }; + } + + if (value instanceof Set) { + return { + close: ']', + entries: Array.from(value.values(), (entryValue, index) => ({ + key: String(index), + pathSegment: index, + value: entryValue, + })), + open: '[', + prefix: { text: `Set(${value.size})` }, + }; + } + + if (isTypedArray(value)) { + return { + close: ']', + entries: getTypedArrayEntries(value), + open: '[', + prefix: { text: `${value.constructor.name}(${value.length})` }, + }; + } + + if (value instanceof Headers) { + return { + close: '}', + entries: Array.from(value.entries(), ([entryKey, entryValue], index) => ({ + key: entryKey, + pathSegment: `${entryKey}-${index}`, + value: entryValue, + })), + open: '{', + prefix: { text: 'Headers' }, + }; + } + + if (value instanceof URLSearchParams) { + return { + close: '}', + entries: Array.from(value.entries(), ([entryKey, entryValue], index) => ({ + key: entryKey, + pathSegment: `${entryKey}-${index}`, + value: entryValue, + })), + open: '{', + prefix: { text: 'URLSearchParams' }, + }; + } + + if (value instanceof Error) { + return { + close: '}', + entries: getErrorEntries(value).map(([entryKey, entryValue]) => ({ + key: entryKey, + pathSegment: entryKey, + value: entryValue, + })), + open: '{', + prefix: { text: value.name || 'Error' }, + }; + } + + if ( + value instanceof ArrayBuffer || + value instanceof DataView || + value instanceof Date || + value instanceof Promise || + value instanceof RegExp || + value instanceof URL || + value instanceof WeakMap || + value instanceof WeakSet + ) { + return null; + } + + if (!isObjectLike(value)) { + return null; + } + + const constructorName = getConstructorName(value); + return { + close: '}', + entries: Object.entries(value).map(([entryKey, entryValue]) => ({ + key: entryKey, + pathSegment: entryKey, + value: entryValue, + })), + open: '{', + prefix: + constructorName && constructorName !== 'Object' + ? { text: constructorName } + : undefined, + }; +} + +function getErrorEntries(error: Error): Array<[string, unknown]> { + const entries: Array<[string, unknown]> = []; + + if (error.message) { + entries.push(['message', error.message]); + } + if (error.stack) { + entries.push(['stack', error.stack]); + } + + const errorWithCause = error as Error & { cause?: unknown }; + if ('cause' in errorWithCause && errorWithCause.cause !== undefined) { + entries.push(['cause', errorWithCause.cause]); + } + + for (const [key, value] of Object.entries(error)) { + if (!entries.some(([existingKey]) => existingKey === key)) { + entries.push([key, value]); + } + } + + return entries; +} + +function getTypedArrayEntries(value: TypedArrayValue): TreeEntry[] { + const entries: TreeEntry[] = []; + for (let index = 0; index < value.length; index += 1) { + entries.push({ + key: String(index), + pathSegment: index, + value: value[index], + }); + } + return entries; +} + +function formatInlineValue(value: unknown, styles: JsonTreeStyles): ReactNode { + if (value === null) { + return null; + } + + if (value === undefined) { + return undefined; + } + + if (typeof value === 'string') { + return {JSON.stringify(value)}; + } + + if (typeof value === 'number') { + return {String(value)}; + } + + if (typeof value === 'bigint') { + return {`${value}n`}; + } + + if (typeof value === 'boolean') { + return {String(value)}; + } + + if (value instanceof Date) { + return {value.toISOString()}; + } + + if (value instanceof RegExp) { + return {String(value)}; + } + + if (value instanceof URL) { + return ( + {JSON.stringify(value.toString())} + ); + } + + if (value instanceof ArrayBuffer) { + return ( + {`ArrayBuffer(${value.byteLength})`} + ); + } + + if (value instanceof DataView) { + return {`DataView(${value.byteLength})`}; + } + + if (value instanceof WeakMap) { + return WeakMap; + } + + if (value instanceof WeakSet) { + return WeakSet; + } + + if (value instanceof Promise) { + return Promise; + } + + if (typeof value === 'function') { + return ( + + [Function + {value.name ? ` ${value.name}` : ''}] + + ); + } + + if (typeof value === 'symbol') { + return {String(value)}; + } + + if (value instanceof Error) { + return ( + {`${value.name}${value.message ? `: ${value.message}` : ''}`} + ); + } + + if (isObjectLike(value)) { + const constructorName = getConstructorName(value); + return ( + + {constructorName && constructorName !== 'Object' + ? constructorName + : '{}'} + + ); + } + + return {String(value)}; +} + +function formatMapKey(key: unknown, index: number): string { + if (typeof key === 'string') return key; + if ( + typeof key === 'number' || + typeof key === 'boolean' || + typeof key === 'bigint' + ) { + return String(key); + } + if (key === null) return 'null'; + if (key === undefined) return 'undefined'; + if (key instanceof Date) return key.toISOString(); + if (key instanceof RegExp || key instanceof URL) return String(key); + if (typeof key === 'symbol') return key.toString(); + return `entry ${index}`; +} + +function getConstructorName(value: object): string | undefined { + const prototype = Object.getPrototypeOf(value); + const ctor = prototype?.constructor; + if (typeof ctor !== 'function') return undefined; + return ctor.name || undefined; +} + +function getPrefixStyle( + prefix: TreePrefix, + styles: JsonTreeStyles +): CSSProperties { + return prefix.italic ? { ...styles.meta, fontStyle: 'italic' } : styles.meta; +} + +// --------------------------------------------------------------------------- +// Public component +// --------------------------------------------------------------------------- + +export interface DataInspectorProps { + /** The data to inspect */ + data: unknown; + /** Initial expand depth (default: 3) */ + expandLevel?: number; + /** Optional name for the root node */ + name?: string; + /** Callback when a stream reference is clicked */ + onStreamClick?: (streamId: string) => void; + /** Callback when a run reference is clicked */ + onRunClick?: (runId: string) => void; + /** Callback when an encrypted marker is clicked (triggers decryption) */ + onDecrypt?: () => void; + /** Whether decryption is currently in progress */ + isDecrypting?: boolean; +} + +export function DataInspector({ + data, + expandLevel = 3, + name, + onStreamClick, + onRunClick, + onDecrypt, + isDecrypting = false, +}: DataInspectorProps) { + const stableData = useStableInspectorData(data); + let wrapped = ( + + ); + + if (onStreamClick) { + wrapped = ( + + {wrapped} + + ); + } + + if (onRunClick) { + wrapped = ( + + {wrapped} + + ); + } + + if (onDecrypt) { + wrapped = ( + + {wrapped} + + ); + } + + return wrapped; +} + +function useStableInspectorData(next: T): T { + const previousRef = useRef(next); + if (!isDeepEqual(previousRef.current, next)) { + previousRef.current = next; + } + return previousRef.current; +} + +function isObjectLike(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +function isDeepEqual(a: unknown, b: unknown, seen = new WeakMap()): boolean { + if (Object.is(a, b)) return true; + + if (a instanceof Date && b instanceof Date) { + return a.getTime() === b.getTime(); + } + + if (a instanceof RegExp && b instanceof RegExp) { + return a.source === b.source && a.flags === b.flags; + } + + if (a instanceof URL && b instanceof URL) { + return a.toString() === b.toString(); + } + + if (a instanceof Map && b instanceof Map) { + if (a.size !== b.size) return false; + for (const [key, value] of a.entries()) { + if (!b.has(key) || !isDeepEqual(value, b.get(key), seen)) return false; + } + return true; + } + + if (a instanceof Set && b instanceof Set) { + if (a.size !== b.size) return false; + for (const value of a.values()) { + if (!b.has(value)) return false; + } + return true; + } + + if (isTypedArray(a) && isTypedArray(b)) { + if (a.constructor !== b.constructor || a.length !== b.length) return false; + for (let i = 0; i < a.length; i += 1) { + if (!Object.is(a[i], b[i])) return false; + } + return true; + } + + if (a instanceof ArrayBuffer && b instanceof ArrayBuffer) { + if (a.byteLength !== b.byteLength) return false; + const left = new Uint8Array(a); + const right = new Uint8Array(b); + for (let i = 0; i < left.length; i += 1) { + if (left[i] !== right[i]) return false; + } + return true; + } + + if (a instanceof Headers && b instanceof Headers) { + return isDeepEqual(Array.from(a.entries()), Array.from(b.entries()), seen); + } + + if (a instanceof URLSearchParams && b instanceof URLSearchParams) { + return a.toString() === b.toString(); + } + + if (a instanceof Error && b instanceof Error) { + return ( + a.name === b.name && + a.message === b.message && + a.stack === b.stack && + isDeepEqual(Object.entries(a), Object.entries(b), seen) + ); + } + + if (!isObjectLike(a) || !isObjectLike(b)) { + return false; + } + + if (seen.get(a) === b) return true; + seen.set(a, b); + + const aIsArray = Array.isArray(a); + const bIsArray = Array.isArray(b); + if (aIsArray !== bIsArray) return false; + + if (aIsArray && bIsArray) { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i += 1) { + if (!isDeepEqual(a[i], b[i], seen)) return false; + } + return true; + } + + const aKeys = Object.keys(a); + const bKeys = Object.keys(b); + if (aKeys.length !== bKeys.length) return false; + + for (const key of aKeys) { + if (!Object.hasOwn(b, key)) return false; + if (!isDeepEqual(a[key], b[key], seen)) return false; + } + + return true; +} diff --git a/packages/web-shared/src/components/ui/data-inspector.tsx b/packages/web-shared/src/components/ui/data-inspector.tsx index 621fdad33a..c29ed5ffe7 100644 --- a/packages/web-shared/src/components/ui/data-inspector.tsx +++ b/packages/web-shared/src/components/ui/data-inspector.tsx @@ -1,757 +1,10 @@ -'use client'; - -/** - * Reusable data inspector component built on react-inspector. - * - * All data rendering in the o11y UI should use this component to ensure - * consistent theming, custom type handling (StreamRef, ClassInstanceRef), - * and expand behavior. - */ - -import { Lock } from 'lucide-react'; -import { - createContext, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; -import { - ObjectInspector, - ObjectLabel, - ObjectName, - ObjectRootLabel, - ObjectValue, -} from 'react-inspector'; -import { useDarkMode } from '../../hooks/use-dark-mode'; -import { ENCRYPTED_DISPLAY_NAME } from '../../lib/hydration'; -import { - type DecodedStreamChunkSource, - type FormattedStreamChunkDisplay, - formatArrayBufferViewForDisplay, -} from '../../lib/stream-display'; -import { - type InspectorThemeExtended, - inspectorThemeDark, - inspectorThemeExtendedDark, - inspectorThemeExtendedLight, - inspectorThemeLight, -} from './inspector-theme'; -import { Spinner } from './spinner'; - -// --------------------------------------------------------------------------- -// StreamRef / ClassInstanceRef type detection -// (inline to avoid circular deps with hydration module) -// --------------------------------------------------------------------------- - -const STREAM_REF_TYPE = '__workflow_stream_ref__'; -const CLASS_INSTANCE_REF_TYPE = '__workflow_class_instance_ref__'; -const RUN_REF_TYPE = '__workflow_run_ref__'; -const BYTES_DISPLAY_TYPE = '__workflow_bytes_display__'; - -interface StreamRef { - __type: typeof STREAM_REF_TYPE; - streamId: string; -} - -interface RunRef { - __type: typeof RUN_REF_TYPE; - runId: string; -} - -interface BytesDisplay { - __type: typeof BYTES_DISPLAY_TYPE; - text: string; - decodedFrom?: DecodedStreamChunkSource; -} - -function deserializeChunkText(text: string): string { - try { - const parsed = JSON.parse(text); - if (typeof parsed === 'string') { - return parsed; - } - return JSON.stringify(parsed, null, 2); - } catch { - return text; - } -} - -function parseChunkData(text: string): unknown { - try { - return JSON.parse(text); - } catch { - return text; - } -} - -function isStreamRef(value: unknown): value is StreamRef { - if (value === null || typeof value !== 'object') return false; - // Check both enumerable and non-enumerable __type (opaque refs use non-enumerable) - const desc = Object.getOwnPropertyDescriptor(value, '__type'); - return desc?.value === STREAM_REF_TYPE; -} - -function isRunRef(value: unknown): value is RunRef { - if (value === null || typeof value !== 'object') return false; - const desc = Object.getOwnPropertyDescriptor(value, '__type'); - return desc?.value === RUN_REF_TYPE; -} - -export function isBytesDisplay(value: unknown): value is BytesDisplay { - if (value === null || typeof value !== 'object') return false; - const desc = Object.getOwnPropertyDescriptor(value, '__type'); - return desc?.value === BYTES_DISPLAY_TYPE; -} - -function isClassInstanceRef(value: unknown): value is { - __type: string; - className: string; - classId: string; - data: unknown; -} { - return ( - value !== null && - typeof value === 'object' && - '__type' in value && - (value as Record).__type === CLASS_INSTANCE_REF_TYPE - ); -} - -// --------------------------------------------------------------------------- -// Stream click context (passed through from the panel) -// --------------------------------------------------------------------------- - -/** - * Context for passing stream click handlers down to DataInspector instances. - * Exported so that parent components (e.g., AttributePanel) can provide the handler. - */ -export const StreamClickContext = createContext< - ((streamId: string) => void) | undefined ->(undefined); - -/** - * Context for passing a decrypt handler down to DataInspector instances. - * When provided, encrypted markers become clickable buttons that trigger decryption. - */ -export type DecryptClickContextValue = { - onDecrypt: () => void; - isDecrypting: boolean; -}; - -export const DecryptClickContext = createContext< - DecryptClickContextValue | undefined ->(undefined); - -export const RunClickContext = createContext< - ((runId: string) => void) | undefined ->(undefined); - -function EncryptedInlineLabel() { - const ctx = useContext(DecryptClickContext); - if (ctx) { - return ( - - ); - } - return ( - - - Encrypted - - ); -} -function StreamRefInline({ streamRef }: { streamRef: StreamRef }) { - const onStreamClick = useContext(StreamClickContext); - const [hovered, setHovered] = useState(false); - return ( - - ); -} - -function RunRefInline({ runRef }: { runRef: RunRef }) { - const onRunClick = useContext(RunClickContext); - const [hovered, setHovered] = useState(false); - return ( - - ); -} - -// --------------------------------------------------------------------------- -// Extended theme context (for colors react-inspector doesn't support natively) -// --------------------------------------------------------------------------- - -const ExtendedThemeContext = createContext( - inspectorThemeExtendedLight -); - -function DecodedBytesChunk({ - decodedText, - source, -}: { - decodedText: string; - source: DecodedStreamChunkSource; -}) { - const [selectedView, setSelectedView] = useState<'decoded' | 'bytes'>( - 'decoded' - ); - const parsed = parseChunkData(decodedText); - - return ( -
- {selectedView === 'decoded' ? ( -
- {typeof parsed === 'string' ? ( - - {deserializeChunkText(parsed)} - - ) : ( - - )} -
- ) : ( - - )} -
-
- - -
-
-
- ); -} - -function DecodedBytesInspector({ - decodedText, - source, -}: { - decodedText: string; - source: DecodedStreamChunkSource; -}) { - const [expanded, setExpanded] = useState(false); - - return ( -
- - {expanded && ( -
- decoded: - - {JSON.stringify(decodedText)} - -
- )} -
- ); -} - -function BytesDisplayLabel({ - name, - display, -}: { - name?: string; - display: BytesDisplay; -}) { - return ( -
- {name != null && } - {name != null && : } - {display.decodedFrom ? ( - - ) : ( - - {display.text} - - )} -
- ); -} - -// --------------------------------------------------------------------------- -// Custom nodeRenderer -// --------------------------------------------------------------------------- - -/** - * Extends the default react-inspector nodeRenderer with special handling - * for ClassInstanceRef, StreamRef, and Date types. - * - * react-inspector renders Date instances as unstyled plain text (no theme - * key exists for them), so we intercept here and apply the magenta color - * from our extended theme β€” matching Node.js util.inspect()'s date style. - * - * Default nodeRenderer reference: - * https://github.com/storybookjs/react-inspector/blob/main/README.md#noderenderer - */ -function NodeRenderer({ - depth, - name, - data, - isNonenumerable, -}: { - depth: number; - name?: string; - data: unknown; - isNonenumerable?: boolean; - expanded?: boolean; -}) { - const extendedTheme = useContext(ExtendedThemeContext); - - if (isBytesDisplay(data)) { - return ; - } - - // Encrypted marker β†’ flat label with Lock icon, clickable when onDecrypt is available - if ( - data !== null && - typeof data === 'object' && - data.constructor?.name === ENCRYPTED_DISPLAY_NAME - ) { - const label = ; - if (depth === 0) { - return label; - } - return ( - - {name != null && } - {name != null && : } - {label} - - ); - } - - // StreamRef β†’ inline clickable badge - if (isStreamRef(data)) { - return ( - - {name != null && } - {name != null && : } - - - ); - } - - // RunRef β†’ inline clickable badge linking to the target run - if (isRunRef(data)) { - return ( - - {name != null && } - {name != null && : } - - - ); - } - - // ClassInstanceRef β†’ show className as type, data as the inspectable value - if (isClassInstanceRef(data)) { - if (depth === 0) { - return ; - } - return ( - - {name != null && } - {name != null && : } - {data.className} - - - ); - } - - // Date β†’ magenta color (Node.js: 'date' β†’ 'magenta') - // react-inspector has no OBJECT_VALUE_DATE_COLOR theme key, so we handle it here. - if (data instanceof Date) { - const dateStr = data.toISOString(); - if (depth === 0) { - return ( - - {dateStr} - - ); - } - return ( - - {name != null && } - {name != null && : } - - {dateStr} - - - ); - } - - // Default rendering (same as react-inspector's built-in) - if (depth === 0) { - return ; - } - return ( - - ); -} - -// --------------------------------------------------------------------------- -// Public component -// --------------------------------------------------------------------------- - -/** - * Create a non-expandable wrapper that carries ref data as non-enumerable - * properties. ObjectInspector won't render children for objects with no - * enumerable keys, but our NodeRenderer can still detect them. - */ -function makeOpaqueRef(ref: Record): unknown { - const opaque = Object.create(null); - for (const [key, value] of Object.entries(ref)) { - Object.defineProperty(opaque, key, { value, enumerable: false }); - } - return opaque; -} - -function makeBytesDisplay(display: FormattedStreamChunkDisplay): unknown { - const opaque = Object.create(null); - Object.defineProperty(opaque, '__type', { - value: BYTES_DISPLAY_TYPE, - enumerable: false, - }); - Object.defineProperty(opaque, 'text', { - value: display.text, - enumerable: false, - }); - Object.defineProperty(opaque, 'decodedFrom', { - value: display.decodedFrom, - enumerable: false, - }); - return opaque; -} - -/** - * Recursively walk data and replace RunRef/StreamRef/typed array objects with - * non-expandable versions so ObjectInspector doesn't show their internals. - * Only recurses into plain objects and arrays to avoid stripping class - * instances (Date, Error, URL, Headers, etc.) that have their own rendering in - * NodeRenderer. Map and Set containers are preserved while their contents are - * prepared for display. - * - * Exported for testing the typed-array detection path used by hydrated - * AI agent stream chunks (e.g. `{ delta: new Uint8Array(...) }`). - */ -export function collapseRefs(data: unknown): unknown { - if (data === null || typeof data !== 'object') return data; - if (ArrayBuffer.isView(data) && !(data instanceof DataView)) { - return makeBytesDisplay(formatArrayBufferViewForDisplay(data)); - } - if (isRunRef(data) || isStreamRef(data)) - return makeOpaqueRef(data as unknown as Record); - if (Array.isArray(data)) return data.map(collapseRefs); - if (data instanceof Map) { - return new Map( - Array.from(data.entries(), ([key, value]) => [ - collapseRefs(key), - collapseRefs(value), - ]) - ); - } - if (data instanceof Set) { - return new Set(Array.from(data.values(), collapseRefs)); - } - // Only recurse into plain objects β€” leave class instances untouched - const proto = Object.getPrototypeOf(data); - if (proto !== Object.prototype && proto !== null) return data; - const result: Record = {}; - for (const [key, value] of Object.entries(data)) { - result[key] = collapseRefs(value); - } - return result; -} - -export interface DataInspectorProps { - /** The data to inspect */ - data: unknown; - /** Initial expand depth (default: 2) */ - expandLevel?: number; - /** Optional name for the root node */ - name?: string; - /** Callback when a stream reference is clicked */ - onStreamClick?: (streamId: string) => void; - /** Callback when a run reference is clicked */ - onRunClick?: (runId: string) => void; - /** Callback when an encrypted marker is clicked (triggers decryption) */ - onDecrypt?: () => void; - /** Whether decryption is currently in progress */ - isDecrypting?: boolean; -} - -export function DataInspector({ - data, - expandLevel = 2, - name, - onStreamClick, - onRunClick, - onDecrypt, - isDecrypting = false, -}: DataInspectorProps) { - const collapsedData = useMemo(() => collapseRefs(data), [data]); - const stableData = useStableInspectorData(collapsedData); - const [initialExpandLevel, setInitialExpandLevel] = useState(expandLevel); - const isDark = useDarkMode(); - const extendedTheme = isDark - ? inspectorThemeExtendedDark - : inspectorThemeExtendedLight; - - useEffect(() => { - // react-inspector reapplies expandLevel on every data change, which can - // reopen paths the user manually collapsed. Apply it only on mount. - setInitialExpandLevel(0); - }, []); - - const content = ( - - - - ); - - let wrapped = content; - - if (onStreamClick) { - wrapped = ( - - {wrapped} - - ); - } - if (onRunClick) { - wrapped = ( - - {wrapped} - - ); - } - if (onDecrypt) { - wrapped = ( - - {wrapped} - - ); - } - - return wrapped; -} - -function useStableInspectorData(next: T): T { - const previousRef = useRef(next); - if (!isDeepEqual(previousRef.current, next)) { - previousRef.current = next; - } - return previousRef.current; -} - -function isObjectLike(value: unknown): value is Record { - return typeof value === 'object' && value !== null; -} - -function isSameBytesDisplay(a: BytesDisplay, b: BytesDisplay): boolean { - return ( - a.text === b.text && - a.decodedFrom?.type === b.decodedFrom?.type && - a.decodedFrom?.encoding === b.decodedFrom?.encoding && - a.decodedFrom?.rawSummary === b.decodedFrom?.rawSummary - ); -} - -function isDeepEqual(a: unknown, b: unknown, seen = new WeakMap()): boolean { - if (Object.is(a, b)) return true; - - if (isBytesDisplay(a) || isBytesDisplay(b)) { - return isBytesDisplay(a) && isBytesDisplay(b) && isSameBytesDisplay(a, b); - } - - if (a instanceof Date && b instanceof Date) { - return a.getTime() === b.getTime(); - } - - if (a instanceof RegExp && b instanceof RegExp) { - return a.source === b.source && a.flags === b.flags; - } - - if (a instanceof Map && b instanceof Map) { - if (a.size !== b.size) return false; - for (const [key, value] of a.entries()) { - if (!b.has(key) || !isDeepEqual(value, b.get(key), seen)) return false; - } - return true; - } - - if (a instanceof Set && b instanceof Set) { - if (a.size !== b.size) return false; - for (const value of a.values()) { - if (!b.has(value)) return false; - } - return true; - } - - if (!isObjectLike(a) || !isObjectLike(b)) { - return false; - } - - if (seen.get(a) === b) return true; - seen.set(a, b); - - const aIsArray = Array.isArray(a); - const bIsArray = Array.isArray(b); - if (aIsArray !== bIsArray) return false; - - if (aIsArray && bIsArray) { - if (a.length !== b.length) return false; - for (let i = 0; i < a.length; i += 1) { - if (!isDeepEqual(a[i], b[i], seen)) return false; - } - return true; - } - - const aKeys = Object.keys(a); - const bKeys = Object.keys(b); - if (aKeys.length !== bKeys.length) return false; - - for (const key of aKeys) { - if (!Object.hasOwn(b, key)) return false; - if (!isDeepEqual(a[key], b[key], seen)) return false; - } - - return true; -} +export { + DataInspector, + DecryptClickContext, + RunClickContext, + StreamClickContext, +} from './data-inspector-tree'; +export type { + DataInspectorProps, + DecryptClickContextValue, +} from './data-inspector-tree'; diff --git a/packages/web-shared/src/components/ui/inspector-theme.ts b/packages/web-shared/src/components/ui/inspector-theme.ts deleted file mode 100644 index 5d04188082..0000000000 --- a/packages/web-shared/src/components/ui/inspector-theme.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Shared theme configuration for react-inspector's ObjectInspector. - * - * Colors follow Geist's Shiki JSON palette so the inspector reads the same - * as highlighted code blocks across the product: - * - property names / punctuation: --ds-gray-1000 (default foreground) - * - strings / numbers / booleans: --ds-green-900 - * - null / undefined: --ds-gray-900 (muted) - * - regexp / function: --ds-purple-900 - * - date: --ds-pink-900 - * - * Because the `--ds-*` tokens adapt to theme automatically, the light and - * dark objects are intentionally identical. - */ - -// --------------------------------------------------------------------------- -// Extended color tokens not supported by react-inspector's built-in theme -// system, applied via our custom nodeRenderer in data-inspector.tsx. -// --------------------------------------------------------------------------- - -export interface InspectorThemeExtended { - /** Color for Date values (Node: 'magenta') */ - OBJECT_VALUE_DATE_COLOR: string; -} - -export const inspectorThemeExtendedLight: InspectorThemeExtended = { - OBJECT_VALUE_DATE_COLOR: 'var(--ds-pink-900)', -}; - -export const inspectorThemeExtendedDark: InspectorThemeExtended = { - OBJECT_VALUE_DATE_COLOR: 'var(--ds-pink-900)', -}; - -// --------------------------------------------------------------------------- -// Shared structural values (same in both themes) -// --------------------------------------------------------------------------- - -const shared = { - BASE_FONT_SIZE: '11px', - BASE_LINE_HEIGHT: 1.4, - BASE_BACKGROUND_COLOR: 'transparent', - OBJECT_PREVIEW_ARRAY_MAX_PROPERTIES: 10, - OBJECT_PREVIEW_OBJECT_MAX_PROPERTIES: 5, - HTML_TAGNAME_TEXT_TRANSFORM: 'lowercase' as const, - ARROW_MARGIN_RIGHT: 3, - ARROW_FONT_SIZE: 12, - TREENODE_FONT_FAMILY: 'var(--font-mono)', - TREENODE_FONT_SIZE: '11px', - TREENODE_LINE_HEIGHT: 1.4, - TREENODE_PADDING_LEFT: 12, - TABLE_DATA_BACKGROUND_IMAGE: 'none', - TABLE_DATA_BACKGROUND_SIZE: '0', -}; - -// --------------------------------------------------------------------------- -// Light theme -// --------------------------------------------------------------------------- - -const geistTheme = { - ...shared, - - // Base text - BASE_COLOR: 'var(--ds-gray-1000)', - - // Property names β€” default foreground (matches JSON key color in Geist Shiki) - OBJECT_NAME_COLOR: 'var(--ds-gray-1000)', - - // Strings & symbols β€” green - OBJECT_VALUE_STRING_COLOR: 'var(--ds-green-900)', - OBJECT_VALUE_SYMBOL_COLOR: 'var(--ds-green-900)', - - // Numbers & booleans β€” green (Geist JSON tokens) - OBJECT_VALUE_NUMBER_COLOR: 'var(--ds-green-900)', - OBJECT_VALUE_BOOLEAN_COLOR: 'var(--ds-green-900)', - - // null β€” muted foreground - OBJECT_VALUE_NULL_COLOR: 'var(--ds-gray-900)', - - // undefined β€” muted foreground - OBJECT_VALUE_UNDEFINED_COLOR: 'var(--ds-gray-900)', - - // RegExp β€” purple - OBJECT_VALUE_REGEXP_COLOR: 'var(--ds-purple-900)', - - // Functions β€” purple - OBJECT_VALUE_FUNCTION_PREFIX_COLOR: 'var(--ds-purple-900)', - - // HTML (rarely used here, kept consistent with the palette) - HTML_TAG_COLOR: 'var(--ds-gray-900)', - HTML_TAGNAME_COLOR: 'var(--ds-blue-900)', - HTML_ATTRIBUTE_NAME_COLOR: 'var(--ds-amber-900)', - HTML_ATTRIBUTE_VALUE_COLOR: 'var(--ds-green-900)', - HTML_COMMENT_COLOR: 'var(--ds-gray-700)', - HTML_DOCTYPE_COLOR: 'var(--ds-gray-700)', - - // Structural - ARROW_COLOR: 'var(--ds-gray-700)', - TABLE_BORDER_COLOR: 'var(--ds-gray-300)', - TABLE_TH_BACKGROUND_COLOR: 'var(--ds-gray-100)', - TABLE_TH_HOVER_COLOR: 'var(--ds-gray-200)', - TABLE_SORT_ICON_COLOR: 'var(--ds-gray-700)', -}; - -export const inspectorThemeLight = geistTheme; - -// --------------------------------------------------------------------------- -// Dark theme -// --------------------------------------------------------------------------- - -export const inspectorThemeDark = geistTheme; diff --git a/packages/web-shared/src/styles.css b/packages/web-shared/src/styles.css index 23adb5f578..a01f1d5570 100644 --- a/packages/web-shared/src/styles.css +++ b/packages/web-shared/src/styles.css @@ -653,3 +653,236 @@ transform: scale(1); } } + +/* Compact JSON tree */ + +.wf-json-view-container { + display: inline; + margin: 0; + padding: 0; + background: transparent; + color: var(--ds-gray-1000); + font-family: + var(--font-mono), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, + 'Liberation Mono', 'Courier New', monospace; + font-size: 11px; + line-height: 20px; + white-space: pre-wrap; + word-wrap: break-word; + overflow-wrap: anywhere; +} + +.wf-json-view-container > .wf-json-view-row { + padding-left: 18px; +} + +.wf-json-view-container > .wf-json-view-row > .wf-json-view-expand-icon, +.wf-json-view-container > .wf-json-view-row > .wf-json-view-collapse-icon { + margin-left: -18px; +} + +.wf-json-view-children { + margin: 0; + padding: 0 0 0 2ch; +} + +.wf-json-view-row { + display: block; + margin: 0; + padding: 0 0 0 18px; +} + +.wf-json-view-row:hover:not(:has(.wf-json-view-row:hover)):not( + :has(.wf-json-view-row ~ .wf-json-view-row) + ) { + background-color: var(--ds-gray-alpha-100); +} + +.wf-json-view-label { + margin-right: 1ch; + color: var(--ds-pink-900); + font-weight: 400; +} + +.wf-json-view-clickable-label { + margin-right: 1ch; + cursor: pointer; + color: var(--ds-pink-900); + font-weight: 400; +} + +.wf-json-view-string { + color: var(--ds-green-900); +} + +.wf-json-view-number { + color: var(--ds-blue-900); +} + +.wf-json-view-boolean { + color: var(--ds-amber-900); +} + +.wf-json-view-null, +.wf-json-view-undefined, +.wf-json-view-other { + color: var(--ds-gray-900); +} + +.wf-json-view-punctuation { + margin-right: 0; + color: var(--ds-gray-1000); + font-weight: 400; +} + +.wf-json-view-expand-icon, +.wf-json-view-collapse-icon { + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 14px; + font-weight: 600; + min-width: 18px; + min-height: 18px; + margin-right: 0; + margin-left: -18px; + padding: 0; + border-radius: 4px; + box-sizing: border-box; + cursor: pointer; + color: var(--ds-gray-900); + user-select: none; + outline: none; + vertical-align: middle; +} + +.wf-json-view-expand-icon::after, +.wf-json-view-collapse-icon::after { + content: ''; + display: block; + width: 14px; + height: 14px; + margin-right: 0; + background-color: currentColor; + background-repeat: no-repeat; + background-position: center; + background-size: 14px 14px; + -webkit-mask-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10.1016 7.29297C10.4921 7.68349 10.4921 8.31651 10.1016 8.70703L6.74805 12.0605L5.6875 11L8.6875 8L5.6875 5L6.74805 3.93945L10.1016 7.29297Z' fill='black'/%3E%3C/svg%3E"); + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; + -webkit-mask-size: 14px 14px; + mask-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10.1016 7.29297C10.4921 7.68349 10.4921 8.31651 10.1016 8.70703L6.74805 12.0605L5.6875 11L8.6875 8L5.6875 5L6.74805 3.93945L10.1016 7.29297Z' fill='black'/%3E%3C/svg%3E"); + mask-repeat: no-repeat; + mask-position: center; + mask-size: 14px 14px; + transition: transform 120ms ease; +} + +.wf-json-view-expand-icon::after { + transform: none; +} + +.wf-json-view-collapse-icon::after { + transform: rotate(90deg); +} + +.wf-json-view-expand-icon:hover, +.wf-json-view-collapse-icon:hover, +.wf-json-view-row:has(> .wf-json-view-clickable-label:hover) + > .wf-json-view-expand-icon, +.wf-json-view-row:has(> .wf-json-view-clickable-label:hover) + > .wf-json-view-collapse-icon { + background-color: var(--ds-gray-alpha-100); + color: var(--ds-gray-900); +} + +.wf-json-view-collapsed { + color: var(--ds-gray-900); +} + +.wf-json-view-collapsed::after { + content: '...'; +} + +.dark-theme .wf-json-view-label, +.dark .wf-json-view-label, +[data-theme='dark'] .wf-json-view-label { + color: var(--ds-pink-700, var(--ds-pink-900)); +} + +.dark-theme .wf-json-view-clickable-label, +.dark .wf-json-view-clickable-label, +[data-theme='dark'] .wf-json-view-clickable-label { + color: var(--ds-pink-700, var(--ds-pink-900)); +} + +.dark-theme .wf-json-view-string, +.dark .wf-json-view-string, +[data-theme='dark'] .wf-json-view-string { + color: var(--ds-blue-700); +} + +.dark-theme .wf-json-view-number, +.dark .wf-json-view-number, +[data-theme='dark'] .wf-json-view-number { + color: var(--ds-blue-700); +} + +.dark-theme .wf-json-view-boolean, +.dark .wf-json-view-boolean, +[data-theme='dark'] .wf-json-view-boolean { + color: var(--ds-amber-700); +} + +.dark-theme .wf-json-view-null, +.dark-theme .wf-json-view-undefined, +.dark-theme .wf-json-view-other, +.dark .wf-json-view-null, +.dark .wf-json-view-undefined, +.dark .wf-json-view-other, +[data-theme='dark'] .wf-json-view-null, +[data-theme='dark'] .wf-json-view-undefined, +[data-theme='dark'] .wf-json-view-other { + color: var(--ds-gray-700); +} + +.dark-theme .wf-json-view-expand-icon, +.dark-theme .wf-json-view-collapse-icon, +.dark .wf-json-view-expand-icon, +.dark .wf-json-view-collapse-icon, +[data-theme='dark'] .wf-json-view-expand-icon, +[data-theme='dark'] .wf-json-view-collapse-icon { + color: #fff; +} + +.dark-theme .wf-json-view-expand-icon:hover, +.dark-theme .wf-json-view-collapse-icon:hover, +.dark-theme + .wf-json-view-row:has(> .wf-json-view-clickable-label:hover) + > .wf-json-view-expand-icon, +.dark-theme + .wf-json-view-row:has(> .wf-json-view-clickable-label:hover) + > .wf-json-view-collapse-icon, +.dark .wf-json-view-expand-icon:hover, +.dark .wf-json-view-collapse-icon:hover, +.dark .wf-json-view-row:has(> .wf-json-view-clickable-label:hover) + > .wf-json-view-expand-icon, +.dark .wf-json-view-row:has(> .wf-json-view-clickable-label:hover) + > .wf-json-view-collapse-icon, +[data-theme='dark'] .wf-json-view-expand-icon:hover, +[data-theme='dark'] .wf-json-view-collapse-icon:hover, +[data-theme='dark'] + .wf-json-view-row:has(> .wf-json-view-clickable-label:hover) + > .wf-json-view-expand-icon, +[data-theme='dark'] + .wf-json-view-row:has(> .wf-json-view-clickable-label:hover) + > .wf-json-view-collapse-icon { + background-color: var(--ds-gray-alpha-100); + color: #fff; +} + +.dark-theme .wf-json-view-collapsed, +.dark .wf-json-view-collapsed, +[data-theme='dark'] .wf-json-view-collapsed { + color: var(--ds-gray-700); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8640dfe50b..b8992e79fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1194,9 +1194,9 @@ importers: react-dom: specifier: 19.1.0 version: 19.1.0(react@19.1.0) - react-inspector: - specifier: 9.0.0 - version: 9.0.0(react@19.1.0) + react-json-view-lite: + specifier: 2.5.0 + version: 2.5.0(react@19.1.0) react-virtuoso: specifier: 4.18.1 version: 4.18.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -13382,11 +13382,6 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - react-inspector@9.0.0: - resolution: {integrity: sha512-w/VJucSeHxlwRa2nfM2k7YhpT1r5EtlDOClSR+L7DyQP91QMdfFEDXDs9bPYN4kzP7umFtom7L0b2GGjph4Kow==} - peerDependencies: - react: ^18.0.0 || ^19.0.0 - react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -13396,6 +13391,12 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-json-view-lite@2.5.0: + resolution: {integrity: sha512-tk7o7QG9oYyELWHL8xiMQ8x4WzjCzbWNyig3uexmkLb54r8jO0yH3WCWx8UZS0c49eSA4QUmG5caiRJ8fAn58g==} + engines: {node: '>=18'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-medium-image-zoom@5.4.0: resolution: {integrity: sha512-BsE+EnFVQzFIlyuuQrZ9iTwyKpKkqdFZV1ImEQN573QPqGrIUuNni7aF+sZwDcxlsuOMayCr6oO/PZR/yJnbRg==} peerDependencies: @@ -29839,16 +29840,16 @@ snapshots: dependencies: react: 19.2.3 - react-inspector@9.0.0(react@19.1.0): - dependencies: - react: 19.1.0 - react-is@16.13.1: {} react-is@17.0.2: {} react-is@18.3.1: {} + react-json-view-lite@2.5.0(react@19.1.0): + dependencies: + react: 19.1.0 + react-medium-image-zoom@5.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: react: 19.2.3