Skip to content
Open
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: 1 addition & 4 deletions apps/local/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.4",
"@uidotdev/usehooks": "^2.4.1",
"@vercel/analytics": "^2.0.1",
"class-variance-authority": "^0.7.1",
Expand Down
2 changes: 1 addition & 1 deletion apps/local/src/App.integration.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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('.json-view-code')).not.toBeNull()
})
fireEvent.click(events[0]!)
fireEvent.click(screen.getByRole('tab', { name: 'JSON' }))
Expand Down
94 changes: 28 additions & 66 deletions apps/local/src/components/json-view.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -25,72 +16,43 @@ function formatJson(code: string) {
}
}

/** Render JSON with Shiki only after its containing event has been expanded. */
/** A string token immediately followed by `:` is an object key. */
function highlightJson(code: string) {
return highlight(code, {
class_name: 'json-view-code',
has_classes: true,
token: (type, _start, end) => {
if (type === 'string' && code[end] === ':') {
return { class: 'tok-key' }
}
},
})
}

/** Render JSON with Twinkleplop after its containing event has been expanded. */
export function JsonView({ code }: JsonViewProps) {
const [html, setHtml] = useState<string>()
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)
}

const copyButton = (
<button
type="button"
aria-label="Copy JSON"
className="inline-flex items-center gap-1.5 rounded-md border border-border bg-background/90 px-2 py-1 text-xs font-medium text-muted-foreground shadow-xs backdrop-blur transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
onClick={copyJson}
>
{copied ? <Check className="size-3.5 text-emerald-500" /> : <Copy className="size-3.5" />}
{copied ? 'Copied' : 'Copy JSON'}
</button>
)

if (!html) {
return (
<div className="relative">
<div className="absolute top-2 right-3 z-10">{copyButton}</div>
{copied ? <span role="status" aria-label="JSON copied" className="sr-only">JSON copied</span> : null}
<pre data-testid="highlighted-json" className="json-view whitespace-pre-wrap break-words p-3 pr-28 text-xs leading-6">
{formattedCode}
</pre>
</div>
)
}

return (
<div className="relative">
<div className="absolute top-2 right-3 z-10">{copyButton}</div>
<div className="absolute top-2 right-3 z-10">
<button
type="button"
aria-label="Copy JSON"
className="inline-flex items-center gap-1.5 rounded-md border border-border bg-background/90 px-2 py-1 text-xs font-medium text-muted-foreground shadow-xs backdrop-blur transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
onClick={copyJson}
>
{copied ? <Check className="size-3.5 text-emerald-500" /> : <Copy className="size-3.5" />}
{copied ? 'Copied' : 'Copy JSON'}
</button>
</div>
{copied ? <span role="status" aria-label="JSON copied" className="sr-only">JSON copied</span> : null}
<div
data-testid="highlighted-json"
Expand Down
33 changes: 26 additions & 7 deletions apps/local/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,35 @@
@apply m-0 break-words bg-transparent p-0 font-mono text-inherit whitespace-pre-wrap;
}

.json-view .shiki {
background-color: transparent !important;
padding: 0 !important;
.json-view .json-view-code {
background-color: transparent;
padding: 0;
color: #24292e;
}

.json-view .shiki span {
color: var(--shiki-light);
.json-view .tok.string {
color: #032f62;
}

.dark .json-view .shiki span {
color: var(--shiki-dark);
.json-view .tok.number,
.json-view .tok.boolean,
.json-view .tok.keyword,
.json-view .tok.tok-key {
color: #005cc5;
}

.dark .json-view .json-view-code {
color: #e1e4e8;
}

.dark .json-view .tok.string {
color: #9ecbff;
}

.dark .json-view .tok.number,
.dark .json-view .tok.boolean,
.dark .json-view .tok.keyword,
.dark .json-view .tok.tok-key {
color: #79b8ff;
}
}
27 changes: 15 additions & 12 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading