Merge remote-tracking branch 'github/master' into xtr/trajectory-inspection-ui
# Conflicts: # packages/client/ui-primitives/src/Menu.tsx # packages/client/ui-trajectory/src/client/TrajectoryCell.tsx # packages/client/ui-trajectory/src/client/TrajectoryView.tsx # packages/client/ui-trajectory/tests/layout.spec.tsx # pnpm-lock.yaml # pnpm-workspace.yaml
This commit is contained in:
@@ -20,6 +20,9 @@ export interface CodeBlockProps {
|
||||
|
||||
/** @returns true only when the host accepted the write. */
|
||||
async function writeClipboard(text: string): Promise<boolean> {
|
||||
// lib.dom types clipboard non-optional, but insecure contexts omit it —
|
||||
// that runtime gap is exactly what this guard detects.
|
||||
/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */
|
||||
if (navigator.clipboard?.writeText) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
@@ -30,6 +33,9 @@ async function writeClipboard(text: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
// jsdom and older hosts: best-effort execCommand path when present.
|
||||
// execCommand('copy') is the only clipboard fallback where the async API
|
||||
// is missing; deprecated but deliberately retained.
|
||||
/* eslint-disable @typescript-eslint/no-deprecated */
|
||||
const exec = typeof document.execCommand === 'function'
|
||||
? document.execCommand.bind(document)
|
||||
: undefined
|
||||
@@ -48,6 +54,7 @@ async function writeClipboard(text: string): Promise<boolean> {
|
||||
} finally {
|
||||
el.remove()
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-deprecated */
|
||||
}
|
||||
|
||||
export function CodeBlock({ code, lang, className }: CodeBlockProps) {
|
||||
@@ -65,20 +72,20 @@ export function CodeBlock({ code, lang, className }: CodeBlockProps) {
|
||||
void writeClipboard(text).then((ok) => {
|
||||
if (!ok) return
|
||||
setCopied(true)
|
||||
window.setTimeout(() => setCopied(false), 1000)
|
||||
window.setTimeout(() => { setCopied(false) }, 1000)
|
||||
})
|
||||
}, [copied, trimmed])
|
||||
|
||||
const body = html === undefined
|
||||
? (
|
||||
<pre className={css.plain}><code>{trimmed}</code></pre>
|
||||
)
|
||||
<pre className={css.plain}><code>{trimmed}</code></pre>
|
||||
)
|
||||
: (
|
||||
// eslint-disable-next-line react/no-danger -- shiki's output is a static
|
||||
// span tree it generated from `code` (no user HTML passes through), the
|
||||
// sanctioned innerHTML consumption path per shiki's own docs.
|
||||
<div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
)
|
||||
// shiki's output is a static span tree it generated from `code` (no user
|
||||
// HTML passes through), the sanctioned innerHTML consumption path per
|
||||
// shiki's own docs.
|
||||
<div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -15,6 +15,8 @@ export function JsonBlock({ label, payload, defaultOpen = false }: {
|
||||
if (!open) return ''
|
||||
let s: string
|
||||
try {
|
||||
// lib typing hides stringify's undefined arm (undefined/function/symbol payloads).
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
s = JSON.stringify(payload, null, 2) ?? String(payload)
|
||||
} catch {
|
||||
s = String(payload)
|
||||
@@ -23,7 +25,7 @@ export function JsonBlock({ label, payload, defaultOpen = false }: {
|
||||
}, [open, payload])
|
||||
return (
|
||||
<div className={css.root}>
|
||||
<button type="button" className={css.toggle} onClick={() => setOpen((v) => !v)}>
|
||||
<button type="button" className={css.toggle} onClick={() => { setOpen(v => !v) }}>
|
||||
{open ? '▾' : '▸'} {label}
|
||||
</button>
|
||||
{open && <pre className={css.body}>{body}</pre>}
|
||||
|
||||
@@ -27,25 +27,25 @@ const safeUrl: UrlTransform = url => sanitizeUrl(url)
|
||||
/** Build the component table; while `streaming`, fences render the plain arm (see CodeBlock). */
|
||||
function buildComponents(streaming: boolean): Components {
|
||||
return {
|
||||
a: ({ href = '', children }) => {
|
||||
const safeHref = sanitizeUrl(href)
|
||||
if (safeHref === '') return <>{children}</>
|
||||
const external = ['http:', 'https:'].includes(new URL(safeHref).protocol)
|
||||
return (
|
||||
<a
|
||||
href={safeHref}
|
||||
{...(external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
},
|
||||
img: ({ alt = '' }) => <span className={css.imageAlt}>{alt}</span>,
|
||||
table: ({ children }) => (
|
||||
<div className={css.tableScroll}>
|
||||
<table>{children}</table>
|
||||
</div>
|
||||
),
|
||||
a: ({ href = '', children }) => {
|
||||
const safeHref = sanitizeUrl(href)
|
||||
if (safeHref === '') return <>{children}</>
|
||||
const external = ['http:', 'https:'].includes(new URL(safeHref).protocol)
|
||||
return (
|
||||
<a
|
||||
href={safeHref}
|
||||
{...(external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
},
|
||||
img: ({ alt = '' }) => <span className={css.imageAlt}>{alt}</span>,
|
||||
table: ({ children }) => (
|
||||
<div className={css.tableScroll}>
|
||||
<table>{children}</table>
|
||||
</div>
|
||||
),
|
||||
// Fenced blocks route through the shared CodeBlock (shiki for registered
|
||||
// grammars, identical-geometry plain fallback for unknown/absent
|
||||
// languages); inline code keeps the default <code> path (the :not(pre)
|
||||
@@ -53,7 +53,9 @@ function buildComponents(streaming: boolean): Components {
|
||||
// plain arm — retokenizing a growing fence on every chunk is quadratic
|
||||
// main-thread work; the finalize swap highlights it once.
|
||||
pre: ({ children }) => {
|
||||
/* v8 ignore next 2 -- the markdown pipeline always hands `pre` its single `code` element; the undefined arm guards a react-markdown representation change. */
|
||||
// The markdown pipeline always hands `pre` its single `code` element;
|
||||
// the undefined arm guards a react-markdown representation change.
|
||||
/* v8 ignore next 2 */
|
||||
const child = isValidElement<{ className?: string; children?: unknown }>(children) ? children : undefined
|
||||
const raw = child?.props.children
|
||||
// A fence whose content isn't one plain string (e.g. an empty fence)
|
||||
|
||||
Reference in New Issue
Block a user