diff --git a/apps/local/package.json b/apps/local/package.json index b652b39f5..4f3d25915 100644 --- a/apps/local/package.json +++ b/apps/local/package.json @@ -17,11 +17,8 @@ "dependencies": { "@radix-ui/react-dialog": "^1.1.23", "@radix-ui/react-slot": "^1.3.3", - "@shikijs/core": "^3.23.0", - "@shikijs/engine-javascript": "^3.23.0", - "@shikijs/langs": "^3.23.0", - "@shikijs/themes": "^3.23.0", "@tailwindcss/vite": "^4.3.3", + "@twinkleplop/json": "^0.1.5", "@uidotdev/usehooks": "^2.4.1", "@vercel/analytics": "^2.0.1", "class-variance-authority": "^0.7.1", diff --git a/apps/local/src/App.integration.test.tsx b/apps/local/src/App.integration.test.tsx index bd434522a..788dcd32c 100644 --- a/apps/local/src/App.integration.test.tsx +++ b/apps/local/src/App.integration.test.tsx @@ -211,7 +211,7 @@ describe('local receiver to viewer integration', () => { fireEvent.click(screen.getByRole('button', { name: 'Copy JSON' })) expect(await screen.findByRole('status', { name: 'JSON copied' })).not.toBeNull() await waitFor(() => { - expect(screen.getByTestId('highlighted-json').querySelector('.shiki')).not.toBeNull() + expect(screen.getByTestId('highlighted-json').querySelector('.twinkleplop')).not.toBeNull() }) fireEvent.click(events[0]!) fireEvent.click(screen.getByRole('tab', { name: 'JSON' })) diff --git a/apps/local/src/components/json-view.tsx b/apps/local/src/components/json-view.tsx index 5ae09ba69..0a5573b09 100644 --- a/apps/local/src/components/json-view.tsx +++ b/apps/local/src/components/json-view.tsx @@ -1,21 +1,12 @@ -import { createHighlighterCore } from '@shikijs/core' -import { createJavaScriptRegexEngine } from '@shikijs/engine-javascript' -import json from '@shikijs/langs/json' -import githubDark from '@shikijs/themes/github-dark' -import githubLight from '@shikijs/themes/github-light' +import { language } from '@twinkleplop/json' import { Check, Copy } from 'lucide-react' -import { useEffect, useState } from 'react' import { useCopyToClipboard } from '@uidotdev/usehooks' type JsonViewProps = { code: string } -const highlighter = createHighlighterCore({ - engine: createJavaScriptRegexEngine(), - langs: [json], - themes: [githubLight, githubDark], -}) +const highlight = language() function formatJson(code: string) { try { @@ -25,41 +16,21 @@ function formatJson(code: string) { } } -/** Render JSON with Shiki only after its containing event has been expanded. */ +function highlightJson(code: string) { + try { + return highlight(code) + } catch { + return undefined + } +} + +/** Render JSON with Twinkleplop once its containing event has been expanded. */ export function JsonView({ code }: JsonViewProps) { - const [html, setHtml] = useState() const [copiedText, copyToClipboard] = useCopyToClipboard() const formattedCode = formatJson(code) + const html = highlightJson(formattedCode) const copied = copiedText === code - useEffect(() => { - let disposed = false - - void highlighter - .then((instance) => - instance.codeToHtml(formattedCode, { - lang: 'json', - themes: { - light: 'github-light', - dark: 'github-dark', - }, - defaultColor: false, - }) - ) - .then((result) => { - if (!disposed) { - setHtml(result) - } - }) - .catch(() => { - // Keep the readable, escaped source fallback if syntax highlighting cannot load. - }) - - return () => { - disposed = true - } - }, [formattedCode]) - const copyJson = () => { void copyToClipboard(code) } diff --git a/apps/local/src/index.css b/apps/local/src/index.css index 2dd5dfc1f..439077811 100644 --- a/apps/local/src/index.css +++ b/apps/local/src/index.css @@ -82,16 +82,39 @@ @apply m-0 break-words bg-transparent p-0 font-mono text-inherit whitespace-pre-wrap; } - .json-view .shiki { + .json-view { + --tok-string: #0a3069; + --tok-number: #0550ae; + --tok-keyword: #cf222e; + --tok-punctuation: #24292f; + } + + .dark .json-view { + --tok-string: #a5d6ff; + --tok-number: #79c0ff; + --tok-keyword: #ff7b72; + --tok-punctuation: #c9d1d9; + } + + .json-view .twinkleplop { background-color: transparent !important; padding: 0 !important; } - .json-view .shiki span { - color: var(--shiki-light); + .json-view .tok.string { + color: var(--tok-string); + } + + .json-view .tok.number { + color: var(--tok-number); + } + + .json-view .tok.boolean, + .json-view .tok.keyword { + color: var(--tok-keyword); } - .dark .json-view .shiki span { - color: var(--shiki-dark); + .json-view .tok.punctuation { + color: var(--tok-punctuation); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2da95d59..247d4d74b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,21 +73,12 @@ importers: '@radix-ui/react-slot': specifier: ^1.3.3 version: 1.3.3(@types/react@19.2.18)(react@19.2.8) - '@shikijs/core': - specifier: ^3.23.0 - version: 3.23.0 - '@shikijs/engine-javascript': - specifier: ^3.23.0 - version: 3.23.0 - '@shikijs/langs': - specifier: ^3.23.0 - version: 3.23.0 - '@shikijs/themes': - specifier: ^3.23.0 - version: 3.23.0 '@tailwindcss/vite': specifier: ^4.3.3 version: 4.3.3(vite@8.2.2(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.5)(yaml@2.9.0)) + '@twinkleplop/json': + specifier: ^0.1.5 + version: 0.1.5 '@uidotdev/usehooks': specifier: ^2.4.1 version: 2.4.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -2453,6 +2444,12 @@ packages: peerDependencies: typescript: '>=5.7.2' + '@twinkleplop/core@0.2.2': + resolution: {integrity: sha512-xJpsL0QdgZ0gKDkTW+S1FAzVzyTns+jKPXER52vc/69qCe6gXCpsoclhTjERnFUbkZgTBEA/iVJxaufhY8Oqrw==} + + '@twinkleplop/json@0.1.5': + resolution: {integrity: sha512-qXB8HgDThT8dYHT/4+S/EcQK4R8qbi6Ezb/h5bZEUK2yh0G3170W9QXF8iDp1JChQ9upyn8BivIAwQZGgcx28g==} + '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} @@ -7769,6 +7766,12 @@ snapshots: dependencies: typescript: 5.9.3 + '@twinkleplop/core@0.2.2': {} + + '@twinkleplop/json@0.1.5': + dependencies: + '@twinkleplop/core': 0.2.2 + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1