import type { SnapShotSource } from "@t3tools/contracts"; import { ImageIcon, TextIcon } from "lucide-react"; import { Suspense, use, useMemo, type CSSProperties } from "react"; import { useTheme } from "../../hooks/useTheme"; import { resolveDiffThemeName } from "../../lib/diffRendering"; import { getSyntaxHighlighterPromise } from "../../lib/syntaxHighlighting"; import { cn } from "../../lib/utils"; import { RenderErrorBoundary } from "../RenderErrorBoundary"; import { Button } from "../ui/button"; import { Popover, PopoverPopup, PopoverTitle, PopoverTrigger } from "../ui/popover"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; export const SNAP_SHOT_ATTACHMENT_FRAME_CLASS = "relative h-28 w-52 max-w-full overflow-hidden rounded-lg border border-border/80"; export interface SnapShotAccessibilityDetails { content: string; format: "json" | "text"; } interface SyntaxToken { readonly content: string; readonly offset: number; readonly color?: string; readonly fontStyle?: number; } function syntaxTokenStyle(token: SyntaxToken): CSSProperties { const fontStyle = token.fontStyle ?? 0; return { ...(token.color ? { color: token.color } : {}), ...(fontStyle & 1 ? { fontStyle: "italic" } : {}), ...(fontStyle & 2 ? { fontWeight: 700 } : {}), ...(fontStyle & 4 ? { textDecoration: "underline" } : {}), }; } function HighlightedAccessibilityJson({ content, theme, }: { content: string; theme: "light" | "dark"; }) { const highlighter = use(getSyntaxHighlighterPromise("json")); const lines = useMemo( () => highlighter.codeToTokens(content, { lang: "json", theme: resolveDiffThemeName(theme), }).tokens, [content, highlighter, theme], ); let lineOffset = 0; return lines.map((line) => { const lineContent = line.map((token) => token.content).join(""); const lineKey = `${lineOffset}:${lineContent}`; const hasNextLine = lineOffset + lineContent.length < content.length; lineOffset += lineContent.length + 1; return ( {line.map((token) => ( {token.content} ))} {hasNextLine ? "\n" : null} ); }); } export function SnapShotAccessibilityData({ details, className, }: { details: SnapShotAccessibilityDetails; className?: string; }) { const { resolvedTheme } = useTheme(); const content = details.format === "json" ? ( ) : ( details.content ); return (
      {content}
    
); } export function snapShotAccessibilityDetails( source: SnapShotSource, ): SnapShotAccessibilityDetails | undefined { if (source.accessibility?.format === "element-tree") { return { content: JSON.stringify(source.accessibility, null, 2), format: "json", }; } const text = source.accessibility?.format === "flat-text" ? source.accessibility.text.trim() : source.accessibleText?.trim(); return text ? { content: text, format: "text" } : undefined; } export function snapShotIncludesAccessibility(source: SnapShotSource): boolean { return Boolean(source.accessibility || source.accessibleText?.trim()); } export function SnapShotContentsButton({ source, className, side = "top", }: { source: SnapShotSource; className?: string; side?: "top" | "right" | "bottom" | "left"; }) { const includesAccessibility = snapShotIncludesAccessibility(source); const ContentsIcon = includesAccessibility ? TextIcon : ImageIcon; const accessibilityDetails = snapShotAccessibilityDetails(source); const tooltip = includesAccessibility ? "Accessibility data" : "No accessibility data"; return ( event.stopPropagation()} size="icon-micro" variant="ghost-muted" /> } /> } > {tooltip}
Accessibility data {accessibilityDetails ? ( ) : includesAccessibility ? (
Structured accessibility elements were included, but they have no readable names or values.
) : (
The app or capture backend did not provide verified accessibility data.
)}
); } export function SnapShotAttachmentDetails({ source, className, }: { source: SnapShotSource; className?: string; }) { return (
{source.appIconDataUrl ? ( ) : (
{source.appName.slice(0, 1)}
)}
{source.appName}
{source.windowTitle || "Captured window"}
); }